phplrt 4.0

Parser Contracts

This document describes a common interface for syntax analysis - turning a source into whatever that source means - together with the interfaces for the errors raised along the way.

The goal is to let anything that uses a parser - a template engine, a configuration loader, a rule engine - depend on the ability to parse rather than on a particular parser, whether it is handwritten, assembled at runtime or generated from a grammar.

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.

1. Specification

1.1 Parser

A parser is an object implementing ParserInterface.

  • The parse() method takes a source implementing ReadableInterface and returns the result of its syntax analysis.
  • The shape of that result is defined by the implementation, which MAY return any value the analyzed source is converted into - an abstract syntax tree, a list of nodes, an array of settings, a number.
  • An implementation SHOULD declare which of those it returns through the TResult template parameter, so that the value is known statically.
1use Phplrt\Contracts\Parser\ParserInterface;
2
3/**
4 * @implements ParserInterface<Node>
5 */
6final class ExpressionParser implements ParserInterface { /* ... */ }
1/**
2 * @param ParserInterface<Node> $parser
3 */
4function walk(ParserInterface $parser, ReadableInterface $source): void
5{
6    $node = $parser->parse($source); // Node, rather than mixed
7}

1.2 Errors

  • Every exception thrown by a parser MUST implement ParserExceptionInterface, internal failures included.
  • An error that occurs before the analysis begins - a source that cannot be recognized, settings that contain errors - is a plain ParserExceptionInterface.
  • An error that occurs after the analysis has begun, and therefore indicates a problem in the analyzed source, MUST implement RuntimeExceptionInterface.

RuntimeExceptionInterface carries the source the error occurred in, the token it occurred on, and the size of the fragment it covers:

  • $length is counted in bytes and starts at the offset of $token.
  • $length MAY be as large as the whole grammar rule the analysis failed on, since a parser fails on a token but the construct at fault is usually larger.
  • $length is null when the size is not known.
 1use Phplrt\Contracts\Parser\Exception\RuntimeExceptionInterface;
 2
 3try {
 4    $ast = $parser->parse($source);
 5} catch (RuntimeExceptionInterface $e) {
 6    \printf(
 7        "%s at offset %d, %d bytes\n",
 8        $e->getMessage(),
 9        $e->token->offset,
10        $e->length ?? $e->token->size,
11    );
12}

Rendering such an error as a snippet of the source is what phplrt/exception does with the same three members.

2. Package

The interfaces described here are provided as part of the phplrt/parser-contracts package:

composer require phplrt/parser-contracts

It contains no parser, and requires nothing but PHP, phplrt/lexer-contracts and phplrt/source-contracts - a syntax error has to name the token it happened on.

A package implementing these interfaces provides the virtual package phplrt/parser-contracts-implementation:

composer require phplrt/parser-contracts-implementation

phplrt/parser provides it, and every parser the compiler generates extends Phplrt\Parser\Parser.

Note that analyze(), which the phplrt parser also has, is deliberately not part of this specification: it is a diagnostic tool rather than part of what makes an object a parser. See Usage.

The package ships a test declaring an anonymous implementation of every interface in it. Changing that test is allowed only in a major release, so no member can be added to any of these interfaces in a minor version.

3. Interfaces

3.1 Phplrt\Contracts\Parser\ParserInterface

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace Phplrt\Contracts\Parser;
 6
 7use Phplrt\Contracts\Parser\Exception\ParserExceptionInterface;
 8use Phplrt\Contracts\Parser\Exception\RuntimeExceptionInterface;
 9use Phplrt\Contracts\Source\ReadableInterface;
10
11/**
12 * Converts a source into the result of its syntax analysis.
13 *
14 * @template TResult of mixed = mixed
15 */
16interface ParserInterface
17{
18    /**
19     * Performs syntax analysis of the given source and returns its result.
20     *
21     * The shape of the result is defined by the implementation, which MAY
22     * return any value the analyzed source is converted into, like an
23     * abstract syntax tree (AST) or a list of its nodes.
24     *
25     * @return TResult the result of the analysis
26     * @throws ParserExceptionInterface if the given source cannot be
27     *         recognized or the parser settings contain errors
28     * @throws RuntimeExceptionInterface if the analyzed source contains errors
29     */
30    public function parse(ReadableInterface $source): mixed;
31}

3.2 Phplrt\Contracts\Parser\Exception\ParserExceptionInterface

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace Phplrt\Contracts\Parser\Exception;
 6
 7/**
 8 * An error of the syntax analysis, including an internal one.
 9 *
10 * Every exception thrown by a parser MUST implement this interface.
11 */
12interface ParserExceptionInterface extends \Throwable {}

3.3 Phplrt\Contracts\Parser\Exception\RuntimeExceptionInterface

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace Phplrt\Contracts\Parser\Exception;
 6
 7use Phplrt\Contracts\Lexer\TokenInterface;
 8use Phplrt\Contracts\Source\ReadableInterface;
 9
10/**
11 * An error that occurs after the syntax analysis has been started and
12 * indicates a problem in the analyzed source.
13 */
14interface RuntimeExceptionInterface extends ParserExceptionInterface
15{
16    /**
17     * The source the error occurred in.
18     */
19    public ReadableInterface $source {
20        get;
21    }
22
23    /**
24     * The token the error occurred on.
25     */
26    public TokenInterface $token {
27        get;
28    }
29
30    /**
31     * The size of the source fragment the error occurred in, in bytes, or
32     * {@see null} in case the size is not known.
33     *
34     * The fragment starts at the offset of the token the error occurred on and
35     * MAY be as large as the whole grammar rule the analysis has failed on.
36     *
37     * @var int<0, max>|null
38     */
39    public ?int $length {
40        get;
41    }
42}

4. See Also