From 7aeb6808bab39e4de39a05c09b7995544043f687 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 10 Feb 2022 14:45:09 -0500 Subject: [PATCH] Implement LeafPattern class --- lib/Docopt.php | 154 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 117 insertions(+), 37 deletions(-) diff --git a/lib/Docopt.php b/lib/Docopt.php index 98e81ff..042dbd8 100644 --- a/lib/Docopt.php +++ b/lib/Docopt.php @@ -3,7 +3,19 @@ declare(strict_types=1); namespace MensBeam\Docopt; /** - * This is as direct a port of Docopt-ng as possible in PHP + * This is as direct a port of Docopt-ng as practical in PHP. + * + * Language notes: + * - An empty list in Python is falsy like an empty array in PHP + * - (None == "") is false in Python, but true in PHP + * - ([1,2] + [3,4]) is equivalent to array_merge([1,2], [3,4]), and this is + * used a lot in Docopt-ng; adding two arrays like this in PHP is allowed, + * but is _NOT_ equivalent + * - ([1,2] * 3) is equivalent to array_merge($arr = [1,2], $arr, $arr); this is rare in Docopt-ng + * - The Python code uses numerous list comprehensions, many of them inefficient + * for their purpose. Some are ported as calls to array_map or array_filter, + * while others are ported as loops for efficiency + * - func(*list) in Python is equivalent to func(...$arr) in PHP, both in calls and definitions */ class Docopt { @@ -121,8 +133,8 @@ class Docopt { # while groups: while ($groups) { # children = groups.pop(0) - $children = array_shift($groups); # parents = [Required, NotRequired, OptionsShortcut, Either, OneOrMore] + $children = array_shift($groups); $parents = [Required::class, NotRequired::class, OptionsShortcut::class, Either::class, OneOrMore::class]; # if any(t in map(type, children) for t in parents): // This line and the next three lines do the following: @@ -234,48 +246,116 @@ class Pattern { } # TSingleMatch = Tuple[Union[int, None], Union["LeafPattern", None]] - +// DEVIATION: This appears to be a type declaration: a fixed-size array of two members with types ?int and ?LeafPattern +// This is not representable directly in PHP, so uses elsewhere are replaced by array type declarations # class LeafPattern(Pattern): - # """Leaf/terminal node of a pattern tree.""" +class LeafPattern extends Pattern { + # def __repr__(self) -> str: + # return "%s(%r, %r)" % (self.__class__.__name__, self.name, self.value) + public function __toString(): string { + // This should be the functional PHP equivalent, depending on circumstance + // TODO: Revisit this + return serialize($this); + } -# def __repr__(self) -> str: -# return "%s(%r, %r)" % (self.__class__.__name__, self.name, self.value) + # def single_match(self, left: List["LeafPattern"]) -> TSingleMatch: + # raise NotImplementedError # pragma: no cover + public function single_match(array $left): array { + throw new \Exception("Not implemented"); + } -# def single_match(self, left: List["LeafPattern"]) -> TSingleMatch: -# raise NotImplementedError # pragma: no cover + # def flat(self, *types) -> List["LeafPattern"]: + # return [self] if not types or type(self) in types else [] + public function flat(string ...$types): array { + if (!$types || in_array(get_class($this), $types)) { + return [$this]; + } else { + return []; + } + } -# def flat(self, *types) -> List["LeafPattern"]: -# return [self] if not types or type(self) in types else [] -# def match(self, left: List["LeafPattern"], collected: List["Pattern"] = None) -> Tuple[bool, List["LeafPattern"], List["Pattern"]]: -# collected = [] if collected is None else collected -# increment: Optional[Any] = None -# pos, match = self.single_match(left) -# if match is None or pos is None: -# return False, left, collected -# left_ = left[:pos] + left[(pos + 1) :] -# same_name = [a for a in collected if a.name == self.name] -# if type(self.value) == int and len(same_name) > 0: -# if isinstance(same_name[0].value, int): -# same_name[0].value += 1 -# return True, left_, collected -# if type(self.value) == int and not same_name: -# match.value = 1 -# return True, left_, collected + [match] -# if same_name and type(self.value) == list: -# if type(match.value) == str: -# increment = [match.value] -# if same_name[0].value is not None and increment is not None: -# if isinstance(same_name[0].value, type(increment)): -# same_name[0].value += increment -# return True, left_, collected -# elif not same_name and type(self.value) == list: -# if isinstance(match.value, str): -# match.value = [match.value] -# return True, left_, collected + [match] -# return True, left_, collected + [match] + # def match(self, left: List["LeafPattern"], collected: List["Pattern"] = None) -> Tuple[bool, List["LeafPattern"], List["Pattern"]]: + public function match(array $left, ?array $collected = null): array { + # collected = [] if collected is None else collected + # increment: Optional[Any] = None + # pos, match = self.single_match(left) + $collected = $collected ?? []; + $increment = null; + [$pos, $match] = $this->single_match($left); + # if match is None or pos is None: + # return False, left, collected + if ($match === null || $pos === null) { + return [false, $left, $collected]; + } + # left_ = left[:pos] + left[(pos + 1) :] + $left_ = array_merge(array_slice($left, 0, $pos), array_slice($left, $pos + 1)); + # same_name = [a for a in collected if a.name == self.name] + // DEVIATION: Here the Python original just wants the first match, but + // filters every collected pattern anyway. We will instead just find the first + $same_name = []; + foreach ($collected as $a) { + // NOTE: The type of Pattern::$name is ?string, and in Python (None == "") is false, so a strict equality is the correct operator + if ($a->name === $this->name) { + $same_name[] = $a; + break; + } + } + # if type(self.value) == int and len(same_name) > 0: + # if isinstance(same_name[0].value, int): + # same_name[0].value += 1 + # return True, left_, collected + if (is_int($this->value) && sizeof($same_name) > 0) { + if (is_int($same_name[0]->value)) { + $same_name[0]->value++; + } + return [true, $left_, $collected]; + } + # if type(self.value) == int and not same_name: + # match.value = 1 + # return True, left_, collected + [match] + if (is_int($this->value) && !$same_name) { + $match->value = 1; + return [true, $left_, array_merge($collected, [$match])]; + } + # if same_name and type(self.value) == list: + if ($same_name && is_array($this->value)) { + # if type(match.value) == str: + # increment = [match.value] + if (is_string($match->value)) { + $increment = [$match->value]; + } + # if same_name[0].value is not None and increment is not None: + if ($name_name[0]->value !== null and $increment !== null) { + # if isinstance(same_name[0].value, type(increment)): + # same_name[0].value += increment + // This is a weird way of asking whether the value and + // increment are both lists; increment cannot be any other + // type here per the logic as written, though it is allowed + // to be other types per its initializer + if (is_array($same_name[0]->value) && is_array($increment)) { + $same_name[0]->value = array_merge($same_name[0]->value, $increment); + } + } + # return True, left_, collected + return [true, $left_, $coollected]; + } + # elif not same_name and type(self.value) == list: + elseif (!$same_name && is_array($this->value)) { + # if isinstance(match.value, str): + # match.value = [match.value] + if (is_string($match->value)) { + $match->value = [$match->value]; + } + # return True, left_, collected + [match] + return [true, $left_, array_merge($collected, [$match])]; + } + # return True, left_, collected + [match] + return [true, $left_, array_merge($collected, [$match])]; + } +} # class BranchPattern(Pattern):