<?php

/**
 * Builder that maps nikic/php-parser AST objects to the first PHP-AST IR classes.
 */
final class PhpIrBuilder
{
    // Embedded resource references collected while lowering literal local asset paths.
    private static array $embeddedResourceReferences = [];
    private static array $literalStreamPathVariables = [];
    private static array $localDeclaredConstants = [];
    private static string $currentPhpSourceFile = '';
    private static int $syntheticClosureCounter = 0;
    private static bool $globalArgvVariableAllowed = true;

    /**
     * Convert a nikic/php-parser AST into a PHP-IR program.
     */
    public static function fromPhpParserAst(array $phpParserAst, string $phpSourceFile = '', array $externalDeclaredFunctionsByInclude = [], array $externalDeclaredStaticMethodsByInclude = []): PhpIrProgram
    {
        // PHP-AST IR validation is intentionally wider than runtime visibility:
        // every function in this file source tree is lowerable, but generated
        // Vala calls still go through runtime registration guards. This lets a
        // call before a conditional declaration compile and then fail at runtime
        // like PHP. External source-set declarations remain top-level include
        // metadata because they only become visible after include activation.
        self::$embeddedResourceReferences = [];
        self::$literalStreamPathVariables = [];
        self::$localDeclaredConstants = self::literalTopLevelConstValues($phpParserAst);
        self::$currentPhpSourceFile = $phpSourceFile;
        self::$syntheticClosureCounter = 0;
        self::$globalArgvVariableAllowed = true;
        $localDeclaredFunctions = self::declaredFunctionNames($phpParserAst, true);
        $localDeclaredStaticMethods = self::declaredStaticMethodNames($phpParserAst, true);
        $declaredFunctions = array_merge(
            $localDeclaredFunctions,
            self::allExternalDeclaredFunctions($externalDeclaredFunctionsByInclude),
        );
        $declaredStaticMethods = array_merge(
            $localDeclaredStaticMethods,
            self::allExternalDeclaredStaticMethods($externalDeclaredStaticMethodsByInclude),
        );
        $stmts = self::stmtsToIr($phpParserAst, $declaredFunctions, $declaredStaticMethods, null, true);
        return new PhpIrProgram(
            $stmts,
            $phpSourceFile,
            $localDeclaredFunctions,
            $localDeclaredStaticMethods,
            array_values(self::$embeddedResourceReferences),
        );
    }

    private static function stmtsToIr(array $phpParserStmts, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null, bool $trackLiteralStreamPathVariables = false, array $closureBindings = []): array
    {
        $savedLiteralStreamPathVariables = self::$literalStreamPathVariables;
        if (!$trackLiteralStreamPathVariables) {
            self::$literalStreamPathVariables = [];
        }
        $stmts = [];
        foreach ($phpParserStmts as $stmt) {
            foreach (self::irNodesToArray(self::stmtToIr($stmt, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings)) as $irStmt) {
                $stmts[] = $irStmt;
            }
            if ($trackLiteralStreamPathVariables) {
                self::updateLiteralStreamPathVariablesAfterStmt($stmt);
            }
        }
        if (!$trackLiteralStreamPathVariables) {
            self::$literalStreamPathVariables = $savedLiteralStreamPathVariables;
        }
        return $stmts;
    }

    private static function functionToIr(PhpParser\Node\Stmt\Function_ $stmt, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): array
    {
        $closureBindings = [];
        $arrowBindings = [];
        $syntheticFunctions = [];
        foreach (self::closureAssignmentsInStmts($stmt->stmts) as $closureAssignment) {
            $variableName = $closureAssignment['variableName'];
            $closure = $closureAssignment['closure'];
            if (!is_string($variableName) || !$closure instanceof PhpParser\Node\Expr\Closure) {
                continue;
            }
            $binding = self::closureBindingForAssignment($stmt, $variableName, $closure);
            $closureBindings[$variableName] = $binding;
            $syntheticFunctions[] = self::closureToSyntheticFunction($closure, $binding, $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

        $bodyStmts = [];
        foreach ($stmt->stmts as $innerStmt) {
            foreach (self::arrayFilterArrowFunctionsInNode($innerStmt) as $arrow) {
                $binding = self::arrowBindingForFunction($stmt, $arrow);
                $arrowBindings[spl_object_id($arrow)] = $binding;
                $syntheticFunctions[] = self::arrowToSyntheticFunction($arrow, $binding, $declaredFunctions, $declaredStaticMethods, $currentClassName);
            }
            $bodyStmts[] = $innerStmt;
        }
        if ($arrowBindings !== []) {
            $closureBindings['__arrowFunctions'] = $arrowBindings;
        }

        [$params, $paramTypes, $paramDefaults] = self::functionParamMetadata($stmt->params, self::nodeStartLine($stmt), 'function', $declaredFunctions, $declaredStaticMethods, $currentClassName);
        $savedGlobalArgvVariableAllowed = self::$globalArgvVariableAllowed;
        self::$globalArgvVariableAllowed = false;
        try {
            $loweredBodyStmts = self::stmtsToIr($bodyStmts, $declaredFunctions, $declaredStaticMethods, $currentClassName, false, $closureBindings);
        } finally {
            self::$globalArgvVariableAllowed = $savedGlobalArgvVariableAllowed;
        }
        $function = new PhpIrFunction(
            self::nodeStartLine($stmt),
            self::nodeEndLine($stmt),
            self::nodeComments($stmt),
            $stmt->name->toString(),
            $params,
            $loweredBodyStmts,
            'public',
            false,
            self::returnTypeName($stmt->returnType),
            $paramTypes,
            $paramDefaults,
        );
        return array_merge($syntheticFunctions, [$function]);
    }


    /**
     * @return list<PhpParser\Node\Expr\ArrowFunction>
     */
    private static function arrayFilterArrowFunctionsInNode(mixed $node): array
    {
        $arrows = [];
        $seen = [];
        self::collectArrayFilterArrowFunctions($node, $arrows, $seen);
        return $arrows;
    }

    /**
     * @param array<int, PhpParser\Node\Expr\ArrowFunction> $arrows
     * @param array<int, bool> $seen
     */
    private static function collectArrayFilterArrowFunctions(mixed $node, array &$arrows, array &$seen): void
    {
        if ($node instanceof PhpParser\Node\Expr\FuncCall
            && self::funcCallName($node) !== null
            && strtolower((string) self::funcCallName($node)) === 'array_filter'
            && isset($node->args[1])
            && $node->args[1]->value instanceof PhpParser\Node\Expr\ArrowFunction
        ) {
            $arrow = $node->args[1]->value;
            $id = spl_object_id($arrow);
            if (!isset($seen[$id])) {
                $seen[$id] = true;
                $arrows[] = $arrow;
            }
        }
        if ($node instanceof PhpParser\Node) {
            foreach ($node->getSubNodeNames() as $name) {
                self::collectArrayFilterArrowFunctions($node->{$name} ?? null, $arrows, $seen);
            }
            return;
        }
        if (is_array($node)) {
            foreach ($node as $item) {
                self::collectArrayFilterArrowFunctions($item, $arrows, $seen);
            }
        }
    }

    private static function closureAssignmentsInStmts(array $stmts): array
    {
        $assignments = [];
        foreach ($stmts as $stmt) {
            if ($stmt instanceof PhpParser\Node\Stmt\Expression
                && $stmt->expr instanceof PhpParser\Node\Expr\Assign
                && $stmt->expr->var instanceof PhpParser\Node\Expr\Variable
                && is_string($stmt->expr->var->name)
                && $stmt->expr->expr instanceof PhpParser\Node\Expr\Closure
            ) {
                $assignments[] = ['variableName' => $stmt->expr->var->name, 'closure' => $stmt->expr->expr];
                continue;
            }

            if ($stmt instanceof PhpParser\Node\Stmt\For_
                || $stmt instanceof PhpParser\Node\Stmt\While_
                || $stmt instanceof PhpParser\Node\Stmt\Do_
                || $stmt instanceof PhpParser\Node\Stmt\Foreach_
            ) {
                $assignments = array_merge($assignments, self::closureAssignmentsInStmts($stmt->stmts));
                continue;
            }

            if ($stmt instanceof PhpParser\Node\Stmt\If_) {
                $assignments = array_merge($assignments, self::closureAssignmentsInStmts($stmt->stmts));
                foreach ($stmt->elseifs as $elseif) {
                    $assignments = array_merge($assignments, self::closureAssignmentsInStmts($elseif->stmts));
                }
                if ($stmt->else !== null) {
                    $assignments = array_merge($assignments, self::closureAssignmentsInStmts($stmt->else->stmts));
                }
                continue;
            }

            if ($stmt instanceof PhpParser\Node\Stmt\Switch_) {
                foreach ($stmt->cases as $case) {
                    $assignments = array_merge($assignments, self::closureAssignmentsInStmts($case->stmts));
                }
                continue;
            }

            if ($stmt instanceof PhpParser\Node\Stmt\TryCatch) {
                $assignments = array_merge($assignments, self::closureAssignmentsInStmts($stmt->stmts));
                foreach ($stmt->catches as $catch) {
                    $assignments = array_merge($assignments, self::closureAssignmentsInStmts($catch->stmts));
                }
                if ($stmt->finally !== null) {
                    $assignments = array_merge($assignments, self::closureAssignmentsInStmts($stmt->finally->stmts));
                }
            }
        }
        return $assignments;
    }

    private static function stmtIsClosureAssignment(mixed $stmt, array $closureBindings): bool
    {
        return $stmt instanceof PhpParser\Node\Stmt\Expression
            && $stmt->expr instanceof PhpParser\Node\Expr\Assign
            && $stmt->expr->var instanceof PhpParser\Node\Expr\Variable
            && is_string($stmt->expr->var->name)
            && isset($closureBindings[$stmt->expr->var->name])
            && $stmt->expr->expr instanceof PhpParser\Node\Expr\Closure;
    }

    private static function closureBindingForAssignment(PhpParser\Node\Stmt\Function_ $ownerFunction, string $variableName, PhpParser\Node\Expr\Closure $closure): array
    {
        if ($closure->static || $closure->byRef) {
            throw new RuntimeException('Unsupported PHP closure for phpast-ir-v0 at line ' . self::nodeStartLine($closure) . ': static closures and by-reference closure returns are not supported yet');
        }
        $captures = [];
        $paramByRefs = [];
        foreach ($closure->uses as $use) {
            if (!$use->var instanceof PhpParser\Node\Expr\Variable || !is_string($use->var->name)) {
                throw new RuntimeException('Unsupported PHP closure use for phpast-ir-v0 at line ' . self::nodeStartLine($use));
            }
            $name = $use->var->name;
            $captures[] = $name;
            if ($use->byRef) {
                $paramByRefs[$name] = true;
            }
        }
        foreach ($closure->params as $param) {
            if (!$param->var instanceof PhpParser\Node\Expr\Variable || !is_string($param->var->name)) {
                throw new RuntimeException('Unsupported PHP closure parameter for phpast-ir-v0 at line ' . self::nodeStartLine($closure));
            }
            if ($param->byRef || $param->variadic) {
                throw new RuntimeException('Unsupported PHP closure parameter for phpast-ir-v0 at line ' . self::nodeStartLine($closure) . ': by-reference and variadic parameters are not supported yet');
            }
        }
        $ownerName = self::identifierPart($ownerFunction->name->toString());
        $varPart = self::identifierPart($variableName);
        $name = '__phpefc_closure_' . $ownerName . '_' . $varPart . '_' . (++self::$syntheticClosureCounter);
        return ['name' => $name, 'captures' => $captures, 'paramByRefs' => $paramByRefs];
    }

    private static function closureToSyntheticFunction(PhpParser\Node\Expr\Closure $closure, array $binding, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): PhpIrFunction
    {
        [$closureParams, $closureParamTypes, $closureParamDefaults] = self::functionParamMetadata($closure->params, self::nodeStartLine($closure), 'closure', $declaredFunctions, $declaredStaticMethods, $currentClassName);
        $captureParams = array_values(array_filter($binding['captures'] ?? [], static fn ($name): bool => is_string($name) && $name !== ''));
        $params = array_merge($captureParams, $closureParams);
        $paramByRefs = is_array($binding['paramByRefs'] ?? null) ? $binding['paramByRefs'] : [];
        $savedGlobalArgvVariableAllowed = self::$globalArgvVariableAllowed;
        self::$globalArgvVariableAllowed = false;
        try {
            $loweredClosureStmts = self::stmtsToIr($closure->stmts, $declaredFunctions, $declaredStaticMethods, $currentClassName);
        } finally {
            self::$globalArgvVariableAllowed = $savedGlobalArgvVariableAllowed;
        }
        return new PhpIrFunction(
            self::nodeStartLine($closure),
            self::nodeEndLine($closure),
            self::nodeComments($closure),
            (string) ($binding['name'] ?? '__phpefc_closure'),
            $params,
            $loweredClosureStmts,
            'private',
            false,
            self::returnTypeName($closure->returnType),
            $closureParamTypes,
            $closureParamDefaults,
            $paramByRefs,
            'synthetic_closure',
        );
    }

    private static function arrowBindingForFunction(PhpParser\Node\Stmt\Function_ $ownerFunction, PhpParser\Node\Expr\ArrowFunction $arrow): array
    {
        if ($arrow->static || $arrow->byRef) {
            throw new RuntimeException('Unsupported PHP arrow function for phpast-ir-v0 at line ' . self::nodeStartLine($arrow) . ': static arrow functions and by-reference returns are not supported yet');
        }
        $params = [];
        foreach ($arrow->params as $param) {
            if (!$param->var instanceof PhpParser\Node\Expr\Variable || !is_string($param->var->name)) {
                throw new RuntimeException('Unsupported PHP arrow function parameter for phpast-ir-v0 at line ' . self::nodeStartLine($arrow));
            }
            if ($param->byRef || $param->variadic) {
                throw new RuntimeException('Unsupported PHP arrow function parameter for phpast-ir-v0 at line ' . self::nodeStartLine($arrow) . ': by-reference and variadic parameters are not supported yet');
            }
            $params[$param->var->name] = true;
        }
        $captures = [];
        foreach (self::arrowFixture2VariableNames($arrow->expr) as $name) {
            if (!isset($params[$name]) && !self::isSuperglobalName($name)) {
                $captures[$name] = $name;
            }
        }
        $ownerName = self::identifierPart($ownerFunction->name->toString());
        $name = '__phpefc_arrow_' . $ownerName . '_' . (++self::$syntheticClosureCounter);
        return ['name' => $name, 'captures' => array_values($captures), 'paramByRefs' => []];
    }

    private static function arrowToSyntheticFunction(PhpParser\Node\Expr\ArrowFunction $arrow, array $binding, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): PhpIrFunction
    {
        [$arrowParams, $arrowParamTypes, $arrowParamDefaults] = self::functionParamMetadata($arrow->params, self::nodeStartLine($arrow), 'arrow function', $declaredFunctions, $declaredStaticMethods, $currentClassName);
        $captureParams = array_values(array_filter($binding['captures'] ?? [], static fn ($name): bool => is_string($name) && $name !== ''));
        $params = array_merge($captureParams, $arrowParams);
        return new PhpIrFunction(
            self::nodeStartLine($arrow),
            self::nodeEndLine($arrow),
            self::nodeComments($arrow),
            (string) ($binding['name'] ?? '__phpefc_arrow'),
            $params,
            [new PhpIrReturn(self::nodeStartLine($arrow->expr), self::nodeEndLine($arrow->expr), [], self::exprToIr($arrow->expr, $declaredFunctions, $declaredStaticMethods, $currentClassName))],
            'private',
            false,
            self::returnTypeName($arrow->returnType),
            $arrowParamTypes,
            $arrowParamDefaults,
            [],
            'synthetic_closure',
        );
    }

    private static function arrowFixture2VariableNames(mixed $expr): array
    {
        $names = [];
        self::collectVariableNamesFromArrowExpression($expr, $names);
        return array_values($names);
    }

    /** @param array<string, string> $names */
    private static function collectVariableNamesFromArrowExpression(mixed $expr, array &$names): void
    {
        if ($expr instanceof PhpParser\Node\Expr\Variable && is_string($expr->name)) {
            $names[$expr->name] = $expr->name;
            return;
        }
        if ($expr instanceof PhpParser\Node\Expr\BinaryOp\NotIdentical
            || $expr instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd
            || $expr instanceof PhpParser\Node\Expr\BinaryOp\SmallerOrEqual
            || $expr instanceof PhpParser\Node\Expr\BinaryOp\GreaterOrEqual
        ) {
            self::collectVariableNamesFromArrowExpression($expr->left, $names);
            self::collectVariableNamesFromArrowExpression($expr->right, $names);
            return;
        }
        if ($expr instanceof PhpParser\Node\Scalar || $expr instanceof PhpParser\Node\Expr\ConstFetch) {
            return;
        }
        throw new RuntimeException('Unsupported PHP arrow function expression for phpast-ir-v0 at line ' . self::nodeStartLine($expr) . ': unsupported expression shape in this slice');
    }

    private static function isSuperglobalName(string $name): bool
    {
        return in_array($name, ['GLOBALS', '_SERVER', '_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_REQUEST', '_ENV'], true);
    }

    private static function updateLiteralStreamPathVariablesAfterStmt(mixed $stmt): void
    {
        if (!$stmt instanceof PhpParser\Node\Stmt\Expression || !$stmt->expr instanceof PhpParser\Node\Expr\Assign) {
            return;
        }
        $assign = $stmt->expr;
        if (!$assign->var instanceof PhpParser\Node\Expr\Variable || !is_string($assign->var->name)) {
            return;
        }
        if ($assign->expr instanceof PhpParser\Node\Scalar\String_) {
            self::$literalStreamPathVariables[$assign->var->name] = $assign->expr->value;
            return;
        }
        unset(self::$literalStreamPathVariables[$assign->var->name]);
    }

    private static function irNodesToArray(mixed $stmtOrStmts): array
    {
        return is_array($stmtOrStmts) ? $stmtOrStmts : [$stmtOrStmts];
    }

    private static function stmtToIr(mixed $stmt, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null, array $closureBindings = []): mixed
    {
        if ($stmt instanceof PhpParser\Node\Stmt\InlineHTML) {
            $text = (string) ($stmt->value ?? '');
            if (str_starts_with($text, '#!')) {
                return new PhpIrNop(
                    self::nodeStartLine($stmt),
                    self::nodeEndLine($stmt),
                    self::nodeComments($stmt),
                );
            }
        }

        if ($stmt instanceof PhpParser\Node\Stmt\Const_) {
            return self::constDeclarationToIr($stmt, $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

        if ($stmt instanceof PhpParser\Node\Stmt\Break_) {
            return new PhpIrFuncCall(self::nodeStartLine($stmt), self::nodeEndLine($stmt), self::nodeComments($stmt), '__phpefc_break', []);
        }

        if ($stmt instanceof PhpParser\Node\Stmt\Continue_) {
            return new PhpIrFuncCall(self::nodeStartLine($stmt), self::nodeEndLine($stmt), self::nodeComments($stmt), '__phpefc_continue', []);
        }

        if ($stmt instanceof PhpParser\Node\Stmt\Switch_) {
            return self::switchToIr($stmt, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings);
        }

        if ($stmt instanceof PhpParser\Node\Stmt\Echo_) {
            return new PhpIrEcho(
                self::nodeStartLine($stmt),
                self::nodeEndLine($stmt),
                self::nodeComments($stmt),
                array_map(static fn (mixed $expr): object => self::exprToIr($expr, $declaredFunctions, $declaredStaticMethods, $currentClassName), $stmt->exprs),
            );
        }

        if (self::stmtIsClosureAssignment($stmt, $closureBindings)) {
            return new PhpIrNop(
                self::nodeStartLine($stmt),
                self::nodeEndLine($stmt),
                self::nodeComments($stmt),
            );
        }

        if ($stmt instanceof PhpParser\Node\Stmt\Expression && $stmt->expr instanceof PhpParser\Node\Expr\Assign) {
            return self::assignmentToIr($stmt->expr, self::nodeStartLine($stmt), self::nodeEndLine($stmt), self::nodeComments($stmt), $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings);
        }

        if ($stmt instanceof PhpParser\Node\Stmt\Expression && $stmt->expr instanceof PhpParser\Node\Expr\AssignOp\Plus) {
            return self::assignPlusToIr($stmt->expr, self::nodeStartLine($stmt), self::nodeEndLine($stmt), self::nodeComments($stmt), $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

        if ($stmt instanceof PhpParser\Node\Stmt\Expression && $stmt->expr instanceof PhpParser\Node\Expr\AssignOp\Concat) {
            return self::assignConcatToIr($stmt->expr, self::nodeStartLine($stmt), self::nodeEndLine($stmt), self::nodeComments($stmt), $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

        if ($stmt instanceof PhpParser\Node\Stmt\For_) {
            return self::forToIr($stmt, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings);
        }

        if ($stmt instanceof PhpParser\Node\Stmt\While_) {
            return self::whileToIr($stmt, $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

        if ($stmt instanceof PhpParser\Node\Stmt\Do_) {
            return self::doWhileToIr($stmt, $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

        if ($stmt instanceof PhpParser\Node\Stmt\Foreach_) {
            return self::foreachToIr($stmt, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings);
        }

        if ($stmt instanceof PhpParser\Node\Stmt\TryCatch) {
            return self::tryCatchToIr($stmt, $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

        if ($stmt instanceof PhpParser\Node\Stmt\Expression && $stmt->expr instanceof PhpParser\Node\Expr\PostInc) {
            return self::postIncToAssignmentIr($stmt->expr, self::nodeStartLine($stmt), self::nodeEndLine($stmt), self::nodeComments($stmt), $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

        if ($stmt instanceof PhpParser\Node\Stmt\Expression && $stmt->expr instanceof PhpParser\Node\Expr\Print_) {
            return new PhpIrPrint(
                self::nodeStartLine($stmt),
                self::nodeEndLine($stmt),
                self::nodeComments($stmt),
                self::exprToIr($stmt->expr->expr, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            );
        }

        if ($stmt instanceof PhpParser\Node\Stmt\Expression && $stmt->expr instanceof PhpParser\Node\Expr\Exit_) {
            $arg = $stmt->expr->expr !== null
                ? self::exprToIr($stmt->expr->expr, $declaredFunctions, $declaredStaticMethods, $currentClassName)
                : new PhpIrIntLiteral(self::nodeStartLine($stmt), self::nodeEndLine($stmt), [], 0);
            return new PhpIrFuncCall(self::nodeStartLine($stmt), self::nodeEndLine($stmt), self::nodeComments($stmt), '__phpefc_exit', [$arg]);
        }

        if ($stmt instanceof PhpParser\Node\Stmt\Expression && $stmt->expr instanceof PhpParser\Node\Expr\FuncCall) {
            return self::funcCallToIr(
                $stmt->expr,
                self::nodeStartLine($stmt),
                self::nodeEndLine($stmt),
                self::nodeComments($stmt),
                $declaredFunctions,
                $declaredStaticMethods,
                $currentClassName,
                $closureBindings,
            );
        }

        if ($stmt instanceof PhpParser\Node\Stmt\Expression && $stmt->expr instanceof PhpParser\Node\Expr\StaticCall) {
            return self::staticCallToIr(
                $stmt->expr,
                self::nodeStartLine($stmt),
                self::nodeEndLine($stmt),
                self::nodeComments($stmt),
                $declaredFunctions,
                $declaredStaticMethods,
                $currentClassName,
            );
        }

        if ($stmt instanceof PhpParser\Node\Stmt\Expression && $stmt->expr instanceof PhpParser\Node\Expr\MethodCall) {
            return self::methodCallToIr(
                $stmt->expr,
                self::nodeStartLine($stmt),
                self::nodeEndLine($stmt),
                self::nodeComments($stmt),
                $declaredFunctions,
                $declaredStaticMethods,
                $currentClassName,
            );
        }

        if ($stmt instanceof PhpParser\Node\Stmt\Unset_) {
            return self::unsetToIr($stmt, $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

        if ($stmt instanceof PhpParser\Node\Stmt\Expression && $stmt->expr instanceof PhpParser\Node\Expr\Include_) {
            return self::includeToIr(
                $stmt->expr,
                self::nodeStartLine($stmt),
                self::nodeEndLine($stmt),
                self::nodeComments($stmt),
                $currentClassName,
            );
        }

        if ($stmt instanceof PhpParser\Node\Stmt\Function_) {
            return self::functionToIr($stmt, $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

        if ($stmt instanceof PhpParser\Node\Stmt\Class_) {
            return self::classToIr($stmt, $declaredFunctions, $declaredStaticMethods);
        }

        if ($stmt instanceof PhpParser\Node\Stmt\Expression && $stmt->expr instanceof PhpParser\Node\Expr\Throw_) {
            return self::throwExprToIr(
                $stmt->expr,
                self::nodeStartLine($stmt),
                self::nodeEndLine($stmt),
                self::nodeComments($stmt),
                $declaredFunctions,
                $declaredStaticMethods,
                $currentClassName,
            );
        }

        if (class_exists(PhpParser\Node\Stmt\Throw_::class) && $stmt instanceof PhpParser\Node\Stmt\Throw_) {
            return self::throwExprToIr(
                $stmt,
                self::nodeStartLine($stmt),
                self::nodeEndLine($stmt),
                self::nodeComments($stmt),
                $declaredFunctions,
                $declaredStaticMethods,
                $currentClassName,
            );
        }

        if ($stmt instanceof PhpParser\Node\Stmt\Return_) {
            if ($stmt->expr instanceof PhpParser\Node\Expr\ArrayDimFetch
                && $stmt->expr->dim instanceof PhpParser\Node\Expr\PreInc
            ) {
                $preInc = $stmt->expr->dim;
                $stmt->expr->dim = $preInc->var;
                return [
                    self::preIncToAssignmentIr($preInc, self::nodeStartLine($preInc), self::nodeEndLine($preInc), [], $declaredFunctions, $declaredStaticMethods, $currentClassName),
                    new PhpIrReturn(
                        self::nodeStartLine($stmt),
                        self::nodeEndLine($stmt),
                        self::nodeComments($stmt),
                        self::exprToIr($stmt->expr, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings),
                    ),
                ];
            }
            return new PhpIrReturn(
                self::nodeStartLine($stmt),
                self::nodeEndLine($stmt),
                self::nodeComments($stmt),
                $stmt->expr !== null ? self::exprToIr($stmt->expr, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings) : null,
            );
        }

        if ($stmt instanceof PhpParser\Node\Stmt\If_) {
            if (self::ifContainsOnlyConditionalFunctionDeclarations($stmt)) {
                return new PhpIrNop(
                    self::nodeStartLine($stmt),
                    self::nodeEndLine($stmt),
                    self::nodeComments($stmt),
                );
            }
            return self::ifToIr($stmt, $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

        if ($stmt instanceof PhpParser\Node\Stmt\Nop) {
            return new PhpIrNop(
                self::nodeStartLine($stmt),
                self::nodeEndLine($stmt),
                self::nodeComments($stmt),
            );
        }

        throw new RuntimeException('Unsupported PHP statement for phpast-ir-v0 at line ' . self::nodeStartLine($stmt) . ': ' . get_debug_type($stmt));
    }

    private static function constDeclarationToIr(PhpParser\Node\Stmt\Const_ $stmt, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): array
    {
        $result = [];
        foreach ($stmt->consts as $const) {
            $result[] = new PhpIrFuncCall(
                self::nodeStartLine($const),
                self::nodeEndLine($const),
                self::nodeComments($stmt),
                'define',
                [
                    new PhpIrStringLiteral(self::nodeStartLine($const), self::nodeEndLine($const), [], $const->name->toString()),
                    self::exprToIr($const->value, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                ],
            );
        }
        return $result;
    }

    private static function literalTopLevelConstValues(array $phpParserStmts): array
    {
        $constants = [];
        foreach ($phpParserStmts as $stmt) {
            if (!$stmt instanceof PhpParser\Node\Stmt\Const_) {
                continue;
            }
            foreach ($stmt->consts as $const) {
                $value = self::literalConstValue($const->value);
                if ($value !== null) {
                    $constants[strtolower($const->name->toString())] = $value;
                }
            }
        }
        return $constants;
    }

    private static function literalConstValue(mixed $expr): ?array
    {
        if ($expr instanceof PhpParser\Node\Scalar\String_) {
            return ['kind' => 'string', 'value' => $expr->value];
        }
        if ($expr instanceof PhpParser\Node\Scalar\LNumber) {
            return ['kind' => 'int', 'value' => $expr->value];
        }
        if ($expr instanceof PhpParser\Node\Scalar\DNumber) {
            return ['kind' => 'float', 'value' => $expr->value];
        }
        if ($expr instanceof PhpParser\Node\Expr\ConstFetch) {
            $lowerName = strtolower($expr->name->toString());
            if ($lowerName === 'true') {
                return ['kind' => 'bool', 'value' => true];
            }
            if ($lowerName === 'false') {
                return ['kind' => 'bool', 'value' => false];
            }
            if ($lowerName === 'null') {
                return ['kind' => 'null'];
            }
        }
        return null;
    }

    private static function localDeclaredConstantFetchToIr(string $name, int $startLine, int $endLine, array $comments): ?object
    {
        $key = strtolower(ltrim($name, '\\'));
        $value = self::$localDeclaredConstants[$key] ?? null;
        if ($value === null) {
            return null;
        }
        if ($value['kind'] === 'string') {
            return new PhpIrStringLiteral($startLine, $endLine, $comments, (string) $value['value']);
        }
        if ($value['kind'] === 'int') {
            return new PhpIrIntLiteral($startLine, $endLine, $comments, (int) $value['value']);
        }
        if ($value['kind'] === 'float') {
            return new PhpIrFloatLiteral($startLine, $endLine, $comments, (float) $value['value']);
        }
        if ($value['kind'] === 'bool') {
            return new PhpIrBoolLiteral($startLine, $endLine, $comments, (bool) $value['value']);
        }
        if ($value['kind'] === 'null') {
            return new PhpIrNullLiteral($startLine, $endLine, $comments);
        }
        return null;
    }

    private static function switchToIr(PhpParser\Node\Stmt\Switch_ $stmt, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null, array $closureBindings = []): array
    {
        $line = self::nodeStartLine($stmt);
        $endLine = self::nodeEndLine($stmt);
        $switchVar = '__phpefc_switch_' . $line . '_expr';
        $switchVarExpr = new PhpIrVariable($line, $endLine, [], $switchVar);
        $init = new PhpIrAssignment(
            $line,
            $endLine,
            self::nodeComments($stmt),
            $switchVarExpr,
            self::exprToIr($stmt->cond, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings),
        );

        $branches = [];
        $defaultStmts = [];
        $pendingConds = [];
        foreach ($stmt->cases as $case) {
            if ($case->cond === null) {
                $defaultStmts = self::stmtsToIr($case->stmts, $declaredFunctions, $declaredStaticMethods, $currentClassName, false, $closureBindings);
                continue;
            }

            $pendingConds[] = self::exprToIr($case->cond, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings);
            if ($case->stmts === []) {
                continue;
            }

            $body = self::stmtsToIr($case->stmts, $declaredFunctions, $declaredStaticMethods, $currentClassName, false, $closureBindings);
            foreach ($pendingConds as $caseCond) {
                $branches[] = [
                    'cond' => new PhpIrEqual($line, $endLine, [], $switchVarExpr, $caseCond),
                    'stmts' => $body,
                ];
            }
            $pendingConds = [];
        }

        if ($branches === [] && $defaultStmts === []) {
            return [$init];
        }

        $first = array_shift($branches);
        $switchBody = [];
        if (is_array($first)) {
            $switchBody[] = new PhpIrIf($line, $endLine, [], $first['cond'], $first['stmts'], $branches, $defaultStmts);
        } else {
            foreach ($defaultStmts as $defaultStmt) {
                $switchBody[] = $defaultStmt;
            }
        }
        $switchBody[] = new PhpIrFuncCall($line, $endLine, [], '__phpefc_break', []);

        return [
            $init,
            new PhpIrWhile($line, $endLine, [], new PhpIrBoolLiteral($line, $endLine, [], true), $switchBody),
        ];
    }

    private static function ifToIr(PhpParser\Node\Stmt\If_ $stmt, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): array
    {
        $prefixStmts = [];
        $condition = $stmt->cond;
        if ($condition instanceof PhpParser\Node\Expr\BooleanNot
            && $condition->expr instanceof PhpParser\Node\Expr\Isset_
            && count($condition->expr->vars) === 1
            && $condition->expr->vars[0] instanceof PhpParser\Node\Expr\ArrayDimFetch
            && $condition->expr->vars[0]->dim instanceof PhpParser\Node\Expr\Assign
        ) {
            $assign = $condition->expr->vars[0]->dim;
            if (!$assign->var instanceof PhpParser\Node\Expr\Variable || !is_string($assign->var->name)) {
                throw new RuntimeException('Unsupported PHP assignment expression for phpast-ir-v0 at line ' . self::nodeStartLine($assign) . ': only simple variable targets are supported yet');
            }
            foreach (self::irNodesToArray(self::assignmentToIr($assign, self::nodeStartLine($assign), self::nodeEndLine($assign), self::nodeComments($assign), $declaredFunctions, $declaredStaticMethods, $currentClassName)) as $irStmt) {
                $prefixStmts[] = $irStmt;
            }
            $condition->expr->vars[0]->dim = $assign->var;
        }
        $ifStmt = new PhpIrIf(
            self::nodeStartLine($stmt),
            self::nodeEndLine($stmt),
            self::nodeComments($stmt),
            self::exprToIr($condition, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            self::stmtsToIr($stmt->stmts, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            array_map(static fn (PhpParser\Node\Stmt\ElseIf_ $elseif): array => [
                'cond' => self::exprToIr($elseif->cond, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                'stmts' => self::stmtsToIr($elseif->stmts, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            ], $stmt->elseifs),
            $stmt->else !== null
                ? self::stmtsToIr($stmt->else->stmts, $declaredFunctions, $declaredStaticMethods, $currentClassName)
                : [],
        );
        $prefixStmts[] = $ifStmt;
        return $prefixStmts;
    }

    private static function whileToIr(PhpParser\Node\Stmt\While_ $stmt, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): PhpIrWhile
    {
        return new PhpIrWhile(
            self::nodeStartLine($stmt),
            self::nodeEndLine($stmt),
            self::nodeComments($stmt),
            self::exprToIr($stmt->cond, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            self::stmtsToIr($stmt->stmts, $declaredFunctions, $declaredStaticMethods, $currentClassName),
        );
    }

    private static function doWhileToIr(PhpParser\Node\Stmt\Do_ $stmt, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): PhpIrWhile
    {
        return new PhpIrWhile(
            self::nodeStartLine($stmt),
            self::nodeEndLine($stmt),
            self::nodeComments($stmt),
            self::exprToIr($stmt->cond, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            self::stmtsToIr($stmt->stmts, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            true,
        );
    }

    private static function tryCatchToIr(PhpParser\Node\Stmt\TryCatch $stmt, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): PhpIrTryCatch
    {
        if (count($stmt->catches) === 0 && $stmt->finally === null) {
            throw new RuntimeException('Unsupported PHP try/catch for phpast-ir-v0 at line ' . self::nodeStartLine($stmt) . ': at least one catch or finally block is required');
        }

        $catchArms = [];
        foreach ($stmt->catches as $catch) {
            if (!$catch->var instanceof PhpParser\Node\Expr\Variable || !is_string($catch->var->name)) {
                throw new RuntimeException('Unsupported PHP catch for phpast-ir-v0 at line ' . self::nodeStartLine($catch) . ': only simple catch variables are supported yet');
            }
            if (count($catch->types) === 0) {
                throw new RuntimeException('Unsupported PHP catch for phpast-ir-v0 at line ' . self::nodeStartLine($catch) . ': catch without type is not supported yet');
            }
            foreach ($catch->types as $type) {
                if (!$type instanceof PhpParser\Node\Name) {
                    throw new RuntimeException('Unsupported PHP catch for phpast-ir-v0 at line ' . self::nodeStartLine($catch) . ': only named catch types are supported yet');
                }
                $catchArms[] = [
                    'className' => $type->toString(),
                    'varName' => $catch->var->name,
                    'stmts' => self::stmtsToIr($catch->stmts, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                ];
            }
        }

        $finallyStmts = $stmt->finally !== null
            ? self::stmtsToIr($stmt->finally->stmts, $declaredFunctions, $declaredStaticMethods, $currentClassName)
            : [];

        return new PhpIrTryCatch(
            self::nodeStartLine($stmt),
            self::nodeEndLine($stmt),
            self::nodeComments($stmt),
            self::stmtsToIr($stmt->stmts, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            $catchArms,
            $finallyStmts,
        );
    }

    private static function forToIr(PhpParser\Node\Stmt\For_ $stmt, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null, array $closureBindings = []): PhpIrFor
    {
        $initStmts = [];
        foreach ($stmt->init as $initExpr) {
            foreach (self::irNodesToArray(self::exprStatementToIr($initExpr, $declaredFunctions, $declaredStaticMethods, $currentClassName)) as $initStmt) {
                $initStmts[] = $initStmt;
            }
        }

        $loopStmts = [];
        foreach ($stmt->loop as $loopExpr) {
            foreach (self::irNodesToArray(self::exprStatementToIr($loopExpr, $declaredFunctions, $declaredStaticMethods, $currentClassName)) as $loopStmt) {
                $loopStmts[] = $loopStmt;
            }
        }

        return new PhpIrFor(
            self::nodeStartLine($stmt),
            self::nodeEndLine($stmt),
            self::nodeComments($stmt),
            $initStmts,
            array_map(static fn (mixed $expr): object => self::exprToIr($expr, $declaredFunctions, $declaredStaticMethods, $currentClassName), $stmt->cond),
            $loopStmts,
            self::stmtsToIr($stmt->stmts, $declaredFunctions, $declaredStaticMethods, $currentClassName, false, $closureBindings),
        );
    }
    private static function foreachToIr(PhpParser\Node\Stmt\Foreach_ $stmt, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null, array $closureBindings = []): PhpIrForeachValue
    {
        $keyName = null;
        if ($stmt->keyVar !== null) {
            if (!$stmt->keyVar instanceof PhpParser\Node\Expr\Variable || !is_string($stmt->keyVar->name)) {
                throw new RuntimeException('Unsupported PHP foreach for phpast-ir-v0 at line ' . self::nodeStartLine($stmt) . ': only simple key variables are supported yet');
            }
            $keyName = $stmt->keyVar->name;
        }
        if (!$stmt->valueVar instanceof PhpParser\Node\Expr\Variable || !is_string($stmt->valueVar->name)) {
            throw new RuntimeException('Unsupported PHP foreach for phpast-ir-v0 at line ' . self::nodeStartLine($stmt) . ': only simple value variables are supported yet');
        }

        $body = [];
        foreach ($stmt->stmts as $innerStmt) {
            foreach (self::irNodesToArray(self::stmtToIr($innerStmt, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings)) as $irStmt) {
                $body[] = $irStmt;
            }
        }

        return new PhpIrForeachValue(
            self::nodeStartLine($stmt),
            self::nodeEndLine($stmt),
            self::nodeComments($stmt),
            self::exprToIr($stmt->expr, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings),
            $keyName,
            $stmt->valueVar->name,
            $body,
        );
    }


    private static function exprStatementToIr(mixed $expr, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): mixed
    {
        if ($expr instanceof PhpParser\Node\Expr\Assign) {
            return self::assignmentToIr($expr, self::nodeStartLine($expr), self::nodeEndLine($expr), self::nodeComments($expr), $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

        if ($expr instanceof PhpParser\Node\Expr\AssignOp\Plus) {
            return self::assignPlusToIr($expr, self::nodeStartLine($expr), self::nodeEndLine($expr), self::nodeComments($expr), $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

        if ($expr instanceof PhpParser\Node\Expr\AssignOp\Concat) {
            return self::assignConcatToIr($expr, self::nodeStartLine($expr), self::nodeEndLine($expr), self::nodeComments($expr), $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

        if ($expr instanceof PhpParser\Node\Expr\PostInc) {
            return self::postIncToAssignmentIr($expr, self::nodeStartLine($expr), self::nodeEndLine($expr), self::nodeComments($expr), $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

        if ($expr instanceof PhpParser\Node\Expr\PreInc) {
            return self::preIncToAssignmentIr($expr, self::nodeStartLine($expr), self::nodeEndLine($expr), self::nodeComments($expr), $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

        throw new RuntimeException("Unsupported PHP for-loop expression for phpast-ir-v0 at line " . self::nodeStartLine($expr) . ": " . get_debug_type($expr));
    }

    private static function preIncToAssignmentIr(PhpParser\Node\Expr\PreInc $expr, int $startLine, int $endLine, array $comments, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): PhpIrAssignment
    {
        if (!$expr->var instanceof PhpParser\Node\Expr\Variable || !is_string($expr->var->name)) {
            throw new RuntimeException("Unsupported PHP pre-increment for phpast-ir-v0 at line " . $startLine . ": only simple variables are supported yet");
        }
        return new PhpIrAssignment(
            $startLine,
            $endLine,
            $comments,
            self::exprToIr($expr->var, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            new PhpIrPlus(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                self::exprToIr($expr->var, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                new PhpIrIntLiteral(self::nodeStartLine($expr), self::nodeEndLine($expr), [], 1),
            ),
        );
    }

    private static function postIncToAssignmentIr(PhpParser\Node\Expr\PostInc $expr, int $startLine, int $endLine, array $comments, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): PhpIrAssignment
    {
        if (!$expr->var instanceof PhpParser\Node\Expr\Variable || !is_string($expr->var->name)) {
            throw new RuntimeException("Unsupported PHP post-increment for phpast-ir-v0 at line " . $startLine . ": only simple variables are supported yet");
        }
        return new PhpIrAssignment(
            $startLine,
            $endLine,
            $comments,
            self::exprToIr($expr->var, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            new PhpIrPlus(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                self::exprToIr($expr->var, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                new PhpIrIntLiteral(self::nodeStartLine($expr), self::nodeEndLine($expr), [], 1),
            ),
        );
    }

    private static function assignPlusToIr(PhpParser\Node\Expr\AssignOp\Plus $assign, int $startLine, int $endLine, array $comments, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): PhpIrAssignment
    {
        if (!$assign->var instanceof PhpParser\Node\Expr\Variable || !is_string($assign->var->name)) {
            throw new RuntimeException('Unsupported PHP compound += assignment for phpast-ir-v0 at line ' . $startLine . ': only simple variables are supported yet');
        }

        return new PhpIrAssignment(
            $startLine,
            $endLine,
            $comments,
            self::exprToIr($assign->var, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            new PhpIrPlus(
                self::nodeStartLine($assign),
                self::nodeEndLine($assign),
                self::nodeComments($assign),
                self::exprToIr($assign->var, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                self::exprToIr($assign->expr, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            ),
        );
    }

    private static function assignConcatToIr(PhpParser\Node\Expr\AssignOp\Concat $assign, int $startLine, int $endLine, array $comments, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): PhpIrAssignment
    {
        if (!$assign->var instanceof PhpParser\Node\Expr\Variable || !is_string($assign->var->name)) {
            throw new RuntimeException('Unsupported PHP compound .= assignment for phpast-ir-v0 at line ' . $startLine . ': only simple variables are supported yet');
        }

        return new PhpIrAssignment(
            $startLine,
            $endLine,
            $comments,
            self::exprToIr($assign->var, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            new PhpIrConcat(
                self::nodeStartLine($assign),
                self::nodeEndLine($assign),
                self::nodeComments($assign),
                self::exprToIr($assign->var, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                self::exprToIr($assign->expr, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            ),
        );
    }

    private static function assignmentToIr(PhpParser\Node\Expr\Assign $assign, int $startLine, int $endLine, array $comments, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null, array $closureBindings = []): object|array
    {
        if ($assign->var instanceof PhpParser\Node\Expr\List_) {
            $result = [];
            foreach ($assign->var->items as $index => $item) {
                if ($item === null) {
                    continue;
                }
                if (!$item instanceof PhpParser\Node\ArrayItem || !$item->value instanceof PhpParser\Node\Expr\Variable || !is_string($item->value->name)) {
                    throw new RuntimeException('Unsupported PHP list assignment for phpast-ir-v0 at line ' . $startLine . ': only simple variable targets are supported yet');
                }
                $dimExpr = $item->key !== null ? $item->key : new PhpParser\Node\Scalar\LNumber($index, $item->getAttributes());
                $result[] = new PhpIrAssignment(
                    $startLine,
                    $endLine,
                    $index === 0 ? $comments : [],
                    self::exprToIr($item->value, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                    new PhpIrArrayDimFetch(
                        self::nodeStartLine($assign->expr),
                        self::nodeEndLine($assign->expr),
                        [],
                        self::exprToIr($assign->expr, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings),
                        self::exprToIr($dimExpr, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                    ),
                );
            }
            return $result;
        }

        if ($assign->var instanceof PhpParser\Node\Expr\ArrayDimFetch) {
            $fetch = $assign->var;
            if (!$fetch->var instanceof PhpParser\Node\Expr\Variable || !is_string($fetch->var->name)) {
                throw new RuntimeException('Unsupported PHP array assignment for phpast-ir-v0 at line ' . $startLine . ': only simple variables are supported yet');
            }
            if ($fetch->dim !== null) {
                return new PhpIrFuncCall(
                    $startLine,
                    $endLine,
                    $comments,
                    '__phpefc_array_set_key',
                    [
                        self::exprToIr($fetch->var, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                        self::exprToIr($fetch->dim, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                        self::exprToIr($assign->expr, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings),
                    ],
                );
            }
            return new PhpIrArrayAppendAssignment(
                $startLine,
                $endLine,
                $comments,
                self::exprToIr($fetch->var, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                self::exprToIr($assign->expr, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings),
            );
        }

        if ($assign->expr instanceof PhpParser\Node\Expr\Assign) {
            $innerAssign = $assign->expr;
            if (!$innerAssign->var instanceof PhpParser\Node\Expr\Variable || !is_string($innerAssign->var->name)) {
                throw new RuntimeException("Unsupported PHP chained assignment for phpast-ir-v0 at line " . $startLine . ": only simple inner variable targets are supported yet");
            }
            $result = self::irNodesToArray(self::assignmentToIr(
                $innerAssign,
                self::nodeStartLine($innerAssign),
                self::nodeEndLine($innerAssign),
                $comments,
                $declaredFunctions,
                $declaredStaticMethods,
                $currentClassName,
                $closureBindings,
            ));
            $result[] = new PhpIrAssignment(
                $startLine,
                $endLine,
                [],
                self::exprToIr($assign->var, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                self::exprToIr($innerAssign->var, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings),
            );
            return $result;
        }

        return new PhpIrAssignment(
            $startLine,
            $endLine,
            $comments,
            self::exprToIr($assign->var, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            self::exprToIr($assign->expr, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings),
        );
    }

    private static function unsetToIr(PhpParser\Node\Stmt\Unset_ $stmt, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): PhpIrUnsetStatement
    {
        $targets = [];
        foreach ($stmt->vars as $target) {
            $targets[] = self::unsetTargetToIr($target, $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

        return new PhpIrUnsetStatement(
            self::nodeStartLine($stmt),
            self::nodeEndLine($stmt),
            self::nodeComments($stmt),
            $targets,
        );
    }

    private static function unsetTargetToIr(mixed $target, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): object|array
    {
        if ($target instanceof PhpParser\Node\Expr\Variable && is_string($target->name)) {
            return self::exprToIr($target, $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

        if ($target instanceof PhpParser\Node\Expr\ArrayDimFetch) {
            if (!$target->var instanceof PhpParser\Node\Expr\Variable || !is_string($target->var->name)) {
                throw new RuntimeException('Unsupported PHP unset target for phpast-ir-v0 at line ' . self::nodeStartLine($target) . ': only simple variable array fetches are supported yet');
            }
            if ($target->dim === null) {
                throw new RuntimeException('Unsupported PHP unset target for phpast-ir-v0 at line ' . self::nodeStartLine($target) . ': array append fetch cannot be unset');
            }
            return [
                'kind' => 'ArrayDimFetch',
                'startLine' => self::nodeStartLine($target),
                'endLine' => self::nodeEndLine($target),
                'comments' => array_map(static fn (PhpIrComment $comment): array => $comment->toArray(), self::nodeComments($target)),
                'var' => self::exprToIr($target->var, $declaredFunctions, $declaredStaticMethods, $currentClassName)->toArray(),
                'dim' => self::exprToIr($target->dim, $declaredFunctions, $declaredStaticMethods, $currentClassName)->toArray(),
            ];
        }

        throw new RuntimeException('Unsupported PHP unset target for phpast-ir-v0 at line ' . self::nodeStartLine($target) . ': ' . get_debug_type($target));
    }

    private static function includeToIr(PhpParser\Node\Expr\Include_ $expr, int $startLine, int $endLine, array $comments, ?string $currentClassName = null): PhpIrFuncCall
    {
        $path = self::staticIncludePath($expr);
        return new PhpIrFuncCall(
            $startLine,
            $endLine,
            $comments,
            "__phpefc_include",
            [$path !== null ? new PhpIrStringLiteral($startLine, $endLine, [], $path) : self::exprToIr($expr->expr, [], [], $currentClassName)],
        );
    }

    private static function staticIncludePath(PhpParser\Node\Expr\Include_ $expr): ?string
    {
        if (!$expr->expr instanceof PhpParser\Node\Scalar\String_) {
            return null;
        }
        return $expr->expr->value;
    }

    private static function classToIr(PhpParser\Node\Stmt\Class_ $stmt, array $declaredFunctions = [], array $declaredStaticMethods = []): PhpIrClass
    {
        if (!$stmt->name instanceof PhpParser\Node\Identifier) {
            throw new RuntimeException('Unsupported PHP class declaration for phpast-ir-v0 at line ' . self::nodeStartLine($stmt) . ': anonymous classes are not supported yet');
        }
        if ($stmt->extends !== null || $stmt->implements !== []) {
            throw new RuntimeException('Unsupported PHP class declaration for phpast-ir-v0 at line ' . self::nodeStartLine($stmt) . ': inheritance/interfaces are not supported yet');
        }
        $className = $stmt->name->toString();
        $methods = [];
        $properties = [];
        foreach ($stmt->stmts as $classStmt) {
            if ($classStmt instanceof PhpParser\Node\Stmt\Property) {
                if (($classStmt->flags & PhpParser\Node\Stmt\Class_::MODIFIER_STATIC) !== 0) {
                    throw new RuntimeException('Unsupported PHP class property for phpast-ir-v0 at line ' . self::nodeStartLine($classStmt) . ': static properties are not supported yet');
                }
                foreach ($classStmt->props as $property) {
                    $properties[] = new PhpIrClassProperty(
                        self::nodeStartLine($property),
                        self::nodeEndLine($property),
                        self::nodeComments($classStmt),
                        $property->name->toString(),
                        self::visibilityFromFlags($classStmt->flags),
                        $property->default !== null ? self::exprToIr($property->default, $declaredFunctions, $declaredStaticMethods, $className) : null,
                        self::returnTypeName($classStmt->type),
                    );
                }
                continue;
            }
            if ($classStmt instanceof PhpParser\Node\Stmt\ClassMethod) {
                $isStatic = ($classStmt->flags & PhpParser\Node\Stmt\Class_::MODIFIER_STATIC) !== 0;
                [$params, $paramTypes, $paramDefaults] = self::functionParamMetadata($classStmt->params, self::nodeStartLine($classStmt), 'class method', $declaredFunctions, $declaredStaticMethods, $className);
                $promotedProperties = self::promotedPropertiesForMethod($classStmt);
                $savedGlobalArgvVariableAllowed = self::$globalArgvVariableAllowed;
                self::$globalArgvVariableAllowed = false;
                try {
                    $loweredMethodStmts = self::stmtsToIr($classStmt->stmts ?? [], $declaredFunctions, $declaredStaticMethods, $className);
                } finally {
                    self::$globalArgvVariableAllowed = $savedGlobalArgvVariableAllowed;
                }
                $methods[] = new PhpIrFunction(
                    self::nodeStartLine($classStmt),
                    self::nodeEndLine($classStmt),
                    self::nodeComments($classStmt),
                    $classStmt->name->toString(),
                    $params,
                    $loweredMethodStmts,
                    self::methodVisibility($classStmt),
                    $isStatic,
                    self::returnTypeName($classStmt->returnType),
                    $paramTypes,
                    $paramDefaults,
                    [],
                    'php_function',
                    $promotedProperties,
                );
                continue;
            }
            if ($classStmt instanceof PhpParser\Node\Stmt\Nop) {
                continue;
            }
            throw new RuntimeException('Unsupported PHP class member for phpast-ir-v0 at line ' . self::nodeStartLine($classStmt) . ': ' . get_debug_type($classStmt));
        }
        return new PhpIrClass(self::nodeStartLine($stmt), self::nodeEndLine($stmt), self::nodeComments($stmt), $className, $methods, $properties);
    }
    private static function promotedPropertiesForMethod(PhpParser\Node\Stmt\ClassMethod $method): array
    {
        $promoted = [];
        foreach ($method->params as $param) {
            $flags = (int) $param->flags;
            if ($flags === 0) {
                continue;
            }
            if (strtolower($method->name->toString()) !== '__construct') {
                throw new RuntimeException('Unsupported PHP promoted property for phpast-ir-v0 at line ' . self::nodeStartLine($param) . ': property promotion is only supported on constructors');
            }
            if (!$param->var instanceof PhpParser\Node\Expr\Variable || !is_string($param->var->name)) {
                throw new RuntimeException('Unsupported PHP promoted property for phpast-ir-v0 at line ' . self::nodeStartLine($param));
            }
            $promoted[$param->var->name] = self::visibilityFromFlags($flags);
        }
        return $promoted;
    }

    private static function visibilityFromFlags(int $flags): string
    {
        if (($flags & PhpParser\Node\Stmt\Class_::MODIFIER_PRIVATE) !== 0) {
            return 'private';
        }
        if (($flags & PhpParser\Node\Stmt\Class_::MODIFIER_PROTECTED) !== 0) {
            return 'protected';
        }
        return 'public';
    }

    private static function functionParamMetadata(array $parserParams, int $line, string $context, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): array
    {
        $params = [];
        $paramTypes = [];
        $paramDefaults = [];
        foreach ($parserParams as $param) {
            if (!$param->var instanceof PhpParser\Node\Expr\Variable || !is_string($param->var->name)) {
                throw new RuntimeException('Unsupported PHP ' . $context . ' parameter for phpast-ir-v0 at line ' . $line);
            }
            if ($param->byRef || $param->variadic) {
                throw new RuntimeException('Unsupported PHP ' . $context . ' parameter for phpast-ir-v0 at line ' . $line . ': by-reference and variadic parameters are not supported yet');
            }
            $name = $param->var->name;
            $params[] = $name;
            $paramType = self::paramTypeName($param->type);
            if ($paramType !== '') {
                $paramTypes[$name] = $paramType;
            }
            if ($param->default !== null) {
                $paramDefaults[$name] = self::exprToIr($param->default, $declaredFunctions, $declaredStaticMethods, $currentClassName)->toArray();
            }
        }
        return [$params, $paramTypes, $paramDefaults];
    }

    private static function paramTypeName(mixed $type): string
    {
        $name = self::returnTypeName($type);
        return match ($name) {
            'int', 'integer' => 'int',
            'float', 'double' => 'float',
            default => '',
        };
    }

    private static function returnTypeName(mixed $type): string
    {
        if ($type === null) {
            return '';
        }
        if ($type instanceof PhpParser\Node\NullableType) {
            return self::returnTypeName($type->type);
        }
        if ($type instanceof PhpParser\Node\Identifier || $type instanceof PhpParser\Node\Name) {
            return strtolower($type->toString());
        }
        throw new RuntimeException('Unsupported PHP return type for phpast-ir-v0 at line ' . self::nodeStartLine($type) . ': ' . get_debug_type($type));
    }

    private static function methodVisibility(PhpParser\Node\Stmt\ClassMethod $method): string
    {
        if (($method->flags & PhpParser\Node\Stmt\Class_::MODIFIER_PRIVATE) !== 0) {
            return 'private';
        }
        if (($method->flags & PhpParser\Node\Stmt\Class_::MODIFIER_PROTECTED) !== 0) {
            return 'protected';
        }
        return 'public';
    }

    private static function exprToIr(mixed $expr, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null, array $closureBindings = []): object
    {
        if ($expr instanceof PhpParser\Node\Scalar\String_) {
            return new PhpIrStringLiteral(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                $expr->value,
            );
        }

        if ($expr instanceof PhpParser\Node\Scalar\InterpolatedString
            || $expr instanceof PhpParser\Node\Scalar\Encapsed
        ) {
            return self::interpolatedStringToIr($expr, $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

        if ($expr instanceof PhpParser\Node\Scalar\LNumber) {
            return new PhpIrIntLiteral(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                $expr->value,
            );
        }

        if ($expr instanceof PhpParser\Node\Scalar\DNumber) {
            return new PhpIrFloatLiteral(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                $expr->value,
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\ConstFetch) {
            return self::constFetchToIr($expr);
        }

        if ($expr instanceof PhpParser\Node\Expr\BooleanNot) {
            return new PhpIrBooleanNot(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                self::exprToIr($expr->expr, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\ErrorSuppress) {
            return new PhpIrErrorSuppress(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                self::exprToIr($expr->expr, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\Isset_) {
            if (count($expr->vars) !== 1) {
                throw new RuntimeException('Unsupported PHP isset expression for phpast-ir-v0 at line ' . self::nodeStartLine($expr) . ': only one target is supported yet');
            }
            return new PhpIrFuncCall(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                '__phpefc_isset',
                [self::exprToIr($expr->vars[0], $declaredFunctions, $declaredStaticMethods, $currentClassName)],
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\Cast\Int_) {
            return new PhpIrFuncCall(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                "intval",
                [self::exprToIr($expr->expr, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings)],
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\Cast\Double) {
            return new PhpIrFuncCall(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                'floatval',
                [self::exprToIr($expr->expr, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings)],
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\Cast\String_) {
            return new PhpIrFuncCall(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                'strval',
                [self::exprToIr($expr->expr, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings)],
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\Cast\Bool_) {
            return new PhpIrFuncCall(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                'boolval',
                [self::exprToIr($expr->expr, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings)],
            );
        }

        if (self::$globalArgvVariableAllowed
            && $expr instanceof PhpParser\Node\Expr\ArrayDimFetch
            && $expr->var instanceof PhpParser\Node\Expr\Variable
            && $expr->var->name === 'argv'
            && $expr->dim instanceof PhpParser\Node\Scalar\LNumber
        ) {
            return new PhpIrFuncCall(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                '__phpefc_argv',
                [new PhpIrIntLiteral(self::nodeStartLine($expr->dim), self::nodeEndLine($expr->dim), self::nodeComments($expr->dim), $expr->dim->value)],
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\ArrayDimFetch) {
            if ($expr->dim === null) {
                throw new RuntimeException("Unsupported PHP array fetch for phpast-ir-v0 at line " . self::nodeStartLine($expr) . ": empty dimension fetch is not supported in expression context");
            }
            return new PhpIrArrayDimFetch(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                self::exprToIr($expr->var, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                self::exprToIr($expr->dim, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\Array_) {
            return self::arrayLiteralToIr($expr, $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

        if ($expr instanceof PhpParser\Node\Expr\PropertyFetch) {
            if (!$expr->name instanceof PhpParser\Node\Identifier) {
                throw new RuntimeException('Unsupported PHP property fetch for phpast-ir-v0 at line ' . self::nodeStartLine($expr) . ': dynamic property names are not supported yet');
            }
            return new PhpIrPropertyFetch(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                self::exprToIr($expr->var, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                $expr->name->toString(),
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\Variable && is_string($expr->name)) {
            if (self::$globalArgvVariableAllowed && $expr->name === 'argv') {
                return new PhpIrFuncCall(
                    self::nodeStartLine($expr),
                    self::nodeEndLine($expr),
                    self::nodeComments($expr),
                    '__phpefc_argv_all',
                    [],
                );
            }
            return new PhpIrVariable(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                $expr->name,
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\BinaryOp\Concat) {
            return new PhpIrConcat(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                self::exprToIr($expr->left, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                self::exprToIr($expr->right, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\BinaryOp\Plus) {
            return new PhpIrPlus(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                self::exprToIr($expr->left, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                self::exprToIr($expr->right, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\BinaryOp\Greater) {
            return new PhpIrGreater(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                self::exprToIr($expr->left, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                self::exprToIr($expr->right, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr) {
            return new PhpIrBooleanOr(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                self::exprToIr($expr->left, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings),
                self::exprToIr($expr->right, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings),
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd) {
            return new PhpIrBooleanAnd(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                self::exprToIr($expr->left, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings),
                self::exprToIr($expr->right, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings),
            );
        }
        if ($expr instanceof PhpParser\Node\Expr\BinaryOp\Equal) {
            return new PhpIrEqual(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                self::exprToIr($expr->left, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                self::exprToIr($expr->right, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\BinaryOp\Identical) {
            return new PhpIrIdentical(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                self::exprToIr($expr->left, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                self::exprToIr($expr->right, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\BinaryOp\NotEqual) {
            return new PhpIrNotEqual(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                self::exprToIr($expr->left, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                self::exprToIr($expr->right, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\BinaryOp\NotIdentical) {
            return new PhpIrNotIdentical(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                self::exprToIr($expr->left, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                self::exprToIr($expr->right, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\BinaryOp\Smaller) {
            return new PhpIrSmaller(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                self::exprToIr($expr->left, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                self::exprToIr($expr->right, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\BinaryOp\SmallerOrEqual) {
            return new PhpIrSmallerOrEqual(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                self::exprToIr($expr->left, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                self::exprToIr($expr->right, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\BinaryOp\GreaterOrEqual) {
            return new PhpIrSmallerOrEqual(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                self::exprToIr($expr->right, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                self::exprToIr($expr->left, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\BinaryOp\Minus) {
            return new PhpIrMinus(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                self::exprToIr($expr->left, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                self::exprToIr($expr->right, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\UnaryMinus) {
            return new PhpIrMinus(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                new PhpIrIntLiteral(self::nodeStartLine($expr), self::nodeEndLine($expr), [], 0),
                self::exprToIr($expr->expr, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\BinaryOp\Mul) {
            return new PhpIrMultiply(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                self::exprToIr($expr->left, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                self::exprToIr($expr->right, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\BinaryOp\Div) {
            return new PhpIrDivide(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                self::exprToIr($expr->left, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                self::exprToIr($expr->right, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\BinaryOp\Mod) {
            return new PhpIrModulo(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                self::exprToIr($expr->left, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings),
                self::exprToIr($expr->right, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings),
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\BinaryOp && $expr->getOperatorSigil() === '&&') {
            return new PhpIrTernary(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                self::exprToIr($expr->left, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                self::exprToIr($expr->right, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                new PhpIrBoolLiteral(self::nodeStartLine($expr), self::nodeEndLine($expr), [], false),
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\BinaryOp\Coalesce) {
            return new PhpIrTernary(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                new PhpIrFuncCall(
                    self::nodeStartLine($expr->left),
                    self::nodeEndLine($expr->left),
                    [],
                    '__phpefc_isset',
                    [self::exprToIr($expr->left, $declaredFunctions, $declaredStaticMethods, $currentClassName)],
                ),
                self::exprToIr($expr->left, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                self::exprToIr($expr->right, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            );
        }

        if ($expr instanceof PhpParser\Node\Expr\FuncCall) {
            return self::funcCallToIr($expr, null, null, null, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings);
        }

        if ($expr instanceof PhpParser\Node\Expr\StaticCall) {
            return self::staticCallToIr($expr, null, null, null, $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

        if ($expr instanceof PhpParser\Node\Expr\New_) {
            return self::newToIr($expr, $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

        if ($expr instanceof PhpParser\Node\Expr\MethodCall) {
            return self::methodCallToIr($expr, null, null, null, $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

        if ($expr instanceof PhpParser\Node\Expr\Ternary) {
            return new PhpIrTernary(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                self::exprToIr($expr->cond, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                self::exprToIr($expr->if ?? $expr->cond, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                self::exprToIr($expr->else, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            );
        }

        throw new RuntimeException('Unsupported PHP expression for phpast-ir-v0 at line ' . self::nodeStartLine($expr) . ': ' . get_debug_type($expr));
    }

    private static function interpolatedStringToIr(mixed $expr, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): object
    {
        $parts = $expr->parts ?? [];
        if ($parts === []) {
            return new PhpIrStringLiteral(self::nodeStartLine($expr), self::nodeEndLine($expr), self::nodeComments($expr), '');
        }

        $result = null;
        foreach ($parts as $part) {
            if ($part instanceof PhpParser\Node\InterpolatedStringPart
                || $part instanceof PhpParser\Node\Scalar\EncapsedStringPart
            ) {
                $partIr = new PhpIrStringLiteral(
                    self::nodeStartLine($part),
                    self::nodeEndLine($part),
                    self::nodeComments($part),
                    (string) ($part->value ?? ''),
                );
            } else {
                $partIr = self::exprToIr($part, $declaredFunctions, $declaredStaticMethods, $currentClassName);
            }

            if ($result === null) {
                $result = $partIr;
                continue;
            }

            $result = new PhpIrConcat(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                $result,
                $partIr,
            );
        }

        return $result ?? new PhpIrStringLiteral(self::nodeStartLine($expr), self::nodeEndLine($expr), self::nodeComments($expr), '');
    }

    private static function constFetchToIr(PhpParser\Node\Expr\ConstFetch $expr): object
    {
        $name = $expr->name->toString();
        $lowerName = strtolower($name);
        if ($lowerName === 'true') {
            return new PhpIrBoolLiteral(self::nodeStartLine($expr), self::nodeEndLine($expr), self::nodeComments($expr), true);
        }
        if ($lowerName === 'false') {
            return new PhpIrBoolLiteral(self::nodeStartLine($expr), self::nodeEndLine($expr), self::nodeComments($expr), false);
        }
        if ($lowerName === 'null') {
            return new PhpIrNullLiteral(self::nodeStartLine($expr), self::nodeEndLine($expr), self::nodeComments($expr));
        }
        $localDeclaredConstant = self::localDeclaredConstantFetchToIr($name, self::nodeStartLine($expr), self::nodeEndLine($expr), self::nodeComments($expr));
        if ($localDeclaredConstant !== null) {
            return $localDeclaredConstant;
        }

        // PHP language literals stay on PHPEFC literal paths. All other
        // static constant names are resolved at runtime through RuntimeEmbed,
        // matching PHP 8 undefined-constant errors instead of a string fallback.
        // TODO: implement PHP namespace fallback rules for unqualified names in
        // namespaced source; this slice preserves the parser-provided static name.
        return new PhpIrConstantFetch(self::nodeStartLine($expr), self::nodeEndLine($expr), self::nodeComments($expr), ltrim($name, '\\'));
    }


    private static function newToIr(PhpParser\Node\Expr\New_ $expr, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): PhpIrNew
    {
        if (!$expr->class instanceof PhpParser\Node\Name) {
            throw new RuntimeException('Unsupported PHP new expression for phpast-ir-v0 at line ' . self::nodeStartLine($expr) . ': dynamic class names are not supported yet');
        }
        return new PhpIrNew(
            self::nodeStartLine($expr),
            self::nodeEndLine($expr),
            self::nodeComments($expr),
            $expr->class->toString(),
            array_map(static fn (PhpParser\Node\Arg $arg): object => self::exprArgToIr($arg, $declaredFunctions, $declaredStaticMethods, $currentClassName), $expr->args),
        );
    }

    private static function methodCallToIr(PhpParser\Node\Expr\MethodCall $expr, ?int $startLine = null, ?int $endLine = null, ?array $comments = null, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): PhpIrMethodCall
    {
        $methodName = null;
        $methodNameExpression = null;
        if ($expr->name instanceof PhpParser\Node\Identifier) {
            $methodName = $expr->name->toString();
        } else {
            $methodNameExpression = self::exprToIr($expr->name, $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }
        return new PhpIrMethodCall(
            $startLine ?? self::nodeStartLine($expr),
            $endLine ?? self::nodeEndLine($expr),
            $comments ?? self::nodeComments($expr),
            self::exprToIr($expr->var, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            $methodName,
            array_map(static fn (PhpParser\Node\Arg $arg): object => self::exprArgToIr($arg, $declaredFunctions, $declaredStaticMethods, $currentClassName), $expr->args),
            $currentClassName ?? '',
            $methodNameExpression,
        );
    }
    private static function arrayLiteralToIr(PhpParser\Node\Expr\Array_ $expr, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): PhpIrArrayLiteral
    {
        $items = [];
        foreach ($expr->items as $item) {
            if (!$item instanceof PhpParser\Node\ArrayItem) {
                throw new RuntimeException('Unsupported PHP array item for phpast-ir-v0 at line ' . self::nodeStartLine($expr));
            }
            if ($item->unpack || $item->byRef) {
                throw new RuntimeException('Unsupported PHP array item for phpast-ir-v0 at line ' . self::nodeStartLine($item) . ': unpack/by-reference items are not supported yet');
            }
            $items[] = new PhpIrArrayItem(
                self::nodeStartLine($item),
                self::nodeEndLine($item),
                self::nodeComments($item),
                $item->key !== null ? self::exprToIr($item->key, $declaredFunctions, $declaredStaticMethods, $currentClassName) : null,
                self::exprToIr($item->value, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            );
        }

        return new PhpIrArrayLiteral(
            self::nodeStartLine($expr),
            self::nodeEndLine($expr),
            self::nodeComments($expr),
            $items,
        );
    }

    private static function throwExprToIr(mixed $throw, int $startLine, int $endLine, array $comments, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): PhpIrThrow
    {
        $expr = $throw->expr ?? null;
        if (!$expr instanceof PhpParser\Node\Expr\New_) {
            return new PhpIrThrow(
                $startLine,
                $endLine,
                $comments,
                '',
                new PhpIrStringLiteral($startLine, $endLine, [], ''),
                new PhpIrIntLiteral($startLine, $endLine, [], 0),
                null,
                self::exprToIr($expr, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            );
        }

        if (!$expr->class instanceof PhpParser\Node\Name) {
            throw new RuntimeException('Unsupported PHP throw for phpast-ir-v0 at line ' . $startLine . ': dynamic exception class cannot be resolved statically yet');
        }

        $className = $expr->class->toString();
        if (!in_array(strtolower($className), ['exception', '\\exception', 'runtimeexception', '\\runtimeexception', 'unexpectedvalueexception', '\\unexpectedvalueexception'], true)) {
            throw new RuntimeException('Unsupported PHP throw for phpast-ir-v0 at line ' . $startLine . ': only Exception/RuntimeException/UnexpectedValueException are supported, got ' . $className);
        }

        $args = $expr->args;
        if (count($args) > 3) {
            throw new RuntimeException('Unsupported PHP throw for phpast-ir-v0 at line ' . $startLine . ': Exception constructor supports only message, code and previous for now');
        }

        $message = count($args) >= 1
            ? self::exprArgToIr($args[0], $declaredFunctions, $declaredStaticMethods, $currentClassName)
            : new PhpIrStringLiteral($startLine, $endLine, [], '');
        $code = count($args) >= 2
            ? self::exprArgToIr($args[1], $declaredFunctions, $declaredStaticMethods, $currentClassName)
            : new PhpIrIntLiteral($startLine, $endLine, [], 0);
        $previous = count($args) >= 3
            ? self::exprArgToIr($args[2], $declaredFunctions, $declaredStaticMethods, $currentClassName)
            : null;

        return new PhpIrThrow($startLine, $endLine, $comments, ltrim($className, '\\'), $message, $code, $previous);
    }

    private static function funcCallToIr(PhpParser\Node\Expr\FuncCall $expr, ?int $startLine = null, ?int $endLine = null, ?array $comments = null, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null, array $closureBindings = []): PhpIrFuncCall
    {
        $functionName = self::funcCallName($expr);
        if ($functionName === null && $expr->name instanceof PhpParser\Node\Expr\Variable && is_string($expr->name->name) && isset($closureBindings[$expr->name->name])) {
            $binding = $closureBindings[$expr->name->name];
            $captureArgs = [];
            foreach (($binding['captures'] ?? []) as $captureName) {
                if (!is_string($captureName) || $captureName === '') {
                    continue;
                }
                $captureArgs[] = new PhpIrVariable(self::nodeStartLine($expr), self::nodeEndLine($expr), [], $captureName);
            }
            $callArgs = array_map(static fn (PhpParser\Node\Arg $arg): object => self::exprArgToIr($arg, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings), $expr->args);
            return new PhpIrFuncCall(
                $startLine ?? self::nodeStartLine($expr),
                $endLine ?? self::nodeEndLine($expr),
                $comments ?? self::nodeComments($expr),
                (string) ($binding['name'] ?? ''),
                array_merge($captureArgs, $callArgs),
            );
        }
        if ($functionName !== null && strtolower($functionName) === 'array_filter' && isset($expr->args[1]) && $expr->args[1]->value instanceof PhpParser\Node\Expr\ArrowFunction) {
            $arrowBindings = is_array($closureBindings['__arrowFunctions'] ?? null) ? $closureBindings['__arrowFunctions'] : [];
            $binding = $arrowBindings[spl_object_id($expr->args[1]->value)] ?? null;
            if (!is_array($binding) || !isset($binding['name']) || !is_string($binding['name'])) {
                throw new RuntimeException('Unsupported PHP arrow function for phpast-ir-v0 at line ' . self::nodeStartLine($expr->args[1]->value) . ': missing synthetic binding');
            }
            if (($binding['captures'] ?? []) !== []) {
                throw new RuntimeException('Unsupported PHP array_filter arrow function for phpast-ir-v0 at line ' . self::nodeStartLine($expr->args[1]->value) . ': captures are not supported in this slice yet');
            }
            return new PhpIrFuncCall($startLine ?? self::nodeStartLine($expr), $endLine ?? self::nodeEndLine($expr), $comments ?? self::nodeComments($expr), '__phpefc_array_filter_callback', [self::exprArgToIr($expr->args[0], $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings), new PhpIrStringLiteral(self::nodeStartLine($expr->args[1]->value), self::nodeEndLine($expr->args[1]->value), [], $binding['name'])]);
        }

        if ($functionName === null || !self::isSupportedFuncCall($functionName, $declaredFunctions)) {
            throw new RuntimeException(self::unsupportedFuncCallMessage($expr));
        }

        $args = array_map(static fn (PhpParser\Node\Arg $arg): object => self::exprArgToIr($arg, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings), $expr->args);
        $args = self::rewriteKnownVariableStreamPathArgs($functionName, $expr->args, $args);
        $args = self::rewriteEmbeddedResourceArgs($functionName, $args);

        return new PhpIrFuncCall(
            $startLine ?? self::nodeStartLine($expr),
            $endLine ?? self::nodeEndLine($expr),
            $comments ?? self::nodeComments($expr),
            $functionName,
            $args,
        );
    }

    private static function rewriteKnownVariableStreamPathArgs(string $functionName, array $originalArgs, array $args): array
    {
        $streamPathArgByFunction = [
            'file_get_contents' => 0,
        ];
        $lookupName = strtolower($functionName);
        if (!array_key_exists($lookupName, $streamPathArgByFunction)) {
            return $args;
        }
        $argIndex = $streamPathArgByFunction[$lookupName];
        if (!isset($originalArgs[$argIndex], $args[$argIndex]) || $args[$argIndex] instanceof PhpIrStringLiteral) {
            return $args;
        }
        $originalValue = $originalArgs[$argIndex]->value ?? null;
        if (!$originalValue instanceof PhpParser\Node\Expr\Variable || !is_string($originalValue->name)) {
            return $args;
        }
        if (!array_key_exists($originalValue->name, self::$literalStreamPathVariables)) {
            return $args;
        }
        $args[$argIndex] = new PhpIrStringLiteral(
            self::nodeStartLine($originalValue),
            self::nodeEndLine($originalValue),
            [],
            self::$literalStreamPathVariables[$originalValue->name],
        );
        return $args;
    }

    private static function rewriteEmbeddedResourceArgs(string $functionName, array $args): array
    {
        $streamPathArgByFunction = [
            'file_get_contents' => 0,
        ];
        $lookupName = strtolower($functionName);
        if (!array_key_exists($lookupName, $streamPathArgByFunction)) {
            return $args;
        }
        $argIndex = $streamPathArgByFunction[$lookupName];
        if (!isset($args[$argIndex]) || !$args[$argIndex] instanceof PhpIrStringLiteral) {
            return $args;
        }
        $literal = $args[$argIndex]->toArray();
        $literalPath = (string) ($literal['value'] ?? '');
        $resource = self::embeddedResourceReferenceForLiteralPath($literalPath, (int) ($literal['startLine'] ?? 0), (int) ($literal['endLine'] ?? 0));
        if ($resource === null) {
            return $args;
        }
        $args[$argIndex] = new PhpIrStringLiteral(
            (int) ($literal['startLine'] ?? 0),
            (int) ($literal['endLine'] ?? 0),
            [],
            'phpef-resource://' . $resource['resourcePath'],
        );
        return $args;
    }

    private static function embeddedResourceReferenceForLiteralPath(string $literalPath, int $startLine, int $endLine): ?array
    {
        if ($literalPath === '' || str_contains($literalPath, '://') || str_starts_with($literalPath, '/') || str_contains($literalPath, "\0")) {
            return null;
        }
        $pathParts = preg_split('#/+#', str_replace('\\', '/', $literalPath));
        if (!is_array($pathParts) || in_array('..', $pathParts, true)) {
            return null;
        }
        $sourceDir = self::$currentPhpSourceFile !== '' ? dirname(self::$currentPhpSourceFile) : '.';
        $fullPath = $sourceDir . DIRECTORY_SEPARATOR . $literalPath;
        if (!is_file($fullPath) || !is_readable($fullPath)) {
            return null;
        }
        $realFullPath = realpath($fullPath);
        if ($realFullPath === false) {
            return null;
        }
        $resourcePath = '/' . ltrim(implode('/', array_filter($pathParts, static fn (string $part): bool => $part !== '' && $part !== '.')), '/');
        if ($resourcePath === '/') {
            return null;
        }
        $size = filesize($realFullPath);
        $hash = hash_file('sha256', $realFullPath);
        if ($size === false || $hash === false) {
            return null;
        }
        $sectionHash = hash('sha256', $resourcePath . "\0" . $hash);
        $sectionName = '.phpef_resource.' . substr($sectionHash, 0, 16);
        if (!isset(self::$embeddedResourceReferences[$resourcePath])) {
            self::$embeddedResourceReferences[$resourcePath] = [
                'resourcePath' => $resourcePath,
                'sourcePath' => $literalPath,
                'sourceFilePath' => $realFullPath,
                'sectionName' => $sectionName,
                'sha256' => $hash,
                'size' => (int) $size,
                'startLine' => $startLine,
                'endLine' => $endLine,
            ];
        }
        return self::$embeddedResourceReferences[$resourcePath];
    }

    private static function staticCallToIr(PhpParser\Node\Expr\StaticCall $expr, ?int $startLine = null, ?int $endLine = null, ?array $comments = null, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): PhpIrStaticCall
    {
        $staticCallName = self::staticCallName($expr, $currentClassName);
        if ($staticCallName === null || !isset($declaredStaticMethods[strtolower($staticCallName)])) {
            throw new RuntimeException(self::unsupportedStaticCallMessage($expr));
        }
        [$className, $methodName] = explode('::', $staticCallName, 2);
        return new PhpIrStaticCall(
            $startLine ?? self::nodeStartLine($expr),
            $endLine ?? self::nodeEndLine($expr),
            $comments ?? self::nodeComments($expr),
            $className,
            $methodName,
            array_map(static fn (PhpParser\Node\Arg $arg): object => self::exprArgToIr($arg, $declaredFunctions, $declaredStaticMethods, $currentClassName), $expr->args),
            $currentClassName ?? '',
        );
    }

    private static function isSupportedFuncCall(string $functionName, array $declaredFunctions = []): bool
    {
        if (isset($declaredFunctions[strtolower($functionName)])) {
            return true;
        }
        if (self::hasRuntimeWrapper($functionName)) {
            return true;
        }

        return function_exists($functionName);
    }

    private static function unsupportedFuncCallMessage(PhpParser\Node\Expr\FuncCall $expr): string
    {
        $functionName = self::funcCallName($expr);
        $line = self::nodeStartLine($expr);

        if ($functionName === null) {
            return 'Unsupported PHP expression for phpast-ir-v0: PhpParser\\Node\\Expr\\FuncCall'
                . ' at line ' . $line
                . '; dynamic function name cannot be resolved statically yet';
        }

        $wrapperName = 'phpfunc_' . strtolower($functionName);
        $existsInSelectedPhpRuntime = function_exists($functionName);
        $reachability = $existsInSelectedPhpRuntime
            ? 'the selected PHP runtime reports function_exists("' . $functionName . '") = true; this call is theoretically reachable once phpef implements lowering/wrapper ' . $wrapperName
            : 'the selected PHP runtime reports function_exists("' . $functionName . '") = false; this function is not currently reachable in this PHP runtime/extension set';

        return 'Unsupported PHP function call for phpast-ir-v0: ' . $functionName
            . ' at line ' . $line
            . '; expected runtime wrapper/lowering: ' . $wrapperName
            . '; ' . $reachability;
    }

    private static function hasRuntimeWrapper(string $functionName): bool
    {
        if (class_exists(Php2ValaFuncResolver::class) && method_exists(Php2ValaFuncResolver::class, 'hasRuntimeFunction')) {
            try {
                return Php2ValaFuncResolver::default()->hasRuntimeFunction($functionName);
            } catch (Throwable) {
                // Fall through to local VAPI metadata probing below.
            }
        }

        $runtimeName = 'phpfunc_' . strtolower($functionName);
        foreach (self::runtimeVapiPaths() as $path) {
            $vapi = @file_get_contents($path);
            if ($vapi !== false && preg_match('/\\bpublic\\s+static\\s+[^;\\n]+\\s+' . preg_quote($runtimeName, '/') . '\\s*\\(/', $vapi) === 1) {
                return true;
            }
        }

        return false;
    }

    private static function runtimeVapiPaths(): array
    {
        $paths = [];
        foreach (['PHPEF_RUNTIME_CORE_VAPI', 'PHPEF_RUNTIME_EMBED_VAPI'] as $envName) {
            $explicit = getenv($envName);
            if (is_string($explicit) && $explicit !== '') {
                $paths[] = $explicit;
            }
        }

        $vapidir = getenv('PHPEF_RUNTIME_VAPIDIR');
        if (is_string($vapidir) && $vapidir !== '') {
            $paths[] = rtrim($vapidir, '/') . '/phpef-runtime-core.vapi';
            $paths[] = rtrim($vapidir, '/') . '/phpef-runtime-embed.vapi';
        }

        $paths[] = '/usr/share/vala/vapi/phpef-runtime-core.vapi';
        $paths[] = '/usr/local/share/vala/vapi/phpef-runtime-core.vapi';
        $paths[] = '/usr/share/vala/vapi/phpef-runtime-embed.vapi';
        $paths[] = '/usr/local/share/vala/vapi/phpef-runtime-embed.vapi';

        return array_values(array_unique($paths));
    }

    private static function funcCallName(PhpParser\Node\Expr\FuncCall $expr): ?string
    {
        if ($expr->name instanceof PhpParser\Node\Name) {
            return $expr->name->toString();
        }

        return null;
    }

    private static function staticCallName(PhpParser\Node\Expr\StaticCall $expr, ?string $currentClassName = null): ?string
    {
        if (!$expr->class instanceof PhpParser\Node\Name || !$expr->name instanceof PhpParser\Node\Identifier) {
            return null;
        }
        $className = $expr->class->toString();
        if (strtolower($className) === 'self') {
            if ($currentClassName === null) {
                return null;
            }
            $className = $currentClassName;
        }
        return $className . '::' . $expr->name->toString();
    }

    private static function unsupportedStaticCallMessage(PhpParser\Node\Expr\StaticCall $expr): string
    {
        $name = self::staticCallName($expr);
        $line = self::nodeStartLine($expr);
        if ($name === null) {
            return 'Unsupported PHP static method call for phpast-ir-v0 at line ' . $line . ': dynamic class/method names are not supported yet';
        }
        $className = explode('::', $name, 2)[0];
        if (in_array(strtolower($className), ['self', 'parent', 'static'], true)) {
            return 'Unsupported PHP static method call for phpast-ir-v0: ' . $name
                . ' at line ' . $line
                . '; self:: is only supported inside a class context and parent::/static:: are not supported yet';
        }
        return 'Unsupported PHP static method call for phpast-ir-v0: ' . $name
            . ' at line ' . $line
            . '; expected a static method declared in this source set and registered by an include before the call';
    }

    /**
     * Return function names declared at top level of a PHP parser AST.
     *
     * The returned map is keyed by lower-case PHP function name and is used by
     * multi-source PHPEF builds so one source file can validate calls to
     * functions declared in another source file.
     */
    public static function declaredFunctionNames(array $phpParserAst, bool $includeConditionalDeclarations = false): array
    {
        $names = [];
        $walk = static function (mixed $node, bool $isTopLevel) use (&$walk, &$names, $includeConditionalDeclarations): void {
            if ($node instanceof PhpParser\Node\Stmt\Function_ && ($isTopLevel || $includeConditionalDeclarations)) {
                $names[strtolower($node->name->toString())] = $node->name->toString();
                return;
            }
            if (!$includeConditionalDeclarations && !$isTopLevel) {
                return;
            }
            if ($node instanceof PhpParser\Node) {
                foreach ($node->getSubNodeNames() as $name) {
                    $walk($node->$name, false);
                }
                return;
            }
            if (is_array($node)) {
                foreach ($node as $child) {
                    $walk($child, $isTopLevel);
                }
            }
        };
        $walk($phpParserAst, true);
        return $names;
    }

    /**
     * Return static method names declared by supported class declarations.
     */
    public static function declaredStaticMethodNames(array $phpParserAst, bool $includeConditionalDeclarations = false): array
    {
        $names = [];
        $walk = static function (mixed $node, bool $isTopLevel) use (&$walk, &$names, $includeConditionalDeclarations): void {
            if ($node instanceof PhpParser\Node\Stmt\Class_ && $node->name instanceof PhpParser\Node\Identifier && ($isTopLevel || $includeConditionalDeclarations)) {
                $className = $node->name->toString();
                foreach ($node->stmts as $classStmt) {
                    if ($classStmt instanceof PhpParser\Node\Stmt\ClassMethod
                        && ($classStmt->flags & PhpParser\Node\Stmt\Class_::MODIFIER_STATIC) !== 0
                    ) {
                        $name = $className . '::' . $classStmt->name->toString();
                        $names[strtolower($name)] = $name;
                    }
                }
            }
            if (!$includeConditionalDeclarations && !$isTopLevel) {
                return;
            }
            if ($node instanceof PhpParser\Node) {
                foreach ($node->getSubNodeNames() as $name) {
                    $walk($node->$name, false);
                }
                return;
            }
            if (is_array($node)) {
                foreach ($node as $child) {
                    $walk($child, $isTopLevel);
                }
            }
        };
        $walk($phpParserAst, true);
        return $names;
    }
    private static function ifContainsOnlyConditionalFunctionDeclarations(PhpParser\Node\Stmt\If_ $stmt): bool
    {
        $allStmts = $stmt->stmts;
        foreach ($stmt->elseifs as $elseif) {
            $allStmts = array_merge($allStmts, $elseif->stmts);
        }
        if ($stmt->else !== null) {
            $allStmts = array_merge($allStmts, $stmt->else->stmts);
        }
        if ($allStmts === []) {
            return true;
        }
        foreach ($allStmts as $innerStmt) {
            if (!$innerStmt instanceof PhpParser\Node\Stmt\Function_) {
                return false;
            }
        }
        return true;
    }

    private static function allExternalDeclaredFunctions(array $externalDeclaredFunctionsByInclude): array
    {
        $functions = [];
        foreach ($externalDeclaredFunctionsByInclude as $declaredFunctions) {
            if (is_array($declaredFunctions)) {
                $functions = array_merge($functions, $declaredFunctions);
            }
        }
        return $functions;
    }

    private static function allExternalDeclaredStaticMethods(array $externalDeclaredStaticMethodsByInclude): array
    {
        $methods = [];
        foreach ($externalDeclaredStaticMethodsByInclude as $declaredMethods) {
            if (is_array($declaredMethods)) {
                $methods = array_merge($methods, $declaredMethods);
            }
        }
        return $methods;
    }

    private static function includedDeclaredFunctions(string $includePath, array $externalDeclaredFunctionsByInclude): array
    {
        $functions = [];
        foreach (self::includeLookupKeys($includePath) as $key) {
            if (isset($externalDeclaredFunctionsByInclude[$key]) && is_array($externalDeclaredFunctionsByInclude[$key])) {
                $functions = array_merge($functions, $externalDeclaredFunctionsByInclude[$key]);
            }
        }
        return $functions;
    }

    /**
     * Return aliases used to match a static include path against source files.
     */
    public static function includeLookupKeys(string $path): array
    {
        $normalized = str_replace('\\', '/', $path);
        return array_values(array_unique([$path, $normalized, ltrim($normalized, './'), basename($normalized)]));
    }

    private static function exprArgToIr(PhpParser\Node\Arg $arg, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null, array $closureBindings = []): object
    {
        if ($arg->unpack) {
            throw new RuntimeException('Unsupported PHP argument for phpast-ir-v0 at line ' . self::nodeStartLine($arg) . ': unpack is not supported yet');
        }

        return self::exprToIr($arg->value, $declaredFunctions, $declaredStaticMethods, $currentClassName, $closureBindings);
    }

    private static function identifierPart(string $value): string
    {
        $part = preg_replace('/[^A-Za-z0-9_]+/', '_', $value) ?? '';
        $part = trim($part, '_');
        if ($part === '') {
            return 'closure';
        }
        if (preg_match('/^[A-Za-z_]/', $part) !== 1) {
            $part = '_' . $part;
        }
        return $part;
    }

    private static function nodeStartLine(PhpParser\Node $node): int
    {
        return (int) $node->getStartLine();
    }

    private static function nodeEndLine(PhpParser\Node $node): int
    {
        return (int) $node->getEndLine();
    }

    private static function nodeComments(PhpParser\Node $node): array
    {
        return array_map(
            static fn (PhpParser\Comment $comment): PhpIrComment => new PhpIrComment(
                $comment instanceof PhpParser\Comment\Doc ? 'DocComment' : 'Comment',
                $comment->getText(),
                (int) $comment->getStartLine(),
                (int) $comment->getEndLine(),
            ),
            $node->getComments(),
        );
    }
}
