<?php

/**
 * Builder that maps PHP-AST IR objects to the first Vala IR classes.
 */
final class ValaIrBuilder
{
    /**
     * Convert a PHP-IR program into a Vala-IR program.
     */
    public static function fromPhpIr(object $phpIr, array $externalUserFunctionsByInclude = [], array $includeActivationFunctions = [], string $localFunctionPrefix = '', string $userFunctionAbiPrefix = 'phpef'): ValaIrProgram
    {
        if (!method_exists($phpIr, 'toArray')) {
            throw new RuntimeException('Expected a PHP-IR object with toArray(), got ' . get_debug_type($phpIr));
        }

        $array = $phpIr->toArray();
        if (($array['format'] ?? null) !== 'phpast-ir-v0' || !isset($array['root']['stmts']) || !is_array($array['root']['stmts'])) {
            throw new RuntimeException('Expected phpast-ir-v0 program');
        }

        $sourceFile = (string) ($array['root']['phpSourceFile'] ?? '');
        $funcResolver = Php2ValaFuncResolver::default();
        $userFunctions = self::userFunctionMap($array['root']['stmts'], $sourceFile, $localFunctionPrefix !== '' ? $localFunctionPrefix : null, $userFunctionAbiPrefix);
        $userFunctions['__phpefc_source_prefix'] = $localFunctionPrefix !== '' ? $localFunctionPrefix : self::sourceFunctionPrefix($sourceFile);
        $userFunctions['__phpefc_source_file'] = $sourceFile;
        $knownUserFunctions = self::mergeKnownUserFunctions(
            self::mergeKnownUserFunctions($userFunctions, self::knownUserFunctionNameMap($array['root']['knownUserFunctionNames'] ?? [])),
            self::allExternalUserFunctions($externalUserFunctionsByInclude)
        );
        $declaredVariables = [];
        $phpDeclarations = [];
        $topLevelStmts = [];
        $deferredHelpers = [];
        $deferredHelperCounter = 0;
        foreach ($array['root']['stmts'] as $stmt) {
            foreach (self::stmtsToArray(self::stmtArrayToValaIr($stmt, $funcResolver, $declaredVariables, $userFunctions, $knownUserFunctions, $includeActivationFunctions, $deferredHelpers, $deferredHelperCounter, false)) as $valaStmt) {
                if ($valaStmt instanceof ValaIrFunctionDeclaration || $valaStmt instanceof ValaIrClassDeclaration) {
                    $phpDeclarations[] = $valaStmt;
                } else {
                    $topLevelStmts[] = $valaStmt;
                }
            }
            if (($stmt['kind'] ?? null) === 'FuncCall' && (string) ($stmt['functionName'] ?? '') === '__phpefc_include') {
                $includePath = self::includePathFromFuncCall($stmt);
                if ($includePath !== null) {
                    $userFunctions = self::mergeIncludedUserFunctions($userFunctions, self::includedUserFunctions($includePath, $externalUserFunctionsByInclude));
                }
            }
        }

        $sourceMainFunctionName = self::sourceMainFunctionName($sourceFile, $localFunctionPrefix);
        $sourceRegisterFunctionName = self::sourceRegisterFunctionNameFromMain($sourceMainFunctionName);
        $sourceStacktraceRegisterFunctionName = self::sourceStacktraceRegisterFunctionNameFromMain($sourceMainFunctionName);
        $stmts = array_merge(
            $phpDeclarations,
            [self::sourceMainFunction($sourceMainFunctionName, $topLevelStmts)],
            $deferredHelpers,
            [self::sourceStacktraceRegisterFunction($sourceStacktraceRegisterFunctionName, $sourceMainFunctionName, $deferredHelpers)],
            [self::sourceRegisterFunction($sourceRegisterFunctionName, $phpDeclarations)],
            self::entrypointFunctions($sourceStacktraceRegisterFunctionName, $sourceRegisterFunctionName, $sourceMainFunctionName),
        );

        return new ValaIrProgram(
            $stmts,
            array_merge(['PhpefRuntimeCore'], $funcResolver->usedNamespaces()),
            $sourceFile,
            [],
        );
    }

    private static function stmtArrayToValaIr(array $stmt, Php2ValaFuncResolver $funcResolver, array &$declaredVariables, array $userFunctions = [], array $knownUserFunctions = [], array $includeActivationFunctions = [], array &$deferredHelpers = [], int &$deferredHelperCounter = 0, bool $valueReturnAllowed = true, string $functionReturnTypeHint = ''): mixed
    {
        return match ($stmt['kind'] ?? null) {
            'Assignment' => self::assignmentToValaIr($stmt, $funcResolver, $declaredVariables, $userFunctions, $knownUserFunctions),
            'ArrayAppendAssignment' => self::arrayAppendAssignmentToValaIr($stmt, $funcResolver, $declaredVariables, $userFunctions, $knownUserFunctions),
            'Unset' => self::unsetToValaIr($stmt, $funcResolver, $declaredVariables, $userFunctions, $knownUserFunctions),
            'Echo' => array_map(
                static fn (array $expr): ValaIrCallStatement => new ValaIrCallStatement(
                    self::line($stmt, 'startLine'),
                    self::line($stmt, 'endLine'),
                    self::comments($stmt),
                    $funcResolver->resolveRuntimeFunction('echo', 1),
                    [self::exprArrayToValaIr($expr, $funcResolver, $userFunctions, $knownUserFunctions)],
                ),
                self::arrayField($stmt, 'exprs'),
            ),
            'FuncCall' => ((string) ($stmt['functionName'] ?? '') === '__phpefc_include'
                ? self::includeToValaIr($stmt, $includeActivationFunctions, $deferredHelpers, $deferredHelperCounter)
                : self::funcCallStatementToValaIr($stmt, $funcResolver, $userFunctions, $knownUserFunctions)),
            'Throw' => self::throwToValaIr($stmt, $funcResolver, $userFunctions, $knownUserFunctions),
            'Print' => new ValaIrCallStatement(
                self::line($stmt, 'startLine'),
                self::line($stmt, 'endLine'),
                self::comments($stmt),
                $funcResolver->resolveRuntimeFunction('print', 1),
                [self::exprArrayToValaIr(self::arrayFieldOne($stmt, 'expr'), $funcResolver, $userFunctions, $knownUserFunctions)],
            ),
            'Function' => self::functionToValaIr($stmt, $funcResolver, $userFunctions, $knownUserFunctions, $includeActivationFunctions, $deferredHelpers, $deferredHelperCounter),
            'Class' => self::classToValaIr($stmt, $funcResolver, $userFunctions, $knownUserFunctions, $includeActivationFunctions, $deferredHelpers, $deferredHelperCounter),
            'StaticCall' => self::staticCallStatementToValaIr($stmt, $funcResolver, $userFunctions, $knownUserFunctions),
            'MethodCall' => self::methodCallStatementToValaIr($stmt, $funcResolver, $userFunctions, $knownUserFunctions),
            'Return' => self::returnToValaIr($stmt, $funcResolver, $userFunctions, $knownUserFunctions, $valueReturnAllowed, $functionReturnTypeHint),
            'If' => self::ifToValaIr($stmt, $funcResolver, $declaredVariables, $userFunctions, $knownUserFunctions, $includeActivationFunctions, $deferredHelpers, $deferredHelperCounter, $valueReturnAllowed, $functionReturnTypeHint),
            'For' => self::forToValaIr($stmt, $funcResolver, $declaredVariables, $userFunctions, $knownUserFunctions, $includeActivationFunctions, $deferredHelpers, $deferredHelperCounter, $valueReturnAllowed, $functionReturnTypeHint),
            'While' => self::whileToValaIr($stmt, $funcResolver, $declaredVariables, $userFunctions, $knownUserFunctions, $includeActivationFunctions, $deferredHelpers, $deferredHelperCounter, $valueReturnAllowed, $functionReturnTypeHint),
            'ForeachValue' => self::foreachValueToValaIr($stmt, $funcResolver, $declaredVariables, $userFunctions, $knownUserFunctions, $includeActivationFunctions, $deferredHelpers, $deferredHelperCounter, $valueReturnAllowed, $functionReturnTypeHint),
            'TryCatch' => self::tryCatchToValaIr($stmt, $funcResolver, $declaredVariables, $userFunctions, $knownUserFunctions, $includeActivationFunctions, $deferredHelpers, $deferredHelperCounter, $valueReturnAllowed, $functionReturnTypeHint),
            'Nop' => new ValaIrNop(
                self::line($stmt, 'startLine'),
                self::line($stmt, 'endLine'),
                self::comments($stmt),
            ),
            default => throw new RuntimeException('Unsupported PHP-IR statement for vala-ir-v0: ' . (string) ($stmt['kind'] ?? 'unknown')),
        };
    }

    private static function tryCatchToValaIr(array $stmt, Php2ValaFuncResolver $funcResolver, array &$declaredVariables, array $userFunctions, array $knownUserFunctions, array $includeActivationFunctions, array &$deferredHelpers, int &$deferredHelperCounter, bool $valueReturnAllowed, string $functionReturnTypeHint): ValaIrTryCatchStatement
    {
        $tryDeclaredVariables = $declaredVariables;
        $tryStmts = [];
        foreach (self::arrayField($stmt, 'tryStmts') as $innerStmt) {
            foreach (self::stmtsToArray(self::stmtArrayToValaIr($innerStmt, $funcResolver, $tryDeclaredVariables, $userFunctions, $knownUserFunctions, $includeActivationFunctions, $deferredHelpers, $deferredHelperCounter, $valueReturnAllowed, $functionReturnTypeHint)) as $valaStmt) {
                $tryStmts[] = $valaStmt;
            }
        }

        $line = self::line($stmt, 'startLine');
        $endLine = self::line($stmt, 'endLine');
        $hostErrorName = '__phpefc_caught_error';
        $caughtThrowableName = '__phpefc_caught_throwable';
        $loweredCatchStmts = [
            new ValaIrVariableDeclaration(
                $line,
                $endLine,
                [],
                'Zval',
                $caughtThrowableName,
                new ValaIrStaticCallExpression($line, $endLine, [], 'PhpefRuntimeCore.exception_get_or_create_throwable', [
                    new ValaIrVariableReference($line, $endLine, [], $hostErrorName),
                ]),
            ),
        ];
        $catchBranches = [];
        foreach (self::arrayField($stmt, 'catchArms') as $arm) {
            if (!is_array($arm)) {
                continue;
            }
            $catchDeclaredVariables = $declaredVariables;
            $catchVarName = (string) ($arm['varName'] ?? 'e');
            $catchDeclaredVariables[$catchVarName] = true;
            $catchBodyStmts = [];
            foreach (self::arrayField($arm, 'stmts') as $innerStmt) {
                foreach (self::stmtsToArray(self::stmtArrayToValaIr($innerStmt, $funcResolver, $catchDeclaredVariables, $userFunctions, $knownUserFunctions, $includeActivationFunctions, $deferredHelpers, $deferredHelperCounter, $valueReturnAllowed, $functionReturnTypeHint)) as $valaStmt) {
                    $catchBodyStmts[] = $valaStmt;
                }
            }
            $catchBinding = isset($declaredVariables[$catchVarName])
                ? new ValaIrAssignmentStatement($line, $endLine, [], $catchVarName, new ValaIrVariableReference($line, $endLine, [], $caughtThrowableName))
                : new ValaIrVariableDeclaration($line, $endLine, [], 'Zval', $catchVarName, new ValaIrVariableReference($line, $endLine, [], $caughtThrowableName));
            $catchBranches[] = [
                'condition' => new ValaIrStaticCallExpression($line, $endLine, [], 'PhpefRuntimeCore.exception_is_a', [
                    new ValaIrVariableReference($line, $endLine, [], $hostErrorName),
                    new ValaIrStringLiteral($line, $endLine, [], self::phpExceptionClass((string) ($arm['className'] ?? 'Exception'))),
                ]),
                'stmts' => array_merge([$catchBinding], $catchBodyStmts),
            ];
        }
        if ($catchBranches === []) {
            $loweredCatchStmts[] = ValaIrThrowStatement::hostErrorRethrow($line, $endLine, [], $hostErrorName);
        } else {
            $firstCatchBranch = array_shift($catchBranches);
            $loweredCatchStmts[] = new ValaIrIfStatement(
                $line,
                $endLine,
                [],
                $firstCatchBranch['condition'],
                $firstCatchBranch['stmts'],
                $catchBranches,
                [ValaIrThrowStatement::hostErrorRethrow($line, $endLine, [], $hostErrorName)],
            );
        }
        $declaredVariables = $tryDeclaredVariables;
        return new ValaIrTryCatchStatement(
            $line,
            $endLine,
            self::comments($stmt),
            $tryStmts,
            'Error',
            $hostErrorName,
            $loweredCatchStmts,
        );
    }

    private static function phpExceptionClass(string $className): string
    {
        while ($className !== '' && $className[0] === chr(92)) {
            $className = substr($className, 1);
        }
        if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $className)) {
            throw new RuntimeException('Invalid PHP exception class name: ' . $className);
        }
        return $className;
    }

    private static function throwToValaIr(array $stmt, Php2ValaFuncResolver $funcResolver, array $userFunctions = [], array $knownUserFunctions = []): ValaIrCallStatement
    {
        $line = self::line($stmt, 'startLine');
        $endLine = self::line($stmt, 'endLine');
        $throwableExpr = is_array($stmt['throwableExpr'] ?? null)
            ? self::exprArrayToValaIr($stmt['throwableExpr'], $funcResolver, $userFunctions, $knownUserFunctions)
            : null;
        if ($throwableExpr !== null) {
            return new ValaIrCallStatement($line, $endLine, self::comments($stmt), 'PhpefRuntimeCore.throw_throwable', [$throwableExpr]);
        }

        $args = [
            new ValaIrStringLiteral($line, $endLine, [], self::phpExceptionClass((string) ($stmt['className'] ?? 'Exception'))),
            self::exprArrayToValaIr(self::arrayFieldOne($stmt, 'message'), $funcResolver, $userFunctions, $knownUserFunctions),
            self::exprArrayToValaIr(self::arrayFieldOne($stmt, 'code'), $funcResolver, $userFunctions, $knownUserFunctions),
        ];
        $sourceFile = (string) ($userFunctions['__phpefc_source_file'] ?? '');
        $args[] = new ValaIrStringLiteral($line, $endLine, [], $sourceFile);
        $args[] = new ValaIrIntLiteral($line, $endLine, [], $line);
        $previous = is_array($stmt['previous'] ?? null)
            ? self::exprArrayToValaIr($stmt['previous'], $funcResolver, $userFunctions, $knownUserFunctions)
            : null;
        if ($previous !== null) {
            $args[] = $previous;
        }

        return new ValaIrCallStatement($line, $endLine, self::comments($stmt), 'PhpefRuntimeCore.throw_exception_at', $args);
    }

    private static function foreachValueToValaIr(array $stmt, Php2ValaFuncResolver $funcResolver, array &$declaredVariables, array $userFunctions, array $knownUserFunctions, array $includeActivationFunctions, array &$deferredHelpers, int &$deferredHelperCounter, bool $valueReturnAllowed, string $functionReturnTypeHint): ValaIrForeachValueStatement
    {
        $valueName = (string) ($stmt['valueName'] ?? '');
        if ($valueName === '') {
            throw new RuntimeException('Unsupported foreach for vala-ir-v0: empty value variable');
        }
        $loopDeclaredVariables = $declaredVariables;
        $loopDeclaredVariables[$valueName] = true;
        $bodyStmts = [];
        foreach (self::arrayField($stmt, 'stmts') as $innerStmt) {
            foreach (self::stmtsToArray(self::stmtArrayToValaIr($innerStmt, $funcResolver, $loopDeclaredVariables, $userFunctions, $knownUserFunctions, $includeActivationFunctions, $deferredHelpers, $deferredHelperCounter, $valueReturnAllowed, $functionReturnTypeHint)) as $valaStmt) {
                $bodyStmts[] = $valaStmt;
            }
        }
        $declaredVariables = $loopDeclaredVariables;
        $deferredHelperCounter++;
        return new ValaIrForeachValueStatement(
            self::line($stmt, 'startLine'),
            self::line($stmt, 'endLine'),
            self::comments($stmt),
            self::exprArrayToValaIr(self::arrayFieldOne($stmt, 'expr'), $funcResolver, $userFunctions, $knownUserFunctions),
            $valueName,
            '__phpefc_foreach_entry_' . $deferredHelperCounter,
            $bodyStmts,
        );
    }

    private static function whileToValaIr(array $stmt, Php2ValaFuncResolver $funcResolver, array &$declaredVariables, array $userFunctions, array $knownUserFunctions, array $includeActivationFunctions, array &$deferredHelpers, int &$deferredHelperCounter, bool $valueReturnAllowed, string $functionReturnTypeHint): ValaIrWhileStatement
    {
        $loopDeclaredVariables = $declaredVariables;
        $bodyStmts = [];
        foreach (self::arrayField($stmt, 'stmts') as $innerStmt) {
            foreach (self::stmtsToArray(self::stmtArrayToValaIr($innerStmt, $funcResolver, $loopDeclaredVariables, $userFunctions, $knownUserFunctions, $includeActivationFunctions, $deferredHelpers, $deferredHelperCounter, $valueReturnAllowed, $functionReturnTypeHint)) as $valaStmt) {
                $bodyStmts[] = $valaStmt;
            }
        }
        $declaredVariables = $loopDeclaredVariables;
        return new ValaIrWhileStatement(
            self::line($stmt, 'startLine'),
            self::line($stmt, 'endLine'),
            self::comments($stmt),
            self::truthyConditionToValaIr(self::arrayFieldOne($stmt, 'cond'), $funcResolver, $userFunctions, $knownUserFunctions),
            $bodyStmts,
        );
    }

    private static function forToValaIr(array $stmt, Php2ValaFuncResolver $funcResolver, array &$declaredVariables, array $userFunctions, array $knownUserFunctions, array $includeActivationFunctions, array &$deferredHelpers, int &$deferredHelperCounter, bool $valueReturnAllowed, string $functionReturnTypeHint): array
    {
        $result = [];
        foreach (self::arrayField($stmt, 'initStmts') as $initStmt) {
            foreach (self::stmtsToArray(self::stmtArrayToValaIr($initStmt, $funcResolver, $declaredVariables, $userFunctions, $knownUserFunctions, $includeActivationFunctions, $deferredHelpers, $deferredHelperCounter, $valueReturnAllowed, $functionReturnTypeHint)) as $valaStmt) {
                $result[] = $valaStmt;
            }
        }

        $loopDeclaredVariables = $declaredVariables;
        $bodyStmts = [];
        foreach (self::arrayField($stmt, 'stmts') as $innerStmt) {
            foreach (self::stmtsToArray(self::stmtArrayToValaIr($innerStmt, $funcResolver, $loopDeclaredVariables, $userFunctions, $knownUserFunctions, $includeActivationFunctions, $deferredHelpers, $deferredHelperCounter, $valueReturnAllowed, $functionReturnTypeHint)) as $valaStmt) {
                $bodyStmts[] = $valaStmt;
            }
        }
        foreach (self::arrayField($stmt, 'loopStmts') as $loopStmt) {
            foreach (self::stmtsToArray(self::stmtArrayToValaIr($loopStmt, $funcResolver, $loopDeclaredVariables, $userFunctions, $knownUserFunctions, $includeActivationFunctions, $deferredHelpers, $deferredHelperCounter, $valueReturnAllowed, $functionReturnTypeHint)) as $valaStmt) {
                $bodyStmts[] = $valaStmt;
            }
        }

        $result[] = new ValaIrWhileStatement(
            self::line($stmt, 'startLine'),
            self::line($stmt, 'endLine'),
            self::comments($stmt),
            self::forConditionToValaIr(self::arrayField($stmt, 'condExprs'), $funcResolver, $userFunctions, $knownUserFunctions),
            $bodyStmts,
        );
        $declaredVariables = $loopDeclaredVariables;
        return $result;
    }

    private static function forConditionToValaIr(array $condExprs, Php2ValaFuncResolver $funcResolver, array $userFunctions, array $knownUserFunctions): object
    {
        if ($condExprs === []) {
            return new ValaIrBoolLiteral(0, 0, [], true);
        }
        if (count($condExprs) !== 1) {
            throw new RuntimeException('Unsupported PHP for-loop condition for vala-ir-v0: multiple condition expressions are not supported yet');
        }

        return self::truthyConditionToValaIr($condExprs[0], $funcResolver, $userFunctions, $knownUserFunctions);
    }

    private static function ifToValaIr(array $stmt, Php2ValaFuncResolver $funcResolver, array &$declaredVariables, array $userFunctions, array $knownUserFunctions, array $includeActivationFunctions, array &$deferredHelpers, int &$deferredHelperCounter, bool $valueReturnAllowed, string $functionReturnTypeHint): ValaIrIfStatement
    {
        $branchDeclaredVariables = $declaredVariables;
        $stmts = [];
        foreach (self::arrayField($stmt, 'stmts') as $innerStmt) {
            foreach (self::stmtsToArray(self::stmtArrayToValaIr($innerStmt, $funcResolver, $branchDeclaredVariables, $userFunctions, $knownUserFunctions, $includeActivationFunctions, $deferredHelpers, $deferredHelperCounter, $valueReturnAllowed, $functionReturnTypeHint)) as $valaStmt) {
                $stmts[] = $valaStmt;
            }
        }

        $elseifs = [];
        foreach (self::arrayField($stmt, 'elseifs') as $elseif) {
            if (!is_array($elseif)) {
                continue;
            }
            $elseifDeclaredVariables = $declaredVariables;
            $elseifStmts = [];
            foreach (self::arrayField($elseif, 'stmts') as $innerStmt) {
                foreach (self::stmtsToArray(self::stmtArrayToValaIr($innerStmt, $funcResolver, $elseifDeclaredVariables, $userFunctions, $knownUserFunctions, $includeActivationFunctions, $deferredHelpers, $deferredHelperCounter, $valueReturnAllowed, $functionReturnTypeHint)) as $valaStmt) {
                    $elseifStmts[] = $valaStmt;
                }
            }
            $elseifs[] = [
                'condition' => self::truthyConditionToValaIr(self::arrayFieldOne($elseif, 'cond'), $funcResolver, $userFunctions, $knownUserFunctions),
                'stmts' => $elseifStmts,
            ];
        }

        $elseDeclaredVariables = $declaredVariables;
        $elseStmts = [];
        foreach (self::arrayField($stmt, 'elseStmts') as $innerStmt) {
            foreach (self::stmtsToArray(self::stmtArrayToValaIr($innerStmt, $funcResolver, $elseDeclaredVariables, $userFunctions, $knownUserFunctions, $includeActivationFunctions, $deferredHelpers, $deferredHelperCounter, $valueReturnAllowed, $functionReturnTypeHint)) as $valaStmt) {
                $elseStmts[] = $valaStmt;
            }
        }

        return new ValaIrIfStatement(
            self::line($stmt, 'startLine'),
            self::line($stmt, 'endLine'),
            self::comments($stmt),
            self::truthyConditionToValaIr(self::arrayFieldOne($stmt, 'cond'), $funcResolver, $userFunctions, $knownUserFunctions),
            $stmts,
            $elseifs,
            $elseStmts,
        );
    }

    private static function truthyConditionToValaIr(array $expr, Php2ValaFuncResolver $funcResolver, array $userFunctions = [], array $knownUserFunctions = []): ValaIrMethodCallExpression
    {
        return new ValaIrMethodCallExpression(
            self::line($expr, 'startLine'),
            self::line($expr, 'endLine'),
            [],
            self::exprArrayToValaIr($expr, $funcResolver, $userFunctions, $knownUserFunctions),
            'to_bool',
            [],
        );
    }

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

    private static function returnToValaIr(array $stmt, Php2ValaFuncResolver $funcResolver, array $userFunctions = [], array $knownUserFunctions = [], bool $valueReturnAllowed = true, string $functionReturnTypeHint = ''): ValaIrReturnStatement
    {
        $expr = self::arrayFieldOne($stmt, 'expr');
        if ($expr === []) {
            return new ValaIrReturnStatement(
                self::line($stmt, 'startLine'),
                self::line($stmt, 'endLine'),
                self::comments($stmt),
                $valueReturnAllowed ? new ValaIrStaticCallExpression(self::line($stmt, 'startLine'), self::line($stmt, 'endLine'), [], 'Zval.from_null', []) : null,
                '',
            );
        }
        if (!$valueReturnAllowed) {
            throw new RuntimeException('Unsupported top-level PHP return value for vala-ir-v0 at line ' . self::line($stmt, 'startLine') . ': source main is void in this slice');
        }
        return new ValaIrReturnStatement(
            self::line($stmt, 'startLine'),
            self::line($stmt, 'endLine'),
            self::comments($stmt),
            self::exprArrayToValaIr($expr, $funcResolver, $userFunctions, $knownUserFunctions),
            self::returnCoerceType($functionReturnTypeHint),
        );
    }

    private static function returnCoerceType(string $returnTypeHint): string
    {
        return match (strtolower($returnTypeHint)) {
            'float', 'double' => 'float',
            default => '',
        };
    }

    private static function funcCallStatementToValaIr(array $stmt, Php2ValaFuncResolver $funcResolver, array $userFunctions = [], array $knownUserFunctions = []): ValaIrCallStatement
    {
        $args = self::arrayField($stmt, 'args');
        $phpCallName = (string) ($stmt['functionName'] ?? '');
        $userFunction = self::userFunctionCallEntry($phpCallName, $userFunctions, $knownUserFunctions);
        if ($userFunction !== null) {
            return new ValaIrCallStatement(
                self::line($stmt, 'startLine'),
                self::line($stmt, 'endLine'),
                self::comments($stmt),
                'PhpefRuntimeCore.call_function',
                [
                    new ValaIrStringLiteral(self::line($stmt, 'startLine'), self::line($stmt, 'endLine'), [], self::userFunctionEntryPhpName($userFunction, $phpCallName)),
                    self::zvalArrayExpression($stmt, $funcResolver, $userFunctions, $knownUserFunctions),
                ],
            );
        }

        return new ValaIrCallStatement(
            self::line($stmt, 'startLine'),
            self::line($stmt, 'endLine'),
            self::comments($stmt),
            self::resolveRuntimeCallName($phpCallName, count($args), $funcResolver),
            array_map(static fn (array $arg): object => self::exprArrayToValaIr($arg, $funcResolver, $userFunctions, $knownUserFunctions), $args),
        );
    }

    private static function staticCallStatementToValaIr(array $stmt, Php2ValaFuncResolver $funcResolver, array $userFunctions = [], array $knownUserFunctions = []): ValaIrCallStatement
    {
        return new ValaIrCallStatement(
            self::line($stmt, 'startLine'),
            self::line($stmt, 'endLine'),
            self::comments($stmt),
            'PhpefRuntimeCore.call_static_method',
            [
                new ValaIrStringLiteral(self::line($stmt, 'startLine'), self::line($stmt, 'endLine'), [], (string) ($stmt['className'] ?? '')),
                self::methodNameToValaIr($stmt, $funcResolver, $userFunctions, $knownUserFunctions),
                self::zvalArrayExpression($stmt, $funcResolver, $userFunctions, $knownUserFunctions),
                new ValaIrStringLiteral(self::line($stmt, 'startLine'), self::line($stmt, 'endLine'), [], (string) ($stmt['currentClassScope'] ?? '')),
            ],
        );
    }


    private static function methodCallStatementToValaIr(array $stmt, Php2ValaFuncResolver $funcResolver, array $userFunctions = [], array $knownUserFunctions = []): ValaIrCallStatement
    {
        return new ValaIrCallStatement(
            self::line($stmt, 'startLine'),
            self::line($stmt, 'endLine'),
            self::comments($stmt),
            'PhpefRuntimeCore.call_instance_method',
            [
                self::exprArrayToValaIr(self::arrayFieldOne($stmt, 'receiver'), $funcResolver, $userFunctions, $knownUserFunctions),
                self::methodNameToValaIr($stmt, $funcResolver, $userFunctions, $knownUserFunctions),
                self::zvalArrayExpression($stmt, $funcResolver, $userFunctions, $knownUserFunctions),
                new ValaIrStringLiteral(self::line($stmt, 'startLine'), self::line($stmt, 'endLine'), [], (string) ($stmt['currentClassScope'] ?? '')),
            ],
        );
    }

    private static function includeToValaIr(array $stmt, array $includeActivationFunctions, array &$deferredHelpers, int &$deferredHelperCounter): array
    {
        $args = self::arrayField($stmt, 'args');
        $pathArg = isset($args[0]) && is_array($args[0]) ? $args[0] : [];
        if (($pathArg['kind'] ?? null) !== 'StringLiteral') {
            return [self::dynamicIncludeToValaIr($stmt, $pathArg, $includeActivationFunctions, $deferredHelpers, $deferredHelperCounter)];
        }
        $path = (string) ($pathArg['value'] ?? '');
        $callee = self::resolveIncludeActivationFunction($path, $includeActivationFunctions);
        return [
            new ValaIrCallStatement(
                self::line($stmt, 'startLine'),
                self::line($stmt, 'endLine'),
                self::comments($stmt),
                self::sourceRegisterFunctionNameFromMain($callee),
                [],
            ),
            new ValaIrCallStatement(
                self::line($stmt, 'startLine'),
                self::line($stmt, 'endLine'),
                [],
                $callee,
                [],
            ),
        ];
    }

    private static function dynamicIncludeToValaIr(array $stmt, array $pathArg, array $includeActivationFunctions, array &$deferredHelpers, int &$deferredHelperCounter): ValaIrCallStatement
    {
        if ($includeActivationFunctions === []) {
            throw new RuntimeException('Unsupported dynamic PHP include for vala-ir-v0 at line ' . self::line($stmt, 'startLine') . ': no source-set include targets are available');
        }

        $funcResolver = Php2ValaFuncResolver::default();
        $line = self::line($stmt, 'startLine');
        $endLine = self::line($stmt, 'endLine');
        $pathExpr = self::exprArrayToValaIr($pathArg, $funcResolver, [], []);
        $helperName = '__phpefc_dynamic_include_' . $line . '_' . (++$deferredHelperCounter);

        $entries = [];
        foreach ($includeActivationFunctions as $path => $callee) {
            if (!is_string($path) || !is_string($callee) || $path === '' || $callee === '') {
                continue;
            }
            $matchSpec = self::dynamicIncludePathMatchSpec($path);
            if (($matchSpec['exactPaths'] ?? []) === [] && ($matchSpec['suffixPaths'] ?? []) === []) {
                continue;
            }

            $registerCallee = self::sourceRegisterFunctionNameFromMain($callee);
            $entryKey = $registerCallee . "\0" . $callee;
            if (!isset($entries[$entryKey])) {
                $entries[$entryKey] = [
                    'sourcePath' => $matchSpec['sourcePath'],
                    'exactPaths' => [],
                    'suffixPaths' => [],
                    'registerCallee' => $registerCallee,
                    'mainCallee' => $callee,
                ];
            }
            $entries[$entryKey]['exactPaths'] = array_values(array_unique(array_merge($entries[$entryKey]['exactPaths'], $matchSpec['exactPaths'])));
            $entries[$entryKey]['suffixPaths'] = array_values(array_unique(array_merge($entries[$entryKey]['suffixPaths'], $matchSpec['suffixPaths'])));
        }

        $deferredHelpers[] = new ValaIrDeferredHelperFunction(
            $line,
            $endLine,
            [],
            $helperName,
            'void',
            [['typeName' => 'Zval', 'name' => '__phpefc_include_expr']],
            [new ValaIrDynamicIncludeDispatchStatement(
                $line,
                $endLine,
                [],
                new ValaIrVariableReference($line, $endLine, [], '__phpefc_include_expr'),
                array_values($entries),
                '__phpefc_include_path',
                self::dynamicIncludeFailureStmts($line, $endLine, '__phpefc_include_path'),
            )],
            '{include}',
        );

        return new ValaIrCallStatement(
            $line,
            $endLine,
            self::comments($stmt),
            $helperName,
            [$pathExpr],
        );
    }


    private static function dynamicIncludeFailureStmts(int $line, int $endLine, string $pathVariable): array
    {
        return [new ValaIrCallStatement(
            $line,
            $endLine,
            [],
            'PhpefRuntimeCore.throw_exception',
            [
                new ValaIrStringLiteral($line, $endLine, [], 'Error'),
                new ValaIrStaticCallExpression($line, $endLine, [], 'Zval.from_string', [
                    new ValaIrBinaryExpression(
                        $line,
                        $endLine,
                        [],
                        '+',
                        new ValaIrBinaryExpression(
                            $line,
                            $endLine,
                            [],
                            '+',
                            new ValaIrStringLiteral($line, $endLine, [], 'Failed opening required "'),
                            new ValaIrVariableReference($line, $endLine, [], $pathVariable),
                        ),
                        new ValaIrStringLiteral($line, $endLine, [], '"'),
                    ),
                ]),
            ],
        )];
    }

    private static function dynamicIncludePathMatchSpec(string $sourcePath): array
    {
        $matchPath = self::dynamicIncludePortableMatchPath($sourcePath);
        $exactPaths = $matchPath !== '' ? [$matchPath] : [];
        $suffixPaths = $matchPath !== '' ? ['/' . $matchPath] : [];

        return [
            'sourcePath' => $matchPath,
            'exactPaths' => array_values(array_unique($exactPaths)),
            'suffixPaths' => array_values(array_unique($suffixPaths)),
        ];
    }

    private static function dynamicIncludePortableMatchPath(string $sourcePath): string
    {
        $normalizedSourcePath = str_replace('\\', '/', $sourcePath);
        $segmentPath = preg_replace("#^(?:\\./)+#", "", $normalizedSourcePath) ?? $normalizedSourcePath;
        $isAbsolutePath = str_starts_with($segmentPath, '/') || preg_match('/^[A-Za-z]:\//', $segmentPath) === 1;
        $segmentPath = ltrim($segmentPath, '/');
        if ($segmentPath === '') {
            return '';
        }

        if ($isAbsolutePath || self::dynamicIncludePathLooksHostSpecific($segmentPath)) {
            return basename($segmentPath);
        }

        return $segmentPath;
    }

    private static function dynamicIncludePathLooksHostSpecific(string $segmentPath): bool
    {
        return str_starts_with($segmentPath, 'home/')
            || str_starts_with($segmentPath, 'Users/');
    }

    private static function assignmentToValaIr(array $stmt, Php2ValaFuncResolver $funcResolver, array &$declaredVariables, array $userFunctions = [], array $knownUserFunctions = []): object
    {
        $var = isset($stmt['var']) && is_array($stmt['var']) ? $stmt['var'] : [];
        if (($var['kind'] ?? null) !== 'Variable' || !isset($var['name']) || !is_string($var['name'])) {
            throw new RuntimeException('Only simple variable assignments are supported for vala-ir-v0');
        }
        $expr = isset($stmt['expr']) && is_array($stmt['expr']) ? $stmt['expr'] : [];
        $name = $var['name'];
        $value = self::exprArrayToValaIr($expr, $funcResolver, $userFunctions, $knownUserFunctions);
        if (isset($declaredVariables[$name])) {
            return new ValaIrAssignmentStatement(
                self::line($stmt, 'startLine'),
                self::line($stmt, 'endLine'),
                self::comments($stmt),
                $name,
                $value,
            );
        }

        $declaredVariables[$name] = true;
        return new ValaIrVariableDeclaration(
            self::line($stmt, 'startLine'),
            self::line($stmt, 'endLine'),
            self::comments($stmt),
            'Zval',
            $name,
            $value,
        );
    }

    private static function unsetToValaIr(array $stmt, Php2ValaFuncResolver $funcResolver, array &$declaredVariables, array $userFunctions = [], array $knownUserFunctions = []): array
    {
        $result = [];
        foreach (self::arrayField($stmt, 'targets') as $index => $target) {
            if (!is_array($target)) {
                throw new RuntimeException('Unsupported unset target for vala-ir-v0 at line ' . self::line($stmt, 'startLine'));
            }
            $comments = $index === 0 ? self::comments($stmt) : [];
            $line = self::line($target, 'startLine');
            $endLine = self::line($target, 'endLine');
            if (($target['kind'] ?? null) === 'Variable' && isset($target['name']) && is_string($target['name'])) {
                $name = $target['name'];
                $undefined = new ValaIrStaticCallExpression($line, $endLine, [], 'Zval.undefined', []);
                if (isset($declaredVariables[$name])) {
                    $result[] = new ValaIrAssignmentStatement($line, $endLine, $comments, $name, $undefined);
                } else {
                    $declaredVariables[$name] = true;
                    $result[] = new ValaIrVariableDeclaration($line, $endLine, $comments, 'Zval', $name, $undefined);
                }
                continue;
            }

            if (($target['kind'] ?? null) === 'ArrayDimFetch') {
                $var = isset($target['var']) && is_array($target['var']) ? $target['var'] : [];
                if (($var['kind'] ?? null) !== 'Variable' || !isset($var['name']) || !is_string($var['name'])) {
                    throw new RuntimeException('Only simple variable array unset targets are supported for vala-ir-v0');
                }
                $name = $var['name'];
                if (!isset($declaredVariables[$name])) {
                    throw new RuntimeException('Array unset on undeclared variable is not supported for vala-ir-v0: $' . $name);
                }
                $dim = isset($target['dim']) && is_array($target['dim']) ? $target['dim'] : [];
                $result[] = new ValaIrCallStatement(
                    $line,
                    $endLine,
                    $comments,
                    $name . '.array_unset_key',
                    [self::exprArrayToValaIr($dim, $funcResolver, $userFunctions, $knownUserFunctions)],
                );
                continue;
            }

            throw new RuntimeException('Unsupported unset target for vala-ir-v0: ' . (string) ($target['kind'] ?? 'unknown'));
        }
        return $result;
    }

    private static function arrayAppendAssignmentToValaIr(array $stmt, Php2ValaFuncResolver $funcResolver, array &$declaredVariables, array $userFunctions = [], array $knownUserFunctions = []): ValaIrCallStatement
    {
        $var = isset($stmt['var']) && is_array($stmt['var']) ? $stmt['var'] : [];
        if (($var['kind'] ?? null) !== 'Variable' || !isset($var['name']) || !is_string($var['name'])) {
            throw new RuntimeException('Only simple variable array appends are supported for vala-ir-v0');
        }
        $name = $var['name'];
        if (!isset($declaredVariables[$name])) {
            throw new RuntimeException('Array append to undeclared variable is not supported for vala-ir-v0: $' . $name);
        }
        $expr = isset($stmt['expr']) && is_array($stmt['expr']) ? $stmt['expr'] : [];
        return new ValaIrCallStatement(
            self::line($stmt, 'startLine'),
            self::line($stmt, 'endLine'),
            self::comments($stmt),
            $name . '.array_append',
            [self::exprArrayToValaIr($expr, $funcResolver, $userFunctions, $knownUserFunctions)],
        );
    }

    private static function exprArrayToValaIr(array $expr, Php2ValaFuncResolver $funcResolver, array $userFunctions = [], array $knownUserFunctions = []): object
    {
        return match ($expr['kind'] ?? null) {
            'StringLiteral' => new ValaIrStaticCallExpression(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                self::comments($expr),
                'Zval.from_string',
                [new ValaIrStringLiteral(
                    self::line($expr, 'startLine'),
                    self::line($expr, 'endLine'),
                    self::comments($expr),
                    (string) ($expr['value'] ?? ''),
                )],
            ),
            'IntLiteral' => new ValaIrStaticCallExpression(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                self::comments($expr),
                'Zval.from_int',
                [new ValaIrIntLiteral(
                    self::line($expr, 'startLine'),
                    self::line($expr, 'endLine'),
                    self::comments($expr),
                    (int) ($expr['value'] ?? 0),
                )],
            ),
            'FloatLiteral' => new ValaIrStaticCallExpression(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                self::comments($expr),
                'Zval.from_float',
                [new ValaIrFloatLiteral(
                    self::line($expr, 'startLine'),
                    self::line($expr, 'endLine'),
                    self::comments($expr),
                    (float) ($expr['value'] ?? 0.0),
                )],
            ),
            'BoolLiteral' => new ValaIrStaticCallExpression(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                self::comments($expr),
                'Zval.from_bool',
                [new ValaIrBoolLiteral(
                    self::line($expr, 'startLine'),
                    self::line($expr, 'endLine'),
                    self::comments($expr),
                    (bool) ($expr['value'] ?? false),
                )],
            ),
            'NullLiteral' => new ValaIrStaticCallExpression(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                self::comments($expr),
                'Zval.from_null',
                [],
            ),
            'ConstantFetch' => new ValaIrStaticCallExpression(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                self::comments($expr),
                'PhpefRuntimeEmbed.phpfunc_constant',
                [new ValaIrStaticCallExpression(
                    self::line($expr, 'startLine'),
                    self::line($expr, 'endLine'),
                    self::comments($expr),
                    'Zval.from_string',
                    [new ValaIrStringLiteral(
                        self::line($expr, 'startLine'),
                        self::line($expr, 'endLine'),
                        self::comments($expr),
                        (string) ($expr['name'] ?? ''),
                    )],
                )],
            ),
            'Variable' => new ValaIrVariableReference(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                self::comments($expr),
                (string) ($expr['name'] ?? '') === 'this' ? '__phpefc_this' : (string) ($expr['name'] ?? ''),
            ),
            'ArrayLiteral' => self::arrayLiteralArrayToValaIr($expr, $funcResolver, $userFunctions, $knownUserFunctions),
            'ArrayDimFetch' => new ValaIrMethodCallExpression(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                self::comments($expr),
                self::exprArrayToValaIr(is_array($expr['var'] ?? null) ? $expr['var'] : [], $funcResolver, $userFunctions, $knownUserFunctions),
                'array_get_key',
                [self::exprArrayToValaIr(is_array($expr['dim'] ?? null) ? $expr['dim'] : [], $funcResolver, $userFunctions, $knownUserFunctions)],
            ),
            'Concat' => new ValaIrMethodCallExpression(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                self::comments($expr),
                self::exprArrayToValaIr(is_array($expr['left'] ?? null) ? $expr['left'] : [], $funcResolver, $userFunctions, $knownUserFunctions),
                'concat',
                [self::exprArrayToValaIr(is_array($expr['right'] ?? null) ? $expr['right'] : [], $funcResolver, $userFunctions, $knownUserFunctions)],
            ),
            'Plus' => new ValaIrMethodCallExpression(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                self::comments($expr),
                self::exprArrayToValaIr(is_array($expr['left'] ?? null) ? $expr['left'] : [], $funcResolver, $userFunctions, $knownUserFunctions),
                'add',
                [self::exprArrayToValaIr(is_array($expr['right'] ?? null) ? $expr['right'] : [], $funcResolver, $userFunctions, $knownUserFunctions)],
            ),
            'Minus' => new ValaIrMethodCallExpression(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                self::comments($expr),
                self::exprArrayToValaIr(is_array($expr['left'] ?? null) ? $expr['left'] : [], $funcResolver, $userFunctions, $knownUserFunctions),
                'subtract',
                [self::exprArrayToValaIr(is_array($expr['right'] ?? null) ? $expr['right'] : [], $funcResolver, $userFunctions, $knownUserFunctions)],
            ),
            'Multiply' => new ValaIrMethodCallExpression(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                self::comments($expr),
                self::exprArrayToValaIr(is_array($expr['left'] ?? null) ? $expr['left'] : [], $funcResolver, $userFunctions, $knownUserFunctions),
                'multiply',
                [self::exprArrayToValaIr(is_array($expr['right'] ?? null) ? $expr['right'] : [], $funcResolver, $userFunctions, $knownUserFunctions)],
            ),
            'Divide' => new ValaIrMethodCallExpression(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                self::comments($expr),
                self::exprArrayToValaIr(is_array($expr['left'] ?? null) ? $expr['left'] : [], $funcResolver, $userFunctions, $knownUserFunctions),
                'divide',
                [self::exprArrayToValaIr(is_array($expr['right'] ?? null) ? $expr['right'] : [], $funcResolver, $userFunctions, $knownUserFunctions)],
            ),
            'Greater' => new ValaIrMethodCallExpression(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                self::comments($expr),
                self::exprArrayToValaIr(is_array($expr['left'] ?? null) ? $expr['left'] : [], $funcResolver, $userFunctions, $knownUserFunctions),
                'greater',
                [self::exprArrayToValaIr(is_array($expr['right'] ?? null) ? $expr['right'] : [], $funcResolver, $userFunctions, $knownUserFunctions)],
            ),
            'Equal' => new ValaIrMethodCallExpression(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                self::comments($expr),
                self::exprArrayToValaIr(is_array($expr['left'] ?? null) ? $expr['left'] : [], $funcResolver, $userFunctions, $knownUserFunctions),
                'equal',
                [self::exprArrayToValaIr(is_array($expr['right'] ?? null) ? $expr['right'] : [], $funcResolver, $userFunctions, $knownUserFunctions)],
            ),
            'Identical' => new ValaIrMethodCallExpression(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                self::comments($expr),
                self::exprArrayToValaIr(is_array($expr['left'] ?? null) ? $expr['left'] : [], $funcResolver, $userFunctions, $knownUserFunctions),
                'identical',
                [self::exprArrayToValaIr(is_array($expr['right'] ?? null) ? $expr['right'] : [], $funcResolver, $userFunctions, $knownUserFunctions)],
            ),
            'NotEqual' => new ValaIrMethodCallExpression(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                self::comments($expr),
                self::exprArrayToValaIr(is_array($expr['left'] ?? null) ? $expr['left'] : [], $funcResolver, $userFunctions, $knownUserFunctions),
                'not_equal',
                [self::exprArrayToValaIr(is_array($expr['right'] ?? null) ? $expr['right'] : [], $funcResolver, $userFunctions, $knownUserFunctions)],
            ),
            'NotIdentical' => new ValaIrMethodCallExpression(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                self::comments($expr),
                self::exprArrayToValaIr(is_array($expr['left'] ?? null) ? $expr['left'] : [], $funcResolver, $userFunctions, $knownUserFunctions),
                'not_identical',
                [self::exprArrayToValaIr(is_array($expr['right'] ?? null) ? $expr['right'] : [], $funcResolver, $userFunctions, $knownUserFunctions)],
            ),
            'Smaller' => new ValaIrMethodCallExpression(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                self::comments($expr),
                self::exprArrayToValaIr(is_array($expr['left'] ?? null) ? $expr['left'] : [], $funcResolver, $userFunctions, $knownUserFunctions),
                'smaller',
                [self::exprArrayToValaIr(is_array($expr['right'] ?? null) ? $expr['right'] : [], $funcResolver, $userFunctions, $knownUserFunctions)],
            ),
            'SmallerOrEqual' => new ValaIrMethodCallExpression(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                self::comments($expr),
                self::exprArrayToValaIr(is_array($expr['left'] ?? null) ? $expr['left'] : [], $funcResolver, $userFunctions, $knownUserFunctions),
                'smaller_or_equal',
                [self::exprArrayToValaIr(is_array($expr['right'] ?? null) ? $expr['right'] : [], $funcResolver, $userFunctions, $knownUserFunctions)],
            ),
            'FuncCall' => self::funcCallExpressionToValaIr($expr, $funcResolver, $userFunctions, $knownUserFunctions),
            'StaticCall' => self::staticCallExpressionToValaIr($expr, $funcResolver, $userFunctions, $knownUserFunctions),
            'New' => self::newExpressionToValaIr($expr, $funcResolver, $userFunctions, $knownUserFunctions),
            'MethodCall' => self::methodCallExpressionToValaIr($expr, $funcResolver, $userFunctions, $knownUserFunctions),
            'Ternary' => new ValaIrTernaryExpression(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                self::comments($expr),
                self::exprArrayToValaIr(is_array($expr['condition'] ?? null) ? $expr['condition'] : [], $funcResolver, $userFunctions, $knownUserFunctions),
                self::exprArrayToValaIr(is_array($expr['ifExpr'] ?? null) ? $expr['ifExpr'] : [], $funcResolver, $userFunctions, $knownUserFunctions),
                self::exprArrayToValaIr(is_array($expr['elseExpr'] ?? null) ? $expr['elseExpr'] : [], $funcResolver, $userFunctions, $knownUserFunctions),
            ),
            default => throw new RuntimeException('Unsupported PHP-IR expression for vala-ir-v0: ' . (string) ($expr['kind'] ?? 'unknown')),
        };
    }

    private static function arrayLiteralArrayToValaIr(array $expr, Php2ValaFuncResolver $funcResolver, array $userFunctions = [], array $knownUserFunctions = []): ValaIrStaticCallExpression
    {
        $items = [];
        foreach (self::arrayField($expr, 'items') as $item) {
            $key = array_key_exists('key', $item) && is_array($item['key'])
                ? self::exprArrayToValaIr($item['key'], $funcResolver, $userFunctions, $knownUserFunctions)
                : new ValaIrNullLiteral(self::line($item, 'startLine'), self::line($item, 'endLine'), []);
            $items[] = new ValaIrConstructExpression(
                self::line($item, 'startLine'),
                self::line($item, 'endLine'),
                self::comments($item),
                'ZvalArrayEntry',
                [
                    $key,
                    self::exprArrayToValaIr(self::arrayFieldOne($item, 'value'), $funcResolver, $userFunctions, $knownUserFunctions),
                ],
            );
        }

        return new ValaIrStaticCallExpression(
            self::line($expr, 'startLine'),
            self::line($expr, 'endLine'),
            self::comments($expr),
            'Zval.from_array',
            [new ValaIrArrayCreationExpression(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                [],
                'ZvalArrayEntry',
                $items,
            )],
        );
    }

    private static function classToValaIr(array $stmt, Php2ValaFuncResolver $funcResolver, array $userFunctions, array $knownUserFunctions, array $includeActivationFunctions, array &$deferredHelpers, int &$deferredHelperCounter): ValaIrClassDeclaration
    {
        $className = (string) ($stmt['name'] ?? '');
        $containerName = self::mangledClassContainerName($className, $userFunctions['__phpefc_source_prefix'] ?? 'unknown_php');
        $methods = [];
        $handlerMethods = [];
        foreach (self::arrayField($stmt, 'methods') as $method) {
            $valaMethod = self::functionToValaIr($method, $funcResolver, $userFunctions, $knownUserFunctions, $includeActivationFunctions, $deferredHelpers, $deferredHelperCounter, $className, $containerName);
            $methods[] = $valaMethod;
            $handlerMethod = self::zeroArgStaticMethodHandler($valaMethod);
            if ($handlerMethod !== null) {
                $handlerMethods[] = $handlerMethod;
            }
        }
        $methods = array_merge($methods, $handlerMethods);
        $methods[] = self::classRegisterFunction($containerName, $className, $methods);
        return new ValaIrClassDeclaration(
            self::line($stmt, 'startLine'),
            self::line($stmt, 'endLine'),
            self::comments($stmt),
            $containerName,
            $className,
            $methods,
        );
    }

    private static function classRegisterFunction(string $containerName, string $className, array $methods): ValaIrFunctionDeclaration
    {
        $stmts = [new ValaIrCallStatement(0, 0, [], 'PhpefRuntimeCore.register_class', [
            new ValaIrStringLiteral(0, 0, [], $className),
        ])];

        foreach ($methods as $method) {
            if (!$method instanceof ValaIrFunctionDeclaration) {
                continue;
            }
            $methodArray = $method->toArray();
            $role = (string) ($methodArray['role'] ?? '');
            if (!in_array($role, ['php_static_method', 'php_instance_method'], true)) {
                continue;
            }
            $phpName = (string) ($methodArray['phpName'] ?? '');
            [$registeredClassName, $methodName] = explode('::', $phpName !== '' ? $phpName : $className . '::' . (string) ($methodArray['name'] ?? ''), 2);
            $params = self::arrayField($methodArray, 'params');
            $visibility = self::runtimeVisibility((string) ($methodArray['visibility'] ?? 'public'));
            $handlerArray = $role === 'php_static_method' ? self::staticMethodHandlerArray($methodArray, $methods) : $methodArray;
            $handlerName = (string) ($handlerArray['name'] ?? $methodArray['name'] ?? '');
            $qualifiedFunctionName = (string) ($handlerArray['qualifiedName'] ?? $methodArray['qualifiedName'] ?? $containerName . '.' . (string) ($methodArray['name'] ?? ''));
            $stmts[] = new ValaIrCallStatement(0, 0, [], $role === 'php_static_method' ? 'PhpefRuntimeCore.register_static_method' : 'PhpefRuntimeCore.register_instance_method', [
                new ValaIrStringLiteral(0, 0, [], $registeredClassName),
                new ValaIrStringLiteral(0, 0, [], $methodName),
                new ValaIrVariableReference(0, 0, [], $handlerName),
                new ValaIrStringLiteral(0, 0, [], $qualifiedFunctionName),
                new ValaIrIntLiteral(0, 0, [], count($params)),
                new ValaIrIntLiteral(0, 0, [], count($params)),
                $visibility,
            ]);
        }

        return new ValaIrFunctionDeclaration(0, 0, [], 'register_class_symbols', [], $stmts, '', 'void', [], null, 'class_register', null, true, $containerName . '.register_class_symbols', 'public');
    }

    private static function collectFunctionLocalVariables(array $stmts, array $skipNames = []): array
    {
        $result = [];
        foreach ($stmts as $stmt) {
            foreach (self::collectFunctionLocalVariablesFromNode($stmt, $skipNames) as $name => $_) {
                $result[$name] = true;
            }
        }
        return $result;
    }

    private static function collectFunctionLocalVariablesFromNode(mixed $node, array $skipNames = []): array
    {
        $result = [];
        if (!is_array($node)) {
            return $result;
        }
        if (($node['kind'] ?? null) === 'ForeachValue') {
            foreach (self::collectFunctionLocalVariablesFromNode($node['expr'] ?? null, $skipNames) as $name => $_) {
                $result[$name] = true;
            }
            $innerSkipNames = $skipNames;
            $valueName = (string) ($node['valueName'] ?? '');
            if ($valueName !== '') {
                $innerSkipNames[$valueName] = true;
            }
            foreach (self::arrayField($node, 'stmts') as $innerStmt) {
                foreach (self::collectFunctionLocalVariablesFromNode($innerStmt, $innerSkipNames) as $name => $_) {
                    $result[$name] = true;
                }
            }
            return $result;
        }
        if (($node['kind'] ?? null) === 'Variable' && isset($node['name']) && is_string($node['name'])) {
            $name = $node['name'];
            if ($name !== 'this' && !isset($skipNames[$name])) {
                $result[$name] = true;
            }
        }
        foreach ($node as $value) {
            if (is_array($value)) {
                foreach (self::collectFunctionLocalVariablesFromNode($value, $skipNames) as $name => $_) {
                    $result[$name] = true;
                }
            }
        }
        return $result;
    }

    private static function functionToValaIr(array $stmt, Php2ValaFuncResolver $funcResolver, array $userFunctions, array $knownUserFunctions, array $includeActivationFunctions, array &$deferredHelpers, int &$deferredHelperCounter, string $className = '', string $containerName = ''): ValaIrFunctionDeclaration
    {
        $phpName = $className !== '' ? $className . '::' . (string) ($stmt['name'] ?? '') : (string) ($stmt['name'] ?? '');
        $isClassMethod = $className !== '';
        $returnTypeHint = (string) ($stmt['returnType'] ?? '');
        $isStaticClassMethod = $isClassMethod && (bool) ($stmt['static'] ?? false);
        $role = $isClassMethod ? ($isStaticClassMethod ? 'php_static_method' : 'php_instance_method') : 'php_function';
        $params = self::arrayField($stmt, 'params');
        $declaredVariables = [];
        $body = [];
        foreach ($params as $index => $paramName) {
            if (!is_string($paramName) || $paramName === '') {
                throw new RuntimeException('Unsupported PHP parameter metadata for vala-ir-v0 in ' . ($phpName !== '' ? $phpName : 'function'));
            }
            if ($paramName === 'this') {
                throw new RuntimeException('Unsupported PHP parameter name for vala-ir-v0 in ' . ($phpName !== '' ? $phpName : 'function') . ': $this');
            }
            $declaredVariables[$paramName] = true;
            $body[] = new ValaIrVariableDeclaration(
                self::line($stmt, 'startLine'),
                self::line($stmt, 'startLine'),
                [],
                'Zval',
                $paramName,
                new ValaIrStaticCallExpression(
                    self::line($stmt, 'startLine'),
                    self::line($stmt, 'startLine'),
                    [],
                    'PhpefRuntimeCore.argument',
                    [new ValaIrVariableReference(0, 0, [], 'args'), new ValaIrIntLiteral(0, 0, [], $index)],
                ),
            );
        }
        foreach (self::collectFunctionLocalVariables(self::arrayField($stmt, 'stmts'), $declaredVariables) as $name => $_) {
            if (isset($declaredVariables[$name])) {
                continue;
            }
            $declaredVariables[$name] = true;
            $body[] = new ValaIrVariableDeclaration(
                self::line($stmt, 'startLine'),
                self::line($stmt, 'startLine'),
                [],
                'Zval',
                $name,
                new ValaIrStaticCallExpression(self::line($stmt, 'startLine'), self::line($stmt, 'startLine'), [], 'Zval.undefined', []),
            );
        }
        foreach (self::arrayField($stmt, 'stmts') as $innerStmt) {
            foreach (self::stmtsToArray(self::stmtArrayToValaIr($innerStmt, $funcResolver, $declaredVariables, $userFunctions, $knownUserFunctions, $includeActivationFunctions, $deferredHelpers, $deferredHelperCounter, true, $returnTypeHint)) as $valaStmt) {
                $body[] = $valaStmt;
            }
        }

        $valaParams = null;
        if ($isStaticClassMethod && $params === []) {
            $valaParams = [];
        } elseif ($isClassMethod && !$isStaticClassMethod) {
            $valaParams = [
                ['typeName' => 'Zval', 'name' => '__phpefc_this'],
                ['typeName' => 'Zval[]', 'name' => 'args'],
            ];
        }
        return new ValaIrFunctionDeclaration(
            self::line($stmt, 'startLine'),
            self::line($stmt, 'endLine'),
            self::comments($stmt),
            $className !== '' ? self::identifierPart((string) ($stmt['name'] ?? 'method')) : self::resolveUserFunctionName($phpName, $userFunctions, 'functionName'),
            self::arrayField($stmt, 'params'),
            $body,
            $phpName,
            'Zval',
            $valaParams,
            null,
            $role,
            null,
            true,
            $containerName !== '' ? $containerName . '.' . self::identifierPart((string) ($stmt['name'] ?? 'method')) : '',
            $className !== '' ? self::visibility((string) ($stmt['visibility'] ?? 'public')) : 'public',
            self::arrayField($stmt, 'paramTypes'),
        );
    }


    private static function zeroArgStaticMethodHandler(ValaIrFunctionDeclaration $method): ?ValaIrFunctionDeclaration
    {
        $methodArray = $method->toArray();
        if ((string) ($methodArray['role'] ?? '') !== 'php_static_method' || self::arrayField($methodArray, 'params') !== []) {
            return null;
        }

        $methodName = (string) ($methodArray['name'] ?? '');
        if ($methodName === '') {
            return null;
        }

        $qualifiedName = (string) ($methodArray['qualifiedName'] ?? $methodName);
        $handlerName = $methodName . '__handler';
        $handlerQualifiedName = str_contains($qualifiedName, '.') ? preg_replace('/[^.]+$/', $handlerName, $qualifiedName) : $handlerName;
        if (!is_string($handlerQualifiedName) || $handlerQualifiedName === '') {
            $handlerQualifiedName = $handlerName;
        }

        return new ValaIrFunctionDeclaration(
            self::line($methodArray, 'startLine'),
            self::line($methodArray, 'endLine'),
            [],
            $handlerName,
            [],
            [],
            (string) ($methodArray['phpName'] ?? ''),
            'Zval',
            [['typeName' => 'Zval[]', 'name' => 'args']],
            new ValaIrStaticCallExpression(0, 0, [], $methodName, []),
            'php_static_method_handler',
            null,
            true,
            $handlerQualifiedName,
            'private',
        );
    }

    private static function staticMethodHandlerArray(array $methodArray, array $methods): array
    {
        $phpName = (string) ($methodArray['phpName'] ?? '');
        foreach ($methods as $candidate) {
            if (!$candidate instanceof ValaIrFunctionDeclaration) {
                continue;
            }
            $candidateArray = $candidate->toArray();
            if ((string) ($candidateArray['role'] ?? '') !== 'php_static_method_handler') {
                continue;
            }
            if ((string) ($candidateArray['phpName'] ?? '') === $phpName) {
                return $candidateArray;
            }
        }
        return $methodArray;
    }

    /**
     * Return the PHP user-function to generated Vala callee map for a PHP-IR program.
     *
     * Multi-source builds pass this map as external function metadata while
     * lowering each PHP-IR file independently to Vala-IR.
     */
    public static function userFunctionMapFromPhpIr(object $phpIr, ?string $functionPrefix = null, string $userFunctionAbiPrefix = 'phpef'): array
    {
        if (!method_exists($phpIr, 'toArray')) {
            throw new RuntimeException('Expected a PHP-IR object with toArray(), got ' . get_debug_type($phpIr));
        }
        $array = $phpIr->toArray();
        if (($array['format'] ?? null) !== 'phpast-ir-v0' || !isset($array['root']['stmts']) || !is_array($array['root']['stmts'])) {
            throw new RuntimeException('Expected phpast-ir-v0 program');
        }
        return self::userFunctionMap($array['root']['stmts'], (string) ($array['root']['phpSourceFile'] ?? ''), $functionPrefix, $userFunctionAbiPrefix);
    }

    /**
     * Return known PHP function names that may be runtime-guarded but not eagerly registered.
     */
    private static function knownUserFunctionNameMap(array $knownUserFunctionNames): array
    {
        $map = [];
        foreach ($knownUserFunctionNames as $normalizedName => $phpName) {
            $name = is_string($phpName) && $phpName !== '' ? $phpName : (string) $normalizedName;
            $map[strtolower($name)] = ['phpName' => $name];
        }
        return $map;
    }

    /**
     * Return a PHP user-function to generated Vala callee map for PHP-IR statements.
     *
     * Function names are normalized to lower case. The source file is accepted for
     * future source-aware mangling, but direct user functions currently keep simple
     * generated names such as phpef_userfunc_abc().
     */
    public static function userFunctionMap(array $stmts, string $sourceFile = '', ?string $functionPrefix = null, string $userFunctionAbiPrefix = 'phpef'): array
    {
        $map = [];
        foreach ($stmts as $stmt) {
            if (is_array($stmt) && ($stmt['kind'] ?? null) === 'Function') {
                $name = (string) ($stmt['name'] ?? '');
                if ($name !== '') {
                    $functionName = self::mangledUserFunctionName($name, $functionPrefix ?? self::sourceFunctionPrefix($sourceFile), $userFunctionAbiPrefix);
                    $map[strtolower($name)] = [
                        'phpName' => $name,
                        'functionName' => $functionName,
                        'callName' => $functionName . '__call',
                    ];
                }
                continue;
            }
            if (is_array($stmt) && ($stmt['kind'] ?? null) === 'Class') {
                $className = (string) ($stmt['name'] ?? '');
                foreach (self::arrayField($stmt, 'methods') as $method) {
                    $methodName = (string) ($method['name'] ?? '');
                    if ($className === '' || $methodName === '') {
                        continue;
                    }
                    $phpName = $className . '::' . $methodName;
                    $functionName = self::mangledStaticMethodName($className, $methodName, $functionPrefix ?? self::sourceFunctionPrefix($sourceFile), $userFunctionAbiPrefix);
                    $map[strtolower($phpName)] = [
                        'phpName' => $phpName,
                        'functionName' => $functionName,
                        'callName' => $functionName . '__call',
                    ];
                }
            }
        }
        return $map;
    }
    private static function resolveIncludeActivationFunction(string $path, array $includeActivationFunctions): string
    {
        foreach (self::includeLookupKeys($path) as $key) {
            if (isset($includeActivationFunctions[$key])) {
                return $includeActivationFunctions[$key];
            }
        }
        throw new RuntimeException('Unsupported PHP include for vala-ir-v0: ' . $path . ' is not part of this PHPEF source set');
    }

    private static function includePathFromFuncCall(array $stmt): ?string
    {
        $args = self::arrayField($stmt, 'args');
        $pathArg = isset($args[0]) && is_array($args[0]) ? $args[0] : [];
        if (($pathArg['kind'] ?? null) !== 'StringLiteral' || !isset($pathArg['value'])) {
            return null;
        }
        return (string) $pathArg['value'];
    }

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

    private static function mergeIncludedUserFunctions(array $existing, array $included): array
    {
        foreach ($included as $phpFunctionName => $valaFunctionName) {
            if (isset($existing[$phpFunctionName]) && $existing[$phpFunctionName] !== $valaFunctionName) {
                throw new RuntimeException('Duplicate PHP function declaration reachable through includes: ' . $phpFunctionName);
            }
            $existing[$phpFunctionName] = $valaFunctionName;
        }
        return $existing;
    }


    private static function allExternalUserFunctions(array $externalUserFunctionsByInclude): array
    {
        $functions = [];
        foreach ($externalUserFunctionsByInclude as $userFunctions) {
            if (is_array($userFunctions)) {
                $functions = self::mergeKnownUserFunctions($functions, $userFunctions);
            }
        }
        return $functions;
    }

    private static function funcCallExpressionToValaIr(array $expr, Php2ValaFuncResolver $funcResolver, array $userFunctions = [], array $knownUserFunctions = []): ValaIrStaticCallExpression
    {
        $phpCallName = (string) ($expr['functionName'] ?? '');
        if ($phpCallName === '__phpefc_argv') {
            return new ValaIrStaticCallExpression(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                self::comments($expr),
                'PhpefRuntimeCore.argv',
                array_map(static fn (array $arg): object => self::exprArrayToValaIr($arg, $funcResolver, $userFunctions, $knownUserFunctions), self::arrayField($expr, 'args')),
            );
        }
        $userFunction = self::userFunctionCallEntry($phpCallName, $userFunctions, $knownUserFunctions);
        if ($userFunction !== null) {
            return new ValaIrStaticCallExpression(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                self::comments($expr),
                'PhpefRuntimeCore.call_function',
                [
                    new ValaIrStringLiteral(self::line($expr, 'startLine'), self::line($expr, 'endLine'), [], self::userFunctionEntryPhpName($userFunction, $phpCallName)),
                    self::zvalArrayExpression($expr, $funcResolver, $userFunctions, $knownUserFunctions),
                ],
            );
        }

        return new ValaIrStaticCallExpression(
            self::line($expr, 'startLine'),
            self::line($expr, 'endLine'),
            self::comments($expr),
            self::resolveRuntimeCallName($phpCallName, count(self::arrayField($expr, 'args')), $funcResolver),
            array_map(static fn (array $arg): object => self::exprArrayToValaIr($arg, $funcResolver, $userFunctions, $knownUserFunctions), self::arrayField($expr, 'args')),
        );
    }

    private static function staticCallExpressionToValaIr(array $expr, Php2ValaFuncResolver $funcResolver, array $userFunctions = [], array $knownUserFunctions = []): ValaIrStaticCallExpression
    {
        return new ValaIrStaticCallExpression(
            self::line($expr, 'startLine'),
            self::line($expr, 'endLine'),
            self::comments($expr),
            'PhpefRuntimeCore.call_static_method',
            [
                new ValaIrStringLiteral(self::line($expr, 'startLine'), self::line($expr, 'endLine'), [], (string) ($expr['className'] ?? '')),
                self::methodNameToValaIr($expr, $funcResolver, $userFunctions, $knownUserFunctions),
                self::zvalArrayExpression($expr, $funcResolver, $userFunctions, $knownUserFunctions),
                new ValaIrStringLiteral(self::line($expr, 'startLine'), self::line($expr, 'endLine'), [], (string) ($expr['currentClassScope'] ?? '')),
            ],
        );
    }

    private static function newExpressionToValaIr(array $expr, Php2ValaFuncResolver $funcResolver, array $userFunctions = [], array $knownUserFunctions = []): ValaIrStaticCallExpression
    {
        return new ValaIrStaticCallExpression(
            self::line($expr, 'startLine'),
            self::line($expr, 'endLine'),
            self::comments($expr),
            'PhpefRuntimeCore.new_object',
            [
                new ValaIrStringLiteral(self::line($expr, 'startLine'), self::line($expr, 'endLine'), [], (string) ($expr['className'] ?? '')),
                self::zvalArrayExpression($expr, $funcResolver, $userFunctions, $knownUserFunctions),
            ],
        );
    }

    private static function methodCallExpressionToValaIr(array $expr, Php2ValaFuncResolver $funcResolver, array $userFunctions = [], array $knownUserFunctions = []): ValaIrStaticCallExpression
    {
        $methodName = (string) ($expr['methodName'] ?? '');
        if ($methodName === 'getMessage' || $methodName === 'getCode' || $methodName === 'getPrevious') {
            $callee = match ($methodName) {
                'getMessage' => 'PhpefRuntimeCore.throwable_get_message',
                'getCode' => 'PhpefRuntimeCore.throwable_get_code',
                'getPrevious' => 'PhpefRuntimeCore.throwable_get_previous',
                default => 'PhpefRuntimeCore.throwable_get_message',
            };
            return new ValaIrStaticCallExpression(
                self::line($expr, 'startLine'),
                self::line($expr, 'endLine'),
                self::comments($expr),
                $callee,
                [self::exprArrayToValaIr(self::arrayFieldOne($expr, 'receiver'), $funcResolver, $userFunctions, $knownUserFunctions)],
            );
        }

        return new ValaIrStaticCallExpression(
            self::line($expr, 'startLine'),
            self::line($expr, 'endLine'),
            self::comments($expr),
            'PhpefRuntimeCore.call_instance_method',
            [
                self::exprArrayToValaIr(self::arrayFieldOne($expr, 'receiver'), $funcResolver, $userFunctions, $knownUserFunctions),
                self::methodNameToValaIr($expr, $funcResolver, $userFunctions, $knownUserFunctions),
                self::zvalArrayExpression($expr, $funcResolver, $userFunctions, $knownUserFunctions),
                new ValaIrStringLiteral(self::line($expr, 'startLine'), self::line($expr, 'endLine'), [], (string) ($expr['currentClassScope'] ?? '')),
            ],
        );
    }

    private static function methodNameToValaIr(array $call, Php2ValaFuncResolver $funcResolver, array $userFunctions, array $knownUserFunctions): object
    {
        $methodNameExpression = $call['methodNameExpression'] ?? null;
        if (is_array($methodNameExpression)) {
            return new ValaIrMethodCallExpression(
                self::line($call, 'startLine'),
                self::line($call, 'endLine'),
                [],
                self::exprArrayToValaIr($methodNameExpression, $funcResolver, $userFunctions, $knownUserFunctions),
                'to_php_string',
                [],
            );
        }

        return new ValaIrStringLiteral(
            self::line($call, 'startLine'),
            self::line($call, 'endLine'),
            [],
            (string) ($call['methodName'] ?? ''),
        );
    }

    private static function zvalArrayExpression(array $call, Php2ValaFuncResolver $funcResolver, array $userFunctions, array $knownUserFunctions): ValaIrArrayCreationExpression
    {
        return new ValaIrArrayCreationExpression(
            self::line($call, 'startLine'),
            self::line($call, 'endLine'),
            [],
            'Zval',
            array_map(static fn (array $arg): object => self::exprArrayToValaIr($arg, $funcResolver, $userFunctions, $knownUserFunctions), self::arrayField($call, 'args')),
        );
    }

    private static function mergeKnownUserFunctions(array $existing, array $included): array
    {
        foreach ($included as $phpFunctionName => $metadata) {
            if (isset($existing[$phpFunctionName]) && $existing[$phpFunctionName] !== $metadata) {
                $existingPhpName = is_array($existing[$phpFunctionName]) ? (string) ($existing[$phpFunctionName]['phpName'] ?? '') : '';
                $includedPhpName = is_array($metadata) ? (string) ($metadata['phpName'] ?? '') : '';
                $existingHasGeneratedName = is_array($existing[$phpFunctionName]) && isset($existing[$phpFunctionName]['functionName']);
                $includedHasGeneratedName = is_array($metadata) && isset($metadata['functionName']);
                if ($existingPhpName !== '' && strtolower($existingPhpName) === strtolower($includedPhpName)) {
                    if (!$existingHasGeneratedName && $includedHasGeneratedName) {
                        $existing[$phpFunctionName] = $metadata;
                    }
                    continue;
                }
                unset($existing[$phpFunctionName]);
                continue;
            }
            $existing[$phpFunctionName] = $metadata;
        }
        return $existing;
    }

    private static function sourceMainFunction(string $name, array $stmts): ValaIrFunctionDeclaration
    {
        return new ValaIrFunctionDeclaration(0, 0, [], $name, [], $stmts, '', 'void', [], null, 'source_main');
    }

    private static function sourceStacktraceRegisterFunction(string $name, string $sourceMainFunctionName, array $deferredHelpers): ValaIrFunctionDeclaration
    {
        $stmts = [new ValaIrCallStatement(0, 0, [], 'PhpefRuntimeCore.register_stacktrace_frame', [
            new ValaIrStringLiteral(0, 0, [], $sourceMainFunctionName),
            new ValaIrStringLiteral(0, 0, [], '{main}'),
        ])];

        foreach ($deferredHelpers as $helper) {
            if (!$helper instanceof ValaIrDeferredHelperFunction) {
                continue;
            }
            $helperArray = $helper->toArray();
            $displayFrame = (string) ($helperArray['stacktraceDisplayFrame'] ?? '');
            if ($displayFrame === '') {
                throw new RuntimeException('Deferred helper requires explicit stacktrace display frame: ' . (string) ($helperArray['name'] ?? ''));
            }
            $stmts[] = new ValaIrCallStatement(0, 0, [], 'PhpefRuntimeCore.register_stacktrace_frame', [
                new ValaIrStringLiteral(0, 0, [], (string) ($helperArray['name'] ?? '')),
                new ValaIrStringLiteral(0, 0, [], $displayFrame),
            ]);
        }

        return new ValaIrFunctionDeclaration(0, 0, [], $name, [], $stmts, '', 'void', [], null, 'source_stacktrace_register');
    }

    private static function sourceRegisterFunction(string $name, array $phpDeclarations): ValaIrFunctionDeclaration
    {
        $stmts = [];
        $registeredClasses = [];
        foreach ($phpDeclarations as $declaration) {
            if (!$declaration instanceof ValaIrClassDeclaration) {
                continue;
            }
            $classArray = $declaration->toArray();
            $classContainerName = (string) ($classArray['name'] ?? '');
            if ($classContainerName === '') {
                continue;
            }
            $stmts[] = new ValaIrCallStatement(0, 0, [], $classContainerName . '.register_class_symbols', []);
        }

        foreach (self::registeredFunctionDeclarations($phpDeclarations) as $functionDeclaration) {
            if (!$functionDeclaration instanceof ValaIrFunctionDeclaration) {
                continue;
            }
            $functionArray = $functionDeclaration->toArray();
            $functionName = (string) ($functionArray['name'] ?? '');
            $qualifiedFunctionName = (string) ($functionArray['qualifiedName'] ?? $functionName);
            $phpName = (string) ($functionArray['phpName'] ?? '');
            $params = self::arrayField($functionArray, 'params');
            $role = (string) ($functionArray['role'] ?? 'php_function');
            if ($role === 'php_static_method') {
                [$className, $methodName] = explode('::', $phpName !== '' ? $phpName : $functionName, 2);
                if (!isset($registeredClasses[strtolower($className)])) {
                    $stmts[] = new ValaIrCallStatement(0, 0, [], 'PhpefRuntimeCore.register_class', [
                        new ValaIrStringLiteral(0, 0, [], $className),
                    ]);
                    $registeredClasses[strtolower($className)] = true;
                }
                $stmts[] = new ValaIrCallStatement(0, 0, [], 'PhpefRuntimeCore.register_static_method', [
                    new ValaIrStringLiteral(0, 0, [], $className),
                    new ValaIrStringLiteral(0, 0, [], $methodName),
                    new ValaIrVariableReference(0, 0, [], $qualifiedFunctionName),
                    new ValaIrStringLiteral(0, 0, [], $qualifiedFunctionName),
                    new ValaIrIntLiteral(0, 0, [], count($params)),
                    new ValaIrIntLiteral(0, 0, [], count($params)),
                ]);
                continue;
            }
            $stmts[] = new ValaIrCallStatement(0, 0, [], 'PhpefRuntimeCore.register_function', [
                new ValaIrStringLiteral(0, 0, [], $phpName !== '' ? $phpName : $functionName),
                new ValaIrVariableReference(0, 0, [], $functionName),
                new ValaIrStringLiteral(0, 0, [], $functionName),
                new ValaIrIntLiteral(0, 0, [], count($params)),
                new ValaIrIntLiteral(0, 0, [], count($params)),
            ]);
        }

        return new ValaIrFunctionDeclaration(0, 0, [], $name, [], $stmts, '', 'void', [], null, 'source_register');
    }

    private static function registeredFunctionDeclarations(array $phpDeclarations): array
    {
        $functions = [];
        foreach ($phpDeclarations as $declaration) {
            if ($declaration instanceof ValaIrFunctionDeclaration) {
                $functions[] = $declaration;
            }
        }
        return $functions;
    }

    private static function entrypointFunctions(string $sourceStacktraceRegisterFunctionName, string $sourceRegisterFunctionName, string $sourceMainFunctionName): array
    {
        return [
            new ValaIrFunctionDeclaration(
                0,
                0,
                [],
                'phpef_main',
                [],
                [
                    new ValaIrCallStatement(0, 0, [], $sourceStacktraceRegisterFunctionName, []),
                    new ValaIrCallStatement(0, 0, [], $sourceRegisterFunctionName, []),
                    new ValaIrCallStatement(0, 0, [], $sourceMainFunctionName, []),
                ],
                '',
                'void',
                [],
                null,
                'entrypoint',
            ),
            new ValaIrFunctionDeclaration(
                0,
                0,
                [],
                'main',
                [],
                [
                    new ValaIrCallStatement(0, 0, [], 'PhpefRuntimeCore.init', [new ValaIrVariableReference(0, 0, [], 'args')]),
                    new ValaIrTryCatchStatement(
                        0,
                        0,
                        [],
                        [
                            new ValaIrCallStatement(0, 0, [], 'phpef_main', []),
                            new ValaIrReturnStatement(0, 0, [], new ValaIrIntLiteral(0, 0, [], 0)),
                        ],
                        'Error',
                        'e',
                        [
                            new ValaIrReturnStatement(0, 0, [], new ValaIrStaticCallExpression(0, 0, [], 'PhpefRuntimeCore.handle_uncaught', [new ValaIrVariableReference(0, 0, [], 'e')])),
                        ],
                    ),
                ],
                '',
                'int',
                [['typeName' => 'string[]', 'name' => 'args']],
                null,
                'c_main',
                null,
                false,
            ),
        ];
    }

    private static function sourceMainFunctionName(string $sourceFile, string $localFunctionPrefix = ''): string
    {
        if ($localFunctionPrefix !== '') {
            return 'phpef_file_' . self::identifierPart($localFunctionPrefix);
        }
        return 'phpef_' . self::sourceFunctionPrefix($sourceFile) . '__main';
    }
    private static function sourceStacktraceRegisterFunctionNameFromMain(string $sourceMainFunction): string
    {
        if (str_ends_with($sourceMainFunction, "__main")) {
            return substr($sourceMainFunction, 0, -strlen("__main")) . "__register_stacktrace_frames";
        }
        return $sourceMainFunction . "__register_stacktrace_frames";
    }


    private static function sourceRegisterFunctionNameFromMain(string $sourceMainFunction): string
    {
        if (str_ends_with($sourceMainFunction, '__main')) {
            return substr($sourceMainFunction, 0, -strlen('__main')) . '__register_functions';
        }
        return $sourceMainFunction . '__register_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 userFunctionCallEntry(string $phpCallName, array $userFunctions, array $knownUserFunctions = []): mixed
    {
        $normalized = strtolower($phpCallName);
        return $userFunctions[$normalized] ?? $knownUserFunctions[$normalized] ?? null;
    }

    private static function resolveRuntimeCallName(string $phpCallName, int $argumentCount, Php2ValaFuncResolver $funcResolver): string
    {
        return $funcResolver->resolveRuntimeFunction($phpCallName, $argumentCount);
    }

    private static function resolveUserFunctionName(string $phpFunctionName, array $userFunctions, string $field): string
    {
        $normalized = strtolower($phpFunctionName);
        if (!isset($userFunctions[$normalized])) {
            throw new RuntimeException('Unknown PHP user function for vala-ir-v0: ' . $phpFunctionName);
        }
        return self::userFunctionEntryName($userFunctions[$normalized], $field);
    }

    private static function userFunctionEntryName(mixed $entry, string $field): string
    {
        if (is_array($entry) && isset($entry[$field]) && is_string($entry[$field])) {
            return $entry[$field];
        }
        if (is_string($entry)) {
            return $field === 'callName' ? $entry . '__call' : $entry;
        }
        throw new RuntimeException('Invalid PHP user-function metadata for vala-ir-v0');
    }

    private static function userFunctionEntryPhpName(mixed $entry, string $fallback): string
    {
        if (is_array($entry) && isset($entry['phpName']) && is_string($entry['phpName']) && $entry['phpName'] !== '') {
            return $entry['phpName'];
        }
        return $fallback;
    }

    private static function mangledUserFunctionName(string $phpFunctionName, string $sourcePrefix, string $userFunctionAbiPrefix = 'phpef'): string
    {
        return self::identifierPart($userFunctionAbiPrefix !== '' ? $userFunctionAbiPrefix : 'phpef') . '_' . self::identifierPart($sourcePrefix !== '' ? $sourcePrefix : 'unknown_php') . '__' . self::identifierPart($phpFunctionName !== '' ? $phpFunctionName : 'function');
    }

    private static function mangledStaticMethodName(string $className, string $methodName, string $sourcePrefix, string $userFunctionAbiPrefix = 'phpef'): string
    {
        return self::identifierPart($userFunctionAbiPrefix !== '' ? $userFunctionAbiPrefix : 'phpef') . '_' . self::identifierPart($sourcePrefix !== '' ? $sourcePrefix : 'unknown_php') . '__' . self::identifierPart($className !== '' ? $className : 'class') . '__' . self::identifierPart($methodName !== '' ? $methodName : 'method');
    }

    private static function mangledClassContainerName(string $className, string $sourcePrefix): string
    {
        return 'phpef_' . self::identifierPart($sourcePrefix !== '' ? $sourcePrefix : 'unknown_php') . '__' . self::identifierPart($className !== '' ? $className : 'class');
    }

    private static function sourceFunctionPrefix(string $sourceFile): string
    {
        if ($sourceFile === '') {
            return 'unknown_php';
        }
        return self::identifierPart(str_replace('\\', '/', $sourceFile));
    }

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

    private static function visibility(string $visibility): string
    {
        $normalized = strtolower($visibility);
        if (!in_array($normalized, ['public', 'protected', 'private'], true)) {
            return 'public';
        }
        return $normalized;
    }

    private static function runtimeVisibility(string $visibility): ValaIrRuntimeVisibilityLiteral
    {
        return new ValaIrRuntimeVisibilityLiteral(0, 0, [], self::visibility($visibility));
    }

    private static function comments(array $node): array
    {
        $comments = [];
        foreach (self::arrayField($node, 'comments') as $comment) {
            $comments[] = new ValaIrComment(
                (string) ($comment['kind'] ?? 'Comment'),
                (string) ($comment['text'] ?? ''),
                self::line($comment, 'startLine'),
                self::line($comment, 'endLine'),
            );
        }
        return $comments;
    }

    private static function arrayField(array $node, string $key): array
    {
        return isset($node[$key]) && is_array($node[$key]) ? $node[$key] : [];
    }

    private static function arrayFieldOne(array $node, string $key): array
    {
        return isset($node[$key]) && is_array($node[$key]) ? $node[$key] : [];
    }

    private static function line(array $node, string $key): int
    {
        return isset($node[$key]) ? (int) $node[$key] : 1;
    }
}
