phplrt 4.0

Tokens and Channels

A token is the smallest thing a lexer produces. It is immutable, and it carries five pieces of information.

1foreach ($lexer->lex(new Source('23 + 42')) as $token) {
2    $token->id;      // 0       - which definition matched
3    $token->name;    // T_DIGIT - its human-readable name, or null
4    $token->value;   // "23"    - the exact text that matched
5    $token->offset;  // 0       - where it starts, in bytes
6    $token->size;    // 2       - how long it is, in bytes
7    $token->channel; // Channel::Default
8}

Identifiers, Not Names

Tokens are compared by identifier, never by name:

1if ($token->id === MyParser::T_DIGIT) {
2    // ...
3}

The identifier is just the position of the token's definition in the lexer, assigned when the lexer is built. Names exist for you and for error messages; the parser does not use them at all, which is why a token is allowed to have no name:

$builder->addPattern('\s++')->hide(); // anonymous, and that is fine

System tokens use negative identifiers, so they can never collide with yours:

Token Identifier Channel
EndOfInputToken -1 EndOfInput
UnknownToken -2 Unknown

Offsets

offset and size are counted in bytes, not in characters, so the two of them address the fragment directly:

1$content = $source->content;
2
3// The exact fragment the token was read from
4$text = \substr($content, $token->offset, $token->size);
5
6// Where the next token starts
7$next = $token->offset + $token->size;

For an ordinary token size is simply strlen($value). It differs only for a token that entered a nested lexer, which is as large as everything that lexer read.

Printing

Tokens are Stringable, and they print in a form meant for error messages:

echo $token;
1"23" (T_DIGIT)      // a named token
2"@" (unknown token) // unrecognized input
3end of input        // the terminal token

Long values are cut off, and control characters are escaped, so you can put a token straight into a message without worrying about what is in it.

Channels

A channel is a label on a token. The lexer decides which channels it reports, and a token on any other channel is read like any other - so the offsets stay right - and then stepped over.

There are four built-in channels:

Channel Meaning
Default An ordinary token. This is what a grammar is written in terms of.
Hidden Read, but not reported: whitespace, comments.
Unknown Text the lexer did not recognize.
EndOfInput The terminal token, exactly one per stream.
1use Phplrt\Contracts\Lexer\Channel;
2
3$builder->addPattern('\s++')->setChannel(Channel::Hidden);
4$builder->addPattern('\s++')->hide();  // the same, shorter
5$builder->addPattern('\s++')->show();  // back to Default

Custom Channels

Hiding a token throws it away. Sometimes you want to keep it, just told apart from the code around it - documentation comments are the usual example. Give it a channel of its own:

1$builder->addPattern('\d++', 'T_DIGIT');
2$builder->addPattern('//[^\n]*+', 'T_COMMENT')->setChannel('comments');
3$builder->addPattern('\s++')->hide();
4
5foreach ($lexer->lex(new Source("1 // hi\n2")) as $token) {
6    echo $token->name, ' on ', $token->channel->name, "\n";
7}
1T_DIGIT on Default
2T_COMMENT on comments
3T_DIGIT on Default

T_COMMENT is in the stream like any other token, and the channel is what tells it apart: a documentation generator reads the comments tokens and ignores the rest.

Choosing What Is Reported

A lexer reports every channel except the ones it is told to skip, and by default that is Hidden alone:

1use Phplrt\Lexer\Lexer;
2
3Lexer::DEFAULT_SKIP_CHANNELS; // [Channel::Hidden]

Nothing stands between the lexer and the grammar, so this is also the answer to "what does the parser see": whatever the lexer reports.

The list is a setting of the lexer, so it goes where the lexer is put together:

 1use Phplrt\Lexer\Builder\Transformer\RuntimeLexerTransformer;
 2
 3$result = $builder->build();
 4
 5// Reports everything but the hidden tokens
 6$lexer = $result->toLexer();
 7
 8// Reports everything, hidden tokens included
 9$verbose = new RuntimeLexerTransformer(skip: [])
10    ->transform($result);

Channels are matched by name, so a custom one is skipped by naming it - the instance you construct does not have to be the one the token carries:

1use Phplrt\Contracts\Lexer\Channel;
2use Phplrt\Contracts\Lexer\UserDefinedChannel;
3
4$parsing = new RuntimeLexerTransformer(skip: [
5    Channel::Hidden,
6    new UserDefinedChannel('comments'),
7])->transform($result);

One description therefore gives you as many lexers as there are readers of the source: one leaving the comments out for the parser, and one reporting them for the tool that wants them.

That is where the list ends up anyway - Lexer takes it as an argument of its own, so a lexer assembled without the builder is configured the same way:

1use Phplrt\Lexer\Lexer;
2
3$lexer = new Lexer(
4    pattern: $result->pattern,
5    channels: $result->channels,
6    names: $result->names,
7    skip: [],
8);

Skipping happens while the source is being read, so a token nobody is going to see is not built in the first place.

Captures

If a token's pattern has capturing groups, whatever they matched is on the token:

1$builder->addPattern('"([^"]*)"', 'T_STRING');
2$builder->addPattern('(\d++)\.(\d++)', 'T_FLOAT');
3
4// "hi" 3.14
5$string->captures; // ["hi"]
6$float->captures;  // ["3", "14"]

Captures are numbered per token, starting at zero - the first group of this token is captures[0], no matter how many groups the tokens above it have.

This saves you from parsing the value twice:

1// Instead of trimming the quotes off $token->value by hand
2$content = $token->captures[0];

A group that matched nothing still counts, so the numbering stays stable:

1$builder->addPattern('(\+|-)?(\d++)', 'T_NUMBER');
2
3// "42"  => captures: ["", "42"]
4// "-42" => captures: ["-", "42"]

The Interface

Write against the contract, not the implementation:

1use Phplrt\Contracts\Lexer\TokenInterface;
2
3function describe(TokenInterface $token): string
4{
5    return \sprintf('%s at %d', $token->name ?? 'anonymous', $token->offset);
6}

Phplrt\Lexer\Token\Token is the standard implementation; TokenEmbedding extends it for nested lexers.