├── .Mime ├── .State ├── .gitignore ├── .travis.yml ├── Bin └── Pp.php ├── CHANGELOG.md ├── Documentation ├── En │ └── Index.xyl └── Fr │ └── Index.xyl ├── Exception ├── Exception.php ├── FinalStateHasNotBeenReached.php ├── IllegalToken.php ├── Lexer.php ├── Rule.php ├── UnexpectedToken.php └── UnrecognizedToken.php ├── Ll1.php ├── Llk ├── Lexer.php ├── Llk.php ├── Llk.pp ├── Parser.php ├── Rule │ ├── Analyzer.php │ ├── Choice.php │ ├── Concatenation.php │ ├── Ekzit.php │ ├── Entry.php │ ├── Invocation.php │ ├── Repetition.php │ ├── Rule.php │ └── Token.php ├── Sampler │ ├── BoundedExhaustive.php │ ├── Coverage.php │ ├── Exception.php │ ├── Sampler.php │ └── Uniform.php └── TreeNode.php ├── README.md ├── Test ├── Integration │ ├── Documentation.php │ └── Llk │ │ ├── Documentation.php │ │ ├── Rule │ │ └── Analyzer.php │ │ └── Soundness.php └── Unit │ ├── Exception │ ├── Exception.php │ ├── FinalStateHasNotBeenReached.php │ ├── IllegalToken.php │ ├── Lexer.php │ ├── Rule.php │ ├── UnexpectedToken.php │ └── UnrecognizedToken.php │ └── Llk │ ├── Lexer.php │ ├── Llk.php │ ├── Rule │ ├── Choice.php │ ├── Concatenation.php │ ├── Ekzit.php │ ├── Entry.php │ ├── Invocation.php │ ├── Repetition.php │ ├── Rule.php │ └── Token.php │ ├── Sampler │ └── Exception.php │ └── TreeNode.php ├── Visitor └── Dump.php ├── bors.toml └── composer.json /.Mime: -------------------------------------------------------------------------------- 1 | text/vnd.hoa.compiler pp 2 | -------------------------------------------------------------------------------- /.State: -------------------------------------------------------------------------------- 1 | finalized 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /composer.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | branches: 4 | only: 5 | - staging 6 | - trying 7 | - master 8 | 9 | matrix: 10 | include: 11 | - php: 5.6 12 | - php: 7.0 13 | - php: 7.1 14 | env: 15 | - ENABLE_DEVTOOLS=true 16 | - php: nightly 17 | - php: hhvm-3.12 18 | sudo: required 19 | dist: trusty 20 | group: edge 21 | - php: hhvm 22 | sudo: required 23 | dist: trusty 24 | group: edge 25 | allow_failures: 26 | - php: nightly 27 | - php: hhvm-3.12 28 | - php: hhvm 29 | fast_finish: true 30 | 31 | os: 32 | - linux 33 | 34 | notifications: 35 | irc: "chat.freenode.net#hoaproject" 36 | 37 | sudo: false 38 | 39 | env: 40 | global: 41 | - secure: "AAAAB3NzaC1yc2EAAAADAQABAAAAgQCoAO780/K7xbK3aPnHJmAyw9B3OGRFx2sSnl6umenksq4kmlzmPaUF9g3cHGcXpM5O33P8Tux//iYGASy5W8f5vbxbWSvlwV/aWFW9oSM6G/01gyW9vRFK0MnXpUMyCF9B4K/RJumGxm9BSxkAFFo2gPHIJrd08SOQeiDLygpcEQ==" 42 | 43 | cache: 44 | directories: 45 | - vendor/ 46 | 47 | before_script: 48 | - export PATH="$PATH:$HOME/.composer/vendor/bin" 49 | - if [[ ! $ENABLE_XDEBUG ]]; then 50 | phpenv config-rm xdebug.ini || echo "ext-xdebug is not available, cannot remove it."; 51 | fi 52 | 53 | script: 54 | - composer install 55 | - vendor/bin/hoa test:run 56 | - if [[ $ENABLE_DEVTOOLS ]]; then 57 | composer global require friendsofphp/php-cs-fixer; 58 | vendor/bin/hoa devtools:cs --diff --dry-run .; 59 | fi 60 | -------------------------------------------------------------------------------- /Bin/Pp.php: -------------------------------------------------------------------------------- 1 | getOption($v)) { 82 | switch ($c) { 83 | case 'v': 84 | switch (strtolower($v)) { 85 | case 'dump': 86 | $visitor = 'Hoa\Compiler\Visitor\Dump'; 87 | 88 | break; 89 | 90 | default: 91 | return $this->usage(); 92 | } 93 | 94 | break; 95 | 96 | case 'c': 97 | $visitor = str_replace('.', '\\', $v); 98 | 99 | break; 100 | 101 | case 's': 102 | $tokenSequence = true; 103 | 104 | break; 105 | 106 | case 't': 107 | $trace = true; 108 | 109 | break; 110 | 111 | case '__ambiguous': 112 | $this->resolveOptionAmbiguity($v); 113 | 114 | break; 115 | 116 | case 'h': 117 | case '?': 118 | default: 119 | return $this->usage(); 120 | } 121 | } 122 | 123 | $this->parser->listInputs($grammar, $language); 124 | 125 | if (empty($grammar) || (empty($language) && '0' !== $language)) { 126 | return $this->usage(); 127 | } 128 | 129 | $compiler = Compiler\Llk::load(new File\Read($grammar)); 130 | $stream = new File\Read($language); 131 | $data = $stream->readAll(); 132 | 133 | try { 134 | $ast = $compiler->parse($data); 135 | } catch (Compiler\Exception $e) { 136 | if (true === $tokenSequence) { 137 | $this->printTokenSequence($compiler, $data); 138 | echo "\n\n"; 139 | } 140 | 141 | throw $e; 142 | 143 | return 1; 144 | } 145 | 146 | if (true === $tokenSequence) { 147 | $this->printTokenSequence($compiler, $data); 148 | echo "\n\n"; 149 | } 150 | 151 | if (true === $trace) { 152 | $this->printTrace($compiler); 153 | echo "\n\n"; 154 | } 155 | 156 | if (null !== $visitor) { 157 | $visitor = Consistency\Autoloader::dnew($visitor); 158 | echo $visitor->visit($ast); 159 | } 160 | 161 | return; 162 | } 163 | 164 | /** 165 | * Print trace. 166 | * 167 | * @param \Hoa\Compiler\Llk\Parser $compiler Compiler. 168 | * @return void 169 | */ 170 | protected function printTrace(Compiler\Llk\Parser $compiler) 171 | { 172 | $i = 0; 173 | 174 | foreach ($compiler->getTrace() as $element) { 175 | if ($element instanceof Compiler\Llk\Rule\Entry) { 176 | $ruleName = $element->getRule(); 177 | $rule = $compiler->getRule($ruleName); 178 | 179 | echo str_repeat('> ', ++$i), 'enter ', $ruleName; 180 | 181 | if (null !== $id = $rule->getNodeId()) { 182 | echo ' (', $id, ')'; 183 | } 184 | 185 | echo "\n"; 186 | } elseif ($element instanceof Compiler\Llk\Rule\Token) { 187 | echo 188 | str_repeat(' ', $i + 1), 189 | 'token ', $element->getTokenName(), 190 | ', consumed ', $element->getValue(), "\n"; 191 | } else { 192 | echo 193 | str_repeat('< ', $i--), 194 | 'ekzit ', $element->getRule(), "\n"; 195 | } 196 | } 197 | 198 | return; 199 | } 200 | 201 | /** 202 | * Print token sequence. 203 | * 204 | * @param \Hoa\Compiler\Llk\Parser $compiler Compiler. 205 | * @param string $data Data to lex. 206 | * @return void 207 | */ 208 | protected function printTokenSequence(Compiler\Llk\Parser $compiler, $data) 209 | { 210 | $lexer = new Compiler\Llk\Lexer(); 211 | $sequence = $lexer->lexMe($data, $compiler->getTokens()); 212 | $format = '%' . (strlen((string) count($sequence)) + 1) . 's ' . 213 | '%-13s %-20s %s %6s' . "\n"; 214 | 215 | $header = sprintf( 216 | $format, 217 | '#', 218 | 'namespace', 219 | 'token name', 220 | 'token value ', 221 | 'offset' 222 | ); 223 | 224 | echo $header, str_repeat('-', strlen($header)), "\n"; 225 | 226 | foreach ($sequence as $i => $token) { 227 | printf( 228 | $format, 229 | $i, 230 | $token['namespace'], 231 | $token['token'], 232 | 30 < $token['length'] 233 | ? mb_substr($token['value'], 0, 29) . '…' 234 | : 'EOF' === $token['token'] 235 | ? str_repeat(' ', 30) 236 | : $token['value'] . 237 | str_repeat(' ', 30 - $token['length']), 238 | $token['offset'] 239 | ); 240 | } 241 | 242 | return; 243 | } 244 | 245 | /** 246 | * The command usage. 247 | * 248 | * @return int 249 | */ 250 | public function usage() 251 | { 252 | echo 253 | 'Usage : compiler:pp [grammar.pp] [language]', "\n", 254 | 'Options :', "\n", 255 | $this->makeUsageOptionsList([ 256 | 'v' => 'Visitor name (only “dump” is supported).', 257 | 'c' => 'Visitor classname (using . instead of \ works).', 258 | 's' => 'Print token sequence.', 259 | 't' => 'Print trace.', 260 | 'help' => 'This help.' 261 | ]), "\n"; 262 | 263 | return; 264 | } 265 | } 266 | 267 | __halt_compiler(); 268 | Compile and visit languages with grammars. 269 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 3.17.08.08 2 | 3 | ## Fixes 4 | 5 | * `llk/parser` Use current token if no error token. (Kirill Nesmeyanov, 2017-08-08T09:35:08+02:00) 6 | # 3.17.01.10 7 | 8 | * Quality: Fix CS. (Ivan Enderlin, 2017-01-10T10:25:16+01:00) 9 | * Quality: Happy new year! (Ivan Enderlin, 2017-01-09T15:21:06+01:00) 10 | * Test: Add the `Decorrelated` interface. (Ivan Enderlin, 2016-10-25T07:49:56+02:00) 11 | * Documentation: Add a Research papers Section. (Ivan Enderlin, 2016-10-24T16:07:32+02:00) 12 | 13 | # 3.16.10.24 14 | 15 | * Documentation: New `README.md` file. (Ivan Enderlin, 2016-10-17T20:49:41+02:00) 16 | * Documentation: Fix `docs` and `source` links. (Ivan Enderlin, 2016-10-05T20:22:48+02:00) 17 | * Documentation: Update `support` properties. (Ivan Enderlin, 2016-10-05T15:54:31+02:00) 18 | * Documentation: Add the Research papers Section. (Ivan Enderlin, 2016-08-15T14:13:30+02:00) 19 | * Documentation: Update possibly generated data. (Ivan Enderlin, 2016-08-15T14:05:42+02:00) 20 | 21 | # 3.16.08.15 22 | 23 | * Quality: Run `hoa devtools:cs`. (Ivan Enderlin, 2016-08-15T11:23:34+02:00) 24 | * Documentation: Refresh some phrasings. (Ivan Enderlin, 2016-08-15T11:12:55+02:00) 25 | * Documentation: Update API documentation. (Ivan Enderlin, 2016-08-15T11:06:36+02:00) 26 | * Test: Write `Hoa\Compiler\Llk\Llk` test suite. (Ivan Enderlin, 2016-08-14T17:40:38+02:00) 27 | * Llk: Update exception messages. (Ivan Enderlin, 2016-08-14T18:00:02+02:00) 28 | * Llk: PP parser only accepts horizontal spaces… (Ivan Enderlin, 2016-08-14T17:42:07+02:00) 29 | * Llk: Correctly order and merge skip tokens. (Ivan Enderlin, 2016-08-14T17:24:52+02:00) 30 | * Llk: The whole class must be abstract. (Ivan Enderlin, 2016-08-14T17:24:24+02:00) 31 | * Test: Write `…r\Llk\Sampler\Exception` test suite. (Ivan Enderlin, 2016-08-14T16:45:12+02:00) 32 | * Test: Write `…ption\UnrecognizedToken` test suite. (Ivan Enderlin, 2016-08-14T16:43:06+02:00) 33 | * Test: Write `…ception\UnexpectedToken` test suite. (Ivan Enderlin, 2016-08-14T16:42:52+02:00) 34 | * Test: Write `…Compiler\Exception\Rule` test suite. (Ivan Enderlin, 2016-08-14T16:42:43+02:00) 35 | * Test: Write `…ompiler\Exception\Lexer` test suite. (Ivan Enderlin, 2016-08-14T16:42:31+02:00) 36 | * Test: Write `…\Exception\IllegalToken` test suite. (Ivan Enderlin, 2016-08-14T16:42:07+02:00) 37 | * Test: Write `…lStateHasNotBeenReached` test suite. (Ivan Enderlin, 2016-08-14T16:41:49+02:00) 38 | * Test: Write `…ler\Exception\Exception` test suite. (Ivan Enderlin, 2016-08-14T16:41:19+02:00) 39 | * Test: Write `…piler\Llk\Rule\Analyzer` test suite. (Ivan Enderlin, 2016-08-14T13:18:39+02:00) 40 | * Analyzer: Fix current rule name. (Ivan Enderlin, 2016-08-14T16:27:20+02:00) 41 | * Analyzer: More detailed exception messages. (Ivan Enderlin, 2016-08-14T16:27:01+02:00) 42 | * PP: Sync `node` token with the analyzer. (Ivan Enderlin, 2016-08-14T16:26:34+02:00) 43 | * Rule: Fix an exception message in the analyzer. (Ivan Enderlin, 2016-08-12T18:04:39+02:00) 44 | * Rule: Update API documentation. (Ivan Enderlin, 2016-08-12T18:04:30+02:00) 45 | * PP: A named token can no longer be unified. (Ivan Enderlin, 2016-08-12T18:03:48+02:00) 46 | * Quality: Fix CS. (Ivan Enderlin, 2016-08-12T17:08:59+02:00) 47 | * Test: Write `…ler\Llk\Rule\Invocation` test suite. (Ivan Enderlin, 2016-08-12T17:06:15+02:00) 48 | * Test: Add test case for the `isInfinite` method. (Ivan Enderlin, 2016-08-12T08:06:15+02:00) 49 | * Rule: Restore infinite max in a repetition. (Ivan Enderlin, 2016-08-12T08:05:19+02:00) 50 | * Quality: Fix CS and API documentation. (Ivan Enderlin, 2016-08-12T07:55:24+02:00) 51 | * Rule: Cast and bound min and max in a repetition. (Ivan Enderlin, 2016-08-12T07:54:43+02:00) 52 | * Test: Write `…ler\Llk\Rule\Repetition` test suite. (Ivan Enderlin, 2016-08-12T07:51:48+02:00) 53 | * Test: Write `…\Compiler\Llk\Rule\Rule` test suite. (Ivan Enderlin, 2016-08-09T08:54:20+02:00) 54 | * Test: Write `…Compiler\Llk\Rule\Token` test suite. (Ivan Enderlin, 2016-08-09T08:19:28+02:00) 55 | * TreeNode: Value default value must be `null`. (Ivan Enderlin, 2016-08-09T08:02:43+02:00) 56 | * TreeNode: Avoid undefined child access. (Ivan Enderlin, 2016-08-08T17:32:09+02:00) 57 | * TreeNode: Avoid undefined token value access. (Ivan Enderlin, 2016-08-08T17:31:28+02:00) 58 | * TreeNode: Force the value to be an array. (Ivan Enderlin, 2016-08-08T17:31:12+02:00) 59 | * Test: JSON soundness test suite has changed. (Ivan Enderlin, 2016-08-08T17:30:20+02:00) 60 | * Test: Use `::class` instead of string classnames. (Ivan Enderlin, 2016-08-08T17:25:28+02:00) 61 | * Test: Fix namespaces. (Ivan Enderlin, 2016-08-08T17:25:16+02:00) 62 | * Test: Write `…Compiler\Llk\Rule\Entry` test suite. (Ivan Enderlin, 2016-08-08T17:22:26+02:00) 63 | * Test: Write `…Compiler\Llk\Rule\Ekzit` test suite. (Ivan Enderlin, 2016-08-08T17:22:03+02:00) 64 | * Test: Write `…\Llk\Rule\Concatenation` test suite. (Ivan Enderlin, 2016-08-08T17:21:45+02:00) 65 | * Test: Write `…ompiler\Llk\Rule\Choice` test suite. (Ivan Enderlin, 2016-08-08T17:21:07+02:00) 66 | * Test: Write `…a\Compiler\Llk\TreeNode` test suite. (Ivan Enderlin, 2016-08-08T17:14:07+02:00) 67 | * Test: Update test cases about Unicode support. (Ivan Enderlin, 2016-08-08T08:14:04+02:00) 68 | * Test: Documentations are integration test suites. (Ivan Enderlin, 2016-08-08T08:03:11+02:00) 69 | * Test: Soundness is an integration test suite. (Ivan Enderlin, 2016-08-08T08:01:45+02:00) 70 | * Test: Write `Hoa\Compiler\Llk\Lexer` test suite. (Ivan Enderlin, 2016-07-15T19:37:06+02:00) 71 | * Test: Fix test suite name. (Ivan Enderlin, 2016-07-15T19:36:55+02:00) 72 | * Documentation: Update API and exception message. (Ivan Enderlin, 2016-07-15T19:36:40+02:00) 73 | * Rule: Use `is_int` to detect if transitional. (Ivan Enderlin, 2016-02-22T12:08:14+01:00) 74 | * Parser: Remove calls to `getCurrentToken` method. (Ivan Enderlin, 2016-02-22T11:58:19+01:00) 75 | * Parser: Cut backtrack if k is reached. (Ivan Enderlin, 2016-02-22T11:28:54+01:00) 76 | * Parser: Simplify a return condition. (Ivan Enderlin, 2016-02-22T11:07:57+01:00) 77 | * Llk: Save pragmas when saving the parser. (Ivan Enderlin, 2016-02-22T10:24:16+01:00) 78 | * Llk: Add the `parser.lookahead` pragma. (Ivan Enderlin, 2016-02-22T10:23:37+01:00) 79 | * Llk: Change pragma `unicode` for `lexer.unicode`. (Ivan Enderlin, 2016-02-22T09:35:39+01:00) 80 | * Llk: Implement pragmas. (Ivan Enderlin, 2016-02-05T17:03:53+01:00) 81 | * Llk: Introduce the “save” parser! (Ivan Enderlin, 2016-01-25T14:26:48+01:00) 82 | * Llk: Token rule can be constructured as kept. (Ivan Enderlin, 2016-01-25T14:25:51+01:00) 83 | * Grammar: Reduce memory with transitional rules. (Ivan Enderlin, 2016-01-25T11:02:35+01:00) 84 | * Grammar: Reduce calls. (Ivan Enderlin, 2016-01-25T10:59:28+01:00) 85 | * Quality: Clean internal API. (Ivan Enderlin, 2016-01-23T09:30:39+01:00) 86 | * Quality: Fix CS. (Ivan Enderlin, 2016-01-22T17:11:26+01:00) 87 | * Parser: Use the lexer as an iterator. (Ivan Enderlin, 2016-01-22T08:43:39+01:00) 88 | * Grammar: Use the lexer as an iterator. (Ivan Enderlin, 2016-01-22T08:43:15+01:00) 89 | * Lexer: Transform it into an iterator (generator). (Ivan Enderlin, 2016-01-22T08:42:42+01:00) 90 | * PP: Remove the author. (Ivan Enderlin, 2016-01-17T14:16:54+01:00) 91 | * Update copyright. (Ivan Enderlin, 2016-01-17T14:15:44+01:00) 92 | 93 | # 3.16.01.14 94 | 95 | * Composer: New stable libraries. (Ivan Enderlin, 2016-01-14T21:44:41+01:00) 96 | 97 | # 3.16.01.11 98 | 99 | * Quality: Drop PHP5.4. (Ivan Enderlin, 2016-01-11T09:15:26+01:00) 100 | * Quality: Run devtools:cs. (Ivan Enderlin, 2016-01-09T08:58:15+01:00) 101 | * Core: Remove `Hoa\Core`. (Ivan Enderlin, 2016-01-09T08:03:04+01:00) 102 | * Consistency: Update `dnew` call. (Ivan Enderlin, 2015-12-09T16:44:21+01:00) 103 | * Consistency: Remove a call to `_define`. (Ivan Enderlin, 2015-12-08T22:27:56+01:00) 104 | * Consistency: Use `Hoa\Consistency`. (Ivan Enderlin, 2015-12-08T10:56:44+01:00) 105 | * Exception: Use `Hoa\Exception`. (Ivan Enderlin, 2015-11-20T07:15:35+01:00) 106 | * Documentation: Format API. (Ivan Enderlin, 2015-12-16T07:38:23+01:00) 107 | * Fix Llk\Llk::parsePP unrecognized instructions exception (lovenunu, 2015-12-14T16:48:09+01:00) 108 | 109 | # 2.15.10.29 110 | 111 | * Test: Specify file type with `hoa://Test/Vfs`. (Ivan Enderlin, 2015-10-29T22:07:56+01:00) 112 | 113 | # 2.15.10.21 114 | 115 | * Fix CS. (Ivan Enderlin, 2015-10-14T17:23:16+02:00) 116 | 117 | # 2.15.08.25 118 | 119 | * Fix phpDoc. (Metalaka, 2015-08-12T20:35:36+02:00) 120 | * Add skip token generation. (Metalaka, 2014-08-25T17:09:56+02:00) 121 | * Add a `.gitignore` file. (Stéphane HULARD, 2015-08-03T11:22:38+02:00) 122 | 123 | # 2.15.05.29 124 | 125 | * Move to PSR-1 and PSR-2. (Ivan Enderlin, 2015-05-04T20:11:09+02:00) 126 | 127 | # 2.15.02.17 128 | 129 | * Add the CHANGELOG.md file. (Ivan Enderlin, 2015-02-17T09:33:56+01:00) 130 | * Update schemas in the documentation. (Ivan Enderlin, 2015-01-23T19:23:04+01:00) 131 | * Happy new year! (Ivan Enderlin, 2015-01-05T14:20:42+01:00) 132 | 133 | # 2.14.12.10 134 | 135 | * Move to PSR-4. (Ivan Enderlin, 2014-12-09T13:40:43+01:00) 136 | 137 | # 2.14.11.26 138 | 139 | * Require `hoa/test`. (Alexis von Glasow, 2014-11-25T23:17:38+01:00) 140 | * `Hoa\Visitor` has been finalized, update `composer.json`. (Ivan Enderlin, 2014-11-15T22:20:21+01:00) 141 | * Fix a bug in the unification. (Ivan Enderlin, 2014-11-11T15:11:18+01:00) 142 | 143 | # 2.14.11.09 144 | 145 | * Use `hoa/iterator` `~1.0`. (Ivan Enderlin, 2014-11-09T10:58:53+01:00) 146 | * Move test into `Hoa\Json`. (Ivan Enderlin, 2014-10-04T12:14:45+02:00) 147 | * `Hoa\Json` has been released. (Ivan Enderlin, 2014-10-03T22:23:12+02:00) 148 | * Add the `getCompiler` method. (Ivan Enderlin, 2014-09-29T09:52:51+02:00) 149 | * Check soundness of other samplers. (Ivan Enderlin, 2014-09-29T09:46:47+02:00) 150 | * `Hoa\Regex` is required. (Ivan Enderlin, 2014-09-28T22:14:19+02:00) 151 | * Add soundness test. (Ivan Enderlin, 2014-09-28T22:09:20+02:00) 152 | * Fix API documentation. (Ivan Enderlin, 2014-09-26T22:35:10+02:00) 153 | * Remove `from`/`import` and update to PHP5.4. (Ivan Enderlin, 2014-09-26T11:14:35+02:00) 154 | * Update documentation. (Ivan Enderlin, 2014-09-26T10:58:43+02:00) 155 | 156 | # 2.14.09.23 157 | 158 | * Add `branch-alias`. (Stéphane PY, 2014-09-23T11:50:46+02:00) 159 | 160 | # 2.14.09.17 161 | 162 | * Drop PHP5.3. (Ivan Enderlin, 2014-09-17T17:06:10+02:00) 163 | * Add the installation section. (Ivan Enderlin, 2014-09-17T17:05:43+02:00) 164 | 165 | (first snapshot) 166 | -------------------------------------------------------------------------------- /Exception/Exception.php: -------------------------------------------------------------------------------- 1 | line = $line; 72 | $this->column = $column; 73 | 74 | return; 75 | } 76 | 77 | /** 78 | * Get column. 79 | * 80 | * @return int 81 | */ 82 | public function getColumn() 83 | { 84 | return $this->column; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Exception/Lexer.php: -------------------------------------------------------------------------------- 1 | line = $line; 72 | $this->column = $column; 73 | 74 | return; 75 | } 76 | 77 | /** 78 | * Get column. 79 | * 80 | * @return int 81 | */ 82 | public function getColumn() 83 | { 84 | return $this->column; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Llk/Lexer.php: -------------------------------------------------------------------------------- 1 | _pcreOptions .= 'u'; 97 | } 98 | 99 | return; 100 | } 101 | 102 | /** 103 | * Text tokenizer: splits the text in parameter in an ordered array of 104 | * tokens. 105 | * 106 | * @param string $text Text to tokenize. 107 | * @param array $tokens Tokens to be returned. 108 | * @return \Generator 109 | * @throws \Hoa\Compiler\Exception\UnrecognizedToken 110 | */ 111 | public function lexMe($text, array $tokens) 112 | { 113 | $this->_text = $text; 114 | $this->_tokens = $tokens; 115 | $this->_nsStack = null; 116 | $offset = 0; 117 | $maxOffset = strlen($this->_text); 118 | $this->_lexerState = 'default'; 119 | $stack = false; 120 | 121 | foreach ($this->_tokens as &$tokens) { 122 | $_tokens = []; 123 | 124 | foreach ($tokens as $fullLexeme => $regex) { 125 | if (false === strpos($fullLexeme, ':')) { 126 | $_tokens[$fullLexeme] = [$regex, null]; 127 | 128 | continue; 129 | } 130 | 131 | list($lexeme, $namespace) = explode(':', $fullLexeme, 2); 132 | 133 | $stack |= ('__shift__' === substr($namespace, 0, 9)); 134 | 135 | unset($tokens[$fullLexeme]); 136 | $_tokens[$lexeme] = [$regex, $namespace]; 137 | } 138 | 139 | $tokens = $_tokens; 140 | } 141 | 142 | if (true == $stack) { 143 | $this->_nsStack = new \SplStack(); 144 | } 145 | 146 | while ($offset < $maxOffset) { 147 | $nextToken = $this->nextToken($offset); 148 | 149 | if (null === $nextToken) { 150 | throw new Compiler\Exception\UnrecognizedToken( 151 | 'Unrecognized token "%s" at line 1 and column %d:' . 152 | "\n" . '%s' . "\n" . 153 | str_repeat(' ', mb_strlen(substr($text, 0, $offset))) . '↑', 154 | 0, 155 | [ 156 | mb_substr(substr($text, $offset), 0, 1), 157 | $offset + 1, 158 | $text 159 | ], 160 | 1, 161 | $offset 162 | ); 163 | } 164 | 165 | if (true === $nextToken['keep']) { 166 | $nextToken['offset'] = $offset; 167 | yield $nextToken; 168 | } 169 | 170 | $offset += strlen($nextToken['value']); 171 | } 172 | 173 | yield [ 174 | 'token' => 'EOF', 175 | 'value' => 'EOF', 176 | 'length' => 0, 177 | 'namespace' => 'default', 178 | 'keep' => true, 179 | 'offset' => $offset 180 | ]; 181 | } 182 | 183 | /** 184 | * Compute the next token recognized at the beginning of the string. 185 | * 186 | * @param int $offset Offset. 187 | * @return array 188 | * @throws \Hoa\Compiler\Exception\Lexer 189 | */ 190 | protected function nextToken($offset) 191 | { 192 | $tokenArray = &$this->_tokens[$this->_lexerState]; 193 | 194 | foreach ($tokenArray as $lexeme => $bucket) { 195 | list($regex, $nextState) = $bucket; 196 | 197 | if (null === $nextState) { 198 | $nextState = $this->_lexerState; 199 | } 200 | 201 | $out = $this->matchLexeme($lexeme, $regex, $offset); 202 | 203 | if (null !== $out) { 204 | $out['namespace'] = $this->_lexerState; 205 | $out['keep'] = 'skip' !== $lexeme; 206 | 207 | if ($nextState !== $this->_lexerState) { 208 | $shift = false; 209 | 210 | if (null !== $this->_nsStack && 211 | 0 !== preg_match('#^__shift__(?:\s*\*\s*(\d+))?$#', $nextState, $matches)) { 212 | $i = isset($matches[1]) ? intval($matches[1]) : 1; 213 | 214 | if ($i > ($c = count($this->_nsStack))) { 215 | throw new Compiler\Exception\Lexer( 216 | 'Cannot shift namespace %d-times, from token ' . 217 | '%s in namespace %s, because the stack ' . 218 | 'contains only %d namespaces.', 219 | 1, 220 | [ 221 | $i, 222 | $lexeme, 223 | $this->_lexerState, 224 | $c 225 | ] 226 | ); 227 | } 228 | 229 | while (1 <= $i--) { 230 | $previousNamespace = $this->_nsStack->pop(); 231 | } 232 | 233 | $nextState = $previousNamespace; 234 | $shift = true; 235 | } 236 | 237 | if (!isset($this->_tokens[$nextState])) { 238 | throw new Compiler\Exception\Lexer( 239 | 'Namespace %s does not exist, called by token %s ' . 240 | 'in namespace %s.', 241 | 2, 242 | [ 243 | $nextState, 244 | $lexeme, 245 | $this->_lexerState 246 | ] 247 | ); 248 | } 249 | 250 | if (null !== $this->_nsStack && false === $shift) { 251 | $this->_nsStack[] = $this->_lexerState; 252 | } 253 | 254 | $this->_lexerState = $nextState; 255 | } 256 | 257 | return $out; 258 | } 259 | } 260 | 261 | return null; 262 | } 263 | 264 | /** 265 | * Check if a given lexeme is matched at the beginning of the text. 266 | * 267 | * @param string $lexeme Name of the lexeme. 268 | * @param string $regex Regular expression describing the lexeme. 269 | * @param int $offset Offset. 270 | * @return array 271 | * @throws \Hoa\Compiler\Exception\Lexer 272 | */ 273 | protected function matchLexeme($lexeme, $regex, $offset) 274 | { 275 | $_regex = str_replace('#', '\#', $regex); 276 | $preg = preg_match( 277 | '#\G(?|' . $_regex . ')#' . $this->_pcreOptions, 278 | $this->_text, 279 | $matches, 280 | 0, 281 | $offset 282 | ); 283 | 284 | if (0 === $preg) { 285 | return null; 286 | } 287 | 288 | if ('' === $matches[0]) { 289 | throw new Compiler\Exception\Lexer( 290 | 'A lexeme must not match an empty value, which is the ' . 291 | 'case of "%s" (%s).', 292 | 3, 293 | [$lexeme, $regex] 294 | ); 295 | } 296 | 297 | return [ 298 | 'token' => $lexeme, 299 | 'value' => $matches[0], 300 | 'length' => mb_strlen($matches[0]) 301 | ]; 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /Llk/Llk.pp: -------------------------------------------------------------------------------- 1 | // 2 | // Hoa 3 | // 4 | // 5 | // @license 6 | // 7 | // New BSD License 8 | // 9 | // Copyright © 2007-2017, Hoa community. All rights reserved. 10 | // 11 | // Redistribution and use in source and binary forms, with or without 12 | // modification, are permitted provided that the following conditions are met: 13 | // * Redistributions of source code must retain the above copyright 14 | // notice, this list of conditions and the following disclaimer. 15 | // * Redistributions in binary form must reproduce the above copyright 16 | // notice, this list of conditions and the following disclaimer in the 17 | // documentation and/or other materials provided with the distribution. 18 | // * Neither the name of the Hoa nor the names of its contributors may be 19 | // used to endorse or promote products derived from this software without 20 | // specific prior written permission. 21 | // 22 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 25 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE 26 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 27 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 28 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 29 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 30 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 31 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | // POSSIBILITY OF SUCH DAMAGE. 33 | // 34 | // Grammar \Hoa\Compiler\Llk. 35 | // 36 | // Provide grammar for the LL(k) parser. 37 | // 38 | // @copyright Copyright © 2007-2017, Hoa community. 39 | // @license New BSD License 40 | // 41 | 42 | 43 | %skip space \s 44 | 45 | %token or \| 46 | %token zero_or_one \? 47 | %token one_or_more \+ 48 | %token zero_or_more \* 49 | %token n_to_m \{[0-9]+,[0-9]+\} 50 | %token zero_to_m \{,[0-9]+\} 51 | %token n_or_more \{[0-9]+,\} 52 | %token exactly_n \{[0-9]+\} 53 | 54 | %token token [a-zA-Z_][a-zA-Z0-9_]* 55 | 56 | %token skipped :: 57 | %token kept_ < 58 | %token _kept > 59 | %token named \(\) 60 | %token node #[a-zA-Z_][a-zA-Z0-9_]*(:[mM])? 61 | 62 | %token capturing_ \( 63 | %token _capturing \) 64 | %token unification_ \[ 65 | %token unification [0-9]+ 66 | %token _unification \] 67 | 68 | #rule: 69 | choice() 70 | 71 | choice: 72 | concatenation() ( ::or:: concatenation() #choice )* 73 | 74 | concatenation: 75 | repetition() ( repetition() #concatenation )* 76 | 77 | repetition: 78 | simple() ( quantifier() #repetition )? ? 79 | 80 | simple: 81 | ::capturing_:: choice() ::_capturing:: 82 | | ::skipped:: ( ::unification_:: ::_unification:: )? 83 | ::skipped:: #skipped 84 | | ::kept_:: ( ::unification_:: ::_unification:: )? 85 | ::_kept:: #kept 86 | | ::named:: 87 | 88 | quantifier: 89 | 90 | | 91 | | 92 | | 93 | | 94 | | 95 | -------------------------------------------------------------------------------- /Llk/Rule/Choice.php: -------------------------------------------------------------------------------- 1 | _rule = $rule; 102 | $this->_data = $data; 103 | $this->_todo = $todo; 104 | $this->_depth = $depth; 105 | $this->_transitional = is_int($rule); 106 | 107 | return; 108 | } 109 | 110 | /** 111 | * Get rule name. 112 | * 113 | * @return string 114 | */ 115 | public function getRule() 116 | { 117 | return $this->_rule; 118 | } 119 | 120 | /** 121 | * Get data. 122 | * 123 | * @return mixed 124 | */ 125 | public function getData() 126 | { 127 | return $this->_data; 128 | } 129 | 130 | /** 131 | * Get todo sequence. 132 | * 133 | * @return array 134 | */ 135 | public function getTodo() 136 | { 137 | return $this->_todo; 138 | } 139 | 140 | /** 141 | * Set depth in trace. 142 | * 143 | * @param int $depth Depth. 144 | * @return int 145 | */ 146 | public function setDepth($depth) 147 | { 148 | $old = $this->_depth; 149 | $this->_depth = $depth; 150 | 151 | return $old; 152 | } 153 | 154 | /** 155 | * Get depth in trace. 156 | * 157 | * @return int 158 | */ 159 | public function getDepth() 160 | { 161 | return $this->_depth; 162 | } 163 | 164 | /** 165 | * Check whether the rule is transitional or not. 166 | * 167 | * @return bool 168 | */ 169 | public function isTransitional() 170 | { 171 | return $this->_transitional; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /Llk/Rule/Repetition.php: -------------------------------------------------------------------------------- 1 | $max) { 84 | throw new Compiler\Exception\Rule( 85 | 'Cannot repeat with a min (%d) greater than max (%d).', 86 | 0, 87 | [$min, $max] 88 | ); 89 | } 90 | 91 | $this->_min = $min; 92 | $this->_max = $max; 93 | 94 | return; 95 | } 96 | 97 | /** 98 | * Get minimum bound. 99 | * 100 | * @return int 101 | */ 102 | public function getMin() 103 | { 104 | return $this->_min; 105 | } 106 | 107 | /** 108 | * Get maximum bound. 109 | * 110 | * @return int 111 | */ 112 | public function getMax() 113 | { 114 | return $this->_max; 115 | } 116 | 117 | /** 118 | * Check whether the maximum repetition is unbounded. 119 | * 120 | * @return bool 121 | */ 122 | public function isInfinite() 123 | { 124 | return -1 === $this->getMax(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Llk/Rule/Rule.php: -------------------------------------------------------------------------------- 1 | setName($name); 120 | $this->setChildren($children); 121 | $this->setNodeId($nodeId); 122 | 123 | return; 124 | } 125 | 126 | /** 127 | * Set rule name. 128 | * 129 | * @param string $name Rule name. 130 | * @return string 131 | */ 132 | public function setName($name) 133 | { 134 | $old = $this->_name; 135 | $this->_name = $name; 136 | 137 | return $old; 138 | } 139 | 140 | /** 141 | * Get rule name. 142 | * 143 | * @return string 144 | */ 145 | public function getName() 146 | { 147 | return $this->_name; 148 | } 149 | 150 | /** 151 | * Set rule's children. 152 | * 153 | * @param mixed $children Children. 154 | * @return mixed 155 | */ 156 | protected function setChildren($children) 157 | { 158 | $old = $this->_children; 159 | $this->_children = $children; 160 | 161 | return $old; 162 | } 163 | 164 | /** 165 | * Get rule's children. 166 | * 167 | * @return mixed 168 | */ 169 | public function getChildren() 170 | { 171 | return $this->_children; 172 | } 173 | 174 | /** 175 | * Set node ID. 176 | * 177 | * @param string $nodeId Node ID. 178 | * @return string 179 | */ 180 | public function setNodeId($nodeId) 181 | { 182 | $old = $this->_nodeId; 183 | 184 | if (false !== $pos = strpos($nodeId, ':')) { 185 | $this->_nodeId = substr($nodeId, 0, $pos); 186 | $this->_nodeOptions = str_split(substr($nodeId, $pos + 1)); 187 | } else { 188 | $this->_nodeId = $nodeId; 189 | $this->_nodeOptions = []; 190 | } 191 | 192 | return $old; 193 | } 194 | 195 | /** 196 | * Get node ID. 197 | * 198 | * @return string 199 | */ 200 | public function getNodeId() 201 | { 202 | return $this->_nodeId; 203 | } 204 | 205 | /** 206 | * Get node options. 207 | * 208 | * @retrun array 209 | */ 210 | public function getNodeOptions() 211 | { 212 | return $this->_nodeOptions; 213 | } 214 | 215 | /** 216 | * Set default ID. 217 | * 218 | * @param string $defaultId Default ID. 219 | * @return string 220 | */ 221 | public function setDefaultId($defaultId) 222 | { 223 | $old = $this->_defaultId; 224 | 225 | if (false !== $pos = strpos($defaultId, ':')) { 226 | $this->_defaultId = substr($defaultId, 0, $pos); 227 | $this->_defaultOptions = str_split(substr($defaultId, $pos + 1)); 228 | } else { 229 | $this->_defaultId = $defaultId; 230 | $this->_defaultOptions = []; 231 | } 232 | 233 | return $old; 234 | } 235 | 236 | /** 237 | * Get default ID. 238 | * 239 | * @return string 240 | */ 241 | public function getDefaultId() 242 | { 243 | return $this->_defaultId; 244 | } 245 | 246 | /** 247 | * Get default options. 248 | * 249 | * @return array 250 | */ 251 | public function getDefaultOptions() 252 | { 253 | return $this->_defaultOptions; 254 | } 255 | 256 | /** 257 | * Set PP representation of the rule. 258 | * 259 | * @param string $pp PP representation. 260 | * @return string 261 | */ 262 | public function setPPRepresentation($pp) 263 | { 264 | $old = $this->_pp; 265 | $this->_pp = $pp; 266 | $this->_transitional = false; 267 | 268 | return $old; 269 | } 270 | 271 | /** 272 | * Get PP representation of the rule. 273 | * 274 | * @return string 275 | */ 276 | public function getPPRepresentation() 277 | { 278 | return $this->_pp; 279 | } 280 | 281 | /** 282 | * Check whether the rule is transitional or not. 283 | * 284 | * @return bool 285 | */ 286 | public function isTransitional() 287 | { 288 | return $this->_transitional; 289 | } 290 | } 291 | 292 | /** 293 | * Flex entity. 294 | */ 295 | Consistency::flexEntity('Hoa\Compiler\Llk\Rule\Rule'); 296 | -------------------------------------------------------------------------------- /Llk/Rule/Token.php: -------------------------------------------------------------------------------- 1 | _tokenName = $tokenName; 134 | $this->_unification = $unification; 135 | $this->setKept($kept); 136 | 137 | return; 138 | } 139 | 140 | /** 141 | * Get token name. 142 | * 143 | * @return string 144 | */ 145 | public function getTokenName() 146 | { 147 | return $this->_tokenName; 148 | } 149 | 150 | /** 151 | * Set token namespace. 152 | * 153 | * @param string $namespace Namespace. 154 | * @return string 155 | */ 156 | public function setNamespace($namespace) 157 | { 158 | $old = $this->_namespace; 159 | $this->_namespace = $namespace; 160 | 161 | return $old; 162 | } 163 | 164 | /** 165 | * Get token namespace. 166 | * 167 | * @return string 168 | */ 169 | public function getNamespace() 170 | { 171 | return $this->_namespace; 172 | } 173 | 174 | /** 175 | * Set representation. 176 | * 177 | * @param string $regex Representation. 178 | * @return string 179 | */ 180 | public function setRepresentation($regex) 181 | { 182 | $old = $this->_regex; 183 | $this->_regex = $regex; 184 | 185 | return $old; 186 | } 187 | 188 | /** 189 | * Get token representation. 190 | * 191 | * @return string 192 | */ 193 | public function getRepresentation() 194 | { 195 | return $this->_regex; 196 | } 197 | 198 | /** 199 | * Get AST of the token representation. 200 | * 201 | * @return \Hoa\Compiler\Llk\TreeNode 202 | */ 203 | public function getAST() 204 | { 205 | if (null === static::$_regexCompiler) { 206 | $stream = new File\Read('hoa://Library/Regex/Grammar.pp'); 207 | $stream->rewind(); 208 | 209 | static::$_regexCompiler = Compiler\Llk::load($stream); 210 | } 211 | 212 | if (null === $this->_ast) { 213 | $this->_ast = static::$_regexCompiler->parse( 214 | $this->getRepresentation() 215 | ); 216 | } 217 | 218 | return $this->_ast; 219 | } 220 | 221 | /** 222 | * Set token value. 223 | * 224 | * @param string $value Value. 225 | * @return string 226 | */ 227 | public function setValue($value) 228 | { 229 | $old = $this->_value; 230 | $this->_value = $value; 231 | 232 | return $old; 233 | } 234 | 235 | /** 236 | * Get token value. 237 | * 238 | * @return string 239 | */ 240 | public function getValue() 241 | { 242 | return $this->_value; 243 | } 244 | 245 | /** 246 | * Set token offset. 247 | * 248 | * @param int $offset Offset. 249 | * @return int 250 | */ 251 | public function setOffset($offset) 252 | { 253 | $old = $this->_offset; 254 | $this->_offset = $offset; 255 | 256 | return $old; 257 | } 258 | 259 | /** 260 | * Get token offset. 261 | * 262 | * @return int 263 | */ 264 | public function getOffset() 265 | { 266 | return $this->_offset; 267 | } 268 | 269 | /** 270 | * Set whether the token is kept or not in the AST. 271 | * 272 | * @param bool $kept Kept. 273 | * @return bool 274 | */ 275 | public function setKept($kept) 276 | { 277 | $old = $this->_kept; 278 | $this->_kept = $kept; 279 | 280 | return $old; 281 | } 282 | 283 | /** 284 | * Check whether the token is kept in the AST or not. 285 | * 286 | * @return bool 287 | */ 288 | public function isKept() 289 | { 290 | return $this->_kept; 291 | } 292 | 293 | /** 294 | * Get unification index. 295 | * 296 | * @return int 297 | */ 298 | public function getUnificationIndex() 299 | { 300 | return $this->_unification; 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /Llk/Sampler/BoundedExhaustive.php: -------------------------------------------------------------------------------- 1 | setLength($length); 107 | 108 | return; 109 | } 110 | 111 | /** 112 | * Get the current iterator value. 113 | * 114 | * @return string 115 | */ 116 | public function current() 117 | { 118 | return $this->_current; 119 | } 120 | 121 | /** 122 | * Get the current iterator key. 123 | * 124 | * @return int 125 | */ 126 | public function key() 127 | { 128 | return $this->_key; 129 | } 130 | 131 | /** 132 | * Useless here. 133 | * 134 | * @return void 135 | */ 136 | public function next() 137 | { 138 | return; 139 | } 140 | 141 | /** 142 | * Rewind the internal iterator pointer. 143 | * 144 | * @return void 145 | */ 146 | public function rewind() 147 | { 148 | $ruleName = $this->_rootRuleName; 149 | $this->_current = null; 150 | $this->_key = -1; 151 | $this->_trace = []; 152 | $handle = new Compiler\Llk\Rule\Ekzit($ruleName, 0); 153 | $this->_todo = [ 154 | $handle, 155 | new Compiler\Llk\Rule\Entry($ruleName, 0, [$handle]) 156 | ]; 157 | 158 | return; 159 | } 160 | 161 | /** 162 | * Compute the current iterator value, i.e. generate a new solution. 163 | * 164 | * @return bool 165 | */ 166 | public function valid() 167 | { 168 | if (false === $this->unfold()) { 169 | return false; 170 | } 171 | 172 | $handle = null; 173 | 174 | foreach ($this->_trace as $trace) { 175 | if ($trace instanceof Compiler\Llk\Rule\Token) { 176 | $handle .= $this->generateToken($trace); 177 | } 178 | } 179 | 180 | ++$this->_key; 181 | $this->_current = $handle; 182 | 183 | return $this->backtrack(); 184 | } 185 | 186 | /** 187 | * Unfold rules from the todo stack. 188 | * 189 | * @return bool 190 | */ 191 | protected function unfold() 192 | { 193 | while (0 < count($this->_todo)) { 194 | $pop = array_pop($this->_todo); 195 | 196 | if ($pop instanceof Compiler\Llk\Rule\Ekzit) { 197 | $this->_trace[] = $pop; 198 | } else { 199 | $ruleName = $pop->getRule(); 200 | $next = $pop->getData(); 201 | $rule = $this->_rules[$ruleName]; 202 | $out = $this->boundedExhaustive($rule, $next); 203 | 204 | if (true !== $out && true !== $this->backtrack()) { 205 | return false; 206 | } 207 | } 208 | } 209 | 210 | return true; 211 | } 212 | 213 | /** 214 | * The bounded-exhaustive algorithm. 215 | * 216 | * @param \Hoa\Compiler\Llk\Rule $rule Rule to cover. 217 | * @param int $next Next rule. 218 | * @return bool 219 | */ 220 | protected function boundedExhaustive(Compiler\Llk\Rule $rule, $next) 221 | { 222 | $children = $rule->getChildren(); 223 | 224 | if ($rule instanceof Compiler\Llk\Rule\Repetition) { 225 | if (0 === $next) { 226 | $this->_trace[] = new Compiler\Llk\Rule\Entry( 227 | $rule->getName(), 228 | $rule->getMin() 229 | ); 230 | 231 | array_pop($this->_todo); 232 | $this->_todo[] = new Compiler\Llk\Rule\Ekzit( 233 | $rule->getName(), 234 | $rule->getMin(), 235 | $this->_todo 236 | ); 237 | 238 | for ($i = 0, $min = $rule->getMin(); $i < $min; ++$i) { 239 | $this->_todo[] = new Compiler\Llk\Rule\Ekzit( 240 | $children, 241 | 0 242 | ); 243 | $this->_todo[] = new Compiler\Llk\Rule\Entry( 244 | $children, 245 | 0 246 | ); 247 | } 248 | } else { 249 | $nbToken = 0; 250 | 251 | foreach ($this->_trace as $trace) { 252 | if ($trace instanceof Compiler\Llk\Rule\Token) { 253 | ++$nbToken; 254 | } 255 | } 256 | 257 | $max = $rule->getMax(); 258 | 259 | if (-1 != $max && $next > $max) { 260 | return false; 261 | } 262 | 263 | $this->_todo[] = new Compiler\Llk\Rule\Ekzit( 264 | $rule->getName(), 265 | $next, 266 | $this->_todo 267 | ); 268 | $this->_todo[] = new Compiler\Llk\Rule\Ekzit($children, 0); 269 | $this->_todo[] = new Compiler\Llk\Rule\Entry($children, 0); 270 | } 271 | 272 | return true; 273 | } elseif ($rule instanceof Compiler\Llk\Rule\Choice) { 274 | if (count($children) <= $next) { 275 | return false; 276 | } 277 | 278 | $this->_trace[] = new Compiler\Llk\Rule\Entry( 279 | $rule->getName(), 280 | $next, 281 | $this->_todo 282 | ); 283 | $nextRule = $children[$next]; 284 | $this->_todo[] = new Compiler\Llk\Rule\Ekzit($nextRule, 0); 285 | $this->_todo[] = new Compiler\Llk\Rule\Entry($nextRule, 0); 286 | 287 | return true; 288 | } elseif ($rule instanceof Compiler\Llk\Rule\Concatenation) { 289 | $this->_trace[] = new Compiler\Llk\Rule\Entry( 290 | $rule->getName(), 291 | $next 292 | ); 293 | 294 | for ($i = count($children) - 1; $i >= 0; --$i) { 295 | $nextRule = $children[$i]; 296 | $this->_todo[] = new Compiler\Llk\Rule\Ekzit($nextRule, 0); 297 | $this->_todo[] = new Compiler\Llk\Rule\Entry($nextRule, 0); 298 | } 299 | 300 | return true; 301 | } elseif ($rule instanceof Compiler\Llk\Rule\Token) { 302 | $nbToken = 0; 303 | 304 | foreach ($this->_trace as $trace) { 305 | if ($trace instanceof Compiler\Llk\Rule\Token) { 306 | ++$nbToken; 307 | } 308 | } 309 | 310 | if ($nbToken >= $this->getLength()) { 311 | return false; 312 | } 313 | 314 | $this->_trace[] = $rule; 315 | array_pop($this->_todo); 316 | 317 | return true; 318 | } 319 | 320 | return false; 321 | } 322 | 323 | /** 324 | * Backtrack to the previous choice-point. 325 | * 326 | * @return bool 327 | */ 328 | protected function backtrack() 329 | { 330 | $found = false; 331 | 332 | do { 333 | $last = array_pop($this->_trace); 334 | 335 | if ($last instanceof Compiler\Llk\Rule\Entry) { 336 | $rule = $this->_rules[$last->getRule()]; 337 | $found = $rule instanceof Compiler\Llk\Rule\Choice; 338 | } elseif ($last instanceof Compiler\Llk\Rule\Ekzit) { 339 | $rule = $this->_rules[$last->getRule()]; 340 | $found = $rule instanceof Compiler\Llk\Rule\Repetition; 341 | } 342 | } while (0 < count($this->_trace) && false === $found); 343 | 344 | if (false === $found) { 345 | return false; 346 | } 347 | 348 | $rule = $last->getRule(); 349 | $next = $last->getData() + 1; 350 | $this->_todo = $last->getTodo(); 351 | $this->_todo[] = new Compiler\Llk\Rule\Entry( 352 | $rule, 353 | $next, 354 | $this->_todo 355 | ); 356 | 357 | return true; 358 | } 359 | 360 | /** 361 | * Set upper-bound, the maximum data length. 362 | * 363 | * @param int $length Length. 364 | * @return int 365 | * @throws \Hoa\Compiler\Exception 366 | */ 367 | public function setLength($length) 368 | { 369 | if (0 >= $length) { 370 | throw new Exception( 371 | 'Length must be greater than 0, given %d.', 372 | 0, 373 | $length 374 | ); 375 | } 376 | 377 | $old = $this->_length; 378 | $this->_length = $length; 379 | 380 | return $old; 381 | } 382 | 383 | /** 384 | * Get upper-bound. 385 | * 386 | * @return int 387 | */ 388 | public function getLength() 389 | { 390 | return $this->_length; 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /Llk/Sampler/Exception.php: -------------------------------------------------------------------------------- 1 | _compiler = $compiler; 114 | $this->_tokens = $compiler->getTokens(); 115 | $this->_rules = $compiler->getRules(); 116 | $this->_tokenSampler = $tokenSampler; 117 | $this->_rootRuleName = $compiler->getRootRule(); 118 | 119 | return; 120 | } 121 | 122 | /** 123 | * Get compiler. 124 | * 125 | * @return \Hoa\Compiler\Llk\Parser 126 | */ 127 | public function getCompiler() 128 | { 129 | return $this->_compiler; 130 | } 131 | 132 | /** 133 | * Get the AST of the current namespace skip token. 134 | * 135 | * @return \Hoa\Compiler\Llk\TreeNode 136 | */ 137 | protected function getSkipTokenAST() 138 | { 139 | if (!isset($this->_skipTokenAST[$this->_currentNamespace])) { 140 | $token = new Compiler\Llk\Rule\Token( 141 | -1, 142 | 'skip', 143 | null, 144 | -1 145 | ); 146 | 147 | $token->setRepresentation( 148 | $this->_tokens[$this->_currentNamespace]['skip'] 149 | ); 150 | 151 | $this->_skipTokenAST[$this->_currentNamespace] = $token->getAST(); 152 | } 153 | 154 | return $this->_skipTokenAST[$this->_currentNamespace]; 155 | } 156 | 157 | /** 158 | * Complete a token (namespace and representation). 159 | * It returns the next namespace. 160 | * 161 | * @param \Hoa\Compiler\Llk\Rule\Token $token Token. 162 | * @return string 163 | */ 164 | protected function completeToken(Compiler\Llk\Rule\Token $token) 165 | { 166 | if (null !== $token->getRepresentation()) { 167 | return $this->_currentNamespace; 168 | } 169 | 170 | $name = $token->getTokenName(); 171 | $token->setNamespace($this->_currentNamespace); 172 | $toNamespace = $this->_currentNamespace; 173 | 174 | if (isset($this->_tokens[$this->_currentNamespace][$name])) { 175 | $token->setRepresentation( 176 | $this->_tokens[$this->_currentNamespace][$name] 177 | ); 178 | } else { 179 | foreach ($this->_tokens[$this->_currentNamespace] as $_name => $regex) { 180 | if (false === strpos($_name, ':')) { 181 | continue; 182 | } 183 | 184 | list($_name, $toNamespace) = explode(':', $_name, 2); 185 | 186 | if ($_name === $name) { 187 | break; 188 | } 189 | } 190 | 191 | $token->setRepresentation($regex); 192 | } 193 | 194 | return $toNamespace; 195 | } 196 | 197 | /** 198 | * Set current token namespace. 199 | * 200 | * @param string $namespace Token namespace. 201 | * @return string 202 | */ 203 | protected function setCurrentNamespace($namespace) 204 | { 205 | $old = $this->_currentNamespace; 206 | $this->_currentNamespace = $namespace; 207 | 208 | return $old; 209 | } 210 | 211 | /** 212 | * Generate a token value. 213 | * Complete and set next token namespace. 214 | * 215 | * @param \Hoa\Compiler\Llk\Rule\Token $token Token. 216 | * @return string 217 | */ 218 | protected function generateToken(Compiler\Llk\Rule\Token $token) 219 | { 220 | $toNamespace = $this->completeToken($token); 221 | $this->setCurrentNamespace($toNamespace); 222 | 223 | $out = $this->_tokenSampler->visit($token->getAST()); 224 | 225 | if (isset($this->_tokens[$this->_currentNamespace]['skip'])) { 226 | $out .= $this->_tokenSampler->visit($this->getSkipTokenAST()); 227 | } 228 | 229 | return $out; 230 | } 231 | } 232 | 233 | /** 234 | * Flex entity. 235 | */ 236 | Consistency::flexEntity('Hoa\Compiler\Llk\Sampler\Sampler'); 237 | -------------------------------------------------------------------------------- /Llk/Sampler/Uniform.php: -------------------------------------------------------------------------------- 1 | _rules as $name => $_) { 87 | $this->_data[$name] = []; 88 | } 89 | 90 | $this->setLength($length); 91 | $this->_sampler = new Math\Sampler\Random(); 92 | 93 | return; 94 | } 95 | 96 | /** 97 | * The random and uniform algorithm. 98 | * 99 | * @param \Hoa\Compiler\Llk\Rule $rule Rule to start. 100 | * @param int $n Size. 101 | * @return string 102 | */ 103 | public function uniform(Compiler\Llk\Rule $rule = null, $n = -1) 104 | { 105 | if (null === $rule && -1 === $n) { 106 | $rule = $this->_rules[$this->_rootRuleName]; 107 | $n = $this->getLength(); 108 | } 109 | 110 | $data = &$this->_data[$rule->getName()][$n]; 111 | $computed = $data['n']; 112 | 113 | if (0 === $n || 0 === $computed) { 114 | return null; 115 | } 116 | 117 | if ($rule instanceof Compiler\Llk\Rule\Choice) { 118 | $children = $rule->getChildren(); 119 | $stat = []; 120 | 121 | foreach ($children as $c => $child) { 122 | $stat[$c] = $this->_data[$child][$n]['n']; 123 | } 124 | 125 | $i = $this->_sampler->getInteger(1, $computed); 126 | 127 | for ($e = 0, $b = $stat[$e], $max = count($stat) - 1; 128 | $e < $max && $i > $b; 129 | $b += $stat[++$e]); 130 | 131 | return $this->uniform($this->_rules[$children[$e]], $n); 132 | } elseif ($rule instanceof Compiler\Llk\Rule\Concatenation) { 133 | $children = $rule->getChildren(); 134 | $out = null; 135 | $Γ = $data['Γ']; 136 | $γ = $Γ[$this->_sampler->getInteger(0, count($Γ) - 1)]; 137 | 138 | foreach ($children as $i => $child) { 139 | $out .= $this->uniform($this->_rules[$child], $γ[$i]); 140 | } 141 | 142 | return $out; 143 | } elseif ($rule instanceof Compiler\Llk\Rule\Repetition) { 144 | $out = null; 145 | $stat = &$data['xy']; 146 | $child = $this->_rules[$rule->getChildren()]; 147 | $b = 0; 148 | $i = $this->_sampler->getInteger(1, $computed); 149 | 150 | foreach ($stat as $α => $st) { 151 | if ($i <= $b += $st['n']) { 152 | break; 153 | } 154 | } 155 | 156 | $Γ = &$st['Γ']; 157 | $γ = &$Γ[$this->_sampler->getInteger(0, count($Γ) - 1)]; 158 | 159 | for ($j = 0; $j < $α; ++$j) { 160 | $out .= $this->uniform($child, $γ[$j]); 161 | } 162 | 163 | return $out; 164 | } elseif ($rule instanceof Compiler\Llk\Rule\Token) { 165 | return $this->generateToken($rule); 166 | } 167 | 168 | return null; 169 | } 170 | 171 | /** 172 | * Recursive method applied to our problematic. 173 | * 174 | * @param \Hoa\Compiler\Llk\Rule $rule Rule to start. 175 | * @param int $n Size. 176 | * @return int 177 | */ 178 | public function count(Compiler\Llk\Rule $rule = null, $n = -1) 179 | { 180 | if (null === $rule || -1 === $n) { 181 | return 0; 182 | } 183 | 184 | $ruleName = $rule->getName(); 185 | 186 | if (isset($this->_data[$ruleName][$n])) { 187 | return $this->_data[$ruleName][$n]['n']; 188 | } 189 | 190 | $this->_data[$ruleName][$n] = ['n' => 0]; 191 | $out = &$this->_data[$ruleName][$n]['n']; 192 | $rule = $this->_rules[$ruleName]; 193 | 194 | if ($rule instanceof Compiler\Llk\Rule\Choice) { 195 | foreach ($rule->getChildren() as $child) { 196 | $out += $this->count($this->_rules[$child], $n); 197 | } 198 | } elseif ($rule instanceof Compiler\Llk\Rule\Concatenation) { 199 | $children = $rule->getChildren(); 200 | $Γ = new Math\Combinatorics\Combination\Gamma( 201 | count($children), 202 | $n 203 | ); 204 | $this->_data[$ruleName][$n]['Γ'] = []; 205 | $handle = &$this->_data[$ruleName][$n]['Γ']; 206 | 207 | foreach ($Γ as $γ) { 208 | $oout = 1; 209 | 210 | foreach ($γ as $α => $_γ) { 211 | $oout *= $this->count($this->_rules[$children[$α]], $_γ); 212 | } 213 | 214 | if (0 !== $oout) { 215 | $handle[] = $γ; 216 | } 217 | 218 | $out += $oout; 219 | } 220 | } elseif ($rule instanceof Compiler\Llk\Rule\Repetition) { 221 | $this->_data[$ruleName][$n]['xy'] = []; 222 | $handle = &$this->_data[$ruleName][$n]['xy']; 223 | $child = $this->_rules[$rule->getChildren()]; 224 | $x = $rule->getMin(); 225 | $y = $rule->getMax(); 226 | 227 | if (-1 === $y) { 228 | $y = $n; 229 | } else { 230 | $y = min($n, $y); 231 | } 232 | 233 | if (0 === $x && $x === $y) { 234 | $out = 1; 235 | } else { 236 | for ($α = $x; $α <= $y; ++$α) { 237 | $ut = 0; 238 | $handle[$α] = ['n' => 0, 'Γ' => []]; 239 | $Γ = new Math\Combinatorics\Combination\Gamma($α, $n); 240 | 241 | foreach ($Γ as $γ) { 242 | $oout = 1; 243 | 244 | foreach ($γ as $β => $_γ) { 245 | $oout *= $this->count($child, $_γ); 246 | } 247 | 248 | if (0 !== $oout) { 249 | $handle[$α]['Γ'][] = $γ; 250 | } 251 | 252 | $ut += $oout; 253 | } 254 | 255 | $handle[$α]['n'] = $ut; 256 | $out += $ut; 257 | } 258 | } 259 | } elseif ($rule instanceof Compiler\Llk\Rule\Token) { 260 | $out = Math\Util::δ($n, 1); 261 | } 262 | 263 | return $out; 264 | } 265 | 266 | /** 267 | * Set upper-bound, the maximum data length. 268 | * 269 | * @param int $length Length. 270 | * @return int 271 | * @throws \Hoa\Compiler\Exception 272 | */ 273 | public function setLength($length) 274 | { 275 | if (0 >= $length) { 276 | throw new Exception( 277 | 'Length must be greater than 0, given %d.', 278 | 0, 279 | $length 280 | ); 281 | } 282 | 283 | $old = $this->_length; 284 | $this->_length = $length; 285 | $this->count( 286 | $this->_compiler->getRule($this->_rootRuleName), 287 | $length 288 | ); 289 | 290 | return $old; 291 | } 292 | 293 | /** 294 | * Get upper-bound. 295 | * 296 | * @return int 297 | */ 298 | public function getLength() 299 | { 300 | return $this->_length; 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /Llk/TreeNode.php: -------------------------------------------------------------------------------- 1 | setId($id); 103 | 104 | if (!empty($value)) { 105 | $this->setValue($value); 106 | } 107 | 108 | $this->setChildren($children); 109 | 110 | if (null !== $parent) { 111 | $this->setParent($parent); 112 | } 113 | 114 | return; 115 | } 116 | 117 | /** 118 | * Set ID. 119 | * 120 | * @param string $id ID. 121 | * @return string 122 | */ 123 | public function setId($id) 124 | { 125 | $old = $this->_id; 126 | $this->_id = $id; 127 | 128 | return $old; 129 | } 130 | 131 | /** 132 | * Get ID. 133 | * 134 | * @return string 135 | */ 136 | public function getId() 137 | { 138 | return $this->_id; 139 | } 140 | 141 | /** 142 | * Set value. 143 | * 144 | * @param array $value Value (token & value). 145 | * @return array 146 | */ 147 | public function setValue(array $value) 148 | { 149 | $old = $this->_value; 150 | $this->_value = $value; 151 | 152 | return $old; 153 | } 154 | 155 | /** 156 | * Get value. 157 | * 158 | * @return array 159 | */ 160 | public function getValue() 161 | { 162 | return $this->_value; 163 | } 164 | 165 | /** 166 | * Get value token. 167 | * 168 | * @return string 169 | */ 170 | public function getValueToken() 171 | { 172 | return 173 | isset($this->_value['token']) 174 | ? $this->_value['token'] 175 | : null; 176 | } 177 | 178 | /** 179 | * Get value value. 180 | * 181 | * @return string 182 | */ 183 | public function getValueValue() 184 | { 185 | return 186 | isset($this->_value['value']) 187 | ? $this->_value['value'] 188 | : null; 189 | } 190 | 191 | /** 192 | * Get token offset. 193 | * 194 | * @return int 195 | */ 196 | public function getOffset() 197 | { 198 | return 199 | isset($this->_value['offset']) 200 | ? $this->_value['offset'] 201 | : 0; 202 | } 203 | 204 | /** 205 | * Check if the node represents a token or not. 206 | * 207 | * @return bool 208 | */ 209 | public function isToken() 210 | { 211 | return !empty($this->_value); 212 | } 213 | 214 | /** 215 | * Prepend a child. 216 | * 217 | * @param \Hoa\Compiler\Llk\TreeNode $child Child. 218 | * @return \Hoa\Compiler\Llk\TreeNode 219 | */ 220 | public function prependChild(TreeNode $child) 221 | { 222 | array_unshift($this->_children, $child); 223 | 224 | return $this; 225 | } 226 | 227 | /** 228 | * Append a child. 229 | * 230 | * @param \Hoa\Compiler\Llk\TreeNode $child Child. 231 | * @return \Hoa\Compiler\Llk\TreeNode 232 | */ 233 | public function appendChild(TreeNode $child) 234 | { 235 | $this->_children[] = $child; 236 | 237 | return $this; 238 | } 239 | 240 | /** 241 | * Set children. 242 | * 243 | * @param array $children Children. 244 | * @return array 245 | */ 246 | public function setChildren(array $children) 247 | { 248 | $old = $this->_children; 249 | $this->_children = $children; 250 | 251 | return $old; 252 | } 253 | 254 | /** 255 | * Get child. 256 | * 257 | * @param int $i Index. 258 | * @return \Hoa\Compiler\Llk\TreeNode 259 | */ 260 | public function getChild($i) 261 | { 262 | return 263 | true === $this->childExists($i) 264 | ? $this->_children[$i] 265 | : null; 266 | } 267 | 268 | /** 269 | * Get children. 270 | * 271 | * @return array 272 | */ 273 | public function getChildren() 274 | { 275 | return $this->_children; 276 | } 277 | 278 | /** 279 | * Get number of children. 280 | * 281 | * @return int 282 | */ 283 | public function getChildrenNumber() 284 | { 285 | return count($this->_children); 286 | } 287 | 288 | /** 289 | * Check if a child exists. 290 | * 291 | * @param int $i Index. 292 | * @return bool 293 | */ 294 | public function childExists($i) 295 | { 296 | return array_key_exists($i, $this->_children); 297 | } 298 | 299 | /** 300 | * Set parent. 301 | * 302 | * @param \Hoa\Compiler\Llk\TreeNode $parent Parent. 303 | * @return \Hoa\Compiler\Llk\TreeNode 304 | */ 305 | public function setParent(TreeNode $parent) 306 | { 307 | $old = $this->_parent; 308 | $this->_parent = $parent; 309 | 310 | return $old; 311 | } 312 | 313 | /** 314 | * Get parent. 315 | * 316 | * @return \Hoa\Compiler\Llk\TreeNode 317 | */ 318 | public function getParent() 319 | { 320 | return $this->_parent; 321 | } 322 | 323 | /** 324 | * Get data. 325 | * 326 | * @return array 327 | */ 328 | public function &getData() 329 | { 330 | return $this->_data; 331 | } 332 | 333 | /** 334 | * Accept a visitor. 335 | * 336 | * @param \Hoa\Visitor\Visit $visitor Visitor. 337 | * @param mixed &$handle Handle (reference). 338 | * @param mixed $eldnah Handle (no reference). 339 | * @return mixed 340 | */ 341 | public function accept( 342 | Visitor\Visit $visitor, 343 | &$handle = null, 344 | $eldnah = null 345 | ) { 346 | return $visitor->visit($this, $handle, $eldnah); 347 | } 348 | 349 | /** 350 | * Remove circular reference to the parent (help the garbage collector). 351 | * 352 | * @return void 353 | */ 354 | public function __destruct() 355 | { 356 | unset($this->_parent); 357 | 358 | return; 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Hoa 3 |

4 | 5 | --- 6 | 7 |

8 | Build status 9 | Code coverage 10 | Packagist 11 | License 12 |

13 |

14 | Hoa is a modular, extensible and 15 | structured set of PHP libraries.
16 | Moreover, Hoa aims at being a bridge between industrial and research worlds. 17 |

18 | 19 | # Hoa\Compiler 20 | 21 | [![Help on IRC](https://img.shields.io/badge/help-%23hoaproject-ff0066.svg)](https://webchat.freenode.net/?channels=#hoaproject) 22 | [![Help on Gitter](https://img.shields.io/badge/help-gitter-ff0066.svg)](https://gitter.im/hoaproject/central) 23 | [![Documentation](https://img.shields.io/badge/documentation-hack_book-ff0066.svg)](https://central.hoa-project.net/Documentation/Library/Compiler) 24 | [![Board](https://img.shields.io/badge/organisation-board-ff0066.svg)](https://waffle.io/hoaproject/compiler) 25 | 26 | This library allows to manipulate LL(1) and LL(k) compiler compilers. A 27 | dedicated grammar description language is provided for the last one: the PP 28 | language. 29 | 30 | [Learn more](https://central.hoa-project.net/Documentation/Library/Compiler). 31 | 32 | ## Installation 33 | 34 | With [Composer](https://getcomposer.org/), to include this library into 35 | your dependencies, you need to 36 | require [`hoa/compiler`](https://packagist.org/packages/hoa/compiler): 37 | 38 | ```sh 39 | $ composer require hoa/compiler '~3.0' 40 | ``` 41 | 42 | For more installation procedures, please read [the Source 43 | page](https://hoa-project.net/Source.html). 44 | 45 | ## Testing 46 | 47 | Before running the test suites, the development dependencies must be installed: 48 | 49 | ```sh 50 | $ composer install 51 | ``` 52 | 53 | Then, to run all the test suites: 54 | 55 | ```sh 56 | $ vendor/bin/hoa test:run 57 | ``` 58 | 59 | For more information, please read the [contributor 60 | guide](https://hoa-project.net/Literature/Contributor/Guide.html). 61 | 62 | ## Quick usage 63 | 64 | As a quick overview, we will look at the PP language and the LL(k) compiler 65 | compiler. 66 | 67 | ### The PP language 68 | 69 | A grammar is constituted by tokens (the units of a word) and rules (please, see 70 | the documentation for an introduction to the language theory). The PP language 71 | declares tokens with the following construction: 72 | 73 | ``` 74 | %token [source_namespace:]name value [-> destination_namespace] 75 | ``` 76 | 77 | The default namespace is `default`. The value of a token is represented by a 78 | [PCRE](http://pcre.org/). We can skip tokens with the `%skip` construction. 79 | 80 | As an example, we will take the *simplified* grammar of the [JSON 81 | language](http://json.org/). The complete grammar is in the 82 | `hoa://Library/Json/Grammar.pp` file. Thus: 83 | 84 | ``` 85 | %skip space \s 86 | // Scalars. 87 | %token true true 88 | %token false false 89 | %token null null 90 | // Strings. 91 | %token quote_ " -> string 92 | %token string:string [^"]+ 93 | %token string:_quote " -> default 94 | // Objects. 95 | %token brace_ { 96 | %token _brace } 97 | // Arrays. 98 | %token bracket_ \[ 99 | %token _bracket \] 100 | // Rest. 101 | %token colon : 102 | %token comma , 103 | %token number \d+ 104 | 105 | value: 106 | | | | string() | object() | array() | number() 107 | 108 | string: 109 | ::quote_:: ::_quote:: 110 | 111 | number: 112 | 113 | 114 | #object: 115 | ::brace_:: pair() ( ::comma:: pair() )* ::_brace:: 116 | 117 | #pair: 118 | string() ::colon:: value() 119 | 120 | #array: 121 | ::bracket_:: value() ( ::comma:: value() )* ::_bracket:: 122 | ``` 123 | 124 | We can see the PP constructions: 125 | 126 | * `rule()` to call a rule; 127 | * `` and `::token::` to declare a token; 128 | * `|` for a disjunction; 129 | * `(…)` to group multiple declarations; 130 | * `e?` to say that `e` is optional; 131 | * `e+` to say that `e` can appear at least 1 time; 132 | * `e*` to say that `e` can appear 0 or many times; 133 | * `e{x,y}` to say that `e` can appear between `x` and `y` times; 134 | * `#node` to create a node the AST (resulting tree); 135 | * `token[i]` to unify tokens value between them. 136 | 137 | Unification is very useful. For example, if we have a token that expresses a 138 | quote (simple or double), we could have: 139 | 140 | ``` 141 | %token quote "|' 142 | %token handle \w+ 143 | 144 | string: 145 | ::quote:: ::quote:: 146 | ``` 147 | 148 | So, the data `"foo"` and `'foo'` will be valid, but also `"foo'` and `'foo"`! To 149 | avoid this, we can add a new constraint on token value by unifying them, thus: 150 | 151 | ``` 152 | string: 153 | ::quote[0]:: ::quote[0]:: 154 | ``` 155 | 156 | All `quote[0]` for the rule instance must have the same value. Another example 157 | is the unification of XML tags name. 158 | 159 | ### LL(k) compiler compiler 160 | 161 | The `Hoa\Compiler\Llk\Llk` class provide helpers to manipulate (load or save) a 162 | compiler. The following code will use the previous grammar to create a compiler, 163 | and we will parse a JSON string. If the parsing succeed, it will produce an AST 164 | (stands for Abstract Syntax Tree) we can visit, for example to dump the AST: 165 | 166 | ```php 167 | // 1. Load grammar. 168 | $compiler = Hoa\Compiler\Llk\Llk::load(new Hoa\File\Read('Json.pp')); 169 | 170 | // 2. Parse a data. 171 | $ast = $compiler->parse('{"foo": true, "bar": [null, 42]}'); 172 | 173 | // 3. Dump the AST. 174 | $dump = new Hoa\Compiler\Visitor\Dump(); 175 | echo $dump->visit($ast); 176 | 177 | /** 178 | * Will output: 179 | * > #object 180 | * > > #pair 181 | * > > > token(string, foo) 182 | * > > > token(true, true) 183 | * > > #pair 184 | * > > > token(string, bar) 185 | * > > > #array 186 | * > > > > token(null, null) 187 | * > > > > token(number, 42) 188 | */ 189 | ``` 190 | 191 | Pretty simple. 192 | 193 | ### Compiler in CLI 194 | 195 | This library proposes a script to parse and apply a visitor on a data with a 196 | specific grammar. Very useful. Moreover, we can use pipe (because 197 | `Hoa\File\Read` —please, see the [`Hoa\File` 198 | library](http://central.hoa-project.net/Resource/Library/File/)— supports `0` as 199 | `stdin`), thus: 200 | 201 | ```sh 202 | $ echo '[1, [1, [2, 3], 5], 8]' | hoa compiler:pp Json.pp 0 --visitor dump 203 | > #array 204 | > > token(number, 1) 205 | > > #array 206 | > > > token(number, 1) 207 | > > > #array 208 | > > > > token(number, 2) 209 | > > > > token(number, 3) 210 | > > > token(number, 5) 211 | > > token(number, 8) 212 | ``` 213 | 214 | You can apply any visitor classes. 215 | 216 | ### Errors 217 | 218 | Errors are well-presented: 219 | 220 | ```sh 221 | $ echo '{"foo" true}' | hoa compiler:pp Json.pp 0 --visitor dump 222 | Uncaught exception (Hoa\Compiler\Exception\UnexpectedToken): 223 | Hoa\Compiler\Llk\Parser::parse(): (0) Unexpected token "true" (true) at line 1 224 | and column 8: 225 | {"foo" true} 226 | ↑ 227 | in hoa://Library/Compiler/Llk/Parser.php at line 1 228 | ``` 229 | 230 | ### Samplers 231 | 232 | Some algorithms are available to generate data based on a grammar. We will give 233 | only one example with the coverage-based generation algorithm that will activate 234 | all branches and tokens in the grammar: 235 | 236 | ```php 237 | $sampler = new Hoa\Compiler\Llk\Sampler\Coverage( 238 | // Grammar. 239 | Hoa\Compiler\Llk\Llk::load(new Hoa\File\Read('Json.pp')), 240 | // Token sampler. 241 | new Hoa\Regex\Visitor\Isotropic(new Hoa\Math\Sampler\Random()) 242 | ); 243 | 244 | foreach ($sampler as $i => $data) { 245 | echo $i, ' => ', $data, "\n"; 246 | } 247 | 248 | /** 249 | * Will output: 250 | * 0 => true 251 | * 1 => {" )o?bz " : null , " %3W) " : [false, 130 , " 6" ] } 252 | * 2 => [{" ny " : true } ] 253 | * 3 => {" Ne;[3 " :[ true , true ] , " th: " : true," C[8} " : true } 254 | */ 255 | ``` 256 | 257 | ## Research papers 258 | 259 | * *Grammar-Based Testing using Realistic Domains in PHP*, 260 | presented at [A-MOST 2012](https://sites.google.com/site/amost2012/) (Montréal, Canada) 261 | ([article](https://hoa-project.net/En/Literature/Research/Amost12.pdf), 262 | [presentation](http://keynote.hoa-project.net/Amost12/EDGB12.pdf), 263 | [details](https://hoa-project.net/En/Event/Amost12.html)). 264 | 265 | ## Documentation 266 | 267 | The 268 | [hack book of `Hoa\Compiler`](https://central.hoa-project.net/Documentation/Library/Compiler) contains 269 | detailed information about how to use this library and how it works. 270 | 271 | To generate the documentation locally, execute the following commands: 272 | 273 | ```sh 274 | $ composer require --dev hoa/devtools 275 | $ vendor/bin/hoa devtools:documentation --open 276 | ``` 277 | 278 | More documentation can be found on the project's website: 279 | [hoa-project.net](https://hoa-project.net/). 280 | 281 | ## Getting help 282 | 283 | There are mainly two ways to get help: 284 | 285 | * On the [`#hoaproject`](https://webchat.freenode.net/?channels=#hoaproject) 286 | IRC channel, 287 | * On the forum at [users.hoa-project.net](https://users.hoa-project.net). 288 | 289 | ## Contribution 290 | 291 | Do you want to contribute? Thanks! A detailed [contributor 292 | guide](https://hoa-project.net/Literature/Contributor/Guide.html) explains 293 | everything you need to know. 294 | 295 | ## License 296 | 297 | Hoa is under the New BSD License (BSD-3-Clause). Please, see 298 | [`LICENSE`](https://hoa-project.net/LICENSE) for details. 299 | -------------------------------------------------------------------------------- /Test/Integration/Documentation.php: -------------------------------------------------------------------------------- 1 | string 63 | %token string:string [^"]+ 64 | %token string:_quote " -> default 65 | // Objects. 66 | %token brace_ { 67 | %token _brace } 68 | // Arrays. 69 | %token bracket_ \[ 70 | %token _bracket \] 71 | // Rest. 72 | %token colon : 73 | %token comma , 74 | %token number \d+ 75 | 76 | value: 77 | | | | string() | object() | array() | number() 78 | 79 | string: 80 | ::quote_:: ::_quote:: 81 | 82 | number: 83 | 84 | 85 | #object: 86 | ::brace_:: pair() ( ::comma:: pair() )* ::_brace:: 87 | 88 | #pair: 89 | string() ::colon:: value() 90 | 91 | #array: 92 | ::bracket_:: value() ( ::comma:: value() )* ::_bracket:: 93 | GRAMMAR; 94 | $_result = << #object 96 | > > #pair 97 | > > > token(string:string, foo) 98 | > > > token(true, true) 99 | > > #pair 100 | > > > token(string:string, bar) 101 | > > > #array 102 | > > > > token(null, null) 103 | > > > > token(number, 42) 104 | 105 | GRAMMAR; 106 | 107 | $this 108 | ->given( 109 | $grammar = new File\ReadWrite('hoa://Test/Vfs/Json.pp?type=file'), 110 | $grammar->writeAll($_grammar), 111 | $compiler = LUT\Llk::load($grammar) 112 | ) 113 | ->when($ast = $compiler->parse('{"foo": true, "bar": [null, 42]}')) 114 | ->then 115 | ->object($ast) 116 | 117 | ->given($dump = new LUT\Visitor\Dump()) 118 | ->when($result = $dump->visit($ast)) 119 | ->then 120 | ->string($result) 121 | ->isEqualTo($_result); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Test/Integration/Llk/Documentation.php: -------------------------------------------------------------------------------- 1 | ::quote:: 61 | GRAMMAR; 62 | 63 | $this 64 | ->given( 65 | $grammar = new File\ReadWrite('hoa://Test/Vfs/WithoutUnification.pp?type=file'), 66 | $grammar->writeAll($_grammar), 67 | $compiler = LUT\Llk::load($grammar) 68 | ) 69 | ->when($result = $compiler->parse('"foo"', null, false)) 70 | ->then 71 | ->boolean($result) 72 | ->isTrue() 73 | 74 | ->when($result = $compiler->parse('\'foo"', null, false)) 75 | ->then 76 | ->boolean($result) 77 | ->isTrue() 78 | 79 | ->when($result = $compiler->parse('"foo\'', null, false)) 80 | ->then 81 | ->boolean($result) 82 | ->isTrue() 83 | 84 | ->when($result = $compiler->parse('\'foo\'', null, false)) 85 | ->then 86 | ->boolean($result) 87 | ->isTrue(); 88 | } 89 | 90 | public function case_unification() 91 | { 92 | $_grammar = << ::quote[0]:: 98 | GRAMMAR; 99 | 100 | $this 101 | ->given( 102 | $grammar = new File\ReadWrite('hoa://Test/Vfs/Unification.pp?type=file'), 103 | $grammar->writeAll($_grammar), 104 | $compiler = LUT\Llk::load($grammar) 105 | ) 106 | ->when($result = $compiler->parse('"foo"', null, false)) 107 | ->then 108 | ->boolean($result) 109 | ->isTrue() 110 | 111 | ->when($result = $compiler->parse('\'foo\'', null, false)) 112 | ->then 113 | ->boolean($result) 114 | ->isTrue() 115 | 116 | ->exception(function () use (&$compiler) { 117 | $compiler->parse('\'foo"', null, false); 118 | }) 119 | ->isInstanceOf(LUT\Exception\UnexpectedToken::class) 120 | 121 | ->exception(function () use (&$compiler) { 122 | $compiler->parse('"foo\'', null, false); 123 | }) 124 | ->isInstanceOf(LUT\Exception\UnexpectedToken::class); 125 | } 126 | 127 | public function case_unification_palindrome() 128 | { 129 | $_grammar = <<given( 138 | $grammar = new File\ReadWrite('hoa://Test/Vfs/Palindrome.pp?type=file'), 139 | $grammar->writeAll($_grammar), 140 | $compiler = LUT\Llk::load($grammar) 141 | ) 142 | ->when($result = $compiler->parse('aa', null, false)) 143 | ->then 144 | ->boolean($result) 145 | ->isTrue() 146 | 147 | ->when($result = $compiler->parse('abba', null, false)) 148 | ->then 149 | ->boolean($result) 150 | ->isTrue() 151 | 152 | ->when($result = $compiler->parse('abccba', null, false)) 153 | ->then 154 | ->boolean($result) 155 | ->isTrue() 156 | 157 | ->when($result = $compiler->parse('abcddcba', null, false)) 158 | ->then 159 | ->boolean($result) 160 | ->isTrue() 161 | 162 | ->exception(function () use (&$compiler) { 163 | $compiler->parse('abcdcba', null, false); 164 | }) 165 | ->isInstanceOf(LUT\Exception\UnexpectedToken::class); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Test/Integration/Llk/Soundness.php: -------------------------------------------------------------------------------- 1 | when($result = new SUT('foo', 0)) 57 | ->then 58 | ->object($result) 59 | ->isInstanceOf(HoaException::class); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Test/Unit/Exception/FinalStateHasNotBeenReached.php: -------------------------------------------------------------------------------- 1 | when($result = new SUT('foo', 0)) 57 | ->then 58 | ->object($result) 59 | ->isInstanceOf(LUT\Exception::class); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Test/Unit/Exception/IllegalToken.php: -------------------------------------------------------------------------------- 1 | given( 57 | $line = 7, 58 | $column = 42 59 | ) 60 | ->when($result = new SUT('foo', 0, 'bar', $line, $column)) 61 | ->then 62 | ->object($result) 63 | ->isInstanceOf(LUT\Exception::class) 64 | ->integer($result->getLine()) 65 | ->isEqualTo($line) 66 | ->integer($result->getColumn()) 67 | ->isEqualTo($column); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Test/Unit/Exception/Lexer.php: -------------------------------------------------------------------------------- 1 | when($result = new SUT('foo', 0)) 57 | ->then 58 | ->object($result) 59 | ->isInstanceOf(LUT\Exception::class); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Test/Unit/Exception/Rule.php: -------------------------------------------------------------------------------- 1 | when($result = new SUT('foo', 0)) 57 | ->then 58 | ->object($result) 59 | ->isInstanceOf(LUT\Exception::class); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Test/Unit/Exception/UnexpectedToken.php: -------------------------------------------------------------------------------- 1 | given( 57 | $line = 7, 58 | $column = 42 59 | ) 60 | ->when($result = new SUT('foo', 0, 'bar', $line, $column)) 61 | ->then 62 | ->object($result) 63 | ->isInstanceOf(LUT\Exception::class) 64 | ->integer($result->getLine()) 65 | ->isEqualTo($line) 66 | ->integer($result->getColumn()) 67 | ->isEqualTo($column); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Test/Unit/Exception/UnrecognizedToken.php: -------------------------------------------------------------------------------- 1 | given( 57 | $line = 7, 58 | $column = 42 59 | ) 60 | ->when($result = new SUT('foo', 0, 'bar', $line, $column)) 61 | ->then 62 | ->object($result) 63 | ->isInstanceOf(LUT\Exception::class) 64 | ->integer($result->getLine()) 65 | ->isEqualTo($line) 66 | ->integer($result->getColumn()) 67 | ->isEqualTo($column); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Test/Unit/Llk/Rule/Choice.php: -------------------------------------------------------------------------------- 1 | when($result = new SUT('foo', ['bar'])) 57 | ->then 58 | ->object($result) 59 | ->isInstanceOf(LUT\Llk\Rule::class); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Test/Unit/Llk/Rule/Concatenation.php: -------------------------------------------------------------------------------- 1 | when($result = new SUT('foo', ['bar'])) 57 | ->then 58 | ->object($result) 59 | ->isInstanceOf(LUT\Llk\Rule::class); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Test/Unit/Llk/Rule/Ekzit.php: -------------------------------------------------------------------------------- 1 | when($result = new SUT('foo', ['bar'])) 57 | ->then 58 | ->object($result) 59 | ->isInstanceOf(LUT\Llk\Rule\Invocation::class); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Test/Unit/Llk/Rule/Entry.php: -------------------------------------------------------------------------------- 1 | when($result = new SUT('foo', ['bar'])) 57 | ->then 58 | ->object($result) 59 | ->isInstanceOf(LUT\Llk\Rule\Invocation::class); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Test/Unit/Llk/Rule/Invocation.php: -------------------------------------------------------------------------------- 1 | given( 56 | $rule = 'foo', 57 | $data = 'bar' 58 | ) 59 | ->when($result = new SUT($rule, $data)) 60 | ->then 61 | ->string($result->getRule()) 62 | ->isEqualTo($rule) 63 | ->string($result->getData()) 64 | ->isEqualTo($data) 65 | ->variable($result->getTodo()) 66 | ->isNull() 67 | ->integer($result->getDepth()) 68 | ->isEqualTo(-1) 69 | ->boolean($result->isTransitional()) 70 | ->isFalse(); 71 | } 72 | 73 | public function case_constructor_with_todo() 74 | { 75 | $this 76 | ->given( 77 | $rule = 'foo', 78 | $data = 'bar', 79 | $todo = ['baz', 'qux'] 80 | ) 81 | ->when($result = new SUT($rule, $data, $todo)) 82 | ->then 83 | ->string($result->getRule()) 84 | ->isEqualTo($rule) 85 | ->string($result->getData()) 86 | ->isEqualTo($data) 87 | ->array($result->getTodo()) 88 | ->isEqualTo($todo) 89 | ->integer($result->getDepth()) 90 | ->isEqualTo(-1) 91 | ->boolean($result->isTransitional()) 92 | ->isFalse(); 93 | } 94 | 95 | public function case_constructor_with_todo_and_depth() 96 | { 97 | $this 98 | ->given( 99 | $rule = 'foo', 100 | $data = 'bar', 101 | $todo = ['baz', 'qux'], 102 | $depth = 42 103 | ) 104 | ->when($result = new SUT($rule, $data, $todo, $depth)) 105 | ->then 106 | ->string($result->getRule()) 107 | ->isEqualTo($rule) 108 | ->string($result->getData()) 109 | ->isEqualTo($data) 110 | ->array($result->getTodo()) 111 | ->isEqualTo($todo) 112 | ->integer($result->getDepth()) 113 | ->isEqualTo($depth) 114 | ->boolean($result->isTransitional()) 115 | ->isFalse(); 116 | } 117 | 118 | public function case_set_depth() 119 | { 120 | $this 121 | ->given( 122 | $rule = 42, 123 | $data = 'bar', 124 | $todo = ['baz', 'qux'], 125 | $depth = 42, 126 | $invocation = new SUT($rule, $data) 127 | ) 128 | ->when($result = $invocation->setDepth($depth)) 129 | ->then 130 | ->integer($result) 131 | ->isEqualTo(-1); 132 | } 133 | 134 | public function case_get_depth() 135 | { 136 | $this 137 | ->given( 138 | $rule = 42, 139 | $data = 'bar', 140 | $todo = ['baz', 'qux'], 141 | $depth = 42, 142 | $invocation = new SUT($rule, $data), 143 | $invocation->setDepth($depth) 144 | ) 145 | ->when($result = $invocation->getDepth()) 146 | ->then 147 | ->integer($result) 148 | ->isEqualTo($depth); 149 | } 150 | 151 | public function case_is_transitional() 152 | { 153 | $this 154 | ->given( 155 | $rule = 42, 156 | $data = 'bar', 157 | $invocation = new SUT($rule, $data) 158 | ) 159 | ->when($result = $invocation->isTransitional()) 160 | ->then 161 | ->boolean($result) 162 | ->isTrue(); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /Test/Unit/Llk/Rule/Repetition.php: -------------------------------------------------------------------------------- 1 | when($result = new SUT('foo', 7, 42, [], 'bar')) 57 | ->then 58 | ->object($result) 59 | ->isInstanceOf(LUT\Llk\Rule::class); 60 | } 61 | 62 | public function case_constructor() 63 | { 64 | $this 65 | ->given( 66 | $name = 'foo', 67 | $min = 7, 68 | $max = 42, 69 | $children = [], 70 | $id = 'bar' 71 | ) 72 | ->when($result = new SUT($name, $min, $max, $children, $id)) 73 | ->then 74 | ->string($result->getName()) 75 | ->isEqualTo($name) 76 | ->integer($result->getMin()) 77 | ->isEqualTo($min) 78 | ->integer($result->getMax()) 79 | ->isEqualTo($max) 80 | ->array($result->getChildren()) 81 | ->isEqualTo($children) 82 | ->string($result->getNodeId()) 83 | ->isEqualTo($id) 84 | ->boolean($result->isInfinite()) 85 | ->isFalse(); 86 | } 87 | 88 | public function case_constructor_min_and_max_are_casted_and_bounded() 89 | { 90 | $this 91 | ->given( 92 | $name = 'foo', 93 | $min = '-7', 94 | $max = '42', 95 | $children = [], 96 | $id = 'bar' 97 | ) 98 | ->when($result = new SUT($name, $min, $max, $children, $id)) 99 | ->then 100 | ->integer($result->getMin()) 101 | ->isEqualTo(0) 102 | ->integer($result->getMax()) 103 | ->isEqualTo(42) 104 | ->boolean($result->isInfinite()) 105 | ->isFalse(); 106 | } 107 | 108 | public function case_constructor_min_is_greater_than_max() 109 | { 110 | $this 111 | ->given( 112 | $name = 'foo', 113 | $min = 2, 114 | $max = 1, 115 | $children = [], 116 | $id = 'bar' 117 | ) 118 | ->exception(function () use ($name, $min, $max, $children, $id) { 119 | new SUT($name, $min, $max, $children, $id); 120 | }) 121 | ->isInstanceOf(LUT\Exception\Rule::class) 122 | ->hasMessage('Cannot repeat with a min (2) greater than max (1).'); 123 | } 124 | 125 | public function case_constructor_infinite_max() 126 | { 127 | $this 128 | ->given( 129 | $name = 'foo', 130 | $min = 2, 131 | $max = -1, 132 | $children = [], 133 | $id = 'bar' 134 | ) 135 | ->when($result = new SUT($name, $min, $max, $children, $id)) 136 | ->then 137 | ->integer($result->getMin()) 138 | ->isEqualTo(2) 139 | ->integer($result->getMax()) 140 | ->isEqualTo(-1) 141 | ->boolean($result->isInfinite()) 142 | ->isTrue(); 143 | } 144 | 145 | public function case_get_min() 146 | { 147 | return $this->case_constructor(); 148 | } 149 | 150 | public function case_get_max() 151 | { 152 | return $this->case_constructor(); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /Test/Unit/Llk/Rule/Rule.php: -------------------------------------------------------------------------------- 1 | given( 56 | $name = 'foo', 57 | $children = ['bar'] 58 | ) 59 | ->when($result = new SUT($name, $children)) 60 | ->then 61 | ->string($result->getName()) 62 | ->isEqualTo($name) 63 | ->array($result->getChildren()) 64 | ->isEqualTo($children) 65 | ->variable($result->getNodeId()) 66 | ->isNull() 67 | ->boolean($result->isTransitional()) 68 | ->isTrue(); 69 | } 70 | 71 | public function case_constructor_with_node_id() 72 | { 73 | $this 74 | ->given( 75 | $name = 'foo', 76 | $children = ['bar'], 77 | $nodeId = 'baz' 78 | ) 79 | ->when($result = new SUT($name, $children, $nodeId)) 80 | ->then 81 | ->string($result->getName()) 82 | ->isEqualTo($name) 83 | ->array($result->getChildren()) 84 | ->isEqualTo($children) 85 | ->string($result->getNodeId()) 86 | ->isEqualTo($nodeId) 87 | ->boolean($result->isTransitional()) 88 | ->isTrue(); 89 | } 90 | 91 | public function case_set_name() 92 | { 93 | $this 94 | ->given( 95 | $name = 'foo', 96 | $children = ['bar'], 97 | $rule = new SUT($name, $children) 98 | ) 99 | ->when($result = $rule->setName('baz')) 100 | ->then 101 | ->string($result) 102 | ->isEqualTo($name); 103 | } 104 | 105 | public function case_get_name() 106 | { 107 | $this 108 | ->given( 109 | $name = 'baz', 110 | $children = ['bar'], 111 | $rule = new SUT('foo', $children), 112 | $rule->setName($name) 113 | ) 114 | ->when($result = $rule->getName()) 115 | ->then 116 | ->string($result) 117 | ->isEqualTo($name); 118 | } 119 | 120 | public function case_set_children() 121 | { 122 | $this 123 | ->given( 124 | $name = 'foo', 125 | $children = ['bar'], 126 | $rule = new SUT($name, $children) 127 | ) 128 | ->when($result = $this->invoke($rule)->setChildren(['baz'])) 129 | ->then 130 | ->array($result) 131 | ->isEqualTo($children); 132 | } 133 | 134 | public function case_get_children() 135 | { 136 | $this 137 | ->given( 138 | $name = 'foo', 139 | $children = ['baz'], 140 | $rule = new SUT($name, ['bar']), 141 | $this->invoke($rule)->setChildren($children) 142 | ) 143 | ->when($result = $rule->getChildren()) 144 | ->then 145 | ->array($result) 146 | ->isEqualTo($children); 147 | } 148 | 149 | public function case_set_node_id() 150 | { 151 | $this 152 | ->given( 153 | $name = 'foo', 154 | $children = ['bar'], 155 | $nodeId = 'id', 156 | $rule = new SUT($name, $children, $nodeId) 157 | ) 158 | ->when($result = $rule->setNodeId('baz:qux')) 159 | ->then 160 | ->string($result) 161 | ->isEqualTo($nodeId); 162 | } 163 | 164 | public function case_get_node_id() 165 | { 166 | $this 167 | ->given( 168 | $name = 'foo', 169 | $children = ['bar'], 170 | $rule = new SUT($name, $children), 171 | $rule->setNodeId('baz') 172 | ) 173 | ->when($result = $rule->getNodeId()) 174 | ->then 175 | ->string($result) 176 | ->isEqualTo('baz'); 177 | } 178 | 179 | public function case_get_node_id_with_options() 180 | { 181 | $this 182 | ->given( 183 | $name = 'foo', 184 | $children = ['bar'], 185 | $rule = new SUT($name, $children), 186 | $rule->setNodeId('baz:qux') 187 | ) 188 | ->when($result = $rule->getNodeId()) 189 | ->then 190 | ->string($result) 191 | ->isEqualTo('baz'); 192 | } 193 | 194 | public function case_get_node_options_empty() 195 | { 196 | $this 197 | ->given( 198 | $name = 'foo', 199 | $children = ['bar'], 200 | $rule = new SUT($name, $children), 201 | $rule->setNodeId('baz') 202 | ) 203 | ->when($result = $rule->getNodeOptions()) 204 | ->then 205 | ->array($result) 206 | ->isEmpty(); 207 | } 208 | 209 | public function case_get_node_options() 210 | { 211 | $this 212 | ->given( 213 | $name = 'foo', 214 | $children = ['bar'], 215 | $rule = new SUT($name, $children), 216 | $rule->setNodeId('baz:qux') 217 | ) 218 | ->when($result = $rule->getNodeOptions()) 219 | ->then 220 | ->array($result) 221 | ->isEqualTo(['q', 'u', 'x']); 222 | } 223 | 224 | public function case_set_default_id() 225 | { 226 | $this 227 | ->given( 228 | $name = 'foo', 229 | $children = ['bar'], 230 | $nodeId = 'id', 231 | $rule = new SUT($name, $children, $nodeId) 232 | ) 233 | ->when($result = $rule->setDefaultId('baz:qux')) 234 | ->then 235 | ->variable($result) 236 | ->isNull(); 237 | } 238 | 239 | public function case_get_default_id() 240 | { 241 | $this 242 | ->given( 243 | $name = 'foo', 244 | $children = ['bar'], 245 | $rule = new SUT($name, $children), 246 | $rule->setDefaultId('baz') 247 | ) 248 | ->when($result = $rule->getDefaultId()) 249 | ->then 250 | ->string($result) 251 | ->isEqualTo('baz'); 252 | } 253 | 254 | public function case_get_default_id_with_options() 255 | { 256 | $this 257 | ->given( 258 | $name = 'foo', 259 | $children = ['bar'], 260 | $rule = new SUT($name, $children), 261 | $rule->setDefaultId('baz:qux') 262 | ) 263 | ->when($result = $rule->getDefaultId()) 264 | ->then 265 | ->string($result) 266 | ->isEqualTo('baz'); 267 | } 268 | 269 | public function case_get_default_options_empty() 270 | { 271 | $this 272 | ->given( 273 | $name = 'foo', 274 | $children = ['bar'], 275 | $rule = new SUT($name, $children), 276 | $rule->setDefaultId('baz') 277 | ) 278 | ->when($result = $rule->getDefaultOptions()) 279 | ->then 280 | ->array($result) 281 | ->isEmpty(); 282 | } 283 | 284 | public function case_get_default_options() 285 | { 286 | $this 287 | ->given( 288 | $name = 'foo', 289 | $children = ['bar'], 290 | $rule = new SUT($name, $children), 291 | $rule->setDefaultId('baz:qux') 292 | ) 293 | ->when($result = $rule->getDefaultOptions()) 294 | ->then 295 | ->array($result) 296 | ->isEqualTo(['q', 'u', 'x']); 297 | } 298 | 299 | public function case_set_pp_representation() 300 | { 301 | $this 302 | ->given( 303 | $name = 'foo', 304 | $children = ['bar'], 305 | $pp = ' ::b:: c()?', 306 | $rule = new SUT($name, $children), 307 | $oldIsTransitional = $rule->isTransitional() 308 | ) 309 | ->when($result = $rule->setPPRepresentation($pp)) 310 | ->then 311 | ->variable($result) 312 | ->isNull() 313 | ->boolean($oldIsTransitional) 314 | ->isTrue() 315 | ->boolean($rule->isTransitional()) 316 | ->isFalse(); 317 | } 318 | 319 | public function case_get_pp_representation() 320 | { 321 | $this 322 | ->given( 323 | $name = 'foo', 324 | $children = ['bar'], 325 | $pp = ' ::b:: c()?', 326 | $rule = new SUT($name, $children), 327 | $rule->setPPRepresentation($pp) 328 | ) 329 | ->when($result = $rule->getPPRepresentation()) 330 | ->then 331 | ->string($result) 332 | ->isEqualTo($pp); 333 | } 334 | 335 | public function case_is_transitional() 336 | { 337 | $this 338 | ->given( 339 | $name = 'foo', 340 | $children = ['bar'], 341 | $pp = ' ::b:: c()?', 342 | $rule = new SUT($name, $children), 343 | $oldIsTransitional = $rule->isTransitional(), 344 | $rule->setPPRepresentation($pp) 345 | ) 346 | ->when($result = $rule->isTransitional()) 347 | ->then 348 | ->boolean($oldIsTransitional) 349 | ->isTrue() 350 | ->boolean($result) 351 | ->isFalse(); 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /Test/Unit/Llk/Rule/Token.php: -------------------------------------------------------------------------------- 1 | when($result = new SUT('name', 'tokenName', 'nodeId', 0)) 57 | ->then 58 | ->object($result) 59 | ->isInstanceOf(LUT\Llk\Rule::class); 60 | } 61 | 62 | public function case_constructor() 63 | { 64 | $this 65 | ->given( 66 | $name = 'foo', 67 | $tokenName = 'bar', 68 | $nodeId = 'baz', 69 | $unification = 0 70 | ) 71 | ->when($result = new SUT($name, $tokenName, $nodeId, $unification)) 72 | ->then 73 | ->string($result->getName()) 74 | ->isEqualTo($name) 75 | ->string($result->getTokenName()) 76 | ->isEqualTo($tokenName) 77 | ->string($result->getNodeId()) 78 | ->isEqualTo($nodeId) 79 | ->integer($result->getUnificationIndex()) 80 | ->isEqualTo($unification) 81 | ->boolean($result->isKept()) 82 | ->isFalse(); 83 | } 84 | 85 | public function case_constructor_with_kept_flag() 86 | { 87 | $this 88 | ->given( 89 | $name = 'foo', 90 | $tokenName = 'bar', 91 | $nodeId = 'baz', 92 | $unification = 0, 93 | $kept = true 94 | ) 95 | ->when($result = new SUT($name, $tokenName, $nodeId, $unification, $kept)) 96 | ->then 97 | ->string($result->getName()) 98 | ->isEqualTo($name) 99 | ->string($result->getTokenName()) 100 | ->isEqualTo($tokenName) 101 | ->string($result->getNodeId()) 102 | ->isEqualTo($nodeId) 103 | ->integer($result->getUnificationIndex()) 104 | ->isEqualTo($unification) 105 | ->boolean($result->isKept()) 106 | ->isTrue(); 107 | } 108 | 109 | public function case_get_token_name() 110 | { 111 | $this 112 | ->given( 113 | $name = 'foo', 114 | $tokenName = 'bar', 115 | $nodeId = 'baz', 116 | $unification = 0, 117 | $token = new SUT($name, $tokenName, $nodeId, $unification) 118 | ) 119 | ->when($result = $token->getTokenName()) 120 | ->then 121 | ->string($result) 122 | ->isEqualTo($tokenName); 123 | } 124 | 125 | public function case_set_namespace() 126 | { 127 | $this 128 | ->given( 129 | $name = 'foo', 130 | $tokenName = 'bar', 131 | $nodeId = 'baz', 132 | $unification = 0, 133 | $namespace = 'qux', 134 | $token = new SUT($name, $tokenName, $nodeId, $unification) 135 | ) 136 | ->when($result = $token->setNamespace($namespace)) 137 | ->then 138 | ->variable($result) 139 | ->isNull(); 140 | } 141 | 142 | public function case_get_namespace() 143 | { 144 | $this 145 | ->given( 146 | $name = 'foo', 147 | $tokenName = 'bar', 148 | $nodeId = 'baz', 149 | $unification = 0, 150 | $namespace = 'qux', 151 | $token = new SUT($name, $tokenName, $nodeId, $unification), 152 | $token->setNamespace($namespace) 153 | ) 154 | ->when($result = $token->getNamespace()) 155 | ->then 156 | ->string($result) 157 | ->isEqualTo($namespace); 158 | } 159 | 160 | public function case_set_representation() 161 | { 162 | $this 163 | ->given( 164 | $name = 'foo', 165 | $tokenName = 'bar', 166 | $nodeId = 'baz', 167 | $unification = 0, 168 | $representation = 'qux', 169 | $token = new SUT($name, $tokenName, $nodeId, $unification) 170 | ) 171 | ->when($result = $token->setRepresentation($representation)) 172 | ->then 173 | ->variable($result) 174 | ->isNull(); 175 | } 176 | 177 | public function case_get_representation() 178 | { 179 | $this 180 | ->given( 181 | $name = 'foo', 182 | $tokenName = 'bar', 183 | $nodeId = 'baz', 184 | $unification = 0, 185 | $representation = 'qux', 186 | $token = new SUT($name, $tokenName, $nodeId, $unification), 187 | $token->setRepresentation($representation) 188 | ) 189 | ->when($result = $token->getRepresentation()) 190 | ->then 191 | ->string($result) 192 | ->isEqualTo($representation); 193 | } 194 | 195 | public function case_get_ast() 196 | { 197 | $this 198 | ->given( 199 | $name = 'foo', 200 | $tokenName = 'bar', 201 | $nodeId = 'baz', 202 | $unification = 0, 203 | $representation = 'qux', 204 | $token = new SUT($name, $tokenName, $nodeId, $unification), 205 | $token->setRepresentation($representation) 206 | ) 207 | ->when($result = $token->getAST()) 208 | ->then 209 | ->object($result) 210 | ->isInstanceOf(LUT\Llk\TreeNode::class) 211 | ->let($dumper = new LUT\Visitor\Dump()) 212 | ->string($dumper->visit($result)) 213 | ->isEqualTo( 214 | '> #expression' . "\n" . 215 | '> > #concatenation' . "\n" . 216 | '> > > token(literal, q)' . "\n" . 217 | '> > > token(literal, u)' . "\n" . 218 | '> > > token(literal, x)' . "\n" 219 | ); 220 | } 221 | 222 | public function case_set_value() 223 | { 224 | $this 225 | ->given( 226 | $name = 'foo', 227 | $tokenName = 'bar', 228 | $nodeId = 'baz', 229 | $unification = 0, 230 | $value = 'qux', 231 | $token = new SUT($name, $tokenName, $nodeId, $unification) 232 | ) 233 | ->when($result = $token->setValue($value)) 234 | ->then 235 | ->variable($result) 236 | ->isNull(); 237 | } 238 | 239 | public function case_get_value() 240 | { 241 | $this 242 | ->given( 243 | $name = 'foo', 244 | $tokenName = 'bar', 245 | $nodeId = 'baz', 246 | $unification = 0, 247 | $value = 'qux', 248 | $token = new SUT($name, $tokenName, $nodeId, $unification), 249 | $token->setValue($value) 250 | ) 251 | ->when($result = $token->getValue()) 252 | ->then 253 | ->string($result) 254 | ->isEqualTo($value); 255 | } 256 | 257 | public function case_set_offset() 258 | { 259 | $this 260 | ->given( 261 | $name = 'foo', 262 | $tokenName = 'bar', 263 | $nodeId = 'baz', 264 | $unification = 0, 265 | $offset = 42, 266 | $token = new SUT($name, $tokenName, $nodeId, $unification) 267 | ) 268 | ->when($result = $token->setOffset($offset)) 269 | ->then 270 | ->integer($result) 271 | ->isZero(); 272 | } 273 | 274 | public function case_get_offset() 275 | { 276 | $this 277 | ->given( 278 | $name = 'foo', 279 | $tokenName = 'bar', 280 | $nodeId = 'baz', 281 | $unification = 0, 282 | $offset = 42, 283 | $token = new SUT($name, $tokenName, $nodeId, $unification), 284 | $token->setOffset($offset) 285 | ) 286 | ->when($result = $token->getOffset()) 287 | ->then 288 | ->integer($result) 289 | ->isEqualTo($offset); 290 | } 291 | 292 | public function case_set_kept() 293 | { 294 | $this 295 | ->given( 296 | $name = 'foo', 297 | $tokenName = 'bar', 298 | $nodeId = 'baz', 299 | $unification = 0, 300 | $kept = true, 301 | $token = new SUT($name, $tokenName, $nodeId, $unification) 302 | ) 303 | ->when($result = $token->setKept($kept)) 304 | ->then 305 | ->boolean($result) 306 | ->isFalse(); 307 | } 308 | 309 | public function case_is_kept() 310 | { 311 | $this 312 | ->given( 313 | $name = 'foo', 314 | $tokenName = 'bar', 315 | $nodeId = 'baz', 316 | $unification = 0, 317 | $kept = true, 318 | $token = new SUT($name, $tokenName, $nodeId, $unification), 319 | $token->setKept($kept) 320 | ) 321 | ->when($result = $token->isKept()) 322 | ->then 323 | ->boolean($result) 324 | ->isTrue(); 325 | } 326 | 327 | public function case_get_unification_index() 328 | { 329 | $this 330 | ->given( 331 | $name = 'foo', 332 | $tokenName = 'bar', 333 | $nodeId = 'baz', 334 | $unification = 42, 335 | $token = new SUT($name, $tokenName, $nodeId, $unification) 336 | ) 337 | ->when($result = $token->getUnificationIndex()) 338 | ->then 339 | ->integer($result) 340 | ->isEqualTo($unification); 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /Test/Unit/Llk/Sampler/Exception.php: -------------------------------------------------------------------------------- 1 | when($result = new SUT('foo', 0)) 57 | ->then 58 | ->object($result) 59 | ->isInstanceOf(LUT\Exception::class); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Visitor/Dump.php: -------------------------------------------------------------------------------- 1 | ', self::$_i) . $element->getId(); 76 | 77 | if (null !== $value = $element->getValue()) { 78 | $out .= 79 | '(' . 80 | ('default' !== $value['namespace'] 81 | ? $value['namespace'] . ':' 82 | : '') . 83 | $value['token'] . ', ' . 84 | $value['value'] . ')'; 85 | } 86 | 87 | $data = $element->getData(); 88 | 89 | if (!empty($data)) { 90 | $out .= ' ' . $this->dumpData($data); 91 | } 92 | 93 | $out .= "\n"; 94 | 95 | foreach ($element->getChildren() as $child) { 96 | $out .= $child->accept($this, $handle, $eldnah); 97 | } 98 | 99 | --self::$_i; 100 | 101 | return $out; 102 | } 103 | 104 | /** 105 | * Dump data. 106 | * 107 | * @param mixed $data Data. 108 | * @return string 109 | */ 110 | protected function dumpData($data) 111 | { 112 | $out = null; 113 | 114 | if (!is_array($data)) { 115 | return $data; 116 | } 117 | 118 | foreach ($data as $key => $value) { 119 | $out .= '[' . $key . ' => ' . $this->dumpData($value) . ']'; 120 | } 121 | 122 | return $out; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /bors.toml: -------------------------------------------------------------------------------- 1 | status = [ 2 | "continuous-integration/travis-ci/push" 3 | ] 4 | timeout_sec = 1800 5 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "hoa/compiler", 3 | "description": "The Hoa\\Compiler library.", 4 | "type" : "library", 5 | "keywords" : ["library", "compiler", "grammar", "language", "ll1", 6 | "llk", "regular", "algebraic", "context-free", "lexer", 7 | "parser", "token", "rule", "pp", "ast", "trace", "syntax", 8 | "sampler", "isotropic", "random", "uniform", "exhaustive", 9 | "coverage"], 10 | "homepage" : "https://hoa-project.net/", 11 | "license" : "BSD-3-Clause", 12 | "authors" : [ 13 | { 14 | "name" : "Ivan Enderlin", 15 | "email": "ivan.enderlin@hoa-project.net" 16 | }, 17 | { 18 | "name" : "Hoa community", 19 | "homepage": "https://hoa-project.net/" 20 | } 21 | ], 22 | "support": { 23 | "email" : "support@hoa-project.net", 24 | "irc" : "irc://chat.freenode.net/hoaproject", 25 | "forum" : "https://users.hoa-project.net/", 26 | "docs" : "https://central.hoa-project.net/Documentation/Library/Compiler", 27 | "source": "https://central.hoa-project.net/Resource/Library/Compiler" 28 | }, 29 | "require": { 30 | "php" : ">=5.5.0", 31 | "hoa/consistency": "~1.0", 32 | "hoa/exception" : "~1.0", 33 | "hoa/file" : "~1.0", 34 | "hoa/iterator" : "~2.0", 35 | "hoa/math" : "~1.0", 36 | "hoa/protocol" : "~1.0", 37 | "hoa/regex" : "~1.0", 38 | "hoa/visitor" : "~2.0" 39 | }, 40 | "require-dev": { 41 | "hoa/json": "~2.0", 42 | "hoa/test": "~2.0" 43 | }, 44 | "autoload": { 45 | "psr-4": { 46 | "Hoa\\Compiler\\": "." 47 | } 48 | }, 49 | "extra": { 50 | "branch-alias": { 51 | "dev-master": "3.x-dev" 52 | } 53 | } 54 | } 55 | --------------------------------------------------------------------------------