Skip to content

Make compatible with PHP's pattern matching RFC#2

Open
thekid wants to merge 28 commits intomasterfrom
feature/rfc-compatibility
Open

Make compatible with PHP's pattern matching RFC#2
thekid wants to merge 28 commits intomasterfrom
feature/rfc-compatibility

Conversation

@thekid
Copy link
Collaborator

@thekid thekid commented Dec 21, 2025

This PR extends the basic type matching capabilities and implements all of https://wiki.php.net/rfc/pattern-matching

Implementation status

Notes

  • The PHP PR currently uses a different match syntax as the one described in the RFC, see here
  • The PHP PR currently includes the range syntax listed under Future scope in the RFC

See also

@thekid thekid marked this pull request as draft December 21, 2025 15:48
@thekid thekid added the enhancement New feature or request label Dec 21, 2025
@thekid thekid marked this pull request as ready for review December 21, 2025 19:19
@thekid
Copy link
Collaborator Author

thekid commented Dec 21, 2025

In Python, the following works:

match command.split():
    case ["drop", *objects]:
        for obj in objects:
            character.drop(obj, current_room)
    # The rest of your commands go here

Maybe we should be able to bind to variables when using the spread operator, too?


The rest pattern gathers all unmatched values into an array and passes it to the binding variable:

if ([1, 2, 3] is [1, ...$rest]) {
  var_dump($rest); // [2, 3]
}

if (['one' => 1, 'two' => 2] is (['one' => 1, ...$rest]) {
  var_dump($rest); // ['two' => 2]
}

🧪 https://gist.github.com/thekid/5f85c8ec6e14644b06cb9b100644529e


See also:

@thekid
Copy link
Collaborator Author

thekid commented Dec 23, 2025

Variable bindings can replace destructuring assignments in most cases:

list() is Note
[$a, $b]= $list $list is [$a, $b]
[$a, , $b]= $list $list is [$a, mixed, $b]
[[$a, $b], $c]= $nested $nested is [[$a, $b], $c]
['key' => $key]= $map $map is ['key' => $key]
[$a[]]= $list *Can only bind variables *
[$a['key']]= $list *Can only bind variables *
[$this->member]= $list *Can only bind variables *

The is operator also has the benefit of not raising errors when $list does not contain the number of elements necessary for destructuring, and instead returns false:

if ($list is [$a, $b, ...]) {
  var_dump($a, $b);
}

// Equivalent of, guarded by type and length checks to prevent errors
if (is_array($list) && sizeof($list) >= 2) {
  [$a, $b]= $list;
  var_dump($a, $b);
}

@thekid
Copy link
Collaborator Author

thekid commented Dec 24, 2025

Following the idea of replacing destructuring assignments, I wanted to see whether I would be able to parse Composer files. Here's what I came up with:

use lang\FormatException;
use util\cmd\Console;

class ComposerFile {

  private function __construct(
    public private(set) string $name,
    public private(set) ?string $type,
    public private(set) array $dependencies,
    public private(set) array $development= [],
    public private(set) array $scripts= [],
  ) { }

  private static function parse(string $input): self {
    if (json_decode($input, true) is [
      'name'        => $name & string,
      'type'        => $type & ?string ?? null,
      'require'     => $dependencies & array,
      'require-dev' => $development & array ?? [],
      'scripts'     => $scripts & array ?? [],
      ...
    ]) return new self($name, $type, $dependencies, $development, $scripts);

    throw new FormatException('Cannnot parse given input');
  }

  public static function main(array $args): void {
    file_get_contents($args[0])
      |> self::parse(...)
      |> Console::writeLine(...)
    ;
  }
}

The missing piece is having a way to make certain keys optional, which I implemented using the null-coalescing operator, inspired by https://wiki.php.net/rfc/destructuring_coalesce. Note: This RFC was declined for confusing syntax, see https://externals.io/message/118829, we might need a different approach here too. JS uses = <default>, see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring


🧪 https://gist.github.com/thekid/97646f250ac2868fb98052019fe8973d (with ?? <default>)
🧪 https://gist.github.com/thekid/12b2eae6cce613b9f75928f93f246370 (with = <default>)

@thekid
Copy link
Collaborator Author

thekid commented Dec 25, 2025

Another interesting idea is integrating is with catch, see https://github.com/tc39/proposal-pattern-matching?tab=readme-ov-file#integration-with-catch

try {
  // ...
} catch ($e is Error(status: 404)) {
  echo 'Not found!';
}

🧪 https://gist.github.com/thekid/6f6798bc05995caac6b7e825d6d5a76f

@thekid
Copy link
Collaborator Author

thekid commented Dec 28, 2025

Supporting implementations of the ArrayAccess and Countable interfaces in array structure patterns may be worthwhile:

if (new ArrayObject([1, 2, 3]) is [$a, $b, $c]) {
  var_dump($a, $b, $c); // 1 2 3
}

🧪 https://gist.github.com/thekid/773803d84b357799ffd65f855bc0181c

@thekid
Copy link
Collaborator Author

thekid commented Jan 31, 2026

The discussion on the mailing list seems to favor match (<expr>) { is <pattern> => <return> } over the match (<expr>) is { ... } variant:

With a per-arm keyword, one could do this:

$result = match ($somevar) {
  is Foo => 'foo', // This matches against the pattern Foo, which is a class name.
  Bar => 'bar', // This matches === against the constant Bar, whose value is whatever.
};

The main reason we cannot just auto-detect whether it's a pattern is the confusion between a class name and constant, since a constant is a legal literal value for an identity match. With per-arm, you can opt-in to pattern matching individually, at the cost of having to remember to repeat the keyword on every line. With per-block, you only have to add the keyword once at the cost of not being able to selectively use patterns or identity matching.

See https://externals.io/message/129490#129920


Implementation: https://gist.github.com/thekid/56ccd4057a3aa7c2e32447d076d52fb5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant