s = $s; } protected function expected(string $expected): ValueException { return new ValueException("expected $expected, got $this->s"); } protected function unexpected(string $value): ValueException { return new ValueException("unexpected $value"); } protected $s; #~~~~ const SPACES_PATTERN = '/^\s+/'; protected function skipSpaces(): void { if (preg_match(self::SPACES_PATTERN, $this->s, $ms)) { $this->s = substr($this->s, strlen($ms[0])); } } #~~~~ protected function isLiteral(string $literal): bool { return substr($this->s, 0, strlen($literal)) === $literal; } protected function skipLiteral(string $literal): void { $pos = strlen($literal); if (substr($this->s, 0, $pos) === $literal) { $this->s = substr($this->s, $pos); } else { throw $this->expected($literal); } $this->skipSpaces(); } #~~~~ const NAME_PATTERN = '/^\S+/'; protected function isName(): bool { if (!preg_match(self::NAME_PATTERN, $this->s, $ms)) return false; $name = $ms[0]; return !in_array($name, ['(', ')', '$']); } protected function parseName(): string { if (!preg_match(self::NAME_PATTERN, $this->s, $ms)) { throw $this->expected(""); } $name = $ms[0]; $this->s = substr($this->s, strlen($name)); $this->skipSpaces(); return $name; } #~~~~ const STRING_PATTERN = "/^'([^']*)'/"; protected function isString(): bool { return preg_match(self::STRING_PATTERN, $this->s, $ms); } protected function parseString(): string { if (!preg_match(self::STRING_PATTERN, $this->s, $ms)) { throw $this->expected(""); } $this->s = substr($this->s, strlen($ms[0])); $this->skipSpaces(); return $ms[1]; } #~~~~ protected function parseNames(): array { if ($this->isName()) return [$this->parseName()]; $names = []; if ($this->isLiteral('(')) { $this->skipLiteral('('); while ($this->isName()) { $names[] = $this->parseName(); if ($this->isLiteral('$')) $this->skipLiteral('$'); } $this->skipLiteral(')'); } else { $names[] = $this->parseName(); } return $names; } protected function parseStrings(): array { if ($this->isString()) return [$this->parseString()]; $strings = []; if ($this->isLiteral('(')) { $this->skipLiteral('('); while ($this->isString()) { $strings[] = $this->parseString(); } $this->skipLiteral(')'); } else { $strings[] = $this->parseString(); } return $strings; } }