function yay_parse(string $source, Directives $directives = null, BlueContext $blueContext = null) : string { if ($gc = gc_enabled()) { gc_disable(); } // important optimization! static $globalDirectives = null; if (null === $globalDirectives) { $globalDirectives = new ArrayObject(); } $directives = $directives ?: new Directives(); $blueContext = $blueContext ?: new BlueContext(); $cg = (object) ['ts' => TokenStream::fromSource($source), 'directives' => $directives, 'cycle' => new Cycle($source), 'globalDirectives' => $globalDirectives, 'blueContext' => $blueContext]; foreach ($cg->globalDirectives as $d) { $cg->directives->add($d); } traverse(midrule(function (TokenStream $ts) use($directives, $blueContext) { $token = $ts->current(); tail_call: if (null === $token) { return; } // skip when something looks like a new macro to be parsed if ('macro' === (string) $token) { return; } // here we do the 'magic' to match and expand userland macros $directives->apply($ts, $token, $blueContext); $token = $ts->next(); goto tail_call; }), consume(chain(token(T_STRING, 'macro')->as('declaration'), optional(repeat(rtoken('/^·\\w+$/')))->as('tags'), lookahead(token('{')), commit(chain(braces()->as('pattern'), operator('>>'), braces()->as('expansion')))->as('body'), optional(token(';'))), CONSUME_DO_TRIM)->onCommit(function (Ast $macroAst) use($cg) { $scope = Map::fromEmpty(); $tags = Map::fromValues(array_map('strval', $macroAst->{'tags'})); $pattern = new Pattern($macroAst->{'declaration'}->line(), $macroAst->{'body pattern'}, $tags, $scope); $expansion = new Expansion($macroAst->{'body expansion'}, $tags, $scope); $macro = new Macro($tags, $pattern, $expansion, $cg->cycle); $cg->directives->add($macro); // allocate the userland macro // allocate the userland macro globally if it's declared as global if ($macro->tags()->contains('·global')) { $cg->globalDirectives[] = $macro; } }))->parse($cg->ts); $expansion = (string) $cg->ts; if ($gc) { gc_enable(); } return $expansion; }
function testPush() { $ts = TokenStream::fromSource("<?php 1 2 3 "); $ts->push(new Token(T_LNUMBER, '4')); $ts->push(new Token(T_WHITESPACE, ' ')); $ts->push(new Token(T_LNUMBER, '5')); $ts->push(new Token(T_WHITESPACE, ' ')); $this->assertEquals('<?php 1 2 3 4 5 ', (string) $ts); $ts = TokenStream::fromEmpty(); $ts->push(new Token(T_STRING, 'A')); $ts->push(new Token(T_WHITESPACE, ' ')); $ts->push(new Token(T_STRING, 'B')); $ts->push(new Token(T_WHITESPACE, ' ')); $this->assertEquals('A B ', (string) $ts); $ts->extract($ts->index(), $ts->index()->next->next->next); $this->assertEquals(' ', (string) $ts); $ts->push(new Token(T_STRING, 'C')); $this->assertEquals(' C', (string) $ts); }
function testastNestedAst() { $ts = TokenStream::fromSource('<?php interface Foo { public abstract function foo(); public abstract static function bar(); function baz(); } '); $modifier = either(token(T_PUBLIC), token(T_STATIC), token(T_PRIVATE)); $modifiers = optional(either(chain($modifier, $modifier), $modifier)); $ast = chain(token(T_OPEN_TAG), chain(token(T_INTERFACE), token(T_STRING)->as('name'), token('{'), optional(repeat(chain(optional(either(repeat(either(token(T_PUBLIC)->as('public'), token(T_STATIC)->as('static'), token(T_ABSTRACT)->as('abstract'))), always(new Token(T_PUBLIC, 'public'))->as('public')))->as('is'), token(T_FUNCTION), token(T_STRING)->as('name'), token('('), token(')'), token(';')), token('}')))->as('methods'), token('}'))->as('interface'))->parse($ts); $this->assertEquals('Foo', (string) $ast->{'interface name'}); $this->assertEquals('foo', (string) $ast->{'interface methods 0 name'}); $this->assertEquals('public', (string) $ast->{'interface methods 0 is public'}); $this->assertEquals('abstract', (string) $ast->{'interface methods 0 is abstract'}); $this->assertEmpty($ast->{'interface methods 0 is static'}); $this->assertEquals('bar', (string) $ast->{'interface methods 1 name'}); $this->assertEquals('public', (string) $ast->{'interface methods 1 is public'}); $this->assertEquals('abstract', (string) $ast->{'interface methods 1 is abstract'}); $this->assertEquals('static', (string) $ast->{'interface methods 1 is static'}); $this->assertEquals('baz', (string) $ast->{'interface methods 2 name'}); $this->assertEquals('public', (string) $ast->{'interface methods 2 is public'}); $this->assertNull($ast->{'interface methods 2 is abstract'}); $this->assertNull($ast->{'interface methods 2 is static'}); }
function testPush() { $ts = TokenStream::fromSource("<?php 1 2 3 "); $ts->push(new Token(T_LNUMBER, '4')); $ts->push(new Token(T_WHITESPACE, ' ')); $ts->push(new Token(T_LNUMBER, '5')); $ts->push(new Token(T_WHITESPACE, ' ')); $this->assertEquals('<?php 1 2 3 4 5 ', (string) $ts); $ts = TokenStream::fromSource('<?php '); $ts->push(new Token(T_STRING, 'A')); $ts->push(new Token(T_WHITESPACE, ' ')); $ts->push(new Token(T_STRING, 'B')); $ts->push(new Token(T_WHITESPACE, ' ')); $this->assertEquals('<?php A B ', (string) $ts); }
function yay_parse(string $source, string $salt) : string { start: if ($gc = gc_enabled()) { gc_disable(); } $ts = TokenStream::fromSource($source); $directives = new Directives(); $cycle = new Cycle($salt); declaration: $from = $ts->index(); if (!($token = $ts->current())) { goto end; } if ($token->contains('macro')) { $declaration = $token; $ts->next(); goto tags; } goto any; tags: $tags = []; while (($token = $ts->current()) && preg_match('/^·\\w+$/', (string) $token)) { $tags[] = $token; $ts->next(); } pattern: $index = $ts->index(); $pattern = []; $level = 1; if (!($token = $ts->current()) || !$token->is('{')) { goto any; } $ts->next(); while (($token = $ts->current()) && ($level += LAYER_DELIMITERS[$token->type()] ?? 0)) { $pattern[] = $token; $token = $ts->step(); } if (!$token || !$token->is('}')) { $ts->jump($index); (new Error(new Expected(new Token('}')), $ts->current(), $ts->last()))->halt(); } $ts->next(); __: // >> $index = $ts->index(); $operator = '>>'; $max = strlen($operator); $buffer = ''; while (mb_strlen($buffer) <= $max && ($token = $ts->current())) { $buffer .= (string) $token; $ts->step(); if ($buffer === $operator) { $ts->skip(T_WHITESPACE); goto expansion; } } $ts->jump($index); (new Error(new Expected(new Token(token::OPERATOR, $operator)), $ts->current(), $ts->last()))->halt(); expansion: $index = $ts->index(); $expansion = []; $level = 1; if (!($token = $ts->current()) || !$token->is('{')) { $ts->jump($index); (new Error(new Expected(new Token('}')), $ts->current(), $ts->last()))->halt(); } $ts->next(); while (($token = $ts->current()) && ($level += LAYER_DELIMITERS[$token->type()] ?? 0)) { $expansion[] = $token; $token = $ts->step(); } if (!$token || !$token->is('}')) { $ts->jump($index); (new Error(new Expected(new Token('}')), $ts->current(), $ts->last()))->halt(); } // optional ';' $token = $ts->next(); if ($token->is(';')) { $ts->next(); } // cleanup $ts->unskip(...TokenStream::SKIPPABLE); $ts->skip(T_WHITESPACE); $ts->extract($from, $ts->index()); $directives->add(new Macro($declaration->line(), $tags, $pattern, $expansion, $cycle)); goto declaration; any: if ($token) { $directives->apply($ts); $ts->next(); } goto declaration; end: $expansion = (string) $ts; if ($gc) { gc_enable(); } return $expansion; }
function expand(TokenStream $ts, Context $context) : TokenStream { $ts = TokenStream::fromSource(yay_parse('<?php ' . (string) $ts, $context->get('directives'), $context->get('blueContext'))); $ts->shift(); return $ts; }