diff --git a/phpunit/code/generated-code-indentation.php b/phpunit/code/generated-code-indentation.php new file mode 100644 index 00000000..e809e93f --- /dev/null +++ b/phpunit/code/generated-code-indentation.php @@ -0,0 +1,20 @@ +addFiles([$source]); + $compiler->prepareFile($source); + $cppFile = $compiler->convertFile($source); + + $this->assertNotNull($cppFile); + $code = file_get_contents($cppFile); + $this->assertIsString($code); + $this->assertStringContainsString( + "\t\twhile (tmp_var_0.next()) {\n\t\t\titem = tmp_var_0.value();", + $code, + ); + $this->assertStringContainsString( + "\t\t\ttry {\n\t\t\t\tif (", + $code, + ); + $this->assertStringContainsString( + "\t\t\t\t} else {\n\t\t\t\t\tif (", + $code, + ); + $this->assertStringContainsString( + "\ttry {\n\t\tphp::checkCallArgCount(1, 1, false);", + $code, + ); + $this->assertStringNotContainsString("\ntry {", $code); + $this->assertStringNotContainsString("\ncatch (zend_object", $code); + $this->assertDoesNotMatchRegularExpression('/}[ \\t]+}/', $code); + } +} diff --git a/phpunit/src/TypeCheckGeneratorTest.php b/phpunit/src/TypeCheckGeneratorTest.php index 3cf4c85e..57716f75 100644 --- a/phpunit/src/TypeCheckGeneratorTest.php +++ b/phpunit/src/TypeCheckGeneratorTest.php @@ -103,4 +103,34 @@ class TypeCheckGeneratorTest extends \PHPUnit\Framework\TestCase $this->assertStringContainsString('php::throwReturnTypeError(', $returnCode); $this->assertStringNotContainsString('must be of type', $returnCode); } + + public function testDynamicStrictScalarArgumentsUseInlinePhpxConversions(): void + { + $compiler = CompilerTest::create(ROOT_PATH); + $this->setProtectedProperty($compiler, 'noLiteralStrings', true); + + foreach ([ + Type::INT => 'php::toIntArgExact', + Type::FLOAT => 'php::toFloatArgExact', + Type::BOOL => 'php::toBoolArgExact', + Type::STR => 'php::toStringArgExact', + ] as $type => $helper) { + $argInfo = new ArgInfo(); + $argInfo->name = 'value'; + $argInfo->phpName = 'value'; + $argInfo->type = $type; + + $expr = $this->invokeMethod( + $compiler, + 'genStrictScalarArgConversion', + [$argInfo, 'dynamic_value', 'Foo\\Bar::run', '2'], + ); + + $this->assertSame( + $helper . '(dynamic_value, php::Str{ZEND_STRL("Foo\\\\Bar::run")}, 2, php::Str{ZEND_STRL("value")})', + $expr, + ); + $this->assertStringNotContainsString('[&]', $expr); + } + } } diff --git a/src/CompilerBase.php b/src/CompilerBase.php index f1324856..d28da198 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1653,7 +1653,12 @@ class CompilerBase implements PropertyAccessContext if (!$stmts) { return ''; } - return $this->getIndent() . implode(PHP_EOL . $this->getIndent(), $stmts) . PHP_EOL; + + $code = ''; + foreach ($stmts as $stmt) { + $code .= $this->formatStatementFragment($stmt) . PHP_EOL; + } + return $code; } protected function genConditionWithCapturedStmts(NodeAbstract $cond, string $openPrefix): string @@ -1673,7 +1678,7 @@ class CompilerBase implements PropertyAccessContext $condExpr = '(' . $condExpr . ')'; } $condExpr = $this->convertConditionExpr($cond, $condExpr); - $code .= $openPrefix . '(' . $condExpr . ') {' . PHP_EOL; + $code .= $this->getIndent() . $openPrefix . '(' . $condExpr . ') {' . PHP_EOL; return $code; } @@ -1815,13 +1820,48 @@ class CompilerBase implements PropertyAccessContext $code = ''; foreach ($lines as $line) { - $code .= $this->getIndent() . $line . PHP_EOL; + $code .= $this->formatStatementFragment($line) . PHP_EOL; } $this->context->leaveScope(); return $code; } + /** + * Statement lowerers may return a multi-line fragment. Some nested lines + * already carry their absolute indentation, while simple statements do + * not. Apply the current scope indentation to every unindented physical + * line instead of only the first line of the fragment. + */ + protected function formatStatementFragment(string $fragment): string + { + $indent = $this->getIndent(); + $fragment = rtrim($fragment, "\r\n"); + $lines = preg_split('/\R/', $fragment); + if ($lines === false) { + return $indent . $fragment; + } + + $firstContentLine = true; + foreach ($lines as &$line) { + if ($line === '') { + continue; + } + if ($firstContentLine) { + // A fragment represents one statement at the current scope. + // Its first physical line must not retain indentation captured + // from an intermediate expression-lowering context. + $line = $indent . ltrim($line); + $firstContentLine = false; + } elseif ($line[0] !== ' ' && $line[0] !== "\t") { + $line = $indent . $line; + } + } + unset($line); + + return implode(PHP_EOL, $lines); + } + protected function assertMustUseResultIsConsumed(NodeAbstract $expr): void { $functionDef = $this->resolveCalledFunctionDef($expr); diff --git a/src/Generator/TypeCheckGenerator.php b/src/Generator/TypeCheckGenerator.php index 11910d41..48a7332d 100644 --- a/src/Generator/TypeCheckGenerator.php +++ b/src/Generator/TypeCheckGenerator.php @@ -72,6 +72,26 @@ trait TypeCheckGenerator return $code; } + protected function genStrictScalarArgConversion( + ArgInfo $argInfo, + string $valueExpr, + string $callableName, + string $argNoExpr + ): string { + $helper = match ($argInfo->type) { + Type::INT => 'php::toIntArgExact', + Type::FLOAT => 'php::toFloatArgExact', + Type::BOOL => 'php::toBoolArgExact', + Type::STR => 'php::toStringArgExact', + default => throw new \LogicException('Not a strict scalar type: ' . $argInfo->type), + }; + $paramName = $argInfo->phpName ?: $this->unescapeVarName($argInfo->name); + + return $helper . '(' . $valueExpr . ', ' + . $this->getLiteralString($callableName) . ', ' . $argNoExpr . ', ' + . $this->getLiteralString($paramName) . ')'; + } + protected function genStrictScalarReturnCheck(string $valueExpr, string $returnType): string { if (!$this->isStrictScalarType($returnType)) { diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 4ad41096..5f955a50 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -245,11 +245,11 @@ trait AssignOpTrait protected function parseAssignToList(Expr $left, Expr $right): string { $items = $left->items; - $code = '{'; + $code = '{' . PHP_EOL; $this->indentLevel++; $tmpVar = $this->genTmpVarName(); $this->addLocalVar($tmpVar, Type::VAR); - $code .= $this->getIndent() . $tmpVar . ' = ' . $this->parseExpr($right) . '; '; + $code .= $this->getIndent() . $tmpVar . ' = ' . $this->parseExpr($right) . ';' . PHP_EOL; foreach ($items as $k => $item) { if (!$item) { continue; @@ -259,14 +259,16 @@ trait AssignOpTrait if ($item->value instanceof Expr\List_) { $nestedTmp = $this->genTmpVarName(); $this->addLocalVar($nestedTmp, Type::ARRAY); - $code .= "{$nestedTmp} = {$tmpVar}.item({$key}); "; - $code .= $this->parseAssignToList($item->value, new Variable($nestedTmp)); + $code .= $this->getIndent() . "{$nestedTmp} = {$tmpVar}.item({$key});" . PHP_EOL; + $code .= $this->getIndent() + . $this->parseAssignToList($item->value, new Variable($nestedTmp)) + . PHP_EOL; } else { $var = $this->parseWritableIdentifier($item->value); if ($this->isVarExpr($item->value) and !$this->hasVar($var)) { $this->addLocalVar($var, Type::VAR); } - $code .= "{$var} = {$tmpVar}.item({$key}); "; + $code .= $this->getIndent() . "{$var} = {$tmpVar}.item({$key});" . PHP_EOL; } } else { abort($item); @@ -274,7 +276,7 @@ trait AssignOpTrait } $this->indentLevel--; - return $code . '}'; + return $code . $this->getIndent() . '}'; } protected function parseAssignFinally( diff --git a/src/Parser/BinaryOpTrait.php b/src/Parser/BinaryOpTrait.php index 5778b37e..a1e24477 100644 --- a/src/Parser/BinaryOpTrait.php +++ b/src/Parser/BinaryOpTrait.php @@ -909,20 +909,24 @@ trait BinaryOpTrait $shortCircuitValue = $op === '&&' ? 'false' : 'true'; $rightCondition = $op === '&&' ? $leftBool : '!(' . $leftBool . ')'; - $code = '[&]() -> bool {'; - $code .= $this->getIndent() . 'if (' . $rightCondition . ') {'; + $code = '[&]() -> bool {' . PHP_EOL; + $this->indentLevel++; + $code .= $this->getIndent() . 'if (' . $rightCondition . ') {' . PHP_EOL; + $this->indentLevel++; $code .= $this->formatCapturedStmtLines($rightBeforeStmts); if ($rightAfterStmts) { $rightTmpVar = $this->addTmpVar(Type::VAR); - $code .= $this->getIndent() . $rightTmpVar . ' = ' . $rightExpr . ';'; + $code .= $this->getIndent() . $rightTmpVar . ' = ' . $rightExpr . ';' . PHP_EOL; $code .= $this->formatCapturedStmtLines($rightAfterStmts); $rightExpr = $rightTmpVar; $rightBool = $this->convertPythonObjectToBool($right, $rightExpr) ?? $this->convertBoolExpr($rightExpr, $this->detectTypeOfExpr($right)); } - $code .= $this->getIndent() . 'return ' . $rightBool . ';'; - $code .= $this->getIndent() . '}'; - $code .= $this->getIndent() . 'return ' . $shortCircuitValue . ';'; + $code .= $this->getIndent() . 'return ' . $rightBool . ';' . PHP_EOL; + $this->indentLevel--; + $code .= $this->getIndent() . '}' . PHP_EOL; + $code .= $this->getIndent() . 'return ' . $shortCircuitValue . ';' . PHP_EOL; + $this->indentLevel--; $code .= $this->getIndent() . '}()'; return $code; diff --git a/src/Parser/ConditionalControlTrait.php b/src/Parser/ConditionalControlTrait.php index 1b16b760..50edde50 100644 --- a/src/Parser/ConditionalControlTrait.php +++ b/src/Parser/ConditionalControlTrait.php @@ -18,7 +18,8 @@ trait ConditionalControlTrait $arms[] = [$elseif->cond, $elseif->stmts]; } - return $this->parseBeforeStmtLines() . PHP_EOL . $this->getIndent() . $this->parseIfChain($arms, $v->else, 0) . PHP_EOL; + return $this->parseBeforeStmtLines() + . $this->parseIfChain($arms, $v->else, 0) . PHP_EOL; } protected function parseIfChain(array $arms, ?Node\Stmt\Else_ $else, int $index): string @@ -27,16 +28,22 @@ trait ConditionalControlTrait if (!$else || $this->isEmptyStmtList($else->stmts)) { return ''; } - return $this->parseBlockStmts($else->stmts); + return $this->parseStmts($else->stmts); } [$cond, $stmts] = $arms[$index]; $code = $this->genConditionWithCapturedStmts($cond, 'if '); $code .= $this->parseBlockStmts($stmts); - $tail = $this->parseIfChain($arms, $else, $index + 1); - if ($tail !== '') { + $hasTail = isset($arms[$index + 1]) || ($else && !$this->isEmptyStmtList($else->stmts)); + if ($hasTail) { $code .= $this->getIndent() . '} else {' . PHP_EOL; + $this->indentLevel++; + $tail = $this->parseIfChain($arms, $else, $index + 1); $code .= $tail; + $this->indentLevel--; + if (!str_ends_with($tail, PHP_EOL)) { + $code .= PHP_EOL; + } } $code .= $this->getIndent() . '}'; return $code; @@ -56,4 +63,3 @@ trait ConditionalControlTrait * 逻辑比较的运算,必须返回 bool 类型. */ } - diff --git a/src/Parser/ExceptionControlFlowTrait.php b/src/Parser/ExceptionControlFlowTrait.php index ab73f16a..ec938a72 100644 --- a/src/Parser/ExceptionControlFlowTrait.php +++ b/src/Parser/ExceptionControlFlowTrait.php @@ -45,7 +45,7 @@ trait ExceptionControlFlowTrait protected function parseTryCatch(mixed $v): string { - $code = $this->parseBeforeStmtLines() . PHP_EOL; + $code = $this->parseBeforeStmtLines(); $code .= 'try {'; $finally = $v->finally; $stmts = $finally ? $this->injectFinallyBeforeReturn($v->stmts, $finally->stmts) : $v->stmts; @@ -68,23 +68,23 @@ trait ExceptionControlFlowTrait $code .= PHP_EOL; $code .= $this->parseBlockStmts($stmts); - $code .= $this->getIndent() . '}' . PHP_EOL; + $code .= $this->getIndent() . '}'; $exVar = $this->genTmpVarName(); $this->addLocalVar($exVar, Type::VAR); - $code .= 'catch(zend_object *_ex) {' . PHP_EOL; + $code .= ' catch (zend_object *_ex) {' . PHP_EOL; + $this->indentLevel++; $code .= $this->getIndent() . $exVar . ' = php::catchException();' . PHP_EOL; if ($catches) { $catchMatched = $this->genTmpVarName(); $code .= $this->getIndent() . 'bool ' . $catchMatched . ' = false;' . PHP_EOL; - $this->indentLevel++; foreach ($catches as $catch) { - $code .= $this->parseCatch($catch, $exVar, $catchMatched, $finally?->stmts ?? []); + $code .= $this->parseCatch($catch, $exVar, $catchMatched, $finally?->stmts ?? []) . PHP_EOL; } - $this->indentLevel--; } - $code .= '}' . PHP_EOL; + $this->indentLevel--; + $code .= $this->getIndent() . '}' . PHP_EOL; if ($finally) { $code .= $this->parseStmts($finally->stmts); @@ -93,7 +93,11 @@ trait ExceptionControlFlowTrait $rethrow = $this->inGeneratorBody ? 'typephp_fiber_rethrow(' . $exVar . ');' : 'php::throwException(php::Object(' . $exVar . '));'; - $code .= 'if (' . $exVar . ') {' . PHP_EOL . $this->getIndent() . $rethrow . PHP_EOL . $this->getIndent() . '}'; + $code .= 'if (' . $exVar . ') {' . PHP_EOL; + $this->indentLevel++; + $code .= $this->getIndent() . $rethrow . PHP_EOL; + $this->indentLevel--; + $code .= $this->getIndent() . '}'; return $code; } @@ -199,7 +203,7 @@ trait ExceptionControlFlowTrait $this->addLocalVar($var, Type::OBJECT); } - $code = $this->parseBeforeStmtLines() . PHP_EOL; + $code = $this->parseBeforeStmtLines(); $code .= $this->getIndent() . 'if (!' . $catchMatched . ' && ' . $exVar . ' && '; $conditions = []; foreach ($types as $type) { diff --git a/src/Parser/ForeachTrait.php b/src/Parser/ForeachTrait.php index 41f9bb97..ec7841df 100644 --- a/src/Parser/ForeachTrait.php +++ b/src/Parser/ForeachTrait.php @@ -27,7 +27,7 @@ trait ForeachTrait if ($item->value instanceof Expr\List_) { $nestedTmpVar = $this->genTmpVarName(); $this->addLocalVar($nestedTmpVar, Type::VAR); - $code .= $this->getIndent() . ' ' . $nestedTmpVar . ' = ' . $listTmpVar . '.item(' . $key . ');' . PHP_EOL; + $code .= $this->getIndent() . $nestedTmpVar . ' = ' . $listTmpVar . '.item(' . $key . ');' . PHP_EOL; $code .= $this->parseForeachItemAsList($nestedTmpVar, $item->value->items); continue; } @@ -35,7 +35,7 @@ trait ForeachTrait if ($this->isVarExpr($item->value) and !$this->hasVar($var)) { $this->addLocalVar($var, Type::VAR); } - $code .= $this->getIndent() . ' ' . $var . ' = ' . $listTmpVar . '.item(' . $key . ');' . PHP_EOL; + $code .= $this->getIndent() . $var . ' = ' . $listTmpVar . '.item(' . $key . ');' . PHP_EOL; } else { $this->fatalError($item, 'Unsupported foreach item type'); } @@ -56,7 +56,7 @@ trait ForeachTrait $keyVar = $this->parseIdentifier($node->keyVar); $this->checkVar($node, $keyVar, $defaultType); - return $this->getIndent() . ' ' . $keyVar . ' = ' . $keyExpr . ';' . PHP_EOL; + return $this->getIndent() . $keyVar . ' = ' . $keyExpr . ';' . PHP_EOL; } protected function parseForeachValueAssignment(Foreach_ $node, string $valueExpr, ?string $valueRefExpr = null): string @@ -75,7 +75,7 @@ trait ForeachTrait } $listTmpVar = $this->genTmpVarName(); $this->addLocalVar($listTmpVar, Type::VAR); - return $this->getIndent() . ' ' . $listTmpVar . ' = ' . $valueExpr . ';' . PHP_EOL + return $this->getIndent() . $listTmpVar . ' = ' . $valueExpr . ';' . PHP_EOL . $this->parseForeachItemAsList($listTmpVar, $node->valueVar->items); } @@ -106,7 +106,7 @@ trait ForeachTrait $this->fatalError($node, 'Cannot bind foreach reference to native variable of type ' . $this->getVarType($valueVar)); } } - return $this->getIndent() . ' ' . $valueRefExpr . '(' . $valueVar . ');' . PHP_EOL; + return $this->getIndent() . $valueRefExpr . '(' . $valueVar . ');' . PHP_EOL; } if ($this->isVarExpr($node->valueVar)) { @@ -114,7 +114,7 @@ trait ForeachTrait $this->checkVar($node, $valueVar); } } - return $this->getIndent() . ' ' . $valueVar . ' = ' . $valueExpr . ';' . PHP_EOL; + return $this->getIndent() . $valueVar . ' = ' . $valueExpr . ';' . PHP_EOL; } protected function parseForeachIterable(Foreach_ $node, string $iterableVar): string @@ -138,8 +138,8 @@ trait ForeachTrait $body = $this->parseForeachBody($node); $this->indentLevel--; - $code .= $this->parseBeforeStmtLines() . PHP_EOL; - $code .= $body . PHP_EOL; + $code .= $this->parseBeforeStmtLines(); + $code .= $body; $code .= $this->getIndent() . '}'; $this->indentLevel--; $code .= PHP_EOL . $this->getIndent() . '}'; diff --git a/src/Parser/NullsafeAccessTrait.php b/src/Parser/NullsafeAccessTrait.php index d992448e..426aae61 100644 --- a/src/Parser/NullsafeAccessTrait.php +++ b/src/Parser/NullsafeAccessTrait.php @@ -85,17 +85,22 @@ trait NullsafeAccessTrait $last = array_key_last($list); $tmpFn = $this->genTmpVarName(); - $code = $comment . 'auto ' . $tmpFn . ' = [&]() -> ' . Type::VAR . '{' . PHP_EOL; + $code = $comment . 'auto ' . $tmpFn . ' = [&]() -> ' . Type::VAR . ' {' . PHP_EOL; + $this->indentLevel++; foreach ($list as $key => $item) { $tmpVar = $this->addTmpVar($key !== $last ? Type::OBJECT : Type::VAR); $ownedTmpVars[] = $tmpVar; if ($item[3]) { - $code .= "if ({$object}.isNull()) { return " . self::VALUE_NULL . '; }'; + $code .= $this->getIndent() . "if ({$object}.isNull()) {" . PHP_EOL; + $this->indentLevel++; + $code .= $this->getIndent() . 'return ' . self::VALUE_NULL . ';' . PHP_EOL; + $this->indentLevel--; + $code .= $this->getIndent() . '}' . PHP_EOL; } if ($item[0] == 'property') { $update = $this->escapeAttrMode($this->isPropertyFetchUpdate($item[2])); - $code .= $this->getIndent() . "{$tmpVar} = {$object}.attr({$item[1]}, {$update});"; + $code .= $this->getIndent() . "{$tmpVar} = {$object}.attr({$item[1]}, {$update});" . PHP_EOL; } else { $methodName = $this->isNamedMethod($item[4]->name) ? $this->parseIdentifier($item[4]->name) @@ -110,22 +115,24 @@ trait NullsafeAccessTrait $this->context->beforeStmtLines = array_slice($this->context->beforeStmtLines, 0, $beforeStmtCount); $this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $afterStmtCount); if ($argBeforeStmts) { - $code .= $this->getIndent() . implode(PHP_EOL . $this->getIndent(), $argBeforeStmts) . PHP_EOL; + $code .= $this->formatCapturedStmtLines($argBeforeStmts); } if ($requiresDynamicScope && $this->methodDef) { $code .= $this->getIndent() . "{$tmpVar} = php::callScoped({$object}, {$item[1]}, " - . $this->getCallableScopeExpr() . ", {$args});"; + . $this->getCallableScopeExpr() . ", {$args});" . PHP_EOL; } else { - $code .= $this->getIndent() . "{$tmpVar} = {$object}.call({$item[1]}, {$args});"; + $code .= $this->getIndent() . "{$tmpVar} = {$object}.call({$item[1]}, {$args});" . PHP_EOL; } if ($argAfterStmts) { - $code .= $this->getIndent() . implode(PHP_EOL . $this->getIndent(), $argAfterStmts) . PHP_EOL; + $code .= $this->formatCapturedStmtLines($argAfterStmts); } } $object = $tmpVar; } - $code .= $this->getIndent() . "return {$object}; };"; + $code .= $this->getIndent() . "return {$object};" . PHP_EOL; + $this->indentLevel--; + $code .= $this->getIndent() . '};'; $this->context->beforeStmtLines[] = $code; // C++ temporaries are function-scoped; release their zvals at the PHP statement boundary. diff --git a/src/Parser/PropertyAccessTrait.php b/src/Parser/PropertyAccessTrait.php index f9455623..c7d9d8cc 100644 --- a/src/Parser/PropertyAccessTrait.php +++ b/src/Parser/PropertyAccessTrait.php @@ -694,18 +694,25 @@ trait PropertyAccessTrait . ', ' . $tmpVar . '.typeStr())'; } - $coercion = $this->compositeTypeNeedsIntToFloatCoercion($typeCheck) - ? 'if (' . $tmpVar . '.isInt()) { ' . $tmpVar . ' = php::toFloat(' . $tmpVar . '); } ' - : ''; - - return '([&]() -> ' . Type::VAR . ' { ' - . $tmpVar . ' = ' . $rightExpr . '; ' - . $coercion - . 'if (UNEXPECTED(!(' . implode(' || ', $conditions) . '))) { ' - . $throwExpr . '; ' - . '} ' - . 'return ' . $tmpVar . '; ' - . '}())'; + $code = '([&]() -> ' . Type::VAR . ' {' . PHP_EOL; + $this->indentLevel++; + $code .= $this->getIndent() . $tmpVar . ' = ' . $rightExpr . ';' . PHP_EOL; + if ($this->compositeTypeNeedsIntToFloatCoercion($typeCheck)) { + $code .= $this->getIndent() . 'if (' . $tmpVar . '.isInt()) {' . PHP_EOL; + $this->indentLevel++; + $code .= $this->getIndent() . $tmpVar . ' = php::toFloat(' . $tmpVar . ');' . PHP_EOL; + $this->indentLevel--; + $code .= $this->getIndent() . '}' . PHP_EOL; + } + $code .= $this->getIndent() . 'if (UNEXPECTED(!(' . implode(' || ', $conditions) . '))) {' . PHP_EOL; + $this->indentLevel++; + $code .= $this->getIndent() . $throwExpr . ';' . PHP_EOL; + $this->indentLevel--; + $code .= $this->getIndent() . '}' . PHP_EOL; + $code .= $this->getIndent() . 'return ' . $tmpVar . ';' . PHP_EOL; + $this->indentLevel--; + + return $code . $this->getIndent() . '}())'; } private function getObjectPropertyAssignTypeCheck(PropertyDef $def): array diff --git a/src/Parser/SelectionExpressionTrait.php b/src/Parser/SelectionExpressionTrait.php index 0c32005c..0e0e7fa6 100644 --- a/src/Parser/SelectionExpressionTrait.php +++ b/src/Parser/SelectionExpressionTrait.php @@ -69,20 +69,26 @@ trait SelectionExpressionTrait $ternaryType = $nativeSelection ? $this->getNativeObjectPointerType($nativeClass) : $this->getNormalAssignType($typeChanged ? Type::VAR : $ifType); - $code = '[&]() -> ' . $ternaryType . '{'; + $code = '[&]() -> ' . $ternaryType . ' {' . PHP_EOL; + $this->indentLevel++; $code .= $this->formatCapturedStmtLines($condBeforeStmts); if ($condAfterStmts) { $condTmpVar = $this->addTmpVar(Type::VAR); - $code .= $this->getIndent() . "{$condTmpVar} = {$cond};"; + $code .= $this->getIndent() . "{$condTmpVar} = {$cond};" . PHP_EOL; $code .= $this->formatCapturedStmtLines($condAfterStmts); $cond = $condTmpVar; } $cond = $this->convertConditionExpr($expr->cond, $cond); - $code .= $this->getIndent() . 'if (' . $cond . ') {'; + $code .= $this->getIndent() . 'if (' . $cond . ') {' . PHP_EOL; + $this->indentLevel++; $code .= $this->formatTernaryReturn($expr->if, $if, $ifBeforeStmts, $ifAfterStmts, $ternaryType, $ifType, $nativeClass); - $code .= $this->getIndent() . '} else {'; + $this->indentLevel--; + $code .= $this->getIndent() . '} else {' . PHP_EOL; + $this->indentLevel++; $code .= $this->formatTernaryReturn($expr->else, $else, $elseBeforeStmts, $elseAfterStmts, $ternaryType, $elseType, $nativeClass); - $code .= $this->getIndent() . '}'; + $this->indentLevel--; + $code .= $this->getIndent() . '}' . PHP_EOL; + $this->indentLevel--; $code .= $this->getIndent() . '}()'; return $code; } @@ -114,13 +120,13 @@ trait SelectionExpressionTrait } else { $tmpVar = $this->addTmpVar($returnType); } - $code .= $this->getIndent() . "{$tmpVar} = {$value};"; + $code .= $this->getIndent() . "{$tmpVar} = {$value};" . PHP_EOL; $code .= $this->formatCapturedStmtLines($afterStmts); - $code .= $this->getIndent() . 'return ' . $tmpVar . ';'; + $code .= $this->getIndent() . 'return ' . $tmpVar . ';' . PHP_EOL; } else { $code .= $returnType === Type::VAR - ? $this->getIndent() . 'return php::Var(' . $value . ');' - : $this->getIndent() . 'return ' . $value . ';'; + ? $this->getIndent() . 'return php::Var(' . $value . ');' . PHP_EOL + : $this->getIndent() . 'return ' . $value . ';' . PHP_EOL; } return $code; } @@ -152,7 +158,8 @@ trait SelectionExpressionTrait $var = $tmpVar; } - $code = '[&]() -> ' . $returnType . '{'; + $code = '[&]() -> ' . $returnType . ' {' . PHP_EOL; + $this->indentLevel++; $default = null; foreach ($expr->arms as $arm) { if ($arm->conds === null) { @@ -160,14 +167,15 @@ trait SelectionExpressionTrait continue; } $matched = $this->genTmpVarName(); - $code .= $this->getIndent() . 'bool ' . $matched . ' = false;'; + $code .= $this->getIndent() . 'bool ' . $matched . ' = false;' . PHP_EOL; foreach ($arm->conds as $cond) { if ($this->isMatchExpr($cond)) { $this->fatalError($arm, 'Match expression cannot be used as a condition'); } $this->assertExprCanBeUsedAsValue($cond, 'match arm condition'); [$condValue, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($cond); - $code .= $this->getIndent() . 'if (!' . $matched . ') {'; + $code .= $this->getIndent() . 'if (!' . $matched . ') {' . PHP_EOL; + $this->indentLevel++; $code .= $this->formatCapturedStmtLines($beforeStmts); if ($afterStmts) { $condClass = $this->detectClassOfExpr($cond); @@ -178,7 +186,7 @@ trait SelectionExpressionTrait } else { $condTmpVar = $this->addTmpVar(Type::VAR); } - $code .= $this->getIndent() . "{$condTmpVar} = {$condValue};"; + $code .= $this->getIndent() . "{$condTmpVar} = {$condValue};" . PHP_EOL; $code .= $this->formatCapturedStmtLines($afterStmts); $condValue = $condTmpVar; } @@ -197,27 +205,38 @@ trait SelectionExpressionTrait // its expression must still be evaluated for effects. $comparison = '(static_cast(' . $condValue . '), false)'; } - $code .= $this->getIndent() . $matched . ' = (' . $comparison . ');'; + $code .= $this->getIndent() . $matched . ' = (' . $comparison . ');' . PHP_EOL; } else { - $code .= $this->getIndent() . $matched . ' = php::same(' . $var . ', ' . $condValue . ');'; + $code .= $this->getIndent() . $matched . ' = php::same(' . $var . ', ' . $condValue . ');' . PHP_EOL; } - $code .= $this->getIndent() . '}'; + $this->indentLevel--; + $code .= $this->getIndent() . '}' . PHP_EOL; } - $code .= $this->getIndent() . 'if (' . $matched . ') {'; + $code .= $this->getIndent() . 'if (' . $matched . ') {' . PHP_EOL; + $this->indentLevel++; $code .= $this->formatMatchReturn($arm->body, $nativeClass); - $code .= $this->getIndent() . '}'; + $this->indentLevel--; + $code .= $this->getIndent() . '}' . PHP_EOL; } if ($default) { - $code .= $this->getIndent() . '{'; + $code .= $this->getIndent() . '{' . PHP_EOL; + $this->indentLevel++; $code .= $this->formatMatchReturn($default, $nativeClass); - $code .= $this->getIndent() . '}'; + $this->indentLevel--; + $code .= $this->getIndent() . '}' . PHP_EOL; } else { + $code .= $this->getIndent() . '{' . PHP_EOL; + $this->indentLevel++; $code .= $nativeSelection - ? $this->getIndent() . '{ php::throwException("UnhandledMatchError", "Unhandled match case"); return nullptr; }' - : $this->getIndent() . '{ return php::throwException("UnhandledMatchError", "Unhandled match case"); }'; + ? $this->getIndent() . 'php::throwException("UnhandledMatchError", "Unhandled match case");' . PHP_EOL + . $this->getIndent() . 'return nullptr;' . PHP_EOL + : $this->getIndent() . 'return php::throwException("UnhandledMatchError", "Unhandled match case");' . PHP_EOL; + $this->indentLevel--; + $code .= $this->getIndent() . '}' . PHP_EOL; } - $code .= '}()'; + $this->indentLevel--; + $code .= $this->getIndent() . '}()'; return $code; } @@ -238,11 +257,11 @@ trait SelectionExpressionTrait } else { $tmpVar = $this->addTmpVar(Type::VAR); } - $code .= $this->getIndent() . "{$tmpVar} = {$value};"; + $code .= $this->getIndent() . "{$tmpVar} = {$value};" . PHP_EOL; $code .= $this->formatCapturedStmtLines($afterStmts); - $code .= $this->getIndent() . 'return ' . $tmpVar . ';'; + $code .= $this->getIndent() . 'return ' . $tmpVar . ';' . PHP_EOL; } else { - $code .= $this->getIndent() . 'return ' . $value . ';'; + $code .= $this->getIndent() . 'return ' . $value . ';' . PHP_EOL; } return $code; } @@ -332,21 +351,27 @@ trait SelectionExpressionTrait $this->addLocalVar($leftTmp, $pointerType); $this->addNativeObject($leftTmp, $nativeClass); - $code = '[&]() -> ' . $pointerType . '{'; + $code = '[&]() -> ' . $pointerType . ' {' . PHP_EOL; + $this->indentLevel++; $code .= $this->formatCapturedStmtLines($leftBefore); - $code .= $this->getIndent() . $leftTmp . ' = ' . $leftValue . ';'; + $code .= $this->getIndent() . $leftTmp . ' = ' . $leftValue . ';' . PHP_EOL; $code .= $this->formatCapturedStmtLines($leftAfter); - $code .= $this->getIndent() . 'if (' . $leftTmp . ' != nullptr) { return ' . $leftTmp . '; }'; + $code .= $this->getIndent() . 'if (' . $leftTmp . ' != nullptr) {' . PHP_EOL; + $this->indentLevel++; + $code .= $this->getIndent() . 'return ' . $leftTmp . ';' . PHP_EOL; + $this->indentLevel--; + $code .= $this->getIndent() . '}' . PHP_EOL; $code .= $this->formatCapturedStmtLines($rightBefore); if ($rightAfter) { $rightTmp = $this->genTmpVarName(); $this->addLocalVar($rightTmp, $pointerType); $this->addNativeObject($rightTmp, $nativeClass); - $code .= $this->getIndent() . $rightTmp . ' = ' . $rightValue . ';'; + $code .= $this->getIndent() . $rightTmp . ' = ' . $rightValue . ';' . PHP_EOL; $code .= $this->formatCapturedStmtLines($rightAfter); $rightValue = $rightTmp; } - $code .= $this->getIndent() . 'return ' . $rightValue . ';'; + $code .= $this->getIndent() . 'return ' . $rightValue . ';' . PHP_EOL; + $this->indentLevel--; return $code . $this->getIndent() . '}()'; } diff --git a/src/Translator.php b/src/Translator.php index 296f12f6..87fc0581 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -3469,13 +3469,18 @@ CODE; // Zend's C frames: those frames perform their cleanup after the handler // returns with EG(exception) set. Convert back to normal Zend exception // propagation at the outermost wrapper. - $cppCode = 'try {' . PHP_EOL; + $this->indentLevel++; + $cppCode = $this->getIndent() . 'try {' . PHP_EOL; + $this->indentLevel++; - $cppCode .= $this->genParameterCountCheck( + $argCountCheck = $this->genParameterCountCheck( $functionDef->argCountRequired, count($functionDef->argInfoList), $functionDef->hasVariadicArg(), ); + if ($argCountCheck !== '') { + $cppCode .= $this->getIndent() . rtrim($argCountCheck) . PHP_EOL; + } $callParams = ''; foreach ($functionDef->argInfoList as $k => $argInfo) { @@ -3494,7 +3499,7 @@ CODE; $cppCode .= $this->getIndent() . $var . '.append(php::getCallArg(i));' . PHP_EOL; } $this->indentLevel--; - $cppCode .= '}' . PHP_EOL; + $cppCode .= $this->getIndent() . '}' . PHP_EOL; $cppCode .= $this->genExtraNamedVariadicArgs($var); } else { if ($argInfo->default !== '') { @@ -3562,11 +3567,14 @@ CODE; } else { $cppCode .= $this->getIndent() . $fn . '(' . $callParams . ');' . PHP_EOL; } - $cppCode .= '}' . PHP_EOL; - $cppCode .= 'catch (zend_object *) {' . PHP_EOL; + $this->indentLevel--; + $cppCode .= $this->getIndent() . '} catch (zend_object *) {' . PHP_EOL; + $this->indentLevel++; $cppCode .= $this->getIndent() . '/* EG(exception) is already set; return control to ZendVM for frame cleanup. */' . PHP_EOL; - $cppCode .= '}' . PHP_EOL; - $cppCode .= '}' . PHP_EOL . PHP_EOL; + $this->indentLevel--; + $cppCode .= $this->getIndent() . '}' . PHP_EOL; + $this->indentLevel--; + $cppCode .= $this->getIndent() . '}' . PHP_EOL . PHP_EOL; return $cppCode; } @@ -3577,13 +3585,14 @@ CODE; * 对于常驻内存型应用,执行完当前逻辑后,会立即进入长时间的事件循环等待。 * 因此,这些变量仅作为临时用途,用完后应即刻销毁,无需长期持有。 */ - $cppCode = "const char *value = " . $this->genCharPtr($entryFile, true) . ';' . PHP_EOL; - $cppCode .= 'php::Var &_SERVER = ' . $this->escapeGlobalVar('_SERVER') . ';' . PHP_EOL; - $cppCode .= '_SERVER.item("PHP_SELF", true) = value;'. PHP_EOL; - $cppCode .= '_SERVER.item("SCRIPT_NAME", true) = value;'. PHP_EOL; - $cppCode .= '_SERVER.item("SCRIPT_FILENAME", true) = value;'. PHP_EOL; - $cppCode .= '_SERVER.item("PATH_TRANSLATED", true) = value;'. PHP_EOL; - $cppCode .= '_SERVER.item("DOCUMENT_ROOT", true) = "";' . PHP_EOL; + $indent = $this->getIndent(); + $cppCode = $indent . "const char *value = " . $this->genCharPtr($entryFile, true) . ';' . PHP_EOL; + $cppCode .= $indent . 'php::Var &_SERVER = ' . $this->escapeGlobalVar('_SERVER') . ';' . PHP_EOL; + $cppCode .= $indent . '_SERVER.item("PHP_SELF", true) = value;'. PHP_EOL; + $cppCode .= $indent . '_SERVER.item("SCRIPT_NAME", true) = value;'. PHP_EOL; + $cppCode .= $indent . '_SERVER.item("SCRIPT_FILENAME", true) = value;'. PHP_EOL; + $cppCode .= $indent . '_SERVER.item("PATH_TRANSLATED", true) = value;'. PHP_EOL; + $cppCode .= $indent . '_SERVER.item("DOCUMENT_ROOT", true) = "";' . PHP_EOL; return $cppCode . PHP_EOL; } @@ -3608,8 +3617,10 @@ CODE; protected function genMethodWrapper(ClassDef $classDef, MethodDef $methodDef): string { $name = $classDef->getNamespacedName(); - $cppCode = 'ZEND_METHOD(' . $name . ', ' . $methodDef->name . '){' . PHP_EOL; + $cppCode = 'ZEND_METHOD(' . $name . ', ' . $methodDef->name . ') {' . PHP_EOL; + $this->indentLevel++; $cppCode .= $this->getIndent() . Type::OBJECT . ' this_(&execute_data->This);' . PHP_EOL; + $this->indentLevel--; $fn = self::PREFIX . $this->getNativeMethodName($classDef, $methodDef); $cppCode .= $this->genWrapperFunctionArgs( $fn, @@ -5056,7 +5067,7 @@ CODE; private function genFunctionWrapper(FunctionDef $functionDef): string { $name = $this->escapeZendFnName($functionDef->getNamespacedName()); - $cppCode = 'ZEND_FUNCTION(' . $name . '){' . PHP_EOL; + $cppCode = 'ZEND_FUNCTION(' . $name . ') {' . PHP_EOL; $fn = self::PREFIX . $this->getNativeName($functionDef->name, $functionDef->namespace); $cppCode .= $this->genWrapperFunctionArgs($fn, $functionDef, $functionDef->getNamespacedName()); diff --git a/src/TypeSystem/NativeTypeCompatibilityTrait.php b/src/TypeSystem/NativeTypeCompatibilityTrait.php index 98a879f4..e223fef9 100644 --- a/src/TypeSystem/NativeTypeCompatibilityTrait.php +++ b/src/TypeSystem/NativeTypeCompatibilityTrait.php @@ -242,20 +242,15 @@ trait NativeTypeCompatibilityTrait if (($type === Type::VAR || $type === Type::REF) && $this->isStrictScalarType($argInfo->type)) { // A native scalar ABI value has already lost its zval type. Preserve // the dynamic value until strict_types validation has completed. - // Keep the check inside the argument expression: beforeStmtLines - // may run before an enclosing assignment or comma expression has - // initialized a compiler-generated argument temporary. - $checkedArg = 'typephp_checked_arg'; - $check = rtrim($this->genStrictScalarParamCheck( + // The PHPX helper evaluates the expression exactly once and returns + // the final native ABI type without an immediately-invoked closure. + $this->checkVarAssignExpr($arg, $argInfo->type, $type); + return $this->genStrictScalarArgConversion( $argInfo, - $checkedArg, + $expr, $callableName, (string) ($argIndex + 1) - )); - $expr = '([&](php::Var ' . $checkedArg . ') -> php::Var {' . PHP_EOL - . $check . PHP_EOL - . $this->getIndent() . 'return ' . $checkedArg . ';' . PHP_EOL - . $this->getIndent() . '})(' . $expr . ')'; + ); } $this->checkVarAssignExpr($arg, $argInfo->type, $type);