Initaler Commit
authorroot <root@x.graph-it.org>
Fri, 14 Oct 2016 09:04:56 +0000 (11:04 +0200)
committerroot <root@x.graph-it.org>
Fri, 14 Oct 2016 09:04:56 +0000 (11:04 +0200)
16 files changed:
.gitignore [new file with mode: 0644]
composer.json [new file with mode: 0644]
composer.lock [new file with mode: 0644]
src/BaseParser.php [new file with mode: 0644]
src/ConcatParser.php [new file with mode: 0644]
src/EBNFGenerator.php [new file with mode: 0644]
src/EBNFParser.php [new file with mode: 0644]
src/EBNFParserBase.php [new file with mode: 0644]
src/EmptyParser.php [new file with mode: 0644]
src/GrammerException.php [new file with mode: 0644]
src/GrammerParser.php [new file with mode: 0644]
src/GreedyMultiParser.php [new file with mode: 0644]
src/LazyAltParser.php [new file with mode: 0644]
src/RegexParser.php [new file with mode: 0644]
src/StringParser.php [new file with mode: 0644]
src/ebnf.ebnf [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..48b8bf9
--- /dev/null
@@ -0,0 +1 @@
+vendor/
diff --git a/composer.json b/composer.json
new file mode 100644 (file)
index 0000000..76beec3
--- /dev/null
@@ -0,0 +1,18 @@
+{
+    "name": "graphit/parser",
+    "type": "library",
+    "authors": [
+        {
+            "name": "Graph-IT",
+            "email": "info@graph-it.com"
+        }
+    ],
+    "require": {
+        "php": ">=5.6.0"
+    },
+    "autoload": {
+        "psr-4": {
+          "Graphit\\Parser\\": "src/"
+        }
+    }
+}
diff --git a/composer.lock b/composer.lock
new file mode 100644 (file)
index 0000000..dff83d1
--- /dev/null
@@ -0,0 +1,20 @@
+{
+    "_readme": [
+        "This file locks the dependencies of your project to a known state",
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
+        "This file is @generated automatically"
+    ],
+    "hash": "58c59c1998c08b0e41abf3c52e03cf24",
+    "content-hash": "885de769d17478ea8942f54d26e6a439",
+    "packages": [],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": {
+        "php": ">=5.6.0"
+    },
+    "platform-dev": []
+}
diff --git a/src/BaseParser.php b/src/BaseParser.php
new file mode 100644 (file)
index 0000000..fe8a71a
--- /dev/null
@@ -0,0 +1,92 @@
+<?php
+
+namespace Graphit\Parser;
+
+abstract class BaseParser
+{
+  protected $internals;
+
+  protected $generator;
+
+  protected $description;
+
+  protected $acceptsEmpty;
+
+  public function __construct(array $internals = array(), $generator = null)
+  {
+    if ( !$this->description) {
+      throw new \Exception("No description prepared!");
+    }
+
+    foreach ($internals as $internal) {
+      if ( !is_string($internal) && !$internal instanceof BaseParser) {
+        throw new \Exception(var_export($internal, true).' is not a string and not a BaseParser');
+      }
+    }
+    $this->internals = $internals;
+
+    if ($generator) {
+      if ( !is_callable($generator)) {
+        throw new \Exception(var_export($generator, true).' is not callable');
+      }
+      $this->generator = $generator;
+    } else {
+      $this->generator = array($this, 'defaultGenerator');
+    }
+  }
+
+  public function getInternals()
+  {
+    return $this->internals;
+  }
+
+  public function getDescription()
+  {
+    return $this->description;
+  }
+
+  protected function parse($string, $p = 0)
+  {
+    if ( !$r = $this->accept($string, $p)) {
+      return false;
+    }
+    return array(
+      'r' => call_user_func($this->generator, $r['r']),
+      'p' => $r['p'],
+    );
+  }
+
+  protected abstract function accept($string, $p);
+
+  protected abstract function evalAcceptsEmpty();
+
+  protected abstract function firstSet();
+
+  public function __toString()
+  {
+    return $this->description;
+  }
+
+  protected function defaultGenerator($result)
+  {
+    return array(
+      't' => (string)$this,
+      'r' => $result,
+    );
+  }
+
+  protected function serializeInternals(array $internals)
+  {
+    $chunks = array();
+    foreach ($internals as $key => $internal) {
+      $chunk = var_export($key, true).' => ';
+      if (is_string($internal)) {
+        $chunk .= var_export($internal, true).' => ';
+      } else {
+        $chunk .= (string)$internal;
+      }
+      $chunks[] = $chunk;
+    }
+    return 'array('.implode(', ', $chunks).')';
+  }
+}
diff --git a/src/ConcatParser.php b/src/ConcatParser.php
new file mode 100644 (file)
index 0000000..df9f80a
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+
+namespace Graphit\Parser;
+
+class ConcatParser extends BaseParser
+{
+  public function __construct(array $internals, $generator = null)
+  {
+    $this->description = 'new '.get_class().'('.$this->serializeInternals($internals).')';
+    parent::__construct($internals, $generator);
+  }
+
+  protected function accept($string, $p)
+  {
+    $r = array();
+    foreach ($this->internals as $internal) {
+      if ($i = $internal->parse($string, $p)) {
+        $r[] = $i['r'];
+        $p = $i['p'];
+
+        continue;
+      }
+      return false;
+    }
+
+    return array(
+      'r' => $r,
+      'p' => $p,
+    );
+  }
+
+  protected function evalAcceptsEmpty()
+  {
+    foreach ($this->internals as $internal) {
+      if ( !$internal->acceptsEmpty) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  protected function firstSet()
+  {
+    $firstSet = array();
+    foreach ($this->internals as $internal) {
+      $firstSet[] = $internal;
+
+      if ( !$internal->acceptsEmpty) {
+        break;
+      }
+    }
+    return $firstSet;
+  }
+}
diff --git a/src/EBNFGenerator.php b/src/EBNFGenerator.php
new file mode 100644 (file)
index 0000000..000565a
--- /dev/null
@@ -0,0 +1,217 @@
+<?php
+
+namespace Graphit\Parser;
+
+class EBNFGenerator
+{
+  protected $parser;
+
+  public function __construct()
+  {
+    $this->parser = new EBNFParser();
+  }
+
+  public function generate($ebnf, $start, $class)
+  {
+    if ( !$ast = $this->parser->ast($ebnf)) {
+      throw new \Exception('Unable to parse given EBNF');
+    }
+
+    if ($pos = strrpos($class, '\\')){
+      $namespace = substr($class, 0, $pos);
+      $classname = substr($class, $pos + 1);
+    } else {
+      $classname = $class;
+    }
+
+    $code = "<?php\n\n";
+    if ($namespace) {
+      $code.= "namespace {$namespace};\n\n";
+    }
+    $code.= "class {$classname} extends \Graphit\Parser\GrammerParser\n{\n";
+
+    $code.= "  public function __construct()\n";
+    $code.= "  {\n";
+    $code.= "    \$s = ".var_export($start, true).";\n";
+
+    $code.= $this->indent($this->genParser($ast), 4)."\n";
+
+    $code.= "    parent::__construct(\$s, \$internals);\n";
+    $code.= "  }\n";
+
+    foreach ($ast['r'][1]['r'] as $rule) {
+      $name = $rule['r'][0]['r'][0]['r'][0];
+
+      $code.= "\n";
+      $code.= "  protected function ";
+      $code.= $this->genGenerator($name);
+      $code.= "(\$result)\n";
+      $code.= "  {\n";
+      $code.= "    \$result = \$this->internals[".var_export($name, true);
+      $code.= "]->defaultGenerator(\$result);\n";
+      $code.= "    \$result['t'] = ".var_export($name, true).";\n";
+      $code.= "    return \$result;\n";
+      $code.= "  }\n";
+    }
+
+    $code.="}\n";
+
+    return $code;
+  }
+
+  protected function genParser(array $object, $generator = null)
+  {
+    $method = 'gen'.ucfirst($object['t']).'Parser';
+    if (method_exists($this, $method)) {
+      return $this->$method($object, $generator);
+    }
+
+    throw new \Exception("Unknow t '{$object['t']}' or  missing method $method");
+  }
+
+  protected function genSyntaxParser($object, $generator)
+  {
+    $code = "\$internals = array(\n";
+    foreach ($object['r'][1]['r'] as $rule) {
+      $code.= $this->indent($this->genParser($rule), 2).",\n";
+    }
+    $code.= ");";
+    return $code;
+  }
+
+  protected function genRuleParser($object)
+  {
+    $name = $object['r'][0]['r'][0]['r'][0];
+    $generator = $this->genGenerator($name);
+
+    $code = var_export($name, true)." => ";
+    $code.= $this->genParser($object['r'][4], $generator);
+    return $code;
+  }
+
+  protected function genAltParser($object, $generator = null)
+  {
+    $code = "new \\Graphit\\Parser\\LazyAltParser(\n";
+    $code.= "  array(\n";
+    foreach ($object['r'] as $internal) {
+      $code.= $this->indent($this->genParser($internal), 4).",\n";
+    }
+    $code.= "  )";
+    $code.= $this->genGeneratorCall($generator, true);
+    $code.= ")";
+    return $code;
+  }
+
+  protected function genConcParser($object, $generator = null)
+  {
+    $code = "new \\Graphit\\Parser\\ConcatParser(\n";
+    $code.= "  array(\n";
+    foreach ($object['r'] as $internal) {
+      $code.= $this->indent($this->genParser($internal), 4).",\n";
+    }
+    $code.= "  )";
+    $code.= $this->genGeneratorCall($generator, true);
+    $code.= ")";
+    return $code;
+  }
+
+  protected function genBarewordParser($object)
+  {
+    $code = var_export($object['r'][0]['r'][0], true);
+    return $code;
+  }
+
+  protected function genSqParser($object, $generator = null)
+  {
+    $code = "new \\Graphit\\Parser\\StringParser(";
+    $code.= $generator? "\n  ": '';
+    //$code.= var_export($object['string'], true);
+    $code.= "'".$object['r'][0]['r'][1]."'";
+    $code.= $this->genGeneratorCall($generator, $generator? true: false);
+    $code.= ")";
+    return $code;
+  }
+
+  protected function genDqParser($object, $generator = null)
+  {
+    $code = "new \\Graphit\\Parser\\StringParser(";
+    $code.= $generator? "\n  ": '';
+    //$code.= var_export($object['string'], true);
+    $code.= '"'.$object['r'][0]['r'][1].'"';
+    $code.= $this->genGeneratorCall($generator, $generator? true: false);
+    $code.= ")";
+    return $code;
+  }
+
+  protected function genRegexParser($object, $generator = null)
+  {
+    $code = "new \\Graphit\\Parser\\RegexParser(";
+    $code.= $generator? "\n  ": '';
+    $code.= var_export($object['r'][0]['r'][0], true);
+    $code.= $this->genGeneratorCall($generator, $generator? true: false);
+    $code.= ")";
+    return $code;
+  }
+
+  protected function genGroupParser($object, $generator = null)
+  {
+    return $this->genParser($object['r'][2], $generator);
+  }
+
+  protected function genRepetitionParser($object, $generator = null)
+  {
+    $code = "new \\Graphit\\Parser\\GreedyMultiParser(\n";
+    $code.= $this->indent($this->genParser($object['r'][2]), 2);
+    $code.= ", 0, null";
+    $code.= $this->genGeneratorCall($generator, true);
+    $code.= ")";
+    return $code;
+  }
+
+  protected function genOptionalParser($object, $generator = null)
+  {
+    $code = "new \\Graphit\\Parser\\GreedyMultiParser(\n";
+    $code.= $this->indent($this->genParser($object['r'][2]), 2);
+    $code.= ", 0, 1";
+    $code.= $this->genGeneratorCall($generator, true);
+    $code.= ")";
+    return $code;
+  }
+
+  protected function genGenerator($source)
+  {
+    return "{$source}Generator";
+  }
+
+  protected function genGeneratorCall($generator, $break = false)
+  {
+    if ( !$generator) {
+      return $break ? "\n" : '';
+    }
+
+    $code = "," . ($break ? "\n  " : '');
+    $code.= "array(\$this, ".var_export($generator, true).")";
+    $code.= ($break ? "\n" : '');
+    return $code;
+  }
+
+  protected function indent($lines, $size = 2, $from = 0)
+  {
+    $pad = str_repeat(' ', $size);
+    $code = '';
+
+    $lines = explode("\n", $lines);
+    for ($i = 0; $i < $from; ++$i) {
+      if (($line = array_shift($lines)) === false) {
+        break;
+      }
+      $code .= "{$line}\n";
+    }
+
+    foreach ($lines as $line) {
+      $code .= "{$pad}{$line}\n";
+    }
+
+    return substr($code, 0, -1);
+  }
+}
diff --git a/src/EBNFParser.php b/src/EBNFParser.php
new file mode 100644 (file)
index 0000000..0750832
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+
+namespace Graphit\Parser;
+
+class EBNFParser extends EBNFParserBase
+{
+  public function __construct()
+  {
+    parent::__construct();
+  }
+
+  protected function altGenerator($results)
+  {
+    if ($results[1]) {
+      array_unshift($results[1], $results[0]);
+      return array(
+        't' => 'alt',
+        'r' => $results[1],
+      );
+    } else {
+      return $results[0];
+    }
+  }
+
+  protected function pipeconclistGenerator($results)
+  {
+    return $results;
+  }
+
+  protected function pipeconcGenerator($results)
+  {
+    return $results[2];
+  }
+
+  protected function concGenerator($results)
+  {
+    if ($results[1]) {
+      array_unshift($results[1], $results[0]['r']);
+      return array(
+        't' => 'conc',
+        'r' => $results[1],
+      );
+    } else {
+      return $results[0]['r'];
+    }
+  }
+
+  protected function commatermlistGenerator($results)
+  {
+    return $results;
+  }
+
+  protected function commatermGenerator($results)
+  {
+    return $results[2]['r'];
+  }
+}
diff --git a/src/EBNFParserBase.php b/src/EBNFParserBase.php
new file mode 100644 (file)
index 0000000..48a4e6a
--- /dev/null
@@ -0,0 +1,302 @@
+<?php
+
+namespace Graphit\Parser;
+
+class EBNFParserBase extends \Graphit\Parser\GrammerParser
+{
+  public function __construct()
+  {
+    $s = 'syntax';
+    $internals = array(
+      'syntax' => new \Graphit\Parser\ConcatParser(
+        array(
+          'space',
+          'rules',
+        ),
+        array($this, 'syntaxGenerator')
+      ),
+      'rules' => new \Graphit\Parser\GreedyMultiParser(
+        'rule', 0, null,
+        array($this, 'rulesGenerator')
+      ),
+      'rule' => new \Graphit\Parser\ConcatParser(
+        array(
+          'bareword',
+          'space',
+          new \Graphit\Parser\StringParser("="),
+          'space',
+          'alt',
+          new \Graphit\Parser\StringParser(";"),
+          'space',
+        ),
+        array($this, 'ruleGenerator')
+      ),
+      'alt' => new \Graphit\Parser\ConcatParser(
+        array(
+          'conc',
+          'pipeconclist',
+        ),
+        array($this, 'altGenerator')
+      ),
+      'pipeconclist' => new \Graphit\Parser\GreedyMultiParser(
+        'pipeconc', 0, null,
+        array($this, 'pipeconclistGenerator')
+      ),
+      'pipeconc' => new \Graphit\Parser\ConcatParser(
+        array(
+          new \Graphit\Parser\StringParser("|"),
+          'space',
+          'conc',
+        ),
+        array($this, 'pipeconcGenerator')
+      ),
+      'conc' => new \Graphit\Parser\ConcatParser(
+        array(
+          'term',
+          'commatermlist',
+        ),
+        array($this, 'concGenerator')
+      ),
+      'commatermlist' => new \Graphit\Parser\GreedyMultiParser(
+        'commaterm', 0, null,
+        array($this, 'commatermlistGenerator')
+      ),
+      'commaterm' => new \Graphit\Parser\ConcatParser(
+        array(
+          new \Graphit\Parser\StringParser(","),
+          'space',
+          'term',
+        ),
+        array($this, 'commatermGenerator')
+      ),
+      'term' => new \Graphit\Parser\LazyAltParser(
+        array(
+          'bareword',
+          'sq',
+          'dq',
+          'regex',
+          'group',
+          'repetition',
+          'optional',
+        ),
+        array($this, 'termGenerator')
+      ),
+      'bareword' => new \Graphit\Parser\ConcatParser(
+        array(
+          new \Graphit\Parser\RegexParser('/^([a-z][a-z ]*[a-z]|[a-z])/'),
+          'space',
+        ),
+        array($this, 'barewordGenerator')
+      ),
+      'sq' => new \Graphit\Parser\ConcatParser(
+        array(
+          new \Graphit\Parser\RegexParser('/^\'([^\']*)\'/'),
+          'space',
+        ),
+        array($this, 'sqGenerator')
+      ),
+      'dq' => new \Graphit\Parser\ConcatParser(
+        array(
+          new \Graphit\Parser\RegexParser('/^"([^"]*)"/'),
+          'space',
+        ),
+        array($this, 'dqGenerator')
+      ),
+      'regex' => new \Graphit\Parser\ConcatParser(
+        array(
+          new \Graphit\Parser\RegexParser('/^\\/\\^([^\\/\\\\]*(\\\\\\/|\\\\[^\\/])?)*\\//'),
+          'space',
+        ),
+        array($this, 'regexGenerator')
+      ),
+      'group' => new \Graphit\Parser\ConcatParser(
+        array(
+          new \Graphit\Parser\StringParser("("),
+          'space',
+          'alt',
+          new \Graphit\Parser\StringParser(")"),
+          'space',
+        ),
+        array($this, 'groupGenerator')
+      ),
+      'repetition' => new \Graphit\Parser\ConcatParser(
+        array(
+          new \Graphit\Parser\StringParser("{"),
+          'space',
+          'alt',
+          new \Graphit\Parser\StringParser("}"),
+          'space',
+        ),
+        array($this, 'repetitionGenerator')
+      ),
+      'optional' => new \Graphit\Parser\ConcatParser(
+        array(
+          new \Graphit\Parser\StringParser("["),
+          'space',
+          'alt',
+          new \Graphit\Parser\StringParser("]"),
+          'space',
+        ),
+        array($this, 'optionalGenerator')
+      ),
+      'space' => new \Graphit\Parser\GreedyMultiParser(
+        new \Graphit\Parser\LazyAltParser(
+          array(
+            'whitespace',
+            'comment',
+          )
+        ), 0, null,
+        array($this, 'spaceGenerator')
+      ),
+      'whitespace' => new \Graphit\Parser\RegexParser(
+        '/^[ \\t\\r\\n]+/',
+        array($this, 'whitespaceGenerator')
+      ),
+      'comment' => new \Graphit\Parser\RegexParser(
+        '/^(\\(\\*\\s+[^*]*\\s+\\*\\)|\\(\\* \\*\\)|\\(\\*\\*\\))/',
+        array($this, 'commentGenerator')
+      ),
+    );
+    parent::__construct($s, $internals);
+  }
+
+  protected function syntaxGenerator($result)
+  {
+    $result = $this->internals['syntax']->defaultGenerator($result);
+    $result['t'] = 'syntax';
+    return $result;
+  }
+
+  protected function rulesGenerator($result)
+  {
+    $result = $this->internals['rules']->defaultGenerator($result);
+    $result['t'] = 'rules';
+    return $result;
+  }
+
+  protected function ruleGenerator($result)
+  {
+    $result = $this->internals['rule']->defaultGenerator($result);
+    $result['t'] = 'rule';
+    return $result;
+  }
+
+  protected function altGenerator($result)
+  {
+    $result = $this->internals['alt']->defaultGenerator($result);
+    $result['t'] = 'alt';
+    return $result;
+  }
+
+  protected function pipeconclistGenerator($result)
+  {
+    $result = $this->internals['pipeconclist']->defaultGenerator($result);
+    $result['t'] = 'pipeconclist';
+    return $result;
+  }
+
+  protected function pipeconcGenerator($result)
+  {
+    $result = $this->internals['pipeconc']->defaultGenerator($result);
+    $result['t'] = 'pipeconc';
+    return $result;
+  }
+
+  protected function concGenerator($result)
+  {
+    $result = $this->internals['conc']->defaultGenerator($result);
+    $result['t'] = 'conc';
+    return $result;
+  }
+
+  protected function commatermlistGenerator($result)
+  {
+    $result = $this->internals['commatermlist']->defaultGenerator($result);
+    $result['t'] = 'commatermlist';
+    return $result;
+  }
+
+  protected function commatermGenerator($result)
+  {
+    $result = $this->internals['commaterm']->defaultGenerator($result);
+    $result['t'] = 'commaterm';
+    return $result;
+  }
+
+  protected function termGenerator($result)
+  {
+    $result = $this->internals['term']->defaultGenerator($result);
+    $result['t'] = 'term';
+    return $result;
+  }
+
+  protected function barewordGenerator($result)
+  {
+    $result = $this->internals['bareword']->defaultGenerator($result);
+    $result['t'] = 'bareword';
+    return $result;
+  }
+
+  protected function sqGenerator($result)
+  {
+    $result = $this->internals['sq']->defaultGenerator($result);
+    $result['t'] = 'sq';
+    return $result;
+  }
+
+  protected function dqGenerator($result)
+  {
+    $result = $this->internals['dq']->defaultGenerator($result);
+    $result['t'] = 'dq';
+    return $result;
+  }
+
+  protected function regexGenerator($result)
+  {
+    $result = $this->internals['regex']->defaultGenerator($result);
+    $result['t'] = 'regex';
+    return $result;
+  }
+
+  protected function groupGenerator($result)
+  {
+    $result = $this->internals['group']->defaultGenerator($result);
+    $result['t'] = 'group';
+    return $result;
+  }
+
+  protected function repetitionGenerator($result)
+  {
+    $result = $this->internals['repetition']->defaultGenerator($result);
+    $result['t'] = 'repetition';
+    return $result;
+  }
+
+  protected function optionalGenerator($result)
+  {
+    $result = $this->internals['optional']->defaultGenerator($result);
+    $result['t'] = 'optional';
+    return $result;
+  }
+
+  protected function spaceGenerator($result)
+  {
+    $result = $this->internals['space']->defaultGenerator($result);
+    $result['t'] = 'space';
+    return $result;
+  }
+
+  protected function whitespaceGenerator($result)
+  {
+    $result = $this->internals['whitespace']->defaultGenerator($result);
+    $result['t'] = 'whitespace';
+    return $result;
+  }
+
+  protected function commentGenerator($result)
+  {
+    $result = $this->internals['comment']->defaultGenerator($result);
+    $result['t'] = 'comment';
+    return $result;
+  }
+}
diff --git a/src/EmptyParser.php b/src/EmptyParser.php
new file mode 100644 (file)
index 0000000..47a4601
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+
+namespace Graphit\Parser;
+
+class EmptyParser extends BaseParser
+{
+  public function __construct($generator = null)
+  {
+    $this->description = 'new '.get_class().'()';
+
+    parent::__construct(array(), $generator);
+  }
+
+  protected function accept($string, $p)
+  {
+    return array(
+      'r' => null,
+      'p' => $p,
+    );
+  }
+
+  protected function evalAcceptsEmpty()
+  {
+    return true;
+  }
+
+  protected function firstSet()
+  {
+    return array();
+  }
+}
diff --git a/src/GrammerException.php b/src/GrammerException.php
new file mode 100644 (file)
index 0000000..4c20c8c
--- /dev/null
@@ -0,0 +1,5 @@
+<?php
+
+namespace Graphit\Parser;
+
+class GrammerException extends \Exception { }
diff --git a/src/GrammerParser.php b/src/GrammerParser.php
new file mode 100644 (file)
index 0000000..d1a8b04
--- /dev/null
@@ -0,0 +1,160 @@
+<?php
+
+namespace Graphit\Parser;
+
+class GrammerParser extends BaseParser
+{
+  protected $s;
+
+  public function __construct($s, array $internals, $generator = null)
+  {
+    $this->description = 'new '.get_class().'('.$this->serializeInternals($internals).')';
+    parent::__construct($internals, $generator);
+
+    $this->s = (string)$s;
+    if ( !isset($this->internals[$this->s])) {
+      throw new GrammerException("No parser given for start rule {$this->s}!");
+    }
+
+    $this->resolveParserNames();
+
+    $this->floodAcceptsEmpty();
+
+    $this->testInifinitGreedy();
+
+    foreach ($this->internals as $internal) {
+      $done = array();
+      $todo = array($internal);
+      while ($current = array_shift($todo)) {
+        $done[] = $current;
+
+        foreach ($current->firstSet() as $next) {
+          if ($next === $current) {
+            throw new GrammerException("Grammer is left recursive in {$internal}!");
+          }
+
+          if (in_array($next, $done, true)) {
+            continue;
+          }
+
+          $todo[] = $next;
+        }
+      }
+    }
+  }
+
+  protected function resolveParserNames()
+  {
+    $done = array();
+    $todo = array($this);
+    while ($current = array_shift($todo)) {
+      $done[] = $current;
+
+      foreach ($current->internals as $key => $internal) {
+        if ($internal instanceof BaseParser) {
+          if ( !in_array($internal, $done, true) && !in_array($internal, $todo, true)) {
+            $todo[] = $internal;
+          }
+          continue;
+        }
+
+        if ( !isset($this->internals[$internal])) {
+          throw new GrammerException("No parser given for rule {$internal} used by {$current}!");
+        }
+
+        $current->internals[$key] = $this->internals[$internal];
+      }
+    }
+  }
+
+  protected function floodAcceptsEmpty()
+  {
+    $change = true;
+    while ($change) {
+      $change = false;
+
+      $done = array();
+      $todo = array($this);
+      while ($current = array_shift($todo)) {
+        $done[] = $current;
+
+        foreach ($current->internals as $internal) {
+          if ($internal->acceptsEmpty) {
+            continue;
+          }
+          if ( !in_array($internal, $done, true) && !in_array($internal, $todo, true)) {
+            $todo[] = $internal;
+          }
+          if ( !$internal->evalAcceptsEmpty()) {
+            continue;
+          }
+
+          $internal->acceptsEmpty = true;
+          $change = true;
+          break;
+        }
+
+        if ($change) {
+          break;
+        }
+      }
+    }
+  }
+
+  protected function testInifinitGreedy()
+  {
+    $done = array();
+    $todo = $this->internals;
+    while ($current = array_shift($todo)) {
+      $done[] = $current;
+
+      foreach ($current->internals as $internal) {
+        if ( !in_array($internal, $done, true) && !in_array($internal, $todo, true)) {
+          $todo[] = $internal;
+        }
+      }
+
+      if ( !$current instanceof GreedyMultiParser) {
+        continue;
+      }
+      if ($current->getOptional() !== null) {
+        continue;
+      }
+      if ($current->internals[0]->acceptsEmpty) {
+        throw new GrammerException("{$current} will cause infinite loops, because the internal parser accepts empty!");
+      }
+    }
+  }
+
+  public function ast($code)
+  {
+    if ($r = $this->parse($code, 0)) {
+      if ($r['p'] !== strlen($code)) {
+        return false;
+      }
+
+      return $r['r']['r'];
+    }
+    return false;
+  }
+
+  protected function accept($string, $p)
+  {
+    if ($r = $this->internals[$this->s]->parse($string, $p)) {
+      return $r;
+    }
+
+    return false;
+  }
+
+  protected function evalAcceptsEmpty()
+  {
+    return $this->internals[$this->s]->acceptsEmpty;
+  }
+
+  protected function firstSet()
+  {
+    return array($this->internals[$this->s]);
+  }
+}
+
diff --git a/src/GreedyMultiParser.php b/src/GreedyMultiParser.php
new file mode 100644 (file)
index 0000000..1e4aa73
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+
+namespace Graphit\Parser;
+
+class GreedyMultiParser extends BaseParser
+{
+  protected $lower;
+
+  protected $optional;
+
+  public function __construct($internal, $lower = 0, $optional = null, $generator = null)
+  {
+    $this->lower = $lower;
+    $this->optional = $optional;
+
+    $this->description = 'new '.get_class()."({$internal}, {$lower}, {$optional})";
+    parent::__construct(array($internal), $generator);
+  }
+
+  public function getLower()
+  {
+    return $this->lower;
+  }
+
+  public function getOptional()
+  {
+    return $this->optional;
+  }
+
+  protected function accept($string, $p)
+  {
+    $r = array();
+    for ($j = 0; $j < $this->lower; $j++) {
+      if ($i = $this->internals[0]->parse($string, $p)) {
+        $r[] = $i['r'];
+        $p = $i['p'];
+
+        continue;
+      }
+      return false;
+    }
+
+    for ($j = 0; $this->optional === null || $j < $this->optional; $j++) {
+      if ($i = $this->internals[0]->parse($string, $p)) {
+        $r[] = $i['r'];
+        $p = $i['p'];
+
+        continue;
+      }
+      break;
+    }
+
+    return array(
+      'r' => $r,
+      'p' => $p,
+    );
+  }
+
+  protected function evalAcceptsEmpty()
+  {
+    return $this->lower == 0 || $this->internals[0]->acceptsEmpty;
+  }
+
+  protected function firstSet()
+  {
+    return $this->internals;
+  }
+}
diff --git a/src/LazyAltParser.php b/src/LazyAltParser.php
new file mode 100644 (file)
index 0000000..7c2efcf
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+
+namespace Graphit\Parser;
+
+class LazyAltParser extends BaseParser
+{
+  public function __construct(array $internals, $generator = null)
+  {
+    if ( !$internals) {
+      throw new GrammerException('At least one internal parser is needed!');
+    }
+
+    $this->description = 'new '.get_class().'('.$this->serializeInternals($internals).')';
+    parent::__construct($internals, $generator);
+  }
+
+  protected function accept($string, $p)
+  {
+    foreach ($this->internals as $internal) {
+      if ($i = $internal->parse($string, $p)) {
+        return array(
+          'r' => $i['r'],
+          'p' => $i['p'],
+        );
+      }
+    }
+    return false;
+  }
+
+  protected function evalAcceptsEmpty()
+  {
+    foreach ($this->internals as $internal) {
+      if ($internal->acceptsEmpty) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  protected function firstSet()
+  {
+    return $this->internals;
+  }
+}
diff --git a/src/RegexParser.php b/src/RegexParser.php
new file mode 100644 (file)
index 0000000..43aae7b
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+
+namespace Graphit\Parser;
+
+class RegexParser extends BaseParser
+{
+  protected $pattern;
+
+  public function __construct($pattern, $generator = null)
+  {
+    $this->pattern = (string)$pattern;
+    if (substr($this->pattern, 1, 1) !== '^') {
+      throw new GrammerException('Pattern {$this->pattern} must anchor at the beginning of the string!');
+    }
+
+    $this->description = 'new '.get_class().'('.var_export($this->pattern, true).')';
+    parent::__construct(array(), $generator);
+  }
+
+  public function getPattern()
+  {
+    return $this->pattern;
+  }
+
+  protected function accept($string, $p)
+  {
+    $matches = array();
+    if (preg_match($this->pattern, substr($string, $p), $matches) !== 1) {
+      return false;
+    }
+
+    return array(
+      'r' => $matches,
+      'p' => $p + strlen($matches[0]),
+    );
+  }
+
+  protected function evalAcceptsEmpty()
+  {
+    return preg_match($this->pattern, '') === 1;
+  }
+
+  protected function firstSet()
+  {
+    return array();
+  }
+}
+
diff --git a/src/StringParser.php b/src/StringParser.php
new file mode 100644 (file)
index 0000000..d63e862
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+
+namespace Graphit\Parser;
+
+class StringParser extends BaseParser
+{
+  protected $needle;
+
+  public function __construct($needle, $generator = null)
+  {
+    $this->needle = (string)$needle;
+
+    $this->description = 'new '.get_class().'('.var_export($this->needle, true).')';
+    parent::__construct(array(), $generator);
+  }
+
+  public function getNeedle()
+  {
+    return $this->needle;
+  }
+
+  protected function accept($string, $p)
+  {
+    if ($this->needle !== '' && strpos($string, $this->needle, $p) !== $p) {
+      return false;
+    }
+
+    return array(
+      'r' => $this->needle,
+      'p' => $p + strlen($this->needle),
+    );
+  }
+
+  protected function evalAcceptsEmpty()
+  {
+    return $this->needle === '';
+  }
+
+  protected function firstSet()
+  {
+    return array();
+  }
+}
+
diff --git a/src/ebnf.ebnf b/src/ebnf.ebnf
new file mode 100644 (file)
index 0000000..1dea68d
--- /dev/null
@@ -0,0 +1,60 @@
+
+syntax
+  = space, rules ;
+
+rules
+  = { rule } ;
+
+rule
+  = bareword, space, "=", space, alt, ";", space ;
+
+alt
+  = conc, pipeconclist ;
+
+pipeconclist
+  = { pipeconc } ;
+
+pipeconc
+  = "|", space, conc ;
+
+conc
+  = term, commatermlist ;
+
+commatermlist
+  = { commaterm } ;
+
+commaterm
+  = ",", space, term ;
+
+term
+  = bareword | sq | dq | regex | group | repetition | optional ;
+
+bareword
+  = /^([a-z][a-z ]*[a-z]|[a-z])/, space ;
+
+sq
+  = /^'([^']*)'/, space ;
+
+dq
+  = /^"([^"]*)"/, space ;
+
+regex
+  = /^\/\^([^\/\\]*(\\\/|\\[^\/])?)*\//, space ;
+
+group
+  = "(", space, alt, ")", space ;
+
+repetition
+  = "{", space, alt, "}", space ;
+
+optional
+  = "[", space, alt, "]", space ;
+
+space
+  = { whitespace | comment } ;
+
+whitespace
+  = /^[ \t\r\n]+/ ;
+
+comment
+  = /^(\(\*\s+[^*]*\s+\*\)|\(\* \*\)|\(\*\*\))/ ;