<?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 string $currentPhpSourceFile = '';

    /**
     * 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::$currentPhpSourceFile = $phpSourceFile;
        $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
    {
        $savedLiteralStreamPathVariables = self::$literalStreamPathVariables;
        if (!$trackLiteralStreamPathVariables) {
            self::$literalStreamPathVariables = [];
        }
        $stmts = [];
        foreach ($phpParserStmts as $stmt) {
            foreach (self::irNodesToArray(self::stmtToIr($stmt, $declaredFunctions, $declaredStaticMethods, $currentClassName)) as $irStmt) {
                $stmts[] = $irStmt;
            }
            if ($trackLiteralStreamPathVariables) {
                self::updateLiteralStreamPathVariablesAfterStmt($stmt);
            }
        }
        if (!$trackLiteralStreamPathVariables) {
            self::$literalStreamPathVariables = $savedLiteralStreamPathVariables;
        }
        return $stmts;
    }

    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): mixed
    {
        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 ($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);
        }

        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\For_) {
            return self::forToIr($stmt, $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

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

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

        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\FuncCall) {
            return self::funcCallToIr(
                $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\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_) {
            $params = [];
            $paramTypes = [];
            foreach ($stmt->params as $param) {
                if (!$param->var instanceof PhpParser\Node\Expr\Variable || !is_string($param->var->name)) {
                    throw new RuntimeException('Unsupported PHP function parameter for phpast-ir-v0 at line ' . self::nodeStartLine($stmt));
                }
                if ($param->byRef || $param->variadic || $param->default !== null) {
                    throw new RuntimeException('Unsupported PHP function parameter for phpast-ir-v0 at line ' . self::nodeStartLine($stmt) . ': by-reference, variadic and default parameters are not supported yet');
                }
                $params[] = $param->var->name;
                $paramType = self::paramTypeName($param->type);
                if ($paramType !== '') {
                    $paramTypes[$param->var->name] = $paramType;
                }
            }
            return new PhpIrFunction(
                self::nodeStartLine($stmt),
                self::nodeEndLine($stmt),
                self::nodeComments($stmt),
                $stmt->name->toString(),
                $params,
                self::stmtsToIr($stmt->stmts, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                'public',
                false,
                self::returnTypeName($stmt->returnType),
                $paramTypes,
            );
        }

        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_) {
            return new PhpIrReturn(
                self::nodeStartLine($stmt),
                self::nodeEndLine($stmt),
                self::nodeComments($stmt),
                $stmt->expr !== null ? self::exprToIr($stmt->expr, $declaredFunctions, $declaredStaticMethods, $currentClassName) : 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 ifToIr(PhpParser\Node\Stmt\If_ $stmt, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): PhpIrIf
    {
        return new PhpIrIf(
            self::nodeStartLine($stmt),
            self::nodeEndLine($stmt),
            self::nodeComments($stmt),
            self::exprToIr($stmt->cond, $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)
                : [],
        );
    }

    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 tryCatchToIr(PhpParser\Node\Stmt\TryCatch $stmt, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): PhpIrTryCatch
    {
        if ($stmt->finally !== null) {
            throw new RuntimeException('Unsupported PHP try/catch for phpast-ir-v0 at line ' . self::nodeStartLine($stmt) . ': finally is not supported yet');
        }
        if (count($stmt->catches) === 0) {
            throw new RuntimeException('Unsupported PHP try/catch for phpast-ir-v0 at line ' . self::nodeStartLine($stmt) . ': at least one catch 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),
                ];
            }
        }

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

    private static function forToIr(PhpParser\Node\Stmt\For_ $stmt, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): 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),
        );
    }
    private static function foreachToIr(PhpParser\Node\Stmt\Foreach_ $stmt, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): PhpIrForeachValue
    {
        if ($stmt->keyVar !== null) {
            throw new RuntimeException('Unsupported PHP foreach for phpast-ir-v0 at line ' . self::nodeStartLine($stmt) . ': key variables are not supported yet');
        }
        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)) as $irStmt) {
                $body[] = $irStmt;
            }
        }

        return new PhpIrForeachValue(
            self::nodeStartLine($stmt),
            self::nodeEndLine($stmt),
            self::nodeComments($stmt),
            self::exprToIr($stmt->expr, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            $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\PostInc) {
            return self::postIncToAssignmentIr($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 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 assignmentToIr(PhpParser\Node\Expr\Assign $assign, int $startLine, int $endLine, array $comments, array $declaredFunctions = [], array $declaredStaticMethods = [], ?string $currentClassName = null): 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),
                        self::exprToIr($dimExpr, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                    ),
                );
            }
            return $result;
        }

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

        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,
            ));
            $result[] = new PhpIrAssignment(
                $startLine,
                $endLine,
                [],
                self::exprToIr($assign->var, $declaredFunctions, $declaredStaticMethods, $currentClassName),
                self::exprToIr($innerAssign->var, $declaredFunctions, $declaredStaticMethods, $currentClassName),
            );
            return $result;
        }

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

    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 = [];
        foreach ($stmt->stmts as $classStmt) {
            if ($classStmt instanceof PhpParser\Node\Stmt\ClassMethod) {
                $isStatic = ($classStmt->flags & PhpParser\Node\Stmt\Class_::MODIFIER_STATIC) !== 0;
                $params = [];
                $paramTypes = [];
                foreach ($classStmt->params as $param) {
                    if (!$param->var instanceof PhpParser\Node\Expr\Variable || !is_string($param->var->name)) {
                        throw new RuntimeException('Unsupported PHP class method parameter for phpast-ir-v0 at line ' . self::nodeStartLine($classStmt));
                    }
                    if ($param->byRef || $param->variadic || $param->default !== null) {
                        throw new RuntimeException('Unsupported PHP class method parameter for phpast-ir-v0 at line ' . self::nodeStartLine($classStmt) . ': by-reference, variadic and default parameters are not supported yet');
                    }
                    $params[] = $param->var->name;
                    $paramType = self::paramTypeName($param->type);
                    if ($paramType !== '') {
                        $paramTypes[$param->var->name] = $paramType;
                    }
                }
                $methods[] = new PhpIrFunction(
                    self::nodeStartLine($classStmt),
                    self::nodeEndLine($classStmt),
                    self::nodeComments($classStmt),
                    $classStmt->name->toString(),
                    $params,
                    self::stmtsToIr($classStmt->stmts ?? [], $declaredFunctions, $declaredStaticMethods, $className),
                    self::methodVisibility($classStmt),
                    $isStatic,
                    self::returnTypeName($classStmt->returnType),
                    $paramTypes,
                );
                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);
    }
    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\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): 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\Cast\Double) {
            return new PhpIrFuncCall(
                self::nodeStartLine($expr),
                self::nodeEndLine($expr),
                self::nodeComments($expr),
                'floatval',
                [self::exprToIr($expr->expr, $declaredFunctions, $declaredStaticMethods, $currentClassName)],
            );
        }

        if ($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\Variable && is_string($expr->name)) {
            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\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\FuncCall) {
            return self::funcCallToIr($expr, null, null, null, $declaredFunctions, $declaredStaticMethods, $currentClassName);
        }

        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));
        }
        // 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): PhpIrFuncCall
    {
        $functionName = self::funcCallName($expr);
        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), $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): 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);
    }

    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(),
        );
    }
}
