1<?php declare(strict_types=1); 2 3/////////////////////////////// 4/// Utility regex constants /// 5/////////////////////////////// 6 7const LIB = '(?(DEFINE) 8 (?<singleQuotedString>\'[^\\\\\']*+(?:\\\\.[^\\\\\']*+)*+\') 9 (?<doubleQuotedString>"[^\\\\"]*+(?:\\\\.[^\\\\"]*+)*+") 10 (?<string>(?&singleQuotedString)|(?&doubleQuotedString)) 11 (?<comment>/\*[^*]*+(?:\*(?!/)[^*]*+)*+\*/) 12 (?<code>\{[^\'"/{}]*+(?:(?:(?&string)|(?&comment)|(?&code)|/)[^\'"/{}]*+)*+}) 13)'; 14 15const PARAMS = '\[(?<params>[^[\]]*+(?:\[(?¶ms)\][^[\]]*+)*+)\]'; 16const ARGS = '\((?<args>[^()]*+(?:\((?&args)\)[^()]*+)*+)\)'; 17 18/////////////////////////////// 19/// Preprocessing functions /// 20/////////////////////////////// 21 22function preprocessGrammar($code) { 23 $code = resolveNodes($code); 24 $code = resolveMacros($code); 25 $code = resolveStackAccess($code); 26 $code = str_replace('$this', '$self', $code); 27 28 return $code; 29} 30 31function resolveNodes($code) { 32 return preg_replace_callback( 33 '~\b(?<name>[A-Z][a-zA-Z_\\\\]++)\s*' . PARAMS . '~', 34 function ($matches) { 35 // recurse 36 $matches['params'] = resolveNodes($matches['params']); 37 38 $params = magicSplit( 39 '(?:' . PARAMS . '|' . ARGS . ')(*SKIP)(*FAIL)|,', 40 $matches['params'] 41 ); 42 43 $paramCode = ''; 44 foreach ($params as $param) { 45 $paramCode .= $param . ', '; 46 } 47 48 return 'new ' . $matches['name'] . '(' . $paramCode . 'attributes())'; 49 }, 50 $code 51 ); 52} 53 54function resolveMacros($code) { 55 return preg_replace_callback( 56 '~\b(?<!::|->)(?!array\()(?<name>[a-z][A-Za-z]++)' . ARGS . '~', 57 function ($matches) { 58 // recurse 59 $matches['args'] = resolveMacros($matches['args']); 60 61 $name = $matches['name']; 62 $args = magicSplit( 63 '(?:' . PARAMS . '|' . ARGS . ')(*SKIP)(*FAIL)|,', 64 $matches['args'] 65 ); 66 67 if ('attributes' === $name) { 68 assertArgs(0, $args, $name); 69 return '$this->getAttributes($this->tokenStartStack[#1], $this->tokenEndStack[$stackPos])'; 70 } 71 72 if ('stackAttributes' === $name) { 73 assertArgs(1, $args, $name); 74 return '$this->getAttributes($this->tokenStartStack[' . $args[0] . '], ' 75 . ' $this->tokenEndStack[' . $args[0] . '])'; 76 } 77 78 if ('init' === $name) { 79 return '$$ = array(' . implode(', ', $args) . ')'; 80 } 81 82 if ('push' === $name) { 83 assertArgs(2, $args, $name); 84 85 return $args[0] . '[] = ' . $args[1] . '; $$ = ' . $args[0]; 86 } 87 88 if ('pushNormalizing' === $name) { 89 assertArgs(2, $args, $name); 90 91 return 'if (' . $args[1] . ' !== null) { ' . $args[0] . '[] = ' . $args[1] . '; } $$ = ' . $args[0] . ';'; 92 } 93 94 if ('toBlock' == $name) { 95 assertArgs(1, $args, $name); 96 97 return 'if (' . $args[0] . ' instanceof Stmt\Block) { $$ = ' . $args[0] . '->stmts; } ' 98 . 'else if (' . $args[0] . ' === null) { $$ = []; } ' 99 . 'else { $$ = [' . $args[0] . ']; }'; 100 } 101 102 if ('parseVar' === $name) { 103 assertArgs(1, $args, $name); 104 105 return 'substr(' . $args[0] . ', 1)'; 106 } 107 108 if ('parseEncapsed' === $name) { 109 assertArgs(3, $args, $name); 110 111 return 'foreach (' . $args[0] . ' as $s) { if ($s instanceof Node\InterpolatedStringPart) {' 112 . ' $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, ' . $args[1] . ', ' . $args[2] . '); } }'; 113 } 114 115 if ('makeNop' === $name) { 116 assertArgs(1, $args, $name); 117 118 return $args[0] . ' = $this->maybeCreateNop($this->tokenStartStack[#1], $this->tokenEndStack[$stackPos])'; 119 } 120 121 if ('makeZeroLengthNop' == $name) { 122 assertArgs(1, $args, $name); 123 124 return $args[0] . ' = $this->maybeCreateZeroLengthNop($this->tokenPos);'; 125 } 126 127 return $matches[0]; 128 }, 129 $code 130 ); 131} 132 133function assertArgs($num, $args, $name) { 134 if ($num != count($args)) { 135 die('Wrong argument count for ' . $name . '().'); 136 } 137} 138 139function resolveStackAccess($code) { 140 $code = preg_replace('/\$\d+/', '$this->semStack[$0]', $code); 141 $code = preg_replace('/#(\d+)/', '$$1', $code); 142 return $code; 143} 144 145function removeTrailingWhitespace($code) { 146 $lines = explode("\n", $code); 147 $lines = array_map('rtrim', $lines); 148 return implode("\n", $lines); 149} 150 151////////////////////////////// 152/// Regex helper functions /// 153////////////////////////////// 154 155function regex($regex) { 156 return '~' . LIB . '(?:' . str_replace('~', '\~', $regex) . ')~'; 157} 158 159function magicSplit($regex, $string) { 160 $pieces = preg_split(regex('(?:(?&string)|(?&comment)|(?&code))(*SKIP)(*FAIL)|' . $regex), $string); 161 162 foreach ($pieces as &$piece) { 163 $piece = trim($piece); 164 } 165 166 if ($pieces === ['']) { 167 return []; 168 } 169 170 return $pieces; 171} 172