├── src ├── Exception │ ├── NotReadableException.php │ ├── PreprocessException.php │ ├── DirectiveDefinitionException.php │ ├── DirectiveEvaluationException.php │ └── PreprocessorException.php ├── Internal │ ├── Expression │ │ ├── Ast │ │ │ ├── Expression.php │ │ │ ├── Literal │ │ │ │ ├── Literal.php │ │ │ │ ├── IdentifierLiteral.php │ │ │ │ ├── BinIntegerLiteral.php │ │ │ │ ├── HexIntegerLiteral.php │ │ │ │ ├── OctIntegerLiteral.php │ │ │ │ ├── BooleanLiteral.php │ │ │ │ ├── StringLiteral.php │ │ │ │ └── IntegerLiteral.php │ │ │ ├── Comparison │ │ │ │ ├── Comparison.php │ │ │ │ ├── Equal.php │ │ │ │ ├── LessThan.php │ │ │ │ ├── NotEqual.php │ │ │ │ ├── GreaterThan.php │ │ │ │ ├── LessThanOrEqual.php │ │ │ │ └── GreaterThanOrEqual.php │ │ │ ├── ExpressionInterface.php │ │ │ ├── Math │ │ │ │ ├── NotExpression.php │ │ │ │ ├── DivExpression.php │ │ │ │ ├── ModExpression.php │ │ │ │ ├── MulExpression.php │ │ │ │ ├── SumExpression.php │ │ │ │ ├── BitwiseNotExpression.php │ │ │ │ ├── SubtractionExpression.php │ │ │ │ ├── BitwiseLeftShiftExpression.php │ │ │ │ ├── BitwiseRightShiftExpression.php │ │ │ │ ├── UnaryMinus.php │ │ │ │ ├── PrefixDecrement.php │ │ │ │ └── PrefixIncrement.php │ │ │ ├── UnaryExpression.php │ │ │ ├── Node.php │ │ │ ├── Logical │ │ │ │ ├── OrExpression.php │ │ │ │ ├── AndExpression.php │ │ │ │ ├── BitwiseAndExpression.php │ │ │ │ ├── BitwiseOrExpression.php │ │ │ │ └── BitwiseXorExpression.php │ │ │ ├── Value │ │ │ │ ├── NullValue.php │ │ │ │ ├── UnrecognizedDefineValue.php │ │ │ │ ├── IntValue.php │ │ │ │ ├── BoolValue.php │ │ │ │ ├── FloatValue.php │ │ │ │ ├── CharValue.php │ │ │ │ └── Value.php │ │ │ ├── BinaryExpression.php │ │ │ └── CastExpression.php │ │ └── Parser.php │ ├── Runtime │ │ ├── Source.php │ │ ├── OutputStack.php │ │ ├── DirectiveExecutor.php │ │ └── SourceExecutor.php │ ├── Lexer │ │ └── Simplifier.php │ └── Lexer.php ├── Environment │ ├── EnvironmentInterface.php │ ├── CVersion.php │ ├── PhpEnvironment.php │ └── StandardEnvironment.php ├── Io │ ├── Normalizer.php │ ├── SourceRepository.php │ └── DirectoriesRepository.php ├── Directive │ ├── ObjectLikeDirective.php │ ├── FunctionDirective.php │ ├── FunctionLikeDirective.php │ ├── Directive.php │ ├── FunctionLikeDirective │ │ └── Compiler.php │ └── Repository.php ├── Option.php ├── Preprocessor.php └── Result.php ├── resources ├── expression │ ├── type-name.pp2 │ ├── unary.pp2 │ ├── literals.pp2 │ ├── lexemes.pp2 │ └── grammar.pp2 └── expression.php ├── bin └── compile.php ├── phpstan.neon ├── LICENSE.md ├── composer.json └── README.md /src/Exception/NotReadableException.php: -------------------------------------------------------------------------------- 1 | 12 | ; 13 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Expression.php: -------------------------------------------------------------------------------- 1 | a->eval() === $this->b->eval(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Comparison/LessThan.php: -------------------------------------------------------------------------------- 1 | a->eval() < $this->b->eval(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Comparison/NotEqual.php: -------------------------------------------------------------------------------- 1 | a->eval() !== $this->b->eval(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Comparison/GreaterThan.php: -------------------------------------------------------------------------------- 1 | a->eval() > $this->b->eval(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Comparison/LessThanOrEqual.php: -------------------------------------------------------------------------------- 1 | a->eval() <= $this->b->eval(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Comparison/GreaterThanOrEqual.php: -------------------------------------------------------------------------------- 1 | a->eval() >= $this->b->eval(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Literal/BinIntegerLiteral.php: -------------------------------------------------------------------------------- 1 | value->eval(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/UnaryExpression.php: -------------------------------------------------------------------------------- 1 | value = $value; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Math/DivExpression.php: -------------------------------------------------------------------------------- 1 | a->eval() / $this->b->eval(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Math/ModExpression.php: -------------------------------------------------------------------------------- 1 | a->eval() % $this->b->eval(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Math/MulExpression.php: -------------------------------------------------------------------------------- 1 | a->eval() * $this->b->eval(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Math/SumExpression.php: -------------------------------------------------------------------------------- 1 | a->eval() + $this->b->eval(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Node.php: -------------------------------------------------------------------------------- 1 | value->eval(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Logical/OrExpression.php: -------------------------------------------------------------------------------- 1 | a->eval() || $this->b->eval(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Math/SubtractionExpression.php: -------------------------------------------------------------------------------- 1 | a->eval() - $this->b->eval(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Logical/AndExpression.php: -------------------------------------------------------------------------------- 1 | a->eval() && $this->b->eval(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Logical/BitwiseAndExpression.php: -------------------------------------------------------------------------------- 1 | a->eval() & $this->b->eval(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Logical/BitwiseOrExpression.php: -------------------------------------------------------------------------------- 1 | a->eval() | $this->b->eval(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Logical/BitwiseXorExpression.php: -------------------------------------------------------------------------------- 1 | a->eval() ^ $this->b->eval(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Math/BitwiseLeftShiftExpression.php: -------------------------------------------------------------------------------- 1 | a->eval() << $this->b->eval(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Math/BitwiseRightShiftExpression.php: -------------------------------------------------------------------------------- 1 | a->eval() >> $this->b->eval(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Math/UnaryMinus.php: -------------------------------------------------------------------------------- 1 | value->eval(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Math/PrefixDecrement.php: -------------------------------------------------------------------------------- 1 | value->eval() - 1; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Math/PrefixIncrement.php: -------------------------------------------------------------------------------- 1 | value->eval() + 1; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Value/NullValue.php: -------------------------------------------------------------------------------- 1 | value = $value; 14 | } 15 | 16 | public function eval(): bool 17 | { 18 | return $this->value; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/BinaryExpression.php: -------------------------------------------------------------------------------- 1 | a = $a; 16 | $this->b = $b; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Value/UnrecognizedDefineValue.php: -------------------------------------------------------------------------------- 1 | load(File::fromPathname($input)) 17 | ->build() 18 | ; 19 | 20 | $result->withClassUsage('FFI\\Preprocessor\\Internal\\Expression\\Ast'); 21 | 22 | \file_put_contents($output, $result->generate()); 23 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Value/IntValue.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | public const VERSION_ISO_C94 = 199409; 13 | 14 | /** 15 | * @var int<0, max> 16 | */ 17 | public const VERSION_ISO_C99 = 199901; 18 | 19 | /** 20 | * @var int<0, max> 21 | */ 22 | public const VERSION_ISO_C11 = 201112; 23 | 24 | /** 25 | * @var int<0, max> 26 | */ 27 | public const VERSION_ISO_C18 = 201710; 28 | } 29 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Value/BoolValue.php: -------------------------------------------------------------------------------- 1 | body = $this->normalizeBody($value); 14 | } 15 | 16 | public function getBody(): string 17 | { 18 | return $this->body; 19 | } 20 | 21 | public function __invoke(string ...$args): string 22 | { 23 | $this->assertArgumentsCount($args); 24 | 25 | return $this->body; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Option.php: -------------------------------------------------------------------------------- 1 | $mask 18 | * @param OptionEnumCase $expected 19 | */ 20 | public static function contains(int $mask, int $expected): bool 21 | { 22 | return ($mask & $expected) === $expected; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Exception/PreprocessorException.php: -------------------------------------------------------------------------------- 1 | setSource($src); 20 | $exception->setToken($tok); 21 | 22 | return $exception; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Directive/FunctionDirective.php: -------------------------------------------------------------------------------- 1 | callback = \Closure::fromCallable($cb); 17 | 18 | $reflection = new \ReflectionFunction($this->callback); 19 | 20 | $this->minArgumentsCount = $reflection->getNumberOfRequiredParameters(); 21 | $this->maxArgumentsCount = $reflection->getNumberOfParameters(); 22 | } 23 | 24 | public function __invoke(string ...$args): string 25 | { 26 | return self::render(($this->callback)(...$args)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Value/Value.php: -------------------------------------------------------------------------------- 1 | value = $value; 20 | } 21 | 22 | protected static function parse(string $value): string 23 | { 24 | return $value; 25 | } 26 | 27 | /** 28 | * @return static 29 | */ 30 | public static function fromToken(TokenInterface $token): self 31 | { 32 | return new static(static::parse($token->getValue())); 33 | } 34 | 35 | public function eval() 36 | { 37 | return $this->value; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - phar://phpstan.phar/conf/bleedingEdge.neon 3 | - vendor/phpstan/phpstan-deprecation-rules/rules.neon 4 | - vendor/phpstan/phpstan-strict-rules/rules.neon 5 | parameters: 6 | level: max 7 | phpVersion: 8 | min: 70400 9 | max: 80400 10 | parallel: 11 | jobSize: 20 12 | maximumNumberOfProcesses: 4 13 | minimumNumberOfJobsPerProcess: 2 14 | paths: 15 | - src 16 | tmpDir: vendor/.cache.phpstan 17 | rememberPossiblyImpureFunctionValues: false 18 | checkTooWideReturnTypesInProtectedAndPublicMethods: true 19 | checkImplicitMixed: true 20 | checkBenevolentUnionTypes: true 21 | reportPossiblyNonexistentGeneralArrayOffset: true 22 | reportPossiblyNonexistentConstantArrayOffset: true 23 | reportAlwaysTrueInLastCondition: true 24 | reportAnyTypeWideningInVarTag: true 25 | checkMissingOverrideMethodAttribute: false 26 | inferPrivatePropertyTypeFromConstructor: true 27 | tipsOfTheDay: false 28 | checkMissingCallableSignature: true 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © Kirill Nesmeyanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Internal/Runtime/Source.php: -------------------------------------------------------------------------------- 1 | { return $children[0]; } 22 | : ::T_PLUS:: UnaryExpression() 23 | ; 24 | 25 | UnaryMinus -> { return new Ast\Math\UnaryMinus($children[0]); } 26 | : ::T_MINUS:: UnaryExpression() 27 | ; 28 | 29 | UnaryNot -> { return new Ast\Math\NotExpression($children[0]); } 30 | : ::T_NOT:: UnaryExpression() 31 | ; 32 | 33 | UnaryBitwiseNot -> { return new Ast\Math\BitwiseNotExpression($children[1]); } 34 | : ::T_BIT_NOT:: UnaryExpression() 35 | ; 36 | 37 | PrefixIncrement -> { return new Ast\Math\PrefixIncrement($children[1]); } 38 | : UnaryExpression() 39 | ; 40 | 41 | PrefixDecrement -> { return new Ast\Math\PrefixDecrement($children[1]); } 42 | : UnaryExpression() 43 | ; 44 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/CastExpression.php: -------------------------------------------------------------------------------- 1 | type = \strtolower($type); 16 | $this->value = $value; 17 | } 18 | 19 | /** 20 | * Approximate cast result. 21 | */ 22 | public function eval() 23 | { 24 | switch ($this->type) { 25 | case 'char': 26 | case 'short': 27 | case 'int': 28 | case 'long': 29 | return (int) $this->value->eval(); 30 | case 'string': 31 | return (string) $this->value->eval(); 32 | case 'float': 33 | case 'double': 34 | return (float) $this->value->eval(); 35 | case 'bool': 36 | return (bool) $this->value->eval(); 37 | default: 38 | $error = \sprintf('Can not cast %s to %s', \get_debug_type($this->value->eval()), $this->type); 39 | throw new \LogicException($error); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Directive/FunctionLikeDirective.php: -------------------------------------------------------------------------------- 1 | args = $this->filter($args); 23 | $this->minArgumentsCount = $this->maxArgumentsCount = \count($this->args); 24 | $this->body = $this->normalizeBody($value); 25 | } 26 | 27 | private function filter(array $args): array 28 | { 29 | $args = \array_map('\\trim', $args); 30 | 31 | return \array_filter($args, static fn(string $arg): bool => $arg !== ''); 32 | } 33 | 34 | public function __invoke(string ...$args): string 35 | { 36 | $this->assertArgumentsCount($args); 37 | 38 | if ($this->compiled === null) { 39 | $this->compiled = Compiler::compile($this->body, $this->args); 40 | } 41 | 42 | return $this->render(($this->compiled)(...$args)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Literal/StringLiteral.php: -------------------------------------------------------------------------------- 1 | 12 | * (6.4.4.4) simple-escape-sequence: one of 13 | * \' \" \? \\ 14 | * \a \b \f \n \r \t \v 15 | * 16 | * 17 | * @var string 18 | */ 19 | private const ESCAPE_SEQUENCES = [ 20 | '\\\\' => '\\', 21 | '\"' => '"', 22 | "\'" => "'", 23 | '\?' => "\u{003F}", 24 | '\a' => "\u{0007}", 25 | '\b' => "\u{0008}", 26 | '\f' => "\u{000C}", 27 | '\n' => "\u{000A}", 28 | '\r' => "\u{000D}", 29 | '\t' => "\u{0009}", 30 | '\v' => "\u{000B}", 31 | ]; 32 | 33 | private string $value; 34 | 35 | private bool $wideChar; 36 | 37 | public function __construct(string $value, bool $wideChar = false) 38 | { 39 | $this->value = $value; 40 | $this->wideChar = $wideChar; 41 | } 42 | 43 | public static function parse(string $value): string 44 | { 45 | return \str_replace( 46 | \array_keys(self::ESCAPE_SEQUENCES), 47 | \array_values(self::ESCAPE_SEQUENCES), 48 | $value 49 | ); 50 | } 51 | 52 | public function eval(): string 53 | { 54 | return $this->value; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Internal/Expression/Ast/Literal/IntegerLiteral.php: -------------------------------------------------------------------------------- 1 | self::TYPE_LONG, 34 | 'l' => self::TYPE_LONG, 35 | 'ul' => self::TYPE_UNSIGNED_LONG, 36 | 'u' => self::TYPE_UNSIGNED_LONG, 37 | 'll' => self::TYPE_LONG_LONG, 38 | 'ull' => self::TYPE_UNSIGNED_LONG_LONG, 39 | ]; 40 | 41 | private int $value; 42 | 43 | private int $type; 44 | 45 | public function __construct(int $value, string $suffix) 46 | { 47 | $this->value = $value; 48 | $this->type = $this->parseType($suffix); 49 | } 50 | 51 | private function parseType(string $suffix): int 52 | { 53 | $type = self::TYPE_MAPPINGS[\strtolower($suffix)] ?? null; 54 | 55 | if ($type === null) { 56 | throw new \LogicException('Unknown integer literal suffix "' . $suffix . '"'); 57 | } 58 | 59 | return $type; 60 | } 61 | 62 | public function eval(): int 63 | { 64 | return $this->value; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Internal/Lexer/Simplifier.php: -------------------------------------------------------------------------------- 1 | getContents(); 42 | 43 | $content = $this->normalizeLineDelimiters($content); 44 | $content = $this->trimComments($content); 45 | 46 | return File::fromSources($content); 47 | } 48 | 49 | private function trimComments(string $source): string 50 | { 51 | return \preg_replace(self::PCRE_COMMENTS, '', $source); 52 | } 53 | 54 | /** 55 | * Normalize windows-aware line breaks 56 | */ 57 | private function normalizeLineDelimiters(string $source): string 58 | { 59 | return \str_replace("\r\n", "\n", $source); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Io/SourceRepository.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class SourceRepository implements RepositoryInterface, RegistrarInterface, \IteratorAggregate 16 | { 17 | /** 18 | * @var array 19 | */ 20 | private array $files = []; 21 | 22 | /** 23 | * @param iterable $files 24 | */ 25 | public function __construct(iterable $files = []) 26 | { 27 | foreach ($files as $file => $source) { 28 | $this->add($file, $source); 29 | } 30 | } 31 | 32 | public function add(string $file, $source, bool $overwrite = false): bool 33 | { 34 | $file = Normalizer::normalize($file); 35 | 36 | if ($overwrite === false && isset($this->files[$file])) { 37 | return false; 38 | } 39 | 40 | $this->files[$file] = File::new($source); 41 | 42 | return true; 43 | } 44 | 45 | public function remove(string $file): bool 46 | { 47 | $file = Normalizer::normalize($file); 48 | 49 | $exists = isset($this->files[$file]); 50 | 51 | unset($this->files[$file]); 52 | 53 | return $exists; 54 | } 55 | 56 | public function getIterator(): \Traversable 57 | { 58 | return new \ArrayIterator($this->files); 59 | } 60 | 61 | public function count(): int 62 | { 63 | return \count($this->files); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Internal/Expression/Parser.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | private array $reducers; 26 | 27 | public function __construct(array $config) 28 | { 29 | $lexer = new Lexer($config['tokens']['default'], $config['skip']); 30 | 31 | $this->reducers = $config['reducers']; 32 | 33 | $this->runtime = new Runtime($lexer, $config['grammar'], [ 34 | Runtime::CONFIG_AST_BUILDER => $this, 35 | Runtime::CONFIG_INITIAL_RULE => $config['initial'], 36 | Runtime::CONFIG_BUFFER => ArrayBuffer::class, 37 | ]); 38 | } 39 | 40 | public function build(ContextInterface $context, $result) 41 | { 42 | $state = $context->getState(); 43 | 44 | if (isset($this->reducers[$state])) { 45 | return $this->reducers[$state]($context, $result); 46 | } 47 | 48 | return null; 49 | } 50 | 51 | public static function fromFile(string $pathname): self 52 | { 53 | return new self(require $pathname); 54 | } 55 | 56 | /** 57 | * @throws \Throwable 58 | */ 59 | public function parse($source, array $options = []): ExpressionInterface 60 | { 61 | return $this->runtime->parse($source, $options); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /resources/expression/literals.pp2: -------------------------------------------------------------------------------- 1 | 2 | 3 | Identifier -> { 4 | return new Ast\Literal\IdentifierLiteral($children->getValue()); 5 | } 6 | : 7 | ; 8 | 9 | Literal 10 | : IntegerLiteral() 11 | | HexIntegerLiteral() 12 | | OctIntegerLiteral() 13 | | BinIntegerLiteral() 14 | | BooleanLiteral() 15 | | StringLiteral() 16 | /*| FloatingLiteral() 17 | | EnumerationLiteral() 18 | | CharacterLiteral()*/ 19 | ; 20 | 21 | IntegerLiteral -> { 22 | return new Ast\Literal\IntegerLiteral((int)$children[0]->getValue(), $children[1]->getValue()); 23 | } 24 | : 25 | ; 26 | 27 | HexIntegerLiteral -> { 28 | return new Ast\Literal\HexIntegerLiteral((string)$children[0]->getValue(), $children[1]->getValue()); 29 | } 30 | : 31 | ; 32 | 33 | OctIntegerLiteral -> { 34 | return new Ast\Literal\OctIntegerLiteral((string)$children[0]->getValue(), $children[1]->getValue()); 35 | } 36 | : 37 | ; 38 | 39 | BinIntegerLiteral -> { 40 | return new Ast\Literal\OctIntegerLiteral((string)$children[0]->getValue(), $children[1]->getValue()); 41 | } 42 | : 43 | ; 44 | 45 | BooleanLiteral -> { 46 | return new Ast\Literal\BooleanLiteral( 47 | $children->getValue() === 'true' 48 | ); 49 | } 50 | : 51 | ; 52 | 53 | FloatingLiteral 54 | : 55 | ; 56 | 57 | EnumerationLiteral 58 | : 59 | ; 60 | 61 | CharacterLiteral 62 | : 63 | ; 64 | 65 | StringLiteral -> { 66 | $value = Ast\Literal\StringLiteral::parse( 67 | $children[1]->getValue() 68 | ); 69 | 70 | return new Ast\Literal\StringLiteral($value, $children[0]->getValue() !== ''); 71 | } 72 | : 73 | ; 74 | 75 | -------------------------------------------------------------------------------- /src/Directive/Directive.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | protected int $minArgumentsCount = 0; 25 | 26 | /** 27 | * @var int<0, max> 28 | */ 29 | protected int $maxArgumentsCount = 0; 30 | 31 | protected function normalizeBody(string $body): string 32 | { 33 | return \str_replace("\\\n", "\n", $body); 34 | } 35 | 36 | /** 37 | * @param list $arguments 38 | */ 39 | protected function assertArgumentsCount(array $arguments): void 40 | { 41 | $haystack = \count($arguments); 42 | 43 | if ($haystack > $this->getMaxArgumentsCount()) { 44 | throw new \ArgumentCountError(\sprintf(self::ERROR_TOO_MANY_ARGUMENTS, $this->getMaxArgumentsCount())); 45 | } 46 | 47 | if ($haystack < $this->getMinArgumentsCount()) { 48 | throw new \ArgumentCountError(\sprintf(self::ERROR_TOO_FEW_ARGUMENTS, $this->getMinArgumentsCount())); 49 | } 50 | } 51 | 52 | public function getMaxArgumentsCount(): int 53 | { 54 | return $this->maxArgumentsCount; 55 | } 56 | 57 | public function getMinArgumentsCount(): int 58 | { 59 | return $this->minArgumentsCount; 60 | } 61 | 62 | public static function render($result): string 63 | { 64 | switch (true) { 65 | case $result === true: 66 | return '1'; 67 | case $result === null: 68 | case $result === false: 69 | return '0'; 70 | default: 71 | return (string) $result; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Environment/PhpEnvironment.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | private const EXPORT_DIRECTIVE_NAMES = [ 15 | 'PHP_', 16 | 'ZEND_', 17 | ]; 18 | 19 | public function applyTo(PreprocessorInterface $pre): void 20 | { 21 | foreach (self::EXPORT_DIRECTIVE_NAMES as $prefix) { 22 | foreach ($this->getCoreConstants() as $constant => $value) { 23 | $isValidType = \is_scalar($value) || $value === null; 24 | 25 | if (!$isValidType || !\str_starts_with($constant, $prefix)) { 26 | continue; 27 | } 28 | 29 | $this->define($pre, $constant, $value); 30 | } 31 | } 32 | } 33 | 34 | private function getCoreConstants(): array 35 | { 36 | $constants = \get_defined_constants(true); 37 | 38 | return (array) ($constants['Core'] ?? $constants['core'] ?? []); 39 | } 40 | 41 | private function define(PreprocessorInterface $pre, string $name, $value): void 42 | { 43 | $pre->directives->define('__' . $name . '__', $this->toCLiteral($value)); 44 | } 45 | 46 | private function toCLiteral($value): string 47 | { 48 | switch (true) { 49 | case \is_string($value) && \strlen($value) === 1: 50 | return "'" . \addcslashes($value, "'") . "'"; 51 | 52 | case \is_string($value): 53 | return '"' . \addcslashes($value, '"') . '"'; 54 | 55 | case \is_float($value): 56 | return \sprintf('%g', $value); 57 | 58 | case \is_int($value): 59 | return (string) $value; 60 | 61 | case \is_bool($value): 62 | return $value ? '1' : '0'; 63 | 64 | case $value === null: 65 | return 'NULL'; 66 | 67 | default: 68 | throw new \LogicException('Non-serializable C literal of PHP type ' . \get_debug_type($value)); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Io/DirectoriesRepository.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class DirectoriesRepository implements RepositoryInterface, RegistrarInterface, \IteratorAggregate 14 | { 15 | /** 16 | * @var list 17 | */ 18 | private array $directories = []; 19 | 20 | private bool $optimizationRequired = false; 21 | 22 | /** 23 | * @param iterable $directories 24 | */ 25 | public function __construct(iterable $directories = []) 26 | { 27 | foreach ($directories as $directory) { 28 | $this->include($directory); 29 | } 30 | } 31 | 32 | public function include(string $directory): void 33 | { 34 | $this->optimizationRequired = true; 35 | 36 | $directory = Normalizer::normalize($directory); 37 | 38 | if ($directory === '') { 39 | throw new \InvalidArgumentException('Directory must not be empty'); 40 | } 41 | 42 | $this->directories[] = $directory; 43 | } 44 | 45 | public function exclude(string $directory): void 46 | { 47 | $filter = static fn(string $haystack): bool => 48 | !\str_starts_with($haystack, Normalizer::normalize($directory)) 49 | ; 50 | 51 | /** @psalm-suppress PropertyTypeCoercion */ 52 | $this->directories = \array_filter($this->directories, $filter); 53 | } 54 | 55 | public function getIterator(): \Traversable 56 | { 57 | if ($this->optimizationRequired) { 58 | $this->optimize(); 59 | } 60 | 61 | return new \ArrayIterator($this->directories); 62 | } 63 | 64 | private function optimize(): void 65 | { 66 | $this->optimizationRequired = false; 67 | 68 | /** @psalm-suppress PropertyTypeCoercion */ 69 | $this->directories = \array_unique($this->directories); 70 | } 71 | 72 | public function count(): int 73 | { 74 | return \count($this->directories); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Directive/FunctionLikeDirective/Compiler.php: -------------------------------------------------------------------------------- 1 | $arguments 47 | * 48 | * @psalm-return \Closure(mixed ...$args): string 49 | */ 50 | public static function compile(string $body, array $arguments): \Closure 51 | { 52 | $template = self::build($body, $arguments); 53 | 54 | return static function (...$args) use ($template): string { 55 | $from = \array_map(static fn(int $i): string => "\0$i\0", \array_keys($args)); 56 | 57 | return \str_replace($from, $args, $template); 58 | }; 59 | } 60 | 61 | private static function build(string $body, array $arguments): string 62 | { 63 | foreach ($arguments as $i => $name) { 64 | $body = self::replace($body, $i, $name); 65 | } 66 | 67 | return $body; 68 | } 69 | 70 | private static function replace(string $body, int $i, string $name): string 71 | { 72 | $pattern = \sprintf(self::TPL_PATTERN, \preg_quote($name, '/')); 73 | 74 | return (string) \preg_replace_callback($pattern, self::callback($i), $body); 75 | } 76 | 77 | private static function callback(int $i): \Closure 78 | { 79 | return static function ($match) use ($i): string { 80 | return $match['MARK'] === self::T_STRINGIZE 81 | ? "\"\0$i\0\"" 82 | : "\0$i\0"; 83 | }; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ffi/preprocessor", 3 | "type": "library", 4 | "description": "Simple C Preprocessor", 5 | "license": "MIT", 6 | "keywords": ["ffi", "parser", "compiler", "c", "headers", "preprocessor"], 7 | "support": { 8 | "source": "https://github.com/php-ffi/preprocessor", 9 | "issues": "https://github.com/php-ffi/preprocessor/issues", 10 | "docs": "https://github.com/php-ffi/preprocessor/blob/master/README.md" 11 | }, 12 | "authors": [ 13 | { 14 | "name": "Nesmeyanov Kirill", 15 | "email": "nesk@xakep.ru", 16 | "homepage": "https://nesk.me", 17 | "role": "maintainer" 18 | } 19 | ], 20 | "require": { 21 | "php": "^7.4|^8.0", 22 | "ffi/preprocessor-contracts": "^1.0", 23 | "psr/log": "^1.0|^2.0|^3.0", 24 | "phplrt/parser": "^3.6", 25 | "phplrt/lexer": "^3.6", 26 | "symfony/polyfill-php80": "^1.27", 27 | "symfony/polyfill-ctype": "^1.27" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "FFI\\Preprocessor\\": "src" 32 | } 33 | }, 34 | "require-dev": { 35 | "phpunit/phpunit": "^9.5", 36 | "friendsofphp/php-cs-fixer": "^3.53", 37 | "phpstan/phpstan": "^2.0", 38 | "phpstan/phpstan-deprecation-rules": "^2.0", 39 | "phpstan/phpstan-strict-rules": "^2.0", 40 | "monolog/monolog": "^2.9|^3.0", 41 | "phplrt/phplrt": "^3.6" 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "FFI\\Preprocessor\\Tests\\": "tests" 46 | } 47 | }, 48 | "provide": { 49 | "ffi/preprocessor-contracts-implementation": "^1.0" 50 | }, 51 | "config": { 52 | "optimize-autoloader": true, 53 | "preferred-install": { 54 | "*": "dist" 55 | }, 56 | "sort-packages": true 57 | }, 58 | "scripts": { 59 | "test": ["@test:unit"], 60 | "test:unit": "phpunit --testdox --testsuite=unit", 61 | "linter": "@linter:check", 62 | "linter:check": "phpstan analyse --configuration phpstan.neon", 63 | "linter:baseline": "@linter:check -- --generate-baseline", 64 | "phpcs": "@phpcs:check", 65 | "phpcs:check": "@phpcs:fix --dry-run", 66 | "phpcs:fix": "php-cs-fixer fix --config=.php-cs-fixer.php --allow-risky=yes --verbose --diff" 67 | }, 68 | "extra": { 69 | "branch-alias": { 70 | "dev-main": "1.0.x-dev", 71 | "dev-master": "1.0.x-dev" 72 | } 73 | }, 74 | "minimum-stability": "dev", 75 | "prefer-stable": true 76 | } 77 | -------------------------------------------------------------------------------- /resources/expression/lexemes.pp2: -------------------------------------------------------------------------------- 1 | 2 | // Constants/Literals 3 | 4 | %token T_HEX_CONSTANT 0x([0-9a-fA-F]+)((?i)[ul]*) 5 | %token T_BIN_CONSTANT 0b([0-1]+)((?i)[ul]*) 6 | %token T_OCT_CONSTANT 0([0-7]+)((?i)[ul]*) 7 | %token T_DEC_CONSTANT ([1-9]\d*|[0-9])((?i)[ul]*) 8 | %token T_FLOAT_CONSTANT \bx\b // TODO 9 | %token T_DEC_FLOAT_CONSTANT \bx\b // TODO 10 | %token T_HEX_FLOAT_CONSTANT \bx\b // TODO 11 | %token T_STRING_LITERAL (L?)"([^"\\]*(?:\\.[^"\\]*)*)" 12 | %token T_CHAR_CONSTANT (L?)'([^'\\]*(?:\\.[^'\\]*)*)' 13 | %token T_BOOL_CONSTANT \b(?:true|false)\b 14 | %token T_NULL_CONSTANT \b(?i)(?:null)\b 15 | 16 | // Punctuators 17 | 18 | %token T_BOOL_OR \|\| 19 | %token T_BOOL_AND && 20 | // %token T_ASSIGN_MUL \*= 21 | %token T_MUL \* 22 | // %token T_ASSIGN_DIV /= 23 | %token T_DIV / 24 | // %token T_ASSIGN_MOD %= 25 | %token T_MOD % 26 | // %token T_ASSIGN_PLUS \+= 27 | %token T_PLUS_PLUS \+\+ 28 | %token T_PLUS \+ 29 | // %token T_ASSIGN_MINUS \-= 30 | %token T_MINUS_MINUS \-\- 31 | %token T_MINUS \- 32 | // %token T_ASSIGN_L_SHIFT <<= 33 | %token T_L_SHIFT << 34 | // %token T_ASSIGN_R_SHIFT >>= 35 | %token T_R_SHIFT >> 36 | // %token T_ASSIGN_BIN_AND &= 37 | %token T_BIN_AND & 38 | // %token T_ASSIGN_BIN_OR \|= 39 | %token T_BIN_OR \| 40 | // %token T_ASSIGN_BIN_XOR \^= 41 | %token T_BIN_XOR \^ 42 | // %token T_ASSIGN_BIT_NOT ~= 43 | %token T_BIT_NOT ~ 44 | // %token T_ARROW \-> 45 | %token T_EQ == 46 | %token T_NEQ != 47 | %token T_GTE >= 48 | %token T_LTE <= 49 | %token T_GT > 50 | %token T_LT < 51 | // %token T_THREE_DOTS \.\.\. 52 | // %token T_DOT \. 53 | %token T_NOT ! 54 | // %token T_TERNARY \? 55 | %token T_ASSIGN = 56 | // %token T_COLON : 57 | %token T_SEMICOLON ; 58 | %token T_COMMA , 59 | // %token T_SQ_BRACKET_OPEN \[ 60 | // %token T_SQ_BRACKET_CLOSE \] 61 | %token T_RND_BRACKET_OPEN \( 62 | %token T_RND_BRACKET_CLOSE \) 63 | // %token T_BRACE_OPEN \{ 64 | // %token T_BRACE_CLOSE \} 65 | 66 | %token T_IDENTIFIER [a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]* 67 | 68 | // Other 69 | 70 | %skip T_WHITESPACE \s+ 71 | %skip T_BLOCK_COMMENT \h*/\*.*?\*/ 72 | %skip T_COMMENT \h*//[^\n]*\n* 73 | -------------------------------------------------------------------------------- /src/Directive/Repository.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | private array $directives = []; 18 | 19 | /** 20 | * @param iterable $directives 21 | */ 22 | public function __construct(iterable $directives = []) 23 | { 24 | foreach ($directives as $directive => $definition) { 25 | $this->define($directive, $definition); 26 | } 27 | } 28 | 29 | /** 30 | * @param string|callable|DirectiveInterface $directive 31 | * 32 | * @throws \ReflectionException 33 | */ 34 | private function cast($directive): DirectiveInterface 35 | { 36 | switch (true) { 37 | case $directive instanceof DirectiveInterface: 38 | return $directive; 39 | case \is_callable($directive): 40 | return new FunctionDirective($directive); 41 | case \is_scalar($directive): 42 | case $directive instanceof \Stringable: 43 | case \is_object($directive) && \method_exists($directive, '__toString'): 44 | return new ObjectLikeDirective((string) $directive); 45 | default: 46 | return $directive; 47 | } 48 | } 49 | 50 | public function define(string $directive, $value = DirectiveInterface::DEFAULT_VALUE): void 51 | { 52 | try { 53 | $expr = $this->cast($value); 54 | } catch (\Throwable $e) { 55 | throw new DirectiveDefinitionException($e->getMessage(), (int) $e->getCode(), $e); 56 | } 57 | 58 | if ($expr instanceof ObjectLikeDirective) { 59 | $this->directives = \array_merge([$directive => $expr], $this->directives); 60 | } else { 61 | $this->directives[$directive] = $expr; 62 | } 63 | } 64 | 65 | public function undef(string $directive): bool 66 | { 67 | $exists = $this->defined($directive); 68 | 69 | unset($this->directives[$directive]); 70 | 71 | return $exists; 72 | } 73 | 74 | public function defined(string $directive): bool 75 | { 76 | return isset($this->directives[$directive]); 77 | } 78 | 79 | public function find(string $directive): ?DirectiveInterface 80 | { 81 | return $this->directives[$directive] ?? null; 82 | } 83 | 84 | public function getIterator(): \Traversable 85 | { 86 | return new \ArrayIterator($this->directives); 87 | } 88 | 89 | public function count(): int 90 | { 91 | return \count($this->directives); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Environment/StandardEnvironment.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | public int $version = CVersion::VERSION_ISO_C18; 23 | 24 | /** 25 | * Defined as {@see true} if the implementation is a hosted implementation, 26 | * one that supports the entire required standard library. Otherwise, 27 | * defined as {@see false}. 28 | */ 29 | public bool $hosted = false; 30 | 31 | /** 32 | * Defined as {@see false} if the implementation doesn't support optional 33 | * standard atomics. 34 | */ 35 | public bool $atomics = false; 36 | 37 | /** 38 | * Defined as {@see false} if the implementation doesn't support optional 39 | * standard threads. 40 | */ 41 | public bool $threads = false; 42 | 43 | /** 44 | * Defined as {@see false} if the implementation doesn't support standard 45 | * variable length arrays. 46 | */ 47 | public bool $vla = false; 48 | 49 | /** 50 | * Expands to an integer literal that starts at 0. The value is incremented 51 | * by 1 every time it's used in a source file, or in included headers of 52 | * the source file. __COUNTER__ remembers its state when you use precompiled 53 | * headers. This macro is always defined. 54 | * 55 | * @var int<0, max> 56 | */ 57 | public int $counter = 0; 58 | 59 | /** 60 | * Standard env constructor. 61 | */ 62 | public function __construct() 63 | { 64 | $this->zone = new \DateTimeZone('UTC'); 65 | } 66 | 67 | /** 68 | * @throws \Throwable 69 | */ 70 | public function applyTo(PreprocessorInterface $pre): void 71 | { 72 | $now = new \DateTime('now', $this->zone); 73 | 74 | $pre->directives->define('__DATE__', $now->format('M d Y')); 75 | $pre->directives->define('__TIME__', $now->format('h:i:s')); 76 | 77 | $pre->directives->define('__STDC__'); 78 | $pre->directives->define('__STDC_VERSION__', (string) $this->version); 79 | $pre->directives->define('__STDC_HOSTED__', $this->hosted ? '1' : '0'); 80 | 81 | if (!$this->atomics) { 82 | $pre->directives->define('__STDC_NO_ATOMICS__', '1'); 83 | } 84 | 85 | if (!$this->hosted) { 86 | $pre->directives->define('__STDC_NO_COMPLEX__', '1'); 87 | } 88 | 89 | if (!$this->threads) { 90 | $pre->directives->define('__STDC_NO_THREADS__', '1'); 91 | } 92 | 93 | if (!$this->vla) { 94 | $pre->directives->define('__STDC_NO_VLA__', '1'); 95 | } 96 | 97 | $pre->directives->define('__COUNTER__', fn() => $this->counter++); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Internal/Runtime/OutputStack.php: -------------------------------------------------------------------------------- 1 | 18 | * #if 1 // IF: $stack->push(true) 19 | * A // >> Stack contains [true]: Output available 20 | * #else // ELSE: $stack->push(! $stack->pop()); 21 | * B // >> Stack contains [false]: Output NOT available 22 | * #endif // END: $stack->pop(); 23 | * 24 | * 25 | * In a more complex example, it will look like this: 26 | * 27 | * 28 | * #if 1 // IF: $stack->push(true) 29 | * A // >> Stack contains [true]: Output available 30 | * #if 0 // IF: $stack->push(false) 31 | * B // >> Stack contains [true, false]: Output NOT available 32 | * #else // ELSE: $stack->push(! $stack->pop()); 33 | * C // >> Stack contains [true, true]: Output available 34 | * #endif // END: $stack->pop(); 35 | * #else // ELSE: $stack->push(! $stack->pop()); 36 | * D // >> Stack contains [false]: Output NOT available 37 | * #endif // END: $stack->pop(); 38 | * 39 | * 40 | * @internal 41 | */ 42 | final class OutputStack implements \Countable 43 | { 44 | private bool $state = true; 45 | 46 | /** 47 | * @var bool[] 48 | */ 49 | private array $stack = []; 50 | 51 | /** 52 | * @var bool[] 53 | */ 54 | private array $completed = []; 55 | 56 | public function isEnabled(): bool 57 | { 58 | return $this->state; 59 | } 60 | 61 | public function push(bool $state): void 62 | { 63 | $this->stack[] = $state; 64 | $this->completed[] = $state; 65 | 66 | if ($this->state && !$state) { 67 | $this->state = false; 68 | } 69 | } 70 | 71 | public function complete(): void 72 | { 73 | $this->assertSize(); 74 | 75 | $this->pop(); 76 | $this->push(true); 77 | } 78 | 79 | public function update(bool $status, bool $completed = false): void 80 | { 81 | \array_pop($this->stack); 82 | \array_pop($this->completed); 83 | 84 | $this->stack[] = $status; 85 | $this->completed[] = $completed; 86 | 87 | $this->refresh(); 88 | } 89 | 90 | public function isCompleted(): bool 91 | { 92 | return \end($this->completed); 93 | } 94 | 95 | private function assertSize(): void 96 | { 97 | if (\count($this->stack) <= 0) { 98 | throw new \LogicException('Output stack is an empty'); 99 | } 100 | } 101 | 102 | public function inverse(): void 103 | { 104 | $this->assertSize(); 105 | 106 | try { 107 | $this->stack[] = !\array_pop($this->stack); 108 | } finally { 109 | $this->refresh(); 110 | } 111 | } 112 | 113 | public function pop(): bool 114 | { 115 | $this->assertSize(); 116 | 117 | try { 118 | \array_pop($this->completed); 119 | 120 | return \array_pop($this->stack); 121 | } finally { 122 | $this->refresh(); 123 | } 124 | } 125 | 126 | private function refresh(): bool 127 | { 128 | foreach ($this->stack as $state) { 129 | if (!$state) { 130 | return $this->state = false; 131 | } 132 | } 133 | 134 | return $this->state = true; 135 | } 136 | 137 | public function count(): int 138 | { 139 | return \count($this->stack); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Preprocessor.php: -------------------------------------------------------------------------------- 1 | > 44 | */ 45 | private array $environments = [ 46 | Environment\PhpEnvironment::class, 47 | Environment\StandardEnvironment::class, 48 | ]; 49 | 50 | public function __construct(?LoggerInterface $logger = null) 51 | { 52 | $this->directives = new DirectivesRepository(); 53 | $this->directories = new DirectoriesRepository(); 54 | $this->sources = new SourceRepository(); 55 | 56 | $this->logger = $logger ?? new NullLogger(); 57 | 58 | foreach ($this->environments as $environment) { 59 | $this->load(new $environment($this)); 60 | } 61 | } 62 | 63 | public function load(EnvironmentInterface $env): void 64 | { 65 | $env->applyTo($this); 66 | } 67 | 68 | /** 69 | * @param int-mask-of $options 70 | * 71 | * @psalm-suppress MissingParamType : PHP 7.4 does not allow mixed type hint. 72 | */ 73 | public function process($source, int $options = Option::NOTHING): Result 74 | { 75 | [$directives, $directories, $sources] = [ 76 | clone $this->directives, 77 | clone $this->directories, 78 | clone $this->sources, 79 | ]; 80 | 81 | $logger = $this->logger ?? new NullLogger(); 82 | $context = new SourceExecutor($directives, $directories, $sources, $logger, $options); 83 | $stream = $context->execute(File::new($source)); 84 | 85 | return new Result($stream, $directives, $directories, $sources, $options); 86 | } 87 | 88 | public function getDirectives(): DirectivesRepositoryInterface 89 | { 90 | return $this->directives; 91 | } 92 | 93 | public function getSources(): SourcesRepositoryInterface 94 | { 95 | return $this->sources; 96 | } 97 | 98 | public function getDirectories(): DirectoriesRepositoryInterface 99 | { 100 | return $this->directories; 101 | } 102 | 103 | public function define(string $directive, $value = DirectiveInterface::DEFAULT_VALUE): void 104 | { 105 | $this->directives->define($directive, $value); 106 | } 107 | 108 | public function undef(string $directive): bool 109 | { 110 | return $this->directives->undef($directive); 111 | } 112 | 113 | public function add(string $file, $source, bool $overwrite = false): bool 114 | { 115 | return $this->sources->add($file, $source, $overwrite); 116 | } 117 | 118 | public function remove(string $file): bool 119 | { 120 | return $this->sources->remove($file); 121 | } 122 | 123 | public function include(string $directory): void 124 | { 125 | $this->directories->include($directory); 126 | } 127 | 128 | public function exclude(string $directory): void 129 | { 130 | $this->directories->exclude($directory); 131 | } 132 | 133 | public function __clone() 134 | { 135 | $this->sources = clone $this->sources; 136 | $this->directories = clone $this->directories; 137 | $this->directives = clone $this->directives; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Internal/Lexer.php: -------------------------------------------------------------------------------- 1 | 99 | */ 100 | private const LEXEMES = [ 101 | self::T_QUOTED_INCLUDE => '^\\h*#\\h*include\\h+"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"', 102 | self::T_ANGLE_BRACKET_INCLUDE => '^\\h*#\\h*include\\h+<\\h*([^\\n]+)\\h*>', 103 | self::T_FUNCTION_MACRO => '^\\h*#\\h*define\\h+(\\w+)\\(([^\\n]+?)\\)\\h*((?:\\\\s|\\\\\\n|[^\\n])+)?$', 104 | self::T_OBJECT_MACRO => '^\\h*#\\h*define\\h+(\\w+)\\h*((?:\\\\s|\\\\\\n|[^\\n])+)?$', 105 | self::T_UNDEF => '^\\h*#\\h*undef\\h+(\\w+)$', 106 | self::T_IFDEF => '^\\h*#\\h*ifdef\\b\\h*((?:\\\\s|\\\\\\n|[^\\n])+)', 107 | self::T_IFNDEF => '^\\h*#\\h*ifndef\\b\\h*((?:\\\\s|\\\\\\n|[^\\n])+)', 108 | self::T_ENDIF => '^\\h*#\\h*endif\\b\\h*', 109 | self::T_IF => '^\\h*#\\h*if\\b\\h*((?:\\\\s|\\\\\\n|[^\\n])+)', 110 | self::T_ELSE_IF => '^\\h*#\\h*elif\\b\\h*((?:\\\\s|\\\\\\n|[^\\n])+)', 111 | self::T_ELSE => '^\\h*#\\h*else', 112 | self::T_ERROR => '^\\h*#\\h*error\\h+((?:\\\\s|\\\\\\n|[^\\n])+)', 113 | self::T_WARNING => '^\\h*#\\h*warning\\h+((?:\\\\s|\\\\\\n|[^\\n])+)', 114 | self::T_SOURCE => '[^\\n]+|\\n+', 115 | ]; 116 | 117 | /** 118 | * @var array 119 | */ 120 | private const MERGE = [ 121 | self::T_SOURCE, 122 | ]; 123 | 124 | private Runtime $runtime; 125 | 126 | private Simplifier $simplifier; 127 | 128 | /** 129 | * Lexer constructor. 130 | */ 131 | public function __construct() 132 | { 133 | $this->simplifier = new Simplifier(); 134 | $this->runtime = new Runtime(self::LEXEMES, [self::T_EOI]); 135 | } 136 | 137 | public function lex($source, int $offset = 0): iterable 138 | { 139 | $source = $this->simplifier->simplify(File::new($source)); 140 | 141 | $stream = $this->runtime->lex($source, $offset); 142 | $previous = null; 143 | 144 | foreach ($stream as $token) { 145 | // Should be merged 146 | foreach (self::MERGE as $merge) { 147 | if ($update = $this->merge($merge, $previous, $token)) { 148 | $previous = $update; 149 | continue 2; 150 | } 151 | } 152 | 153 | if ($previous) { 154 | yield $previous; 155 | } 156 | 157 | $previous = $token; 158 | } 159 | 160 | if ($previous) { 161 | yield $previous; 162 | } 163 | } 164 | 165 | private function merge(string $name, ?TokenInterface $prev, TokenInterface $current): ?TokenInterface 166 | { 167 | if ($prev && $prev->getName() === $name && $current->getName() === $name) { 168 | $body = $prev->getValue() . $current->getValue(); 169 | 170 | return new Token($name, $body, $prev->getOffset()); 171 | } 172 | 173 | return null; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/Result.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | private const BUILTIN_DIRECTIVES = [ 24 | 'FFI_SCOPE', 25 | 'FFI_LIB', 26 | ]; 27 | 28 | private ?string $result = null; 29 | 30 | /** 31 | * @var iterable 32 | */ 33 | private iterable $stream; 34 | 35 | private DirectivesRepositoryInterface $directives; 36 | private DirectoriesRepositoryInterface $directories; 37 | private SourcesRepositoryInterface $sources; 38 | 39 | /** 40 | * @var int<0, max> 41 | */ 42 | private int $options; 43 | 44 | /** 45 | * @psalm-type OptionEnumCase = Option::* 46 | * 47 | * @param iterable $stream 48 | * @param int-mask-of $options 49 | */ 50 | public function __construct( 51 | iterable $stream, 52 | DirectivesRepositoryInterface $directives, 53 | DirectoriesRepositoryInterface $directories, 54 | SourcesRepositoryInterface $sources, 55 | int $options = 0 56 | ) { 57 | $this->stream = $stream; 58 | $this->directives = $directives; 59 | $this->directories = $directories; 60 | $this->sources = $sources; 61 | $this->options = $options; 62 | } 63 | 64 | public function getDirectives(): DirectivesRepositoryInterface 65 | { 66 | $this->compileIfNotCompiled(); 67 | 68 | return $this->directives; 69 | } 70 | 71 | private function compileIfNotCompiled(): void 72 | { 73 | if ($this->result === null) { 74 | $this->result = $this->withoutRecursionDepth(function (): string { 75 | $result = ''; 76 | 77 | foreach ($this->stream as $chunk) { 78 | $result .= $chunk; 79 | } 80 | 81 | return $result; 82 | }); 83 | } 84 | } 85 | 86 | /** 87 | * @template TResult of mixed 88 | * 89 | * @param callable():TResult $context 90 | * 91 | * @return TResult 92 | */ 93 | private function withoutRecursionDepth(callable $context) 94 | { 95 | if (($beforeRecursionDepth = \ini_get('xdebug.max_nesting_level')) !== false) { 96 | \ini_set('xdebug.max_nesting_level', '-1'); 97 | } 98 | 99 | if (($beforeMode = \ini_get('xdebug.mode')) !== false) { 100 | \ini_set('xdebug.mode', 'off'); 101 | } 102 | 103 | try { 104 | return $context(); 105 | } finally { 106 | if ($beforeRecursionDepth !== false) { 107 | \ini_set('xdebug.max_nesting_level', $beforeRecursionDepth); 108 | } 109 | 110 | if ($beforeMode !== false) { 111 | \ini_set('xdebug.mode', $beforeMode); 112 | } 113 | } 114 | } 115 | 116 | public function getDirectories(): DirectoriesRepositoryInterface 117 | { 118 | $this->compileIfNotCompiled(); 119 | 120 | return $this->directories; 121 | } 122 | 123 | public function getSources(): SourcesRepositoryInterface 124 | { 125 | $this->compileIfNotCompiled(); 126 | 127 | return $this->sources; 128 | } 129 | 130 | public function __toString(): string 131 | { 132 | $this->compileIfNotCompiled(); 133 | 134 | /** @psalm-suppress PossiblyNullArgument An execution of "compileIfNotCompiled" fills the result */ 135 | return $this->minify($this->injectBuiltinDirectives($this->result)); 136 | } 137 | 138 | private function minify(string $result): string 139 | { 140 | if (!Option::contains($this->options, Option::KEEP_EXTRA_LINE_FEEDS)) { 141 | $result = \preg_replace('/\n{2,}/ium', "\n", $result) ?? $result; 142 | $result = \trim($result, "\n"); 143 | } 144 | 145 | return $result; 146 | } 147 | 148 | private function injectBuiltinDirectives(string $result): string 149 | { 150 | if (!Option::contains($this->options, Option::SKIP_BUILTIN_DIRECTIVES)) { 151 | foreach (self::BUILTIN_DIRECTIVES as $name) { 152 | $directive = $this->directives->find($name); 153 | 154 | if ($directive !== null) { 155 | $result = \sprintf('#define %s %s', $name, Directive::render($directive())) 156 | . "\n" . $result; 157 | } 158 | } 159 | } 160 | 161 | return $result; 162 | } 163 | 164 | /** 165 | * @param non-empty-string $property 166 | */ 167 | public function __get(string $property): \Traversable 168 | { 169 | switch ($property) { 170 | case 'sources': 171 | return $this->getSources(); 172 | case 'directives': 173 | return $this->getDirectives(); 174 | case 'directories': 175 | return $this->getDirectories(); 176 | default: 177 | throw new \LogicException( 178 | \sprintf('Undefined property: %s::$%s', self::class, $property) 179 | ); 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple C-lang Preprocessor 2 | 3 | This implementation of a preprocessor based in part on 4 | [ISO/IEC 9899:TC2](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1124.pdf). 5 | 6 | ## Requirements 7 | 8 | - PHP >= 7.4 9 | 10 | ## Installation 11 | 12 | Library is available as composer repository and can be installed using the 13 | following command in a root of your project. 14 | 15 | ```sh 16 | $ composer require ffi/preprocessor 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```php 22 | use FFI\Preprocessor\Preprocessor; 23 | 24 | $pre = new Preprocessor(); 25 | 26 | echo $pre->process(' 27 | #define VK_DEFINE_HANDLE(object) typedef struct object##_T* object; 28 | 29 | #if !defined(VK_DEFINE_NON_DISPATCHABLE_HANDLE) 30 | #if defined(__LP64__) || defined(_WIN64) || (defined(__x86_64__) && !defined(__ILP32__) ) || defined(_M_X64) || defined(__ia64) || defined (_M_IA64) || defined(__aarch64__) || defined(__powerpc64__) 31 | #define VK_DEFINE_NON_DISPATCHABLE_HANDLE(object) typedef struct object##_T *object; 32 | #else 33 | #define VK_DEFINE_NON_DISPATCHABLE_HANDLE(object) typedef uint64_t object; 34 | #endif 35 | #endif 36 | 37 | VK_DEFINE_HANDLE(VkInstance) 38 | VK_DEFINE_NON_DISPATCHABLE_HANDLE(VkSemaphore) 39 | '); 40 | 41 | // 42 | // Expected Output: 43 | // 44 | // typedef struct VkInstance_T* VkInstance; 45 | // typedef uint64_t VkSemaphore; 46 | // 47 | ``` 48 | 49 | ## Directives 50 | 51 | ### Supported Directives 52 | 53 | - [x] `#include "file.h"` local-first include 54 | - [x] `#include ` global-first include 55 | - [x] `#define name` defining directives 56 | - [x] `#define name value` object-like macro 57 | - [x] `#define name(arg) value` function-like macro 58 | - [x] `#define name(arg) xxx##arg` concatenation 59 | - [x] `#define name(arg) #arg` stringizing 60 | - [x] `#undef name` removing directives 61 | - [x] `#ifdef name` "if directive defined" condition 62 | - [x] `#ifndef name` "if directive not defined" condition 63 | - [x] `#if EXPRESSION` if condition 64 | - [x] `#elif EXPRESSION` else if condition 65 | - [x] `#else` else condition 66 | - [x] `#endif` completion of a condition 67 | - [x] `#error message` error message directive 68 | - [x] `#warning message` warning message directive 69 | - [ ] `#line 66 "filename"` line and file override 70 | - [ ] `#pragma XXX` compiler control 71 | - [ ] `#pragma once` 72 | - [ ] `#assert XXX` compiler assertion 73 | - [ ] `#unassert XXX` compiler assertion 74 | - [ ] `#ident XXX` 75 | - [ ] `#sccs XXX` 76 | 77 | ### Expression Grammar 78 | 79 | #### Comparison Operators 80 | 81 | - [x] `A > B` greater than 82 | - [x] `A < B` less than 83 | - [x] `A == B` equal 84 | - [x] `A != B` not equal 85 | - [x] `A >= B` greater than or equal 86 | - [x] `A <= B` less than or equal 87 | 88 | #### Logical Operators 89 | 90 | - [x] `! A` logical NOT 91 | - [x] `A && B` conjunction 92 | - [x] `A || B` disjunction 93 | - [x] `(...)` grouping 94 | 95 | #### Arithmetic Operators 96 | 97 | - [x] `A + B` math addition 98 | - [x] `A - B` math subtraction 99 | - [x] `A * B` math multiplication 100 | - [x] `A / B` math division 101 | - [x] `A % B` modulo 102 | - [ ] `A++` increment 103 | - [x] `++A` prefix form 104 | - [ ] `A--` decrement 105 | - [x] `--A` prefix form 106 | - [x] `+A` unary plus 107 | - [x] `-A` unary minus 108 | - [ ] `&A` unary addr 109 | - [ ] `*A` unary pointer 110 | 111 | #### Bitwise Operators 112 | 113 | - [x] `~A` bitwise NOT 114 | - [x] `A & B` bitwise AND 115 | - [x] `A | B` bitwise OR 116 | - [x] `A ^ B` bitwise XOR 117 | - [x] `A << B` bitwise left shift 118 | - [x] `A >> B` bitwise right shift 119 | 120 | #### Other Operators 121 | 122 | - [x] `defined(X)` defined macro 123 | - [ ] `A ? B : C` ternary 124 | - [ ] `sizeof VALUE` sizeof 125 | - [ ] `sizeof(TYPE)` sizeof type 126 | 127 | ### Literals 128 | 129 | - [x] `true`, `false` boolean 130 | - [x] `42` decimal integer literal 131 | - [x] `42u`, `42U` unsigned int 132 | - [x] `42l`, `42L` long int 133 | - [x] `42ul`, `42UL` unsigned long int 134 | - [x] `42ll`, `42LL` long long int 135 | - [x] `42ull`, `42ULL` unsigned long long int 136 | - [x] `042` octal integer literal 137 | - [x] `0x42` hexadecimal integer literal 138 | - [x] `0b42` binary integer literal 139 | - [x] `"string"` string (char array) 140 | - [x] `L"string"` string (wide char array) 141 | - [x] `"\•"` escape sequences in strings 142 | - [ ] `"\•••"` arbitrary octal value in strings 143 | - [ ] `"\X••"` arbitrary hexadecimal value in strings 144 | - [ ] `'x'` char literal 145 | - [ ] `'\•'` escape sequences 146 | - [ ] `'\•••'` arbitrary octal value 147 | - [ ] `'\X••'` arbitrary hexadecimal value 148 | - [ ] `L'x'` wide character literal 149 | - [ ] `42.0` double 150 | - [ ] `42f`, `42F` float 151 | - [ ] `42l`, `42L` long double 152 | - [ ] `42E` exponential form 153 | - [ ] `0.42e23` exponential form 154 | - [ ] `NULL` null macro 155 | 156 | ### Type Casting 157 | 158 | - [x] `(char)42` 159 | - [x] `(short)42` 160 | - [x] `(int)42` 161 | - [x] `(long)42` 162 | - [x] `(float)42` 163 | - [x] `(double)42` 164 | - [x] `(bool)42` (Out of ISO/IEC 9899:TC2 specification) 165 | - [x] `(string)42` (Out of ISO/IEC 9899:TC2 specification) 166 | - [ ] `(void)42` 167 | - [ ] `(long type)42` Casting to a long type (`long int`, `long double`, etc) 168 | - [ ] `(const type)42` Casting to a constant type (`const char`, etc) 169 | - [ ] `(unsigned type)42` Casting to unsigned type (`unsigned int`, `unsigned long`, etc) 170 | - [ ] `(signed type)42` Casting to signed type (`signed int`, `signed long`, etc) 171 | - [ ] Pointers (`void *`, etc) 172 | - [ ] References (`unsigned int`, `unsigned long`, etc) 173 | 174 | ### Object Like Directive 175 | 176 | ```php 177 | use FFI\Preprocessor\Preprocessor; 178 | use FFI\Preprocessor\Directive\ObjectLikeDirective; 179 | 180 | $pre = new Preprocessor(); 181 | 182 | // #define A 183 | $pre->define('A'); 184 | 185 | // #define B 42 186 | $pre->define('B', '42'); 187 | 188 | // #define С 42 189 | $pre->define('С', new ObjectLikeDirective('42')); 190 | ``` 191 | 192 | ## Function Like Directive 193 | 194 | ```php 195 | use FFI\Preprocessor\Preprocessor; 196 | use FFI\Preprocessor\Directive\FunctionLikeDirective; 197 | 198 | $pre = new Preprocessor(); 199 | 200 | // #define C(object) object##_T* object; 201 | $pre->define('C', function (string $arg) { 202 | return "${arg}_T* ${arg};"; 203 | }); 204 | 205 | // #define D(object) object##_T* object; 206 | $pre->define('D', new FunctionLikeDirective(['object'], 'object##_T* object')); 207 | ``` 208 | 209 | ## Include Directories 210 | 211 | ```php 212 | use FFI\Preprocessor\Preprocessor; 213 | 214 | $pre = new Preprocessor(); 215 | 216 | $pre->include('/path/to/directory'); 217 | $pre->exclude('some'); 218 | ``` 219 | 220 | ## Message Handling 221 | 222 | ```php 223 | use FFI\Preprocessor\Preprocessor; 224 | 225 | $logger = new Psr3LoggerImplementation(); 226 | 227 | $pre = new Preprocessor($logger); 228 | 229 | $pre->process(' 230 | #error Error message 231 | // Will be sent to the logger: 232 | // - LoggerInterface::error("Error message") 233 | 234 | #warning Warning message 235 | // Will be sent to the logger: 236 | // - LoggerInterface::warning("Warning message") 237 | '); 238 | ``` 239 | -------------------------------------------------------------------------------- /resources/expression/grammar.pp2: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * ----------------------------------------------------------------------------- 4 | * ISO/IEC 9899:TC2 5 | * ----------------------------------------------------------------------------- 6 | * 7 | * Language Syntax Summary 8 | * 9 | */ 10 | 11 | %include lexemes.pp2 12 | %include literals.pp2 13 | %include type-name.pp2 14 | %include unary.pp2 15 | 16 | %pragma root RootExpression 17 | 18 | RootExpression -> { return $children[0]; } 19 | : Expression() 20 | ; 21 | 22 | /** 23 | * ----------------------------------------------------------------------------- 24 | * Constant Expressions 25 | * ----------------------------------------------------------------------------- 26 | * 27 | * A constant expression can be evaluated during translation rather than 28 | * runtime, and accordingly may be used in any place that a constant may be. 29 | * 30 | * constant-expression: 31 | * conditional-expression 32 | * 33 | */ 34 | 35 | Expression 36 | : ConditionalExpression() 37 | ; 38 | 39 | /** 40 | * (6.5.15) conditional-expression: 41 | * logical-OR-expression 42 | * logical-OR-expression ? expression : conditional-expression 43 | */ 44 | ConditionalExpression 45 | : LogicalOrExpression() 46 | ; 47 | 48 | /** 49 | * (6.5.14) logical-OR-expression: 50 | * logical-AND-expression 51 | * logical-OR-expression || logical-AND-expression 52 | */ 53 | LogicalOrExpression -> { 54 | if (\count($children) === 2) { 55 | return new Ast\Logical\OrExpression($children[0], $children[1]); 56 | } 57 | 58 | return $children; 59 | } 60 | : LogicalAndExpression() (::T_BOOL_OR:: LogicalOrExpression())? 61 | ; 62 | 63 | /** 64 | * (6.5.13) logical-AND-expression: 65 | * inclusive-OR-expression 66 | * logical-AND-expression && inclusive-OR-expression 67 | */ 68 | LogicalAndExpression -> { 69 | if (\count($children) === 2) { 70 | return new Ast\Logical\AndExpression($children[0], $children[1]); 71 | } 72 | 73 | return $children; 74 | } 75 | : InclusiveOrExpression() (::T_BOOL_AND:: LogicalAndExpression())? 76 | ; 77 | 78 | /** 79 | * (6.5.12) inclusive-OR-expression: 80 | * exclusive-OR-expression 81 | * inclusive-OR-expression | exclusive-OR-expression 82 | */ 83 | InclusiveOrExpression -> { 84 | if (\count($children) === 2) { 85 | return new Ast\Logical\BitwiseOrExpression($children[0], $children[1]); 86 | } 87 | 88 | return $children; 89 | } 90 | : ExclusiveOrExpression() (::T_BIN_OR:: InclusiveOrExpression())? 91 | ; 92 | 93 | /** 94 | * (6.5.11) exclusive-OR-expression: 95 | * AND-expression 96 | * exclusive-OR-expression ^ AND-expression 97 | */ 98 | ExclusiveOrExpression -> { 99 | if (\count($children) === 2) { 100 | return new Ast\Logical\BitwiseXorExpression($children[0], $children[1]); 101 | } 102 | 103 | return $children; 104 | } 105 | : AndExpression() (::T_BIN_XOR:: ExclusiveOrExpression())? 106 | ; 107 | 108 | /** 109 | * (6.5.10) AND-expression: 110 | * equality-expression 111 | * AND-expression & equality-expression 112 | */ 113 | AndExpression -> { 114 | if (\count($children) === 2) { 115 | return new Ast\Logical\BitwiseAndExpression($children[0], $children[1]); 116 | } 117 | 118 | return $children; 119 | } 120 | : EqualityExpression() (::T_BIN_AND:: AndExpression())? 121 | ; 122 | 123 | /** 124 | * (6.5.9) equality-expression: 125 | * relational-expression 126 | * equality-expression == relational-expression 127 | * equality-expression != relational-expression 128 | 129 | * (6.5.8) relational-expression: 130 | * shift-expression 131 | * relational-expression < shift-expression 132 | * relational-expression > shift-expression 133 | * relational-expression <= shift-expression 134 | * relational-expression >= shift-expression 135 | */ 136 | EqualityExpression -> { 137 | if (\count($children) === 3) { 138 | switch ($children[1]->getName()) { 139 | case 'T_EQ': return new Ast\Comparison\Equal($children[0], $children[2]); 140 | case 'T_NEQ': return new Ast\Comparison\NotEqual($children[0], $children[2]); 141 | case 'T_GT': return new Ast\Comparison\GreaterThan($children[0], $children[2]); 142 | case 'T_GTE': return new Ast\Comparison\GreaterThanOrEqual($children[0], $children[2]); 143 | case 'T_LT': return new Ast\Comparison\LessThan($children[0], $children[2]); 144 | case 'T_LTE': return new Ast\Comparison\LessThanOrEqual($children[0], $children[2]); 145 | } 146 | } 147 | 148 | return $children; 149 | } 150 | : ShiftExpression() ((|||||) EqualityExpression())? 151 | ; 152 | 153 | /** 154 | * (6.5.7) shift-expression: 155 | * additive-expression 156 | * shift-expression << additive-expression 157 | * shift-expression >> additive-expression 158 | */ 159 | ShiftExpression -> { 160 | if (\count($children) === 3) { 161 | switch ($children[1]->getName()) { 162 | case 'T_L_SHIFT': return new Ast\Math\BitwiseLeftShiftExpression($children[0], $children[2]); 163 | case 'T_R_SHIFT': return new Ast\Math\BitwiseRightShiftExpression($children[0], $children[2]); 164 | } 165 | } 166 | 167 | return $children; 168 | } 169 | : AdditiveExpression() ((|) ShiftExpression())? 170 | ; 171 | 172 | 173 | /** 174 | * (6.5.6) additive-expression: 175 | * multiplicative-expression 176 | * additive-expression + multiplicative-expression 177 | * additive-expression - multiplicative-expression 178 | */ 179 | AdditiveExpression -> { 180 | if (\count($children) === 3) { 181 | switch ($children[1]->getName()) { 182 | case 'T_PLUS': return new Ast\Math\SumExpression($children[0], $children[2]); 183 | case 'T_MINUS': return new Ast\Math\SubtractionExpression($children[0], $children[2]); 184 | } 185 | } 186 | 187 | return $children; 188 | } 189 | : MultiplicativeExpression() ((|) AdditiveExpression())? 190 | ; 191 | 192 | /** 193 | * (6.5.5) multiplicative-expression: 194 | * cast-expression 195 | * multiplicative-expression * cast-expression 196 | * multiplicative-expression / cast-expression 197 | * multiplicative-expression % cast-expression 198 | */ 199 | MultiplicativeExpression -> { 200 | while (\count($children) >= 3) { 201 | [$a, $op, $b] = [ 202 | \array_shift($children), 203 | \array_shift($children), 204 | \array_shift($children), 205 | ]; 206 | 207 | switch ($op->getName()) { 208 | case 'T_MOD': 209 | \array_unshift($children, new Ast\Math\ModExpression($a, $b)); 210 | break; 211 | 212 | case 'T_DIV': 213 | \array_unshift($children, new Ast\Math\DivExpression($a, $b)); 214 | break; 215 | 216 | case 'T_MUL': 217 | \array_unshift($children, new Ast\Math\MulExpression($a, $b)); 218 | break; 219 | } 220 | } 221 | 222 | return $children; 223 | } 224 | // Force Left Associativity. 225 | // The algorithm is a little more complicated, but in this case it is necessary. 226 | : (CastExpression() (||))* CastExpression() 227 | ; 228 | 229 | 230 | /** 231 | * (6.5.4) cast-expression: 232 | * unary-expression 233 | * ( type-name ) cast-expression 234 | */ 235 | CastExpression -> { 236 | if (\is_array($children) && \count($children) === 2) { 237 | return new Ast\CastExpression($children[0]->getValue(), $children[1]); 238 | } 239 | 240 | return $children; 241 | } 242 | : ::T_RND_BRACKET_OPEN:: TypeName() ::T_RND_BRACKET_CLOSE:: CastExpression() 243 | | UnaryExpression() 244 | ; 245 | 246 | PrimaryExpression 247 | : Identifier() 248 | | Literal() 249 | | ::T_RND_BRACKET_OPEN:: Expression() ::T_RND_BRACKET_CLOSE:: 250 | ; 251 | -------------------------------------------------------------------------------- /src/Internal/Runtime/DirectiveExecutor.php: -------------------------------------------------------------------------------- 1 | directives = $directives; 53 | } 54 | 55 | /** 56 | * Executes all directives in passed body and returns the result 57 | * of all replacements. 58 | * 59 | * The second argument is responsible for the execution context. 60 | * Substitutions can be performed both in the body of the source code 61 | * and in directive expressions. 62 | * 63 | * @param DirectiveExecutorContext $ctx 64 | * 65 | * @throws DirectiveEvaluationException 66 | */ 67 | public function replace(string $body, int $ctx = self::CTX_SOURCE): string 68 | { 69 | if ($ctx === self::CTX_EXPRESSION) { 70 | // Replace "defined(X)" expression 71 | $body = $this->replaceDefinedExpression($body); 72 | } 73 | 74 | return $this->replaceDefinedDirectives($body); 75 | } 76 | 77 | /** 78 | * Replaces all occurrences of "defined(xxx)" with the result of their 79 | * execution. 80 | * 81 | * Such replacements can only be found inside directive expressions. 82 | * 83 | * 84 | * // before 85 | * #if defined(example) 86 | * #if defined(__FILE__) 87 | * 88 | * // after 89 | * #if false 90 | * #if true 91 | * 92 | */ 93 | private function replaceDefinedExpression(string $body): string 94 | { 95 | $lookup = fn(array $matches): string => 96 | /** @psalm-suppress MixedArgument */ 97 | $this->directives->defined($matches[1]) ? 'true' : 'false' 98 | ; 99 | 100 | return \preg_replace_callback(self::PCRE_DEFINED, $lookup, $body); 101 | } 102 | 103 | /** 104 | * Replaces all declared directives with the result of their execution. 105 | * 106 | * @psalm-suppress MixedInferredReturnType 107 | */ 108 | private function replaceDefinedDirectives(string $body): string 109 | { 110 | $stream = $this->findAndReplace($body); 111 | 112 | while ($stream->valid()) { 113 | try { 114 | /** @psalm-suppress MixedArgument */ 115 | $stream->send($this->execute($stream->key(), $stream->current())); 116 | } catch (\Throwable $e) { 117 | $stream->throw($e); 118 | } 119 | } 120 | 121 | /** @psalm-suppress MixedReturnStatement */ 122 | return $stream->getReturn(); 123 | } 124 | 125 | /** 126 | * Applies substitution rules for every registered directive 127 | * in passed body argument. 128 | * 129 | * @see DirectiveExecutor::findDirectiveAndReplace() 130 | */ 131 | private function findAndReplace(string $body): \Generator 132 | { 133 | /** 134 | * @var string $name 135 | * @var DirectiveInterface $directive 136 | */ 137 | foreach ($this->directives as $name => $directive) { 138 | $stream = $this->findDirectiveAndReplace($name, $directive, $body); 139 | 140 | try { 141 | yield from $stream; 142 | 143 | $body = (string) $stream->getReturn(); 144 | } catch (\Throwable $e) { 145 | $stream->throw($e); 146 | } 147 | } 148 | 149 | return $body; 150 | } 151 | 152 | /** 153 | * Function for runtime replacements of a specific directive: 154 | * 155 | * 156 | * $body = 'ExampleDirective(1, 2)'; 157 | * 158 | * $replacements = $this->findDirectiveAndReplace('ExampleDirective', ..., $body); 159 | * 160 | * while ($replacements->valid()) { 161 | * // $name = 'ExampleDirective'; 162 | * // $arguments = [1, 2]; 163 | * [$name, $arguments] = [$replacements->key(), $replacements->current()]; 164 | * 165 | * $replacements->send('result of ' . \implode(' and ', $arguments)); 166 | * } 167 | * 168 | * $replacements->getReturn(); // string(17) "result of 1 and 2" 169 | * 170 | * 171 | * @psalm-return \Generator 172 | */ 173 | private function findDirectiveAndReplace(string $name, DirectiveInterface $directive, string $body): \Generator 174 | { 175 | // / 176 | // This boolean variable includes preprocessor optimizations 177 | // and means that do not need to do a lookahead to read 178 | // additional directive arguments. 179 | // 180 | $isSimpleDirective = !$directive instanceof FunctionLikeDirectiveInterface 181 | || $directive->getMaxArgumentsCount() === 0; 182 | 183 | $coroutine = $this->findDirectiveAndUpdateBody($name, $body); 184 | 185 | while ($coroutine->valid()) { 186 | // Start and End offsets for substitutions 187 | [$from, $to] = [$coroutine->key(), $coroutine->current()]; 188 | 189 | try { 190 | // Returns the name of the found directive and its arguments. 191 | // Back it MAY accept a string to replace the found entry. 192 | $arguments = $isSimpleDirective ? [] : $this->extractArguments($body, $to); 193 | 194 | if (!$isSimpleDirective && $arguments === []) { 195 | // Workaround for a case when macro functions are not used 196 | // as functions. In such cases, all substitutions should be 197 | // ignored. 198 | $coroutine->next(); 199 | continue; 200 | } 201 | 202 | $replacement = yield $name => $arguments; 203 | } catch (\Throwable $e) { 204 | $token = $this->createTokenForSource($name, $body, $from, $to); 205 | 206 | throw PreprocessException::fromSource($e->getMessage(), File::fromSources($body), $token); 207 | } 208 | 209 | // In the case that replacement is not required, then we move 210 | // on to the next detected directive. 211 | if (!\is_string($replacement)) { 212 | $coroutine->next(); 213 | continue; 214 | } 215 | 216 | // Otherwise, we update the body in the replacement stream for 217 | // the specified directive. 218 | $prefix = \substr($body, 0, $from); 219 | $suffix = \substr($body, $to); 220 | 221 | $coroutine->send($body = $prefix . $replacement . $suffix); 222 | } 223 | 224 | return $coroutine->getReturn(); 225 | } 226 | 227 | /** 228 | * Finds all occurrences of directive name in the body and their offsets. 229 | * 230 | * If a new string value is passed to the generator, then the processed 231 | * body will be updated with this new value. 232 | * 233 | * 234 | * $stream = $this->findDirective('example', $body); 235 | * 236 | * while ($stream->valid()) { 237 | * $offset = $stream->current(); 238 | * // Do replace define "example" at offset "$offset" 239 | * $stream->send($newBody); 240 | * } 241 | * 242 | * 243 | * @psalm-return \Generator 244 | * 245 | * @psalm-suppress MixedReturnTypeCoercion 246 | */ 247 | private function findDirectiveAndUpdateBody(string $name, string $body): \Generator 248 | { 249 | [$length, $offset] = [\strlen($name), 0]; 250 | 251 | while (isset($body[$offset])) { 252 | $offset = @\strpos($body, $name, $offset); 253 | 254 | if (!\is_int($offset)) { 255 | break; 256 | } 257 | 258 | if ($this->isPartOfName($offset, $length, $body)) { 259 | ++$offset; 260 | continue; 261 | } 262 | 263 | /** @psalm-suppress RedundantConditionGivenDocblockType */ 264 | if (\is_string($replacement = yield $offset => $offset + $length)) { 265 | $body = $replacement; 266 | } 267 | 268 | ++$offset; 269 | } 270 | 271 | return $body; 272 | } 273 | 274 | /** 275 | * Returns {@see true} if the directive in the specified offset is part 276 | * of another name or {@see false} instead. For example: 277 | * 278 | * 279 | * $this->isPartOfName(0, 3, 'abcd'); // true ("abc" is part of "abcd") 280 | * $this->isPartOfName(0, 3, 'abc()'); // false ("abc" is a full name) 281 | * 282 | */ 283 | private function isPartOfName(int $offset, int $length, string $body): bool 284 | { 285 | $startsWithNameChar = $offset !== 0 && $this->isAvailableInNames($body[$offset - 1]); 286 | 287 | return 288 | // When starts with [_a-z0-9] 289 | $startsWithNameChar 290 | // Or ends with [_a-z0-9] 291 | || $this->isAvailableInNames($body[$offset + $length] ?? ''); 292 | } 293 | 294 | /** 295 | * Returns {@see true} if the char is valid when used in function 296 | * and variable names or {@see false} otherwise. 297 | */ 298 | private function isAvailableInNames(string $char): bool 299 | { 300 | return $char === '_' || \ctype_alnum($char); 301 | } 302 | 303 | /** 304 | * Method for reading arguments that should be located after the 305 | * specified offset. 306 | * 307 | * Note that the method returns a new offset by reference. 308 | */ 309 | private function extractArguments(string $body, int &$offset): array 310 | { 311 | $initial = $offset; 312 | 313 | // Skip all whitespaces 314 | while (\ctype_space($body[$offset] ?? '')) { 315 | ++$offset; 316 | } 317 | 318 | // If there is no "(" parenthesis after the whitespace characters, 319 | // then no further search should be performed, since the arguments 320 | // for the specified directive were not passed. 321 | if (($body[$offset] ?? '') !== '(') { 322 | $offset = $initial; 323 | 324 | return []; 325 | } 326 | 327 | [$arguments, $buffer, $depth] = [[], '', 0]; 328 | 329 | do { 330 | // Current character in body. 331 | $current = $body[$offset++] ?? ''; 332 | 333 | switch ($current) { 334 | // In the case that the current offset has exceeded the 335 | // allowable size, then we consider that no arguments were 336 | // passed. 337 | // 338 | // This situation can arise if the closing parenthesis is 339 | // missing in the source code. 340 | case '': 341 | $offset = $initial; 342 | 343 | return []; 344 | 345 | // To count the same number of open and close parentheses. 346 | // 347 | // In the case that the opening parenthesis "(" is part of the 348 | // argument (depth > 0), then add it in the buffer. 349 | case '(': 350 | if ($depth !== 0) { 351 | $buffer .= $current; 352 | } 353 | ++$depth; 354 | break; 355 | 356 | // To count the same number of open and close parentheses. 357 | // 358 | // In the case that this is the last close parenthesis, then 359 | // return all arguments or add ")" in the buffer otherwise. 360 | case ')': 361 | $depth--; 362 | 363 | if ($depth === 0) { 364 | return [...$arguments, \trim($buffer)]; 365 | } 366 | 367 | $buffer .= $current; 368 | break; 369 | 370 | // Directive arguments separator. 371 | // 372 | // If the current nesting level does not exceed one, it 373 | // creates a new argument from the current buffer. 374 | case ',': 375 | if ($depth === 1) { 376 | $arguments[] = \trim($buffer); 377 | $buffer = ''; 378 | } else { 379 | $buffer .= $current; 380 | } 381 | break; 382 | 383 | // All other characters are simply added to the 384 | // current buffer. 385 | default: 386 | $buffer .= $current; 387 | } 388 | } while ($depth !== 0); 389 | 390 | return $arguments; 391 | } 392 | 393 | private function createTokenForSource(string $name, string $body, int $from, int $to): TokenInterface 394 | { 395 | $slice = \substr($body, $from, $to - $from); 396 | 397 | return new Token($name, $slice, $from); 398 | } 399 | 400 | /** 401 | * Accepts the name of a directive and its arguments, and returns the 402 | * result of executing that directive. 403 | * 404 | * @param non-empty-string $name 405 | * 406 | * @throws DirectiveEvaluationException 407 | */ 408 | public function execute(string $name, array $arguments = []): string 409 | { 410 | $directive = $this->directives->find($name); 411 | 412 | if ($directive === null) { 413 | if ($arguments === []) { 414 | return $name; 415 | } 416 | 417 | throw new DirectiveEvaluationException(\sprintf(self::ERROR_DIRECTIVE_NOT_FOUND, $name)); 418 | } 419 | 420 | try { 421 | return $directive(...$arguments); 422 | } catch (DirectiveEvaluationException $e) { 423 | throw $e; 424 | } catch (\Throwable $e) { 425 | throw new DirectiveEvaluationException($e->getMessage(), (int) $e->getCode(), $e); 426 | } 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /resources/expression.php: -------------------------------------------------------------------------------- 1 | , 12 | * ... 13 | * }, 14 | * skip: list, 15 | * grammar: array, 16 | * reducers: array, 17 | * transitions?: array 18 | * } 19 | */ 20 | return [ 21 | 'initial' => 27, 22 | 'tokens' => [ 23 | 'default' => [ 24 | 'T_HEX_CONSTANT' => '0x([0-9a-fA-F]+)((?i)[ul]*)', 25 | 'T_BIN_CONSTANT' => '0b([0-1]+)((?i)[ul]*)', 26 | 'T_OCT_CONSTANT' => '0([0-7]+)((?i)[ul]*)', 27 | 'T_DEC_CONSTANT' => '([1-9]\\d*|[0-9])((?i)[ul]*)', 28 | 'T_FLOAT_CONSTANT' => '\\bx\\b', 29 | 'T_DEC_FLOAT_CONSTANT' => '\\bx\\b', 30 | 'T_HEX_FLOAT_CONSTANT' => '\\bx\\b', 31 | 'T_STRING_LITERAL' => '(L?)"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"', 32 | 'T_CHAR_CONSTANT' => '(L?)\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'', 33 | 'T_BOOL_CONSTANT' => '\\b(?:true|false)\\b', 34 | 'T_NULL_CONSTANT' => '\\b(?i)(?:null)\\b', 35 | 'T_BOOL_OR' => '\\|\\|', 36 | 'T_BOOL_AND' => '&&', 37 | 'T_MUL' => '\\*', 38 | 'T_DIV' => '/', 39 | 'T_MOD' => '%', 40 | 'T_PLUS_PLUS' => '\\+\\+', 41 | 'T_PLUS' => '\\+', 42 | 'T_MINUS_MINUS' => '\\-\\-', 43 | 'T_MINUS' => '\\-', 44 | 'T_L_SHIFT' => '<<', 45 | 'T_R_SHIFT' => '>>', 46 | 'T_BIN_AND' => '&', 47 | 'T_BIN_OR' => '\\|', 48 | 'T_BIN_XOR' => '\\^', 49 | 'T_BIT_NOT' => '~', 50 | 'T_EQ' => '==', 51 | 'T_NEQ' => '!=', 52 | 'T_GTE' => '>=', 53 | 'T_LTE' => '<=', 54 | 'T_GT' => '>', 55 | 'T_LT' => '<', 56 | 'T_NOT' => '!', 57 | 'T_ASSIGN' => '=', 58 | 'T_SEMICOLON' => ';', 59 | 'T_COMMA' => ',', 60 | 'T_RND_BRACKET_OPEN' => '\\(', 61 | 'T_RND_BRACKET_CLOSE' => '\\)', 62 | 'T_IDENTIFIER' => '[a-zA-Z_\\x80-\\xff][a-zA-Z0-9_\\x80-\\xff]*', 63 | 'T_WHITESPACE' => '\\s+', 64 | 'T_BLOCK_COMMENT' => '\\h*/\\*.*?\\*/', 65 | 'T_COMMENT' => '\\h*//[^\\n]*\\n*', 66 | ], 67 | ], 68 | 'skip' => [ 69 | 'T_WHITESPACE', 70 | 'T_BLOCK_COMMENT', 71 | 'T_COMMENT', 72 | ], 73 | 'transitions' => [], 74 | 'grammar' => [ 75 | new \Phplrt\Parser\Grammar\Lexeme('T_IDENTIFIER', true), 76 | new \Phplrt\Parser\Grammar\Lexeme('T_DEC_CONSTANT', true), 77 | new \Phplrt\Parser\Grammar\Lexeme('T_HEX_CONSTANT', true), 78 | new \Phplrt\Parser\Grammar\Lexeme('T_OCT_CONSTANT', true), 79 | new \Phplrt\Parser\Grammar\Lexeme('T_BIN_CONSTANT', true), 80 | new \Phplrt\Parser\Grammar\Lexeme('T_BOOL_CONSTANT', true), 81 | new \Phplrt\Parser\Grammar\Lexeme('T_STRING_LITERAL', true), 82 | new \Phplrt\Parser\Grammar\Alternation([1, 2, 3, 4, 5, 6]), 83 | new \Phplrt\Parser\Grammar\Lexeme('T_FLOAT_CONSTANT', true), 84 | new \Phplrt\Parser\Grammar\Lexeme('T_DEC_CONSTANT', true), 85 | new \Phplrt\Parser\Grammar\Lexeme('T_CHAR_CONSTANT', true), 86 | new \Phplrt\Parser\Grammar\Lexeme('T_IDENTIFIER', true), 87 | new \Phplrt\Parser\Grammar\Concatenation([11]), 88 | new \Phplrt\Parser\Grammar\Concatenation([25, 20]), 89 | new \Phplrt\Parser\Grammar\Concatenation([26, 20]), 90 | new \Phplrt\Parser\Grammar\Alternation([0, 7, 85]), 91 | new \Phplrt\Parser\Grammar\Concatenation([21, 20]), 92 | new \Phplrt\Parser\Grammar\Concatenation([22, 20]), 93 | new \Phplrt\Parser\Grammar\Concatenation([23, 20]), 94 | new \Phplrt\Parser\Grammar\Concatenation([24, 20]), 95 | new \Phplrt\Parser\Grammar\Alternation([13, 14, 15, 16, 17, 18, 19]), 96 | new \Phplrt\Parser\Grammar\Lexeme('T_PLUS', false), 97 | new \Phplrt\Parser\Grammar\Lexeme('T_MINUS', false), 98 | new \Phplrt\Parser\Grammar\Lexeme('T_NOT', false), 99 | new \Phplrt\Parser\Grammar\Lexeme('T_BIT_NOT', false), 100 | new \Phplrt\Parser\Grammar\Lexeme('T_PLUS_PLUS', true), 101 | new \Phplrt\Parser\Grammar\Lexeme('T_MINUS_MINUS', true), 102 | new \Phplrt\Parser\Grammar\Concatenation([28]), 103 | new \Phplrt\Parser\Grammar\Concatenation([29]), 104 | new \Phplrt\Parser\Grammar\Concatenation([30]), 105 | new \Phplrt\Parser\Grammar\Concatenation([31, 34]), 106 | new \Phplrt\Parser\Grammar\Concatenation([35, 38]), 107 | new \Phplrt\Parser\Grammar\Lexeme('T_BOOL_OR', false), 108 | new \Phplrt\Parser\Grammar\Concatenation([32, 30]), 109 | new \Phplrt\Parser\Grammar\Optional(33), 110 | new \Phplrt\Parser\Grammar\Concatenation([39, 42]), 111 | new \Phplrt\Parser\Grammar\Lexeme('T_BOOL_AND', false), 112 | new \Phplrt\Parser\Grammar\Concatenation([36, 31]), 113 | new \Phplrt\Parser\Grammar\Optional(37), 114 | new \Phplrt\Parser\Grammar\Concatenation([43, 46]), 115 | new \Phplrt\Parser\Grammar\Lexeme('T_BIN_OR', false), 116 | new \Phplrt\Parser\Grammar\Concatenation([40, 35]), 117 | new \Phplrt\Parser\Grammar\Optional(41), 118 | new \Phplrt\Parser\Grammar\Concatenation([47, 50]), 119 | new \Phplrt\Parser\Grammar\Lexeme('T_BIN_XOR', false), 120 | new \Phplrt\Parser\Grammar\Concatenation([44, 39]), 121 | new \Phplrt\Parser\Grammar\Optional(45), 122 | new \Phplrt\Parser\Grammar\Concatenation([51, 60]), 123 | new \Phplrt\Parser\Grammar\Lexeme('T_BIN_AND', false), 124 | new \Phplrt\Parser\Grammar\Concatenation([48, 43]), 125 | new \Phplrt\Parser\Grammar\Optional(49), 126 | new \Phplrt\Parser\Grammar\Concatenation([61, 66]), 127 | new \Phplrt\Parser\Grammar\Lexeme('T_EQ', true), 128 | new \Phplrt\Parser\Grammar\Lexeme('T_NEQ', true), 129 | new \Phplrt\Parser\Grammar\Lexeme('T_GT', true), 130 | new \Phplrt\Parser\Grammar\Lexeme('T_LT', true), 131 | new \Phplrt\Parser\Grammar\Lexeme('T_GTE', true), 132 | new \Phplrt\Parser\Grammar\Lexeme('T_LTE', true), 133 | new \Phplrt\Parser\Grammar\Alternation([52, 53, 54, 55, 56, 57]), 134 | new \Phplrt\Parser\Grammar\Concatenation([58, 47]), 135 | new \Phplrt\Parser\Grammar\Optional(59), 136 | new \Phplrt\Parser\Grammar\Concatenation([67, 72]), 137 | new \Phplrt\Parser\Grammar\Lexeme('T_L_SHIFT', true), 138 | new \Phplrt\Parser\Grammar\Lexeme('T_R_SHIFT', true), 139 | new \Phplrt\Parser\Grammar\Alternation([62, 63]), 140 | new \Phplrt\Parser\Grammar\Concatenation([64, 51]), 141 | new \Phplrt\Parser\Grammar\Optional(65), 142 | new \Phplrt\Parser\Grammar\Concatenation([79, 73]), 143 | new \Phplrt\Parser\Grammar\Lexeme('T_PLUS', true), 144 | new \Phplrt\Parser\Grammar\Lexeme('T_MINUS', true), 145 | new \Phplrt\Parser\Grammar\Alternation([68, 69]), 146 | new \Phplrt\Parser\Grammar\Concatenation([70, 61]), 147 | new \Phplrt\Parser\Grammar\Optional(71), 148 | new \Phplrt\Parser\Grammar\Alternation([82, 20]), 149 | new \Phplrt\Parser\Grammar\Lexeme('T_DIV', true), 150 | new \Phplrt\Parser\Grammar\Lexeme('T_MUL', true), 151 | new \Phplrt\Parser\Grammar\Lexeme('T_MOD', true), 152 | new \Phplrt\Parser\Grammar\Alternation([74, 75, 76]), 153 | new \Phplrt\Parser\Grammar\Concatenation([73, 77]), 154 | new \Phplrt\Parser\Grammar\Repetition(78, 0, INF), 155 | new \Phplrt\Parser\Grammar\Lexeme('T_RND_BRACKET_OPEN', false), 156 | new \Phplrt\Parser\Grammar\Lexeme('T_RND_BRACKET_CLOSE', false), 157 | new \Phplrt\Parser\Grammar\Concatenation([80, 12, 81, 73]), 158 | new \Phplrt\Parser\Grammar\Lexeme('T_RND_BRACKET_OPEN', false), 159 | new \Phplrt\Parser\Grammar\Lexeme('T_RND_BRACKET_CLOSE', false), 160 | new \Phplrt\Parser\Grammar\Concatenation([83, 28, 84]), 161 | ], 162 | 'reducers' => [ 163 | 0 => static function (\Phplrt\Parser\Context $ctx, $children) { 164 | return new Ast\Literal\IdentifierLiteral($children->getValue()); 165 | }, 166 | 1 => static function (\Phplrt\Parser\Context $ctx, $children) { 167 | return new Ast\Literal\IntegerLiteral((int)$children[0]->getValue(), $children[1]->getValue()); 168 | }, 169 | 2 => static function (\Phplrt\Parser\Context $ctx, $children) { 170 | return new Ast\Literal\HexIntegerLiteral((string)$children[0]->getValue(), $children[1]->getValue()); 171 | }, 172 | 3 => static function (\Phplrt\Parser\Context $ctx, $children) { 173 | return new Ast\Literal\OctIntegerLiteral((string)$children[0]->getValue(), $children[1]->getValue()); 174 | }, 175 | 4 => static function (\Phplrt\Parser\Context $ctx, $children) { 176 | return new Ast\Literal\OctIntegerLiteral((string)$children[0]->getValue(), $children[1]->getValue()); 177 | }, 178 | 5 => static function (\Phplrt\Parser\Context $ctx, $children) { 179 | return new Ast\Literal\BooleanLiteral( 180 | $children->getValue() === 'true' 181 | ); 182 | }, 183 | 6 => static function (\Phplrt\Parser\Context $ctx, $children) { 184 | $value = Ast\Literal\StringLiteral::parse( 185 | $children[1]->getValue() 186 | ); 187 | 188 | return new Ast\Literal\StringLiteral($value, $children[0]->getValue() !== ''); 189 | }, 190 | 13 => static function (\Phplrt\Parser\Context $ctx, $children) { 191 | return new Ast\Math\PrefixIncrement($children[1]); 192 | }, 193 | 14 => static function (\Phplrt\Parser\Context $ctx, $children) { 194 | return new Ast\Math\PrefixDecrement($children[1]); 195 | }, 196 | 16 => static function (\Phplrt\Parser\Context $ctx, $children) { 197 | return $children[0]; 198 | }, 199 | 17 => static function (\Phplrt\Parser\Context $ctx, $children) { 200 | return new Ast\Math\UnaryMinus($children[0]); 201 | }, 202 | 18 => static function (\Phplrt\Parser\Context $ctx, $children) { 203 | return new Ast\Math\NotExpression($children[0]); 204 | }, 205 | 19 => static function (\Phplrt\Parser\Context $ctx, $children) { 206 | return new Ast\Math\BitwiseNotExpression($children[1]); 207 | }, 208 | 27 => static function (\Phplrt\Parser\Context $ctx, $children) { 209 | return $children[0]; 210 | }, 211 | 30 => static function (\Phplrt\Parser\Context $ctx, $children) { 212 | if (\count($children) === 2) { 213 | return new Ast\Logical\OrExpression($children[0], $children[1]); 214 | } 215 | 216 | return $children; 217 | }, 218 | 31 => static function (\Phplrt\Parser\Context $ctx, $children) { 219 | if (\count($children) === 2) { 220 | return new Ast\Logical\AndExpression($children[0], $children[1]); 221 | } 222 | 223 | return $children; 224 | }, 225 | 35 => static function (\Phplrt\Parser\Context $ctx, $children) { 226 | if (\count($children) === 2) { 227 | return new Ast\Logical\BitwiseOrExpression($children[0], $children[1]); 228 | } 229 | 230 | return $children; 231 | }, 232 | 39 => static function (\Phplrt\Parser\Context $ctx, $children) { 233 | if (\count($children) === 2) { 234 | return new Ast\Logical\BitwiseXorExpression($children[0], $children[1]); 235 | } 236 | 237 | return $children; 238 | }, 239 | 43 => static function (\Phplrt\Parser\Context $ctx, $children) { 240 | if (\count($children) === 2) { 241 | return new Ast\Logical\BitwiseAndExpression($children[0], $children[1]); 242 | } 243 | 244 | return $children; 245 | }, 246 | 47 => static function (\Phplrt\Parser\Context $ctx, $children) { 247 | if (\count($children) === 3) { 248 | switch ($children[1]->getName()) { 249 | case 'T_EQ': return new Ast\Comparison\Equal($children[0], $children[2]); 250 | case 'T_NEQ': return new Ast\Comparison\NotEqual($children[0], $children[2]); 251 | case 'T_GT': return new Ast\Comparison\GreaterThan($children[0], $children[2]); 252 | case 'T_GTE': return new Ast\Comparison\GreaterThanOrEqual($children[0], $children[2]); 253 | case 'T_LT': return new Ast\Comparison\LessThan($children[0], $children[2]); 254 | case 'T_LTE': return new Ast\Comparison\LessThanOrEqual($children[0], $children[2]); 255 | } 256 | } 257 | 258 | return $children; 259 | }, 260 | 51 => static function (\Phplrt\Parser\Context $ctx, $children) { 261 | if (\count($children) === 3) { 262 | switch ($children[1]->getName()) { 263 | case 'T_L_SHIFT': return new Ast\Math\BitwiseLeftShiftExpression($children[0], $children[2]); 264 | case 'T_R_SHIFT': return new Ast\Math\BitwiseRightShiftExpression($children[0], $children[2]); 265 | } 266 | } 267 | 268 | return $children; 269 | }, 270 | 61 => static function (\Phplrt\Parser\Context $ctx, $children) { 271 | if (\count($children) === 3) { 272 | switch ($children[1]->getName()) { 273 | case 'T_PLUS': return new Ast\Math\SumExpression($children[0], $children[2]); 274 | case 'T_MINUS': return new Ast\Math\SubtractionExpression($children[0], $children[2]); 275 | } 276 | } 277 | 278 | return $children; 279 | }, 280 | 67 => static function (\Phplrt\Parser\Context $ctx, $children) { 281 | while (\count($children) >= 3) { 282 | [$a, $op, $b] = [ 283 | \array_shift($children), 284 | \array_shift($children), 285 | \array_shift($children), 286 | ]; 287 | 288 | switch ($op->getName()) { 289 | case 'T_MOD': 290 | \array_unshift($children, new Ast\Math\ModExpression($a, $b)); 291 | break; 292 | 293 | case 'T_DIV': 294 | \array_unshift($children, new Ast\Math\DivExpression($a, $b)); 295 | break; 296 | 297 | case 'T_MUL': 298 | \array_unshift($children, new Ast\Math\MulExpression($a, $b)); 299 | break; 300 | } 301 | } 302 | 303 | return $children; 304 | }, 305 | 73 => static function (\Phplrt\Parser\Context $ctx, $children) { 306 | if (\is_array($children) && \count($children) === 2) { 307 | return new Ast\CastExpression($children[0]->getValue(), $children[1]); 308 | } 309 | 310 | return $children; 311 | }, 312 | ], 313 | ]; -------------------------------------------------------------------------------- /src/Internal/Runtime/SourceExecutor.php: -------------------------------------------------------------------------------- 1 | 61 | */ 62 | private int $options; 63 | 64 | /** 65 | * @param int-mask-of $options 66 | */ 67 | public function __construct( 68 | DirectivesRepository $directives, 69 | DirectoriesRepository $directories, 70 | SourceRepository $sources, 71 | LoggerInterface $logger, 72 | int $options 73 | ) { 74 | $this->directives = $directives; 75 | $this->directories = $directories; 76 | $this->sources = $sources; 77 | $this->logger = $logger; 78 | $this->options = $options; 79 | 80 | $this->lexer = new Lexer(); 81 | $this->stack = new OutputStack(); 82 | $this->executor = new DirectiveExecutor($this->directives); 83 | $this->expressions = Parser::fromFile(self::GRAMMAR_PATHNAME); 84 | } 85 | 86 | /** 87 | * @return \Traversable 88 | * @throws PreprocessorException 89 | * 90 | * @psalm-suppress ArgumentTypeCoercion 91 | */ 92 | public function execute(ReadableInterface $source): \Traversable 93 | { 94 | try { 95 | /** @var iterable $stream */ 96 | $stream = $this->lexer->lex($this->read($source)); 97 | } catch (RuntimeExceptionInterface $e) { 98 | throw PreprocessException::fromSource($e->getMessage(), $source, $e->getToken(), $e); 99 | } 100 | 101 | foreach ($stream as $token) { 102 | try { 103 | switch ($token->getName()) { 104 | case Lexer::T_ERROR: 105 | yield from $this->doError($token, $source); 106 | break; 107 | 108 | case Lexer::T_WARNING: 109 | yield from $this->doWarning($token, $source); 110 | break; 111 | 112 | case Lexer::T_QUOTED_INCLUDE: 113 | case Lexer::T_ANGLE_BRACKET_INCLUDE: 114 | yield from $this->doInclude($token, $source); 115 | break; 116 | 117 | case Lexer::T_IFDEF: 118 | yield from $this->doIfDefined($token, $source); 119 | break; 120 | 121 | case Lexer::T_IFNDEF: 122 | yield from $this->doIfNotDefined($token, $source); 123 | break; 124 | 125 | case Lexer::T_ENDIF: 126 | yield from $this->doEndIf($token, $source); 127 | break; 128 | 129 | case Lexer::T_IF: 130 | yield from $this->doIf($token, $source); 131 | break; 132 | 133 | case Lexer::T_ELSE_IF: 134 | yield from $this->doElseIf($token, $source); 135 | break; 136 | 137 | case Lexer::T_ELSE: 138 | yield from $this->doElse($token, $source); 139 | break; 140 | 141 | case Lexer::T_OBJECT_MACRO: 142 | yield from $this->doObjectLikeDirective($token, $source); 143 | break; 144 | 145 | case Lexer::T_FUNCTION_MACRO: 146 | yield from $this->doFunctionLikeDirective($token, $source); 147 | break; 148 | 149 | case Lexer::T_UNDEF: 150 | yield from $this->doRemoveDefine($token, $source); 151 | break; 152 | 153 | case Lexer::T_SOURCE: 154 | yield $this->doRenderCode($token); 155 | break; 156 | 157 | default: 158 | throw new \LogicException(\sprintf('Non implemented token "%s"', $token->getName())); 159 | } 160 | } catch (RuntimeExceptionInterface $e) { 161 | $message = $e instanceof RuntimeException 162 | ? $e->getOriginalMessage() 163 | : $e->getMessage(); 164 | 165 | $exception = new PreprocessException($message, (int) $e->getCode(), $e); 166 | $exception->setSource($source); 167 | $exception->setToken($token); 168 | 169 | throw $exception; 170 | } catch (\Throwable $e) { 171 | throw PreprocessException::fromSource($e->getMessage(), $source, $token, $e); 172 | } 173 | } 174 | } 175 | 176 | /** 177 | * @param list $comments 178 | * 179 | * @return list 180 | */ 181 | private function debug(ReadableInterface $source, TokenInterface $token, array $comments = []): iterable 182 | { 183 | if (Option::contains($this->options, Option::KEEP_DEBUG_COMMENTS)) { 184 | $line = Position::fromOffset($source, $token->getOffset()) 185 | ->getLine(); 186 | 187 | return [ 188 | '#// ' . $this->sourceToString($source) . ':' . $line . "\n", 189 | /** @psalm-suppress DuplicateArrayKey */ 190 | ...\array_map(static fn(string $line): string => "#// $line\n", $comments), 191 | ]; 192 | } 193 | 194 | return []; 195 | } 196 | 197 | private function sourceToString(ReadableInterface $source): string 198 | { 199 | return $source instanceof FileInterface 200 | ? $source->getPathname() 201 | : '{' . $source->getHash() . '}' 202 | ; 203 | } 204 | 205 | private function read(ReadableInterface $source): string 206 | { 207 | $content = $source->getContents(); 208 | 209 | return \str_replace("\r", '', $content); 210 | } 211 | 212 | /** 213 | * @return list 214 | * 215 | * @psalm-suppress PossiblyNullReference first value of Composite Token cannot be null 216 | */ 217 | private function doError(Composite $tok, ReadableInterface $src): iterable 218 | { 219 | if (!$this->stack->isEnabled()) { 220 | return []; 221 | } 222 | 223 | $message = $this->escape(\trim($tok[0]->getValue())); 224 | 225 | $this->logger->error($message, [ 226 | 'position' => Position::fromOffset($tok->getOffset()), 227 | 'source' => $src, 228 | ]); 229 | 230 | return $this->debug($src, $tok, ['error ' . $message]); 231 | } 232 | 233 | /** 234 | * Replaces all occurrences of \ + \ n with normal line break. 235 | * 236 | * A backslash "\" + "\n" means the continuation of an expression, which 237 | * means it is not a significant character. 238 | * 239 | * 240 | * #if some\ 241 | * any 242 | * 243 | * 244 | * Contain this value: 245 | * 246 | * 247 | * "some\ 248 | * any" 249 | * 250 | * 251 | * And should replace into: 252 | * 253 | * 254 | * "some 255 | * any" 256 | * 257 | */ 258 | private function escape(string $body): string 259 | { 260 | return \str_replace("\\\n", "\n", $body); 261 | } 262 | 263 | /** 264 | * @return iterable 265 | * 266 | * @psalm-suppress PossiblyNullReference first value of Composite Token cannot be null 267 | */ 268 | private function doWarning(Composite $tok, ReadableInterface $src): iterable 269 | { 270 | if (!$this->stack->isEnabled()) { 271 | return []; 272 | } 273 | 274 | $message = $this->escape(\trim($tok[0]->getValue())); 275 | 276 | $this->logger->warning($message, [ 277 | 'position' => Position::fromOffset($tok->getOffset()), 278 | 'source' => $src, 279 | ]); 280 | 281 | return $this->debug($src, $tok, ['warning ' . $message]); 282 | } 283 | 284 | /** 285 | * @return iterable 286 | * @throws \Throwable 287 | * 288 | * @psalm-suppress PossiblyNullReference first value of Composite Token cannot be null 289 | * @psalm-suppress PossiblyNullArgument same as PossiblyNullReference 290 | */ 291 | private function doInclude(Composite $token, ReadableInterface $src): iterable 292 | { 293 | if (!$this->stack->isEnabled()) { 294 | return []; 295 | } 296 | 297 | $isQuotedInclude = $token->getName() === Lexer::T_QUOTED_INCLUDE; 298 | 299 | $filename = $isQuotedInclude 300 | ? \str_replace('\"', '"', $token[0]->getValue()) 301 | : $token[0]->getValue(); 302 | 303 | try { 304 | $inclusion = $this->lookup($src, $filename, $isQuotedInclude); 305 | } catch (\Throwable $e) { 306 | throw NotReadableException::fromSource($e->getMessage(), $src, $token[0]); 307 | } 308 | 309 | yield from $this->debug($src, $token, ['include ' . $this->sourceToString($inclusion)]); 310 | yield from $this->execute($inclusion); 311 | } 312 | 313 | private function lookup(ReadableInterface $source, string $file, bool $withLocal): ReadableInterface 314 | { 315 | $file = $this->normalizeRelativePathname($file); 316 | 317 | /** 318 | * Local overridden sources should be a priority. 319 | * 320 | * @var non-empty-string $name 321 | * @var ReadableInterface $out 322 | */ 323 | foreach ($this->sources as $name => $out) { 324 | if ($this->normalizeRelativePathname($name) === $file) { 325 | return $out; 326 | } 327 | } 328 | 329 | if ($source instanceof FileInterface && $withLocal) { 330 | $pathname = \dirname($source->getPathname()) . \DIRECTORY_SEPARATOR . $file; 331 | 332 | if (\is_file($pathname)) { 333 | return File::fromPathname($pathname); 334 | } 335 | } 336 | 337 | foreach ($this->directories as $directory) { 338 | $pathname = $directory . \DIRECTORY_SEPARATOR . $file; 339 | 340 | if (\is_file($pathname)) { 341 | return File::fromPathname($pathname); 342 | } 343 | } 344 | 345 | throw new \LogicException(\sprintf('"%s": No such file or directory', $file)); 346 | } 347 | 348 | private function normalizeRelativePathname(string $file): string 349 | { 350 | $file = \trim($file, " \t\n\r\0\x0B/\\"); 351 | 352 | return \str_replace(['\\', '/'], \DIRECTORY_SEPARATOR, $file); 353 | } 354 | 355 | /** 356 | * @return iterable 357 | * 358 | * @psalm-suppress PossiblyNullReference first value of Composite Token cannot be null 359 | */ 360 | private function doIfDefined(Composite $token, ReadableInterface $source): iterable 361 | { 362 | if (!$this->stack->isEnabled()) { 363 | $this->stack->push(false); 364 | 365 | return []; 366 | } 367 | 368 | $name = $this->escape($token[0]->getValue()); 369 | 370 | assert($name !== '', 'Directive name cannot be empty'); 371 | $defined = $this->directives->defined($name); 372 | 373 | $this->stack->push($defined); 374 | 375 | return $this->debug($source, $token, ['if defined ' . $name]); 376 | } 377 | 378 | /** 379 | * @return iterable 380 | * 381 | * @psalm-suppress PossiblyNullReference first value of Composite Token cannot be null 382 | */ 383 | private function doIfNotDefined(Composite $token, ReadableInterface $source): iterable 384 | { 385 | if (!$this->stack->isEnabled()) { 386 | $this->stack->push(false); 387 | 388 | return []; 389 | } 390 | 391 | /** @psalm-var non-empty-string $name */ 392 | $name = $this->escape($token[0]->getValue()); 393 | 394 | $defined = $this->directives->defined($name); 395 | 396 | $this->stack->push(!$defined); 397 | 398 | return $this->debug($source, $token, ['if not defined ' . $name]); 399 | } 400 | 401 | /** 402 | * @return iterable 403 | */ 404 | private function doEndIf(TokenInterface $token, ReadableInterface $source): iterable 405 | { 406 | try { 407 | $this->stack->pop(); 408 | } catch (\LogicException $e) { 409 | throw new \LogicException('#endif directive without #if'); 410 | } 411 | 412 | return $this->debug($source, $token, ['endif']); 413 | } 414 | 415 | /** 416 | * @return iterable 417 | * @throws RuntimeExceptionInterface 418 | * @throws \Throwable 419 | * 420 | * @psalm-suppress PossiblyNullReference first value of Composite Token cannot be null 421 | */ 422 | private function doIf(Composite $token, ReadableInterface $source): iterable 423 | { 424 | if (!$this->stack->isEnabled()) { 425 | $this->stack->push(false); 426 | 427 | return []; 428 | } 429 | 430 | $this->stack->push($this->eval($token)); 431 | 432 | return $this->debug($source, $token, ['if ' . $token[0]->getValue()]); 433 | } 434 | 435 | /** 436 | * @throws RuntimeExceptionInterface 437 | * @throws \Throwable 438 | * 439 | * @psalm-suppress PossiblyNullReference first value of Composite Token cannot be null 440 | */ 441 | private function eval(Composite $token): bool 442 | { 443 | $body = $this->escape($token[0]->getValue()); 444 | 445 | $processed = $this->replace($body, DirectiveExecutor::CTX_EXPRESSION); 446 | 447 | $ast = $this->expressions->parse($processed); 448 | 449 | return (bool) $ast->eval(); 450 | } 451 | 452 | /** 453 | * @param DirectiveExecutorContext $ctx 454 | */ 455 | private function replace(string $body, int $ctx): string 456 | { 457 | return $this->executor->replace($body, $ctx); 458 | } 459 | 460 | /** 461 | * @return iterable 462 | * @throws RuntimeExceptionInterface 463 | * @throws \Throwable 464 | */ 465 | private function doElseIf(Composite $token, ReadableInterface $source): iterable 466 | { 467 | if (!$this->stack->isCompleted() && $this->eval($token)) { 468 | $this->stack->complete(); 469 | 470 | return []; 471 | } 472 | 473 | $this->stack->update(false, $this->stack->isCompleted()); 474 | 475 | return $this->debug($source, $token, ['else if ' . $token->getValue()]); 476 | } 477 | 478 | /** 479 | * @return iterable 480 | */ 481 | private function doElse(TokenInterface $token, ReadableInterface $source): iterable 482 | { 483 | try { 484 | if (!$this->stack->isCompleted()) { 485 | $this->stack->inverse(); 486 | } else { 487 | $this->stack->update(false); 488 | } 489 | } catch (\LogicException $e) { 490 | throw new \LogicException('#else directive without #if'); 491 | } 492 | 493 | return $this->debug($source, $token, ['else']); 494 | } 495 | 496 | /** 497 | * @return iterable 498 | * @throws DirectiveDefinitionExceptionInterface 499 | * 500 | * @psalm-suppress PossiblyNullReference first value of Composite Token cannot be null 501 | */ 502 | private function doObjectLikeDirective(Composite $token, ReadableInterface $source): iterable 503 | { 504 | if (!$this->stack->isEnabled()) { 505 | return []; 506 | } 507 | 508 | // Name 509 | /** @psalm-var non-empty-string $name */ 510 | $name = \trim($token[0]->getValue()); 511 | 512 | // Value 513 | $value = \count($token) === 1 ? DirectiveInterface::DEFAULT_VALUE : \trim($token[1]->getValue()); 514 | $value = $this->replace($value, DirectiveExecutor::CTX_EXPRESSION); 515 | 516 | $this->directives->define($name, new ObjectLikeDirective($value)); 517 | 518 | return $this->debug($source, $token, ['define ' . $name . ' = ' . ($value ?: '""')]); 519 | } 520 | 521 | /** 522 | * @return iterable 523 | * @throws DirectiveDefinitionExceptionInterface 524 | * 525 | * @psalm-suppress PossiblyNullReference The values of Composite Token cannot be null in this cases 526 | */ 527 | private function doFunctionLikeDirective(Composite $token, ReadableInterface $source): iterable 528 | { 529 | if (!$this->stack->isEnabled()) { 530 | return []; 531 | } 532 | 533 | // Name 534 | /** @psalm-var non-empty-string $name */ 535 | $name = \trim($token[0]->getValue()); 536 | 537 | // Arguments 538 | $args = \explode(',', $token[1]->getValue()); 539 | 540 | // Value 541 | $value = \count($token) === 2 ? DirectiveInterface::DEFAULT_VALUE : \trim($token[2]->getValue()); 542 | $value = $this->replace($value, DirectiveExecutor::CTX_EXPRESSION); 543 | 544 | $this->directives->define($name, new FunctionLikeDirective($args, $value)); 545 | 546 | return $this->debug($source, $token, [ 547 | 'define ' . $name . '(' . $token[1]->getValue() . ') = ' . ($value ?: '""'), 548 | ]); 549 | } 550 | 551 | /** 552 | * @return iterable 553 | * 554 | * @psalm-suppress PossiblyNullReference first value of Composite Token cannot be null 555 | */ 556 | private function doRemoveDefine(Composite $token, ReadableInterface $source): iterable 557 | { 558 | if (!$this->stack->isEnabled()) { 559 | return []; 560 | } 561 | 562 | $name = $this->escape($token[0]->getValue()); 563 | 564 | assert($name !== '', 'Directive name cannot be empty'); 565 | $this->directives->undef($name); 566 | 567 | return $this->debug($source, $token, ['undef ' . $name]); 568 | } 569 | 570 | private function doRenderCode(TokenInterface $token): string 571 | { 572 | if (!$this->stack->isEnabled()) { 573 | return ''; 574 | } 575 | 576 | $body = $this->escape($token->getValue()); 577 | 578 | return $this->replace($body, DirectiveExecutor::CTX_SOURCE); 579 | } 580 | } 581 | --------------------------------------------------------------------------------