using Gee;

public class PhpToValaTranspiler : Object {
    private string source_path;
    private string runtime_name;
    private string output_mode;
    private bool optimize_local_vars;
    private bool optimize_file_func_vars;
    private bool optimize_cross_file_func_vars;
    public bool transpiling_local_function = false;
    public string current_local_function_name = "";
    public HashMap<string, string> variable_types = new HashMap<string, string> ();
    private HashMap<string, string> current_parameter_php_types = new HashMap<string, string> ();
    public HashMap<string, string> variable_aliases = new HashMap<string, string> ();
    private HashMap<string, string> object_class_names = new HashMap<string, string> ();
    private HashMap<string, FunctionBinding> function_bindings = new HashMap<string, FunctionBinding> ();
    private HashMap<string, FunctionBinding> native_function_bindings = new HashMap<string, FunctionBinding> ();
    private HashMap<string, FunctionBinding> embed_function_bindings = new HashMap<string, FunctionBinding> ();
    private HashMap<string, string> defined_constants = new HashMap<string, string> ();
    private HashMap<string, bool> local_closure_variables = new HashMap<string, bool> ();
    public HashMap<string, LocalFunction> local_functions = new HashMap<string, LocalFunction> ();
    private HashMap<string, PhpClass> php_classes = new HashMap<string, PhpClass> ();
    private HashMap<string, PhpClass> external_php_classes = new HashMap<string, PhpClass> ();
    public HashMap<string, LocalFunction> external_functions = new HashMap<string, LocalFunction> ();
    private HashMap<string, bool> used_function_lookup = new HashMap<string, bool> ();
    private ArrayList<string> used_function_names = new ArrayList<string> ();
    private HashMap<string, bool> used_external_function_mapping_lookup = new HashMap<string, bool> ();
    private ArrayList<string> used_external_function_mappings = new ArrayList<string> ();
    private HashMap<string, bool> used_pwphpbytecode_mapping_lookup = new HashMap<string, bool> ();
    private ArrayList<string> used_pwphpbytecode_mappings = new ArrayList<string> ();
    private HashMap<string, bool> used_pwphpbytecode_loader_lookup = new HashMap<string, bool> ();
    private ArrayList<string> used_pwphpbytecode_loaders = new ArrayList<string> ();
    private HashMap<string, string> current_class_typed_properties = new HashMap<string, string> ();
    private HashMap<string, string> stream_select_read_resources = new HashMap<string, string> ();
    private HashMap<string, bool> warned_missing_embed_static = new HashMap<string, bool> ();
    private HashMap<string, bool> informed_missing_native_runtime = new HashMap<string, bool> ();
    private ArrayList<string> source_line_files = new ArrayList<string> ();
    private ArrayList<int> source_line_numbers = new ArrayList<int> ();
    private string last_resolved_source = "";
    private SourceParts? last_source_parts = null;
    private string last_runtime_metadata = "";
    private string last_optimizer_status = "";
    private string output_indent_unit = "    ";
    private int temp_variable_counter = 0;

    private Regex assignment_regex;
    private Regex function_call_regex;
    private Regex function_expression_regex;
    private Regex int_regex;
    private Regex double_regex;
    private PhpToValaLocalVarsOptimizer local_vars_optimizer;
    private PhpToValaFileFuncVarsOptimizer file_func_vars_optimizer;

    public PhpToValaTranspiler (string source_path, bool optimize_local_vars = false, bool optimize_file_func_vars = true, bool optimize_cross_file_func_vars = false, string runtime_name = "both-runtimes", string output_mode = "executable") throws RegexError {
        this.source_path = source_path;
        this.runtime_name = runtime_name;
        this.output_mode = output_mode;
        this.optimize_cross_file_func_vars = optimize_cross_file_func_vars;
        this.optimize_file_func_vars = optimize_file_func_vars || optimize_cross_file_func_vars;
        // file-func-vars is a stronger optimizer stage. It intentionally includes
        // the local-vars assumptions, so generated file-level native helpers and
        // plain zval-wrapper functions share the same local type inference rules.
        this.optimize_local_vars = optimize_local_vars || this.optimize_file_func_vars;
        assignment_regex = new Regex ("^\\$([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*(.*);$");
        function_call_regex = new Regex ("^([A-Za-z_][A-Za-z0-9_]*)\\s*\\((.*)\\);$");
        function_expression_regex = new Regex ("^([A-Za-z_][A-Za-z0-9_]*)\\s*\\((.*)\\)$");
        int_regex = new Regex ("^-?[0-9]+$");
        double_regex = new Regex ("^-?[0-9]+\\.[0-9]+$");
        function_bindings = PhpToValaRuntimeBindings.load (runtime_name);
        native_function_bindings = PhpToValaRuntimeBindings.load ("native-runtime");
        embed_function_bindings = PhpToValaRuntimeBindings.load ("embed-runtime");
        local_vars_optimizer = new PhpToValaLocalVarsOptimizer (this);
        file_func_vars_optimizer = new PhpToValaFileFuncVarsOptimizer (this);
    }

    public void load_php_stub_file (string stub_path) throws Error {
        string stub_source;
        FileUtils.get_contents (stub_path, out stub_source);
        ResolvedSource resolved = PhpToValaSourceParser.resolve_compile_time_includes (stub_path, stub_source);
        SourceParts stub_parts = PhpToValaSourceParser.extract_local_functions (stub_path, PhpToValaSourceParser.strip_php_tags (resolved.source));
        string stub_package_name = Path.get_basename (Path.get_dirname (stub_path));
        foreach (LocalFunction local_function in stub_parts.functions) {
            apply_php2vala_stub_metadata (local_function);
            local_function.stub_package_name = stub_package_name;
            external_functions[local_function.name] = local_function;
        }
        foreach (PhpClass php_class in stub_parts.classes) {
            apply_php2vala_class_stub_metadata (php_class);
            external_php_classes[php_class.name] = php_class;
        }
        string stub_namespace = extract_php_namespace (stub_source);
        if (stub_namespace != "") {
            foreach (PhpClass php_class in stub_parts.classes) {
                external_php_classes[stub_namespace + "\\" + php_class.name] = php_class;
            }
        }
    }

    public bool optimizes_cross_file_func_vars () {
        return optimize_cross_file_func_vars;
    }

    public bool is_library_output () {
        return output_mode == "library";
    }

    public string zval_local_function_name (LocalFunction local_function) {
        return output_mode == "library" ? local_function.name + "_zval" : local_function.name;
    }

    public void mark_external_function_used (LocalFunction external_function, string call_name) {
        string package_name = external_function.stub_package_name ?? "unknown";
        string mapping = "%s -> %s (%s)".printf (external_function.name, call_name, package_name);
        if (used_external_function_mapping_lookup.has_key (mapping)) {
            return;
        }
        used_external_function_mapping_lookup[mapping] = true;
        used_external_function_mappings.add (mapping);
    }

    private void mark_pwphpbytecode_method_used (PhpClass php_class, string method_name) {
        string loader_symbol = php_class.pwphpbytecode_loader_symbol ?? "";
        string mapping = "%s::%s -> %s(); PhpPwBytecode.call_static_method_zval".printf (php_class.name, method_name, loader_symbol);
        if (used_pwphpbytecode_mapping_lookup.has_key (mapping)) {
            return;
        }
        used_pwphpbytecode_mapping_lookup[mapping] = true;
        used_pwphpbytecode_mappings.add (mapping);
        if (!used_pwphpbytecode_loader_lookup.has_key (loader_symbol)) {
            used_pwphpbytecode_loader_lookup[loader_symbol] = true;
            used_pwphpbytecode_loaders.add (loader_symbol);
        }
    }

    private string build_user_defined_function_metadata () {
        var metadata = new StringBuilder ();
        metadata.append ("\nphp2vala user-defined function mappings:\n");
        if (used_external_function_mappings.size == 0) {
            metadata.append ("- none\n");
        } else {
            foreach (string mapping in used_external_function_mappings) {
                metadata.append_printf ("- %s\n", mapping);
            }
        }
        metadata.append ("\nphp2vala pwphpbytecode class mappings:\n");
        if (used_pwphpbytecode_mappings.size == 0) {
            metadata.append ("- none\n");
        } else {
            foreach (string mapping in used_pwphpbytecode_mappings) {
                metadata.append_printf ("- %s\n", mapping);
            }
        }
        return metadata.str;
    }

    private void apply_php2vala_stub_metadata (LocalFunction local_function) {
        local_function.static_call_name = php2vala_doc_value (local_function.doc_comment, "@php2vala-static");
        local_function.static_return_type = php2vala_doc_value (local_function.doc_comment, "@php2vala-static-return");
        local_function.zval_call_name = php2vala_doc_value (local_function.doc_comment, "@php2vala-zval");
    }

    private void apply_php2vala_class_stub_metadata (PhpClass php_class) {
        php_class.pwphpbytecode_loader_symbol = php2vala_doc_value (php_class.doc_comment, "@pwphpbytecode-loader");
        php_class.pwphpbytecode_stub_source = php2vala_doc_value (php_class.doc_comment, "@pwphpbytecode-package");
        if (php_class.pwphpbytecode_loader_symbol == null) {
            php_class.pwphpbytecode_loader_symbol = php2vala_doc_value (php_class.doc_comment, "@php2vala-pwphpbytecode-loader");
        }
        if (php_class.pwphpbytecode_stub_source == null) {
            php_class.pwphpbytecode_stub_source = php2vala_doc_value (php_class.doc_comment, "@php2vala-pwphpbytecode-package");
        }
    }

    private static string? php2vala_doc_value (string doc_comment, string key) {
        foreach (string raw_line in doc_comment.split ("\n")) {
            string line = raw_line.strip ();
            if (line.has_prefix ("*")) {
                line = line.substring (1).strip ();
            }
            if (!line.has_prefix (key)) {
                continue;
            }
            string value = line.substring (key.length).strip ();
            return value == "" ? null : value;
        }
        return null;
    }

    public string transpile (string php_source) throws Error {
        variable_types.clear ();
        object_class_names.clear ();
        local_closure_variables.clear ();
        local_functions.clear ();
        php_classes.clear ();
        used_function_lookup.clear ();
        used_function_names.clear ();
        used_external_function_mapping_lookup.clear ();
        used_external_function_mappings.clear ();
        used_pwphpbytecode_mapping_lookup.clear ();
        used_pwphpbytecode_mappings.clear ();
        used_pwphpbytecode_loader_lookup.clear ();
        used_pwphpbytecode_loaders.clear ();
        warned_missing_embed_static.clear ();
        informed_missing_native_runtime.clear ();
        ResolvedSource resolved = PhpToValaSourceParser.resolve_compile_time_includes (source_path, php_source);
        last_resolved_source = PhpToValaSourceParser.strip_php_tags (resolved.source);
        source_line_files = resolved.files;
        source_line_numbers = resolved.lines;
        output_indent_unit = detect_indent_unit (resolved.source);
        temp_variable_counter = 0;

        SourceParts source_parts = PhpToValaSourceParser.extract_local_functions (source_path, last_resolved_source);
        foreach (LocalFunction local_function in source_parts.functions) {
            local_functions[local_function.name] = local_function;
        }
        foreach (PhpClass php_class in source_parts.classes) {
            php_classes[php_class.name] = php_class;
        }

        return transpile_zval_first (source_parts);
    }

    private string transpile_zval_first (SourceParts source_parts) throws Error {
        var statements = PhpToValaSourceParser.collect_statements (source_parts.main_source);
        bool has_main_commands = has_executable_statements (statements);

        if (output_mode == "executable" && !has_main_commands) {
            throw new PhpToValaError.UNSUPPORTED (
                "Cannot build executable output: PHP source has no top-level main commands after compile-time include/function/class extraction"
            );
        }
        if (output_mode == "library" && has_main_commands) {
            throw new PhpToValaError.UNSUPPORTED (
                "Cannot build library output: PHP source has top-level main commands; move them to functions/classes or build with --executable"
            );
        }

        var body = new StringBuilder ();
        if (output_mode == "executable") {
            body.append ("public int main (string[] args) {\n");
            append_indented_line (body, 1, "for (int i = 1; i < args.length; i++) {");
            append_indented_line (body, 2, "if (args[i] == \"--debug--php2vala\") {");
            append_indented_line (body, 3, "stdout.printf (\"%s\", PHP2VALA_DEBUG_INFO);");
            append_indented_line (body, 3, "return 0;");
            append_indented_line (body, 2, "}");
            append_indented_line (body, 1, "}");
            body.append ("\n");
            append_indented_line (body, 1, "try {");

            variable_types = new HashMap<string, string> ();
            variable_aliases = new HashMap<string, string> ();
            object_class_names = new HashMap<string, string> ();
            local_closure_variables = new HashMap<string, bool> ();
            variable_aliases["args"] = "args_value";
            variable_types["argv"] = "Php.zval";
            append_indented_line (body, 2, "Php.zval argv = Php.array_new ();");
            append_indented_line (body, 2, "foreach (string __php2vala_arg in args) { Php.array_append (argv, Php.zval.string_value (__php2vala_arg)); }");
            append_zval_predeclared_variables (body, statements, 2);
        }
        bool body_ends_with_return = false;
        int body_block_level = 0;
        if (output_mode == "executable") {
            for (int i = 0; i < statements.texts.size; i++) {
                string? vala_line = transpile_zval_line (statements.texts[i], statements.lines[i]);
                if (vala_line == null) {
                    continue;
                }
                validate_no_untranslated_php (statements.texts[i], statements.lines[i], statements.columns[i], vala_line);
                body_block_level = append_generated_block (body, 2, vala_line, body_block_level);
                body_ends_with_return = vala_line.strip ().has_prefix ("return ");
            }
            if (!body_ends_with_return) {
                append_indented_line (body, 2, "return 0;");
            }
            append_indented_line (body, 1, "} catch (Error e) {");
            append_indented_line (body, 2, "stderr.printf (\"%s\\n\", e.message);");
            append_indented_line (body, 2, "return 1;");
            append_indented_line (body, 1, "}");
            body.append ("}\n");
        }

        var local_function_output = new StringBuilder ();
        append_zval_classes (local_function_output, source_parts.classes);
        append_zval_local_functions (local_function_output, source_parts.functions);

        var output = new StringBuilder ();
        string runtime_metadata = PhpToValaRuntimeMetadata.build (runtime_name, optimize_local_vars || optimize_file_func_vars, function_bindings, used_function_names);
        string user_defined_function_metadata = build_user_defined_function_metadata ();
        string optimizer_status = optimize_cross_file_func_vars ? "stage 4 cross-file-func-vars enabled" : (optimize_file_func_vars ? "file-func-vars enabled" : (optimize_local_vars ? "local-vars enabled" : "local-vars disabled"));
        last_source_parts = source_parts;
        last_runtime_metadata = runtime_metadata + user_defined_function_metadata;
        last_optimizer_status = optimizer_status;
        output.append ("using Gee;\n\n");
        PhpToValaRuntimeMetadata.append_comment (output, last_runtime_metadata);
        output.append_printf ("/* php2vala optimizer: %s */\n", optimizer_status);
        output.append ("private delegate Php.zval __php2vala_zval_closure () throws Error;\n\n");
        if (output_mode == "executable") {
            PhpToValaRuntimeMetadata.append_debug_constant (output, last_runtime_metadata + "\nphp2vala runtime: " + runtime_name + "\nphp2vala optimizer: " + optimizer_status + "\n");
        }
        append_pwphpbytecode_loader_declarations (output);
        output.append (local_function_output.str);
        output.append (body.str);
        return output.str;
    }

    private void append_pwphpbytecode_loader_declarations (StringBuilder output) {
        if (used_pwphpbytecode_loaders.size == 0) {
            return;
        }

        foreach (string loader_symbol in used_pwphpbytecode_loaders) {
            output.append_printf ("[CCode (cname = \"%s\")]\n", escape_vala_string (loader_symbol));
            output.append_printf ("private extern static void %s ();\n\n", loader_symbol);
        }
    }

    public string build_php_stub () throws Error {
        if (last_source_parts == null) {
            throw new PhpToValaError.UNSUPPORTED ("Cannot emit PHP stub before transpile()");
        }

        SourceParts source_parts = (SourceParts) last_source_parts;
        var output = new StringBuilder ();
        output.append ("<?php\n");
        output.append ("/*\n");
        output.append (" * php2vala generated PHP stub. Declarative only; do not execute as application code.\n");
        foreach (string raw_line in last_runtime_metadata.split ("\n")) {
            output.append (" * ");
            output.append (raw_line);
            output.append ("\n");
        }
        output.append_printf (" * php2vala optimizer: %s\n", last_optimizer_status);
        output.append (" */\n\n");

        foreach (LocalFunction local_function in source_parts.functions) {
            append_php_stub_function (output, local_function, "");
            output.append ("\n");
        }
        foreach (PhpClass php_class in source_parts.classes) {
            append_php_stub_doc_comment (output, php_class.doc_comment, "");
            output.append_printf ("class %s {\n", php_class.name);
            foreach (LocalFunction method in php_class.methods) {
                append_php_stub_function (output, method, "    ");
            }
            output.append ("}\n\n");
        }

        return output.str;
    }

    private void append_php_stub_function (StringBuilder output, LocalFunction local_function, string indent) {
        append_php_stub_doc_comment (output, local_function.doc_comment, indent);
        if (optimize_cross_file_func_vars && indent == "" && file_func_vars_optimizer.can_emit (local_function)) {
            output.append (indent);
            output.append ("/**\n");
            output.append (indent);
            output.append_printf (" * @php2vala-static %s\n", file_func_vars_optimizer.native_function_name (local_function));
            output.append (indent);
            output.append_printf (" * @php2vala-static-return %s\n", file_func_vars_optimizer.infer_native_return_type (local_function));
            output.append (indent);
            output.append_printf (" * @php2vala-zval %s\n", file_func_vars_optimizer.zval_function_name (local_function));
            output.append (indent);
            output.append (" */\n");
        }
        output.append (indent);
        if (indent != "") {
            output.append ("public ");
            if (local_function.is_static) {
                output.append ("static ");
            }
        }
        output.append ("function ");
        output.append (local_function.name);
        output.append (" (");
        output.append (php_stub_parameter_list (local_function));
        output.append (")");
        output.append (": ");
        output.append (local_function.return_type ?? "mixed");
        output.append (" {}\n");
    }

    private void append_php_stub_doc_comment (StringBuilder output, string doc_comment, string indent) {
        string clean_doc_comment = doc_comment.strip ();
        if (clean_doc_comment == "") {
            return;
        }
        foreach (string line in clean_doc_comment.split ("\n")) {
            string clean_line = line.strip ();
            output.append (indent);
            if (clean_line.has_prefix ("*")) {
                output.append (" ");
            }
            output.append (clean_line);
            output.append ("\n");
        }
    }

    private string php_stub_parameter_list (LocalFunction local_function) {
        var output = new StringBuilder ();
        for (int i = 0; i < local_function.parameters.size; i++) {
            LocalParameter parameter = local_function.parameters[i];
            if (i > 0) {
                output.append (", ");
            }
            if (parameter.type_name != null && parameter.type_name != "") {
                output.append (parameter.type_name);
                output.append (" ");
            }
            output.append ("$");
            output.append (parameter.name);
            if (parameter.default_value != null) {
                output.append (" = ");
                output.append (parameter.default_value);
            }
        }
        return output.str;
    }

    private bool has_executable_statements (Statements statements) {
        for (int i = 0; i < statements.texts.size; i++) {
            if (is_executable_statement (statements.texts[i])) {
                return true;
            }
        }
        return false;
    }

    private bool is_executable_statement (string statement) {
        string clean = statement.strip ();
        if (clean == "") {
            return false;
        }
        if (clean.has_prefix ("/*") || clean.has_prefix ("//") || clean.has_prefix ("#")) {
            return false;
        }
        return true;
    }

    private void append_zval_local_functions (StringBuilder output, ArrayList<LocalFunction> functions) throws Error {
        foreach (LocalFunction local_function in functions) {
            HashMap<string, string> previous_variable_types = variable_types;
            HashMap<string, string> previous_parameter_php_types = current_parameter_php_types;
            HashMap<string, string> previous_object_class_names = object_class_names;
            HashMap<string, bool> previous_local_closure_variables = local_closure_variables;
            bool previous_transpiling_local_function = transpiling_local_function;
            string previous_local_function_name = current_local_function_name;
            variable_types = new HashMap<string, string> ();
            current_parameter_php_types = new HashMap<string, string> ();
            object_class_names = new HashMap<string, string> ();
            local_closure_variables = new HashMap<string, bool> ();
            transpiling_local_function = true;
            current_local_function_name = local_function.name;
            foreach (LocalParameter parameter in local_function.parameters) {
                variable_types[parameter.name] = "Php.zval";
                if (parameter.type_name != null && parameter.type_name != "") {
                    current_parameter_php_types[parameter.name] = parameter.type_name;
                }
            }

            if (optimize_file_func_vars && file_func_vars_optimizer.can_emit (local_function)) {
                file_func_vars_optimizer.append_native_function_with_zval_wrapper (output, local_function);
                variable_types = previous_variable_types;
                current_parameter_php_types = previous_parameter_php_types;
                object_class_names = previous_object_class_names;
                local_closure_variables = previous_local_closure_variables;
                transpiling_local_function = previous_transpiling_local_function;
                current_local_function_name = previous_local_function_name;
                continue;
            }

            if (optimize_local_vars && local_vars_optimizer.can_emit (local_function)) {
                local_vars_optimizer.append_zval_local_function (output, local_function);
                variable_types = previous_variable_types;
                current_parameter_php_types = previous_parameter_php_types;
                object_class_names = previous_object_class_names;
                local_closure_variables = previous_local_closure_variables;
                transpiling_local_function = previous_transpiling_local_function;
                current_local_function_name = previous_local_function_name;
                continue;
            }

            bool emits_void_exit_function = optimize_cross_file_func_vars && local_function_last_statement_exits (local_function);
            output.append_printf (
                "public static %s %s (%s) throws Error {\n",
                emits_void_exit_function ? "void" : "Php.zval",
                zval_local_function_name (local_function),
                build_zval_local_function_parameter_list (local_function)
            );
            var statements = PhpToValaSourceParser.collect_statements (local_function.body, local_function.line + 1);
            append_zval_predeclared_variables (output, statements, 1);
            bool last_statement_terminates = false;
            int function_block_level = 0;
            for (int i = 0; i < statements.texts.size; i++) {
                string? vala_line = transpile_zval_line (statements.texts[i], statements.lines[i]);
                if (vala_line == null) {
                    continue;
                }
                validate_no_untranslated_php (statements.texts[i], statements.lines[i], statements.columns[i], vala_line);
                function_block_level = append_generated_block (output, 1, vala_line, function_block_level);
                last_statement_terminates = zval_statement_terminates_function (vala_line);
            }
            if (!emits_void_exit_function && !last_statement_terminates) {
                append_indented_line (output, 1, "return Php.zval.null_value ();");
            }
            output.append ("}\n\n");

            variable_types = previous_variable_types;
            current_parameter_php_types = previous_parameter_php_types;
            object_class_names = previous_object_class_names;
            local_closure_variables = previous_local_closure_variables;
            transpiling_local_function = previous_transpiling_local_function;
            current_local_function_name = previous_local_function_name;
        }
    }

    private void append_zval_classes (StringBuilder output, ArrayList<PhpClass> classes) throws Error {
        foreach (PhpClass php_class in classes) {
            HashMap<string, string> typed_properties = collect_typed_class_properties (php_class);
            output.append_printf ("public class %s : Object {\n", php_class.name);
            append_indented_line (output, 1, "public Php.zval __php2vala_props = Php.array_new ();");
            foreach (string property_name in typed_properties.keys) {
                append_indented_line (output, 1, "private %s %s = null;".printf (typed_properties[property_name], property_name));
            }
            output.append ("\n");

            foreach (LocalFunction method in php_class.methods) {
                HashMap<string, string> previous_variable_types = variable_types;
                HashMap<string, string> previous_parameter_php_types = current_parameter_php_types;
                HashMap<string, string> previous_object_class_names = object_class_names;
                HashMap<string, bool> previous_local_closure_variables = local_closure_variables;
                HashMap<string, string> previous_class_typed_properties = current_class_typed_properties;
                HashMap<string, string> previous_stream_select_read_resources = stream_select_read_resources;
                bool previous_transpiling_local_function = transpiling_local_function;
                string previous_local_function_name = current_local_function_name;

                variable_types = new HashMap<string, string> ();
                current_parameter_php_types = new HashMap<string, string> ();
                object_class_names = new HashMap<string, string> ();
                local_closure_variables = new HashMap<string, bool> ();
                current_class_typed_properties = typed_properties;
                stream_select_read_resources = new HashMap<string, string> ();
                transpiling_local_function = true;
                current_local_function_name = php_class.name + "::" + method.name;
                foreach (string property_name in typed_properties.keys) {
                    variable_types["this." + property_name] = typed_properties[property_name];
                }
                foreach (LocalParameter parameter in method.parameters) {
                    variable_types[parameter.name] = "Php.zval";
                    if (parameter.type_name != null && parameter.type_name != "") {
                        current_parameter_php_types[parameter.name] = parameter.type_name;
                    }
                }

                output.append_printf (
                    "    public %sPhp.zval %s (%s) throws Error {\n",
                    method.is_static ? "static " : "",
                    method.name,
                    build_zval_local_function_parameter_list (method)
                );

                if (method.name == "__construct") {
                    foreach (LocalParameter parameter in method.parameters) {
                        if (parameter.is_promoted_property) {
                            append_indented_line (
                                output,
                                2,
                                "Php.array_set (this.__php2vala_props, Php.zval.string_value (\"%s\"), %s);".printf (parameter.name, parameter.name)
                            );
                        }
                    }
                }

                var statements = PhpToValaSourceParser.collect_statements (method.body, method.line + 1);
                append_zval_predeclared_variables (output, statements, 2);
                bool last_statement_terminates = false;
                int method_block_level = 0;
                for (int i = 0; i < statements.texts.size; i++) {
                    string? vala_line = transpile_zval_line (statements.texts[i], statements.lines[i]);
                    if (vala_line == null) {
                        continue;
                    }
                    validate_no_untranslated_php (statements.texts[i], statements.lines[i], statements.columns[i], vala_line);
                    method_block_level = append_generated_block (output, 2, vala_line, method_block_level);
                    last_statement_terminates = zval_statement_terminates_function (vala_line);
                }
                if (!last_statement_terminates) {
                    append_indented_line (output, 2, "return Php.zval.null_value ();");
                }
                output.append ("    }\n\n");

                variable_types = previous_variable_types;
                current_parameter_php_types = previous_parameter_php_types;
                object_class_names = previous_object_class_names;
                local_closure_variables = previous_local_closure_variables;
                current_class_typed_properties = previous_class_typed_properties;
                stream_select_read_resources = previous_stream_select_read_resources;
                transpiling_local_function = previous_transpiling_local_function;
                current_local_function_name = previous_local_function_name;
            }

            output.append ("}\n\n");
        }
    }

    private HashMap<string, string> collect_typed_class_properties (PhpClass php_class) throws Error {
        var properties = new HashMap<string, string> ();
        if (!(optimize_local_vars || optimize_file_func_vars)) {
            return properties;
        }
        foreach (LocalFunction method in php_class.methods) {
            var statements = PhpToValaSourceParser.collect_statements (method.body, method.line + 1);
            for (int i = 0; i < statements.texts.size; i++) {
                string statement = statements.texts[i].strip ();
                if (!statement.has_prefix ("$this->") || !statement.has_suffix (";")) {
                    continue;
                }
                int equals_index = find_top_level_assignment_index (statement);
                if (equals_index < 0) {
                    continue;
                }
                string property_access = statement.substring (0, equals_index).strip ();
                string property_name = property_access.substring ("$this->".length).strip ();
                if (!is_identifier (property_name)) {
                    continue;
                }
                string raw_expression = trim_trailing_semicolon (statement.substring (equals_index + 1).strip ());
                string? type_name = typed_static_assignment_return_type (raw_expression);
                if (type_name == null) {
                    continue;
                }
                if (properties.has_key (property_name) && properties[property_name] != type_name) {
                    continue;
                }
                properties[property_name] = type_name;
            }
        }
        return properties;
    }

    public static bool zval_statement_terminates_function (string vala_line) {
        string clean = vala_line.strip ();
        return clean.has_prefix ("return ") || clean.has_prefix ("Process.exit ");
    }

    private bool local_function_last_statement_exits (LocalFunction local_function) {
        var statements = PhpToValaSourceParser.collect_statements (local_function.body, local_function.line + 1);
        for (int i = statements.texts.size - 1; i >= 0; i--) {
            string statement = statements.texts[i].strip ();
            if (!is_executable_statement (statement)) {
                continue;
            }
            return statement.has_prefix ("exit")
                || statement.has_prefix ("die")
                || statement.has_prefix ("return exit")
                || statement.has_prefix ("return die");
        }
        return false;
    }

    private static bool function_body_exits (string body) {
        foreach (string raw_line in body.split ("\n")) {
            string line = raw_line.strip ();
            if (line.has_prefix ("exit") || line.has_prefix ("die")) {
                return true;
            }
        }
        return false;
    }

    public void validate_no_untranslated_php (string php_statement, int php_line, int php_column, string vala_line) throws Error {
        foreach (string raw_line in vala_line.split ("\n")) {
            string code = strip_vala_line_strings_and_comments (raw_line);
            if (code.index_of ("$") >= 0
                || code.index_of ("<?") >= 0
                || code.index_of ("?>") >= 0
                || has_bare_function_call (code, "die")
                || has_bare_function_call (code, "exit")
                || has_bare_function_call (code, "var_dump")
                || has_bare_keyword (code, "echo")) {
                string token_context = PhpToValaSourceParser.debug_tokens_for_source_line (source_path, last_resolved_source, php_line);
                throw new PhpToValaError.UNSUPPORTED (
                    "Not implemented at %s:%d:%d: PHP statement was only partially translated: %s -> %s\n%s",
                    source_path,
                    php_line,
                    php_column,
                    php_statement,
                    vala_line,
                    token_context
                );
            }
        }
    }

    private static string strip_vala_line_strings_and_comments (string line) {
        var output = new StringBuilder ();
        bool in_single_quote = false;
        bool in_double_quote = false;
        bool escaped = false;

        for (int i = 0; i < line.length; i++) {
            char c = line[i];
            char next = i + 1 < line.length ? line[i + 1] : '\0';

            if (!in_single_quote && !in_double_quote && c == '/' && next == '/') {
                break;
            }

            if (escaped) {
                escaped = false;
                continue;
            }
            if ((in_single_quote || in_double_quote) && c == '\\') {
                escaped = true;
                continue;
            }
            if (c == '\'' && !in_double_quote) {
                in_single_quote = !in_single_quote;
                continue;
            }
            if (c == '"' && !in_single_quote) {
                in_double_quote = !in_double_quote;
                continue;
            }
            if (!in_single_quote && !in_double_quote) {
                output.append_c (c);
            }
        }

        return output.str;
    }

    private static bool has_bare_function_call (string code, string name) {
        int start = 0;
        while (start < code.length) {
            int index = code.index_of (name, start);
            if (index < 0) {
                return false;
            }

            bool valid_left = index == 0 || (!is_identifier_char (code[index - 1]) && code[index - 1] != '.');
            int next_index = index + name.length;
            bool valid_right = next_index >= code.length || !is_identifier_char (code[next_index]);
            if (valid_left && valid_right) {
                while (next_index < code.length && (code[next_index] == ' ' || code[next_index] == '\t')) {
                    next_index++;
                }
                if (next_index < code.length && code[next_index] == '(') {
                    return true;
                }
            }

            start = index + name.length;
        }
        return false;
    }

    private static bool has_bare_keyword (string code, string name) {
        int start = 0;
        while (start < code.length) {
            int index = code.index_of (name, start);
            if (index < 0) {
                return false;
            }

            bool valid_left = index == 0 || !is_identifier_char (code[index - 1]);
            int next_index = index + name.length;
            bool valid_right = next_index >= code.length || !is_identifier_char (code[next_index]);
            if (valid_left && valid_right) {
                return true;
            }

            start = index + name.length;
        }
        return false;
    }

    private static string detect_indent_unit (string source) {
        foreach (string line in source.split ("\n")) {
            if (line.strip () == "") {
                continue;
            }

            var indent = new StringBuilder ();
            for (int i = 0; i < line.length; i++) {
                char c = line[i];
                if (c != ' ' && c != '\t') {
                    break;
                }
                indent.append_c (c);
            }

            if (indent.len == 0) {
                continue;
            }

            string candidate = indent.str;
            int tab_index = candidate.index_of ("\t");
            if (tab_index >= 0) {
                return "\t";
            }
            return candidate.length > 4 ? "    " : candidate;
        }

        return "    ";
    }

    private string indent_string (int level) {
        var output = new StringBuilder ();
        for (int i = 0; i < level; i++) {
            output.append (output_indent_unit);
        }
        return output.str;
    }

    public void append_indented_line (StringBuilder output, int level, string line) {
        output.append (indent_string (level));
        output.append (line);
        output.append ("\n");
    }

    public int append_generated_block (StringBuilder output, int base_level, string generated_block, int initial_relative_level = 0) {
        int relative_level = initial_relative_level;
        foreach (string raw_line in generated_block.split ("\n")) {
            string line = raw_line.strip ();
            if (line == "") {
                output.append ("\n");
                continue;
            }

            bool starts_with_close = line.has_prefix ("}");
            int line_relative_level = relative_level;
            if (starts_with_close && line_relative_level > 0) {
                line_relative_level--;
            }

            append_indented_line (output, base_level + line_relative_level, line);

            int opens = count_char (line, '{');
            int closes = count_char (line, '}');
            if (starts_with_close && closes > 0) {
                closes--;
            }
            relative_level = line_relative_level + opens - closes;
            if (relative_level < 0) {
                relative_level = 0;
            }
        }
        return relative_level;
    }

    private void append_zval_predeclared_variables (StringBuilder output, Statements statements, int indent_level) {
        var names = new ArrayList<string> ();
        for (int i = 0; i < statements.texts.size; i++) {
            string statement = statements.texts[i].strip ();
            string? assignment_name = parse_assignment_name (statement);
            if (assignment_name != null) {
                string assignment_expression = parse_assignment_expression (statement).strip ();
                if (optimize_local_vars || optimize_file_func_vars) {
                    string? typed_native_type = typed_native_assignment_return_type (assignment_expression);
                    if (typed_native_type != null) {
                        string storage_name = ensure_variable_alias_for_name (assignment_name);
                        if (!variable_types.has_key (storage_name)) {
                            variable_types[storage_name] = typed_native_type;
                            append_indented_line (output, indent_level, "%s %s = 0;".printf (typed_native_type, storage_name));
                        }
                        continue;
                    }
                    string? typed_static_type = typed_static_assignment_return_type (assignment_expression);
                    if (typed_static_type != null) {
                        string storage_name = ensure_variable_alias_for_name (assignment_name);
                        if (!variable_types.has_key (storage_name)) {
                            variable_types[storage_name] = typed_static_type;
                            append_indented_line (output, indent_level, "%s %s = null;".printf (typed_static_type, storage_name));
                        }
                        continue;
                    }
                }
                if (!assignment_expression.has_prefix ("new ")
                    && !assignment_expression.has_prefix ("function")) {
                    add_zval_predeclare_name (names, assignment_name);
                }
            }
            add_runtime_out_argument_predeclare_names (names, statement);
        }

        foreach (string name in names) {
            string storage_name = ensure_variable_alias_for_name (name);
            if (variable_types.has_key (storage_name)) {
                continue;
            }
            variable_types[storage_name] = "Php.zval";
            append_indented_line (output, indent_level, "Php.zval %s = Php.zval.null_value ();".printf (storage_name));
        }
    }

    public void append_runtime_out_argument_predeclared_variables (StringBuilder output, Statements statements, int indent_level) {
        var names = new ArrayList<string> ();
        for (int i = 0; i < statements.texts.size; i++) {
            add_runtime_out_argument_predeclare_names (names, statements.texts[i].strip ());
        }

        foreach (string name in names) {
            string storage_name = ensure_variable_alias_for_name (name);
            if (variable_types.has_key (storage_name)) {
                continue;
            }
            variable_types[storage_name] = "Php.zval";
            append_indented_line (output, indent_level, "Php.zval %s = Php.zval.null_value ();".printf (storage_name));
        }
    }

    private void add_runtime_out_argument_predeclare_names (ArrayList<string> names, string statement) {
        int search_index = 0;
        while (search_index < statement.length) {
            int open_index = statement.index_of ("(", search_index);
            if (open_index < 0) {
                return;
            }
            int name_end = open_index;
            while (name_end > 0 && statement[name_end - 1].isspace ()) {
                name_end--;
            }
            int name_start = name_end;
            while (name_start > 0) {
                char ch = statement[name_start - 1];
                if (!(ch.isalnum () || ch == '_')) {
                    break;
                }
                name_start--;
            }
            if (name_start == name_end) {
                search_index = open_index + 1;
                continue;
            }
            string function_name = statement.substring (name_start, name_end - name_start);
            int close_index = find_matching_paren (statement, open_index);
            if (open_index < 0 || close_index < 0) {
                search_index = open_index + 1;
                continue;
            }

            var args = split_top_level (statement.substring (open_index + 1, close_index - open_index - 1), ',');
            string runtime_function_name;
            FunctionBinding? binding = resolve_zval_runtime_binding_for_arg_count_silent (function_name, args.size, out runtime_function_name);
            if (binding == null) {
                search_index = open_index + 1;
                continue;
            }
            for (int i = 0; i < args.size; i++) {
                string parameter_type = runtime_parameter_type_for_arg (binding, i);
                if (!is_out_runtime_parameter_type (parameter_type)) {
                    continue;
                }
                string raw_arg = args[i].strip ();
                if (!raw_arg.has_prefix ("$")) {
                    continue;
                }
                string name = replace_php_variables (raw_arg);
                if (is_identifier (name) && name != "this") {
                    add_zval_predeclare_name (names, name);
                }
            }
            search_index = open_index + 1;
        }
    }

    private void add_zval_predeclare_name (ArrayList<string> names, string name) {
        if (name == "this" || variable_types.has_key (name)) {
            return;
        }
        foreach (string existing in names) {
            if (existing == name) {
                return;
            }
        }
        names.add (name);
    }

    private string runtime_parameter_type_for_arg (FunctionBinding binding, int arg_index) {
        int index = binding.runtime_parameter_index (arg_index);
        int limit = binding.runtime_parameter_limit ();
        if (index >= limit && limit > binding.runtime_parameter_offset ()) {
            return binding.parameter_types[limit - 1];
        }
        return binding.parameter_types[index];
    }

    private static int count_char (string value, char needle) {
        int count = 0;
        for (int i = 0; i < value.length; i++) {
            if (value[i] == needle) {
                count++;
            }
        }
        return count;
    }

    public string build_zval_local_function_parameter_list (LocalFunction local_function) {
        var output = new StringBuilder ();
        foreach (LocalParameter parameter in local_function.parameters) {
            if (output.len > 0) {
                output.append (", ");
            }
            output.append_printf ("owned Php.zval %s", parameter.name);
            if (parameter.default_value != null) {
                output.append_printf (" = %s", convert_zval_expression_no_throw (parameter.default_value, local_function.line));
            }
        }
        return output.str;
    }

    private string? transpile_zval_line (owned string line, int php_line_number) throws Error {
        line = line.strip ();
        if (line.has_prefix ("//")) {
            return line;
        }
        if (line.has_prefix ("#")) {
            return "//" + line.substring (1);
        }
        if (line.has_prefix ("/*")) {
            return line.replace ("\n", "\n        ");
        }
        if (line.has_prefix ("@")) {
            return transpile_zval_line (line.substring (1).strip (), php_line_number);
        }
        if (line.has_suffix (";flush();")) {
            string first = line.substring (0, line.length - "flush();".length).strip ();
            mark_function_used ("flush");
            string flush_name = map_php_runtime_function_name ("flush", "flush_zval", php_line_number);
            return transpile_zval_line (first, php_line_number) + "\n" + flush_name + " (" + source_location_args (php_line_number).substring (2) + ");";
        }
        if (line == "{" || line == "}") {
            return line;
        }
        if (line == "else{") {
            return "else {";
        }
        if (line.has_prefix ("} else")) {
            return "} else {";
        }
        if (line == "try" || line == "try {") {
            return "{";
        }
        if (line == "do" || line == "do {") {
            return "do {";
        }
        if (line == "finally" || line == "finally {") {
            return "{";
        }
        if (line.has_prefix ("} finally")) {
            return "}\n{";
        }
        if (line.has_prefix ("echo ")) {
            return transpile_zval_echo (line.substring (5).strip (), php_line_number);
        }
        if (line.has_prefix ("const ")) {
            return transpile_zval_const (line, php_line_number);
        }
        if (line.has_prefix ("if(") || line.has_prefix ("if (")) {
            return transpile_zval_inline_condition (line, "if", php_line_number);
        }
        if (line.has_prefix ("elseif(") || line.has_prefix ("elseif (")) {
            return transpile_zval_inline_condition (line, "else if", php_line_number);
        }
        if (line.has_prefix ("else ")) {
            string nested = transpile_zval_line (line.substring (5).strip (), php_line_number);
            return @"else { $nested }";
        }
        if (line.has_prefix ("foreach(") || line.has_prefix ("foreach (")) {
            return transpile_zval_foreach (line, php_line_number);
        }
        if (line.has_prefix ("switch(") || line.has_prefix ("switch (")) {
            return transpile_zval_switch (line, php_line_number);
        }
        if (line.has_prefix ("case ")) {
            return transpile_zval_case (line, php_line_number);
        }
        if (line == "default:") {
            return "default:";
        }
        if (line.has_prefix ("for(") || line.has_prefix ("for (")) {
            return transpile_zval_for (line, php_line_number);
        }
        if (line.has_prefix ("while(") || line.has_prefix ("while (")) {
            return transpile_zval_while (line, php_line_number);
        }
        if (line.has_prefix ("} while")) {
            return transpile_zval_do_while_end (line, php_line_number);
        }
        if (line.has_prefix ("return")) {
            string expression = line.substring ("return".length).strip ();
            expression = trim_trailing_semicolon (expression);
            if (expression.has_prefix ("(") && expression.has_suffix (")")) {
                expression = expression.substring (1, expression.length - 2).strip ();
            }
            return expression == "" ? "return Php.zval.null_value ();" : "return %s;".printf (convert_zval_return_expression (expression, php_line_number));
        }
        if (line.has_prefix ("throw ")) {
            return transpile_zval_throw (line, php_line_number);
        }
        if (line == "exit;" || line == "die;") {
            return transpile_zval_exit ("", php_line_number, line.has_prefix ("die") ? "die" : "exit");
        }
        if (line == "continue;" || line == "break;") {
            return line;
        }
        string? dynamic_call = transpile_zval_dynamic_this_call (line, php_line_number);
        if (dynamic_call != null) {
            return dynamic_call;
        }
        dynamic_call = transpile_zval_variable_function_call (line, php_line_number);
        if (dynamic_call != null) {
            return dynamic_call;
        }
        string? inline_stream_write = transpile_zval_inline_fwrite_fopen_call (line, php_line_number);
        if (inline_stream_write != null) {
            return inline_stream_write;
        }
        string? exec_call = transpile_zval_exec_call (line, php_line_number);
        if (exec_call != null) {
            return exec_call;
        }
        string? increment = transpile_zval_increment (line, php_line_number);
        if (increment != null) {
            return increment;
        }
        string? list_assignment = transpile_zval_list_assignment (line, php_line_number);
        if (list_assignment != null) {
            return list_assignment;
        }
        string? object_property_assignment = transpile_zval_object_property_assignment (line, php_line_number);
        if (object_property_assignment != null) {
            return object_property_assignment;
        }
        string? closure_assignment = transpile_zval_closure_assignment (line, php_line_number);
        if (closure_assignment != null) {
            return closure_assignment;
        }

        string? compound_assignment = transpile_zval_compound_assignment (line, php_line_number);
        if (compound_assignment != null) {
            return compound_assignment;
        }
        string? array_assignment = transpile_zval_array_assignment (line, php_line_number);
        if (array_assignment != null) {
            return array_assignment;
        }
        string? new_object_assignment = transpile_zval_new_object_assignment (line, php_line_number);
        if (new_object_assignment != null) {
            return new_object_assignment;
        }
        string? object_method_call = transpile_zval_object_method_call (line, php_line_number);
        if (object_method_call != null) {
            return object_method_call;
        }
        string? static_method_call = transpile_zval_static_method_call (line, php_line_number);
        if (static_method_call != null) {
            return static_method_call;
        }

        string? assignment_name = null;
        string raw_assignment_expression = "";
        MatchInfo assignment_match;
        if (assignment_regex.match (line, 0, out assignment_match)) {
            assignment_name = assignment_match.fetch (1);
            raw_assignment_expression = assignment_match.fetch (2).strip ();
        } else {
            assignment_name = parse_assignment_name (line);
            if (assignment_name != null) {
                raw_assignment_expression = parse_assignment_expression (line);
            } else {
                int dollar_index = line.index_of ("$");
                int equals_index = line.index_of ("=");
                if (dollar_index >= 0 && equals_index > dollar_index && line.has_suffix (";")) {
                    string candidate = line.substring (dollar_index + 1, equals_index - dollar_index - 1).strip ();
                    if (is_identifier (candidate)) {
                        assignment_name = candidate;
                        raw_assignment_expression = trim_trailing_semicolon (line.substring (equals_index + 1).strip ());
                    }
                }
            }
        }
        if (assignment_name != null) {
            ensure_variable_alias_for_name (assignment_name);
            string raw_expression = raw_assignment_expression;
            string storage_name = variable_aliases.has_key (assignment_name) ? variable_aliases[assignment_name] : assignment_name;
            string? array_filter_assignment = transpile_zval_array_filter_assignment (storage_name, raw_expression, php_line_number);
            if (array_filter_assignment != null) {
                return array_filter_assignment;
            }
            string? short_array_assignment = transpile_zval_short_array_assignment (storage_name, raw_expression, php_line_number);
            if (short_array_assignment != null) {
                return short_array_assignment;
            }
            string? chained_assignment = transpile_zval_chained_assignment (assignment_name, raw_expression, php_line_number);
            if (chained_assignment != null) {
                return chained_assignment;
            }
            string? stream_select_assignment = transpile_zval_stream_select_assignment (storage_name, raw_expression, php_line_number);
            if (stream_select_assignment != null) {
                return stream_select_assignment;
            }
            string? typed_native_assignment = transpile_zval_typed_native_assignment (storage_name, raw_expression, php_line_number);
            if (typed_native_assignment != null) {
                return typed_native_assignment;
            }
            string? typed_static_assignment = transpile_zval_typed_static_assignment (storage_name, raw_expression, php_line_number);
            if (typed_static_assignment != null) {
                return typed_static_assignment;
            }
            string expression = convert_zval_expression (raw_expression, php_line_number);
            if (local_functions.has_key (assignment_name) && !variable_aliases.has_key (assignment_name)) {
                storage_name = assignment_name + "_result";
                variable_aliases[assignment_name] = storage_name;
            }
            if (!variable_types.has_key (storage_name)) {
                variable_types[storage_name] = "Php.zval";
                return @"Php.zval $storage_name = $expression;";
            }
            return @"$storage_name = $expression;";
        }

        MatchInfo match;
        if (function_call_regex.match (line, 0, out match)) {
            string php_function_name = match.fetch (1);
            if (php_function_name == "exit" || php_function_name == "die") {
                return transpile_zval_exit (match.fetch (2).strip (), php_line_number, php_function_name);
            }
            LocalFunction? called_local_function = local_functions[php_function_name];
            if (called_local_function != null && function_body_exits (called_local_function.body)) {
                string call = convert_zval_expression (php_function_name + "(" + match.fetch (2).strip () + ")", php_line_number);
                return transpiling_local_function ? call + ";\nreturn Php.zval.null_value ();" : call + ";\nreturn 0;";
            }
            if (external_functions.has_key (php_function_name)) {
                LocalFunction? external_function = external_functions[php_function_name];
                if (external_function != null) {
                    string raw_args = match.fetch (2).strip ();
                    validate_php_stub_argument_count (external_function, raw_args, php_line_number, php_function_name);
                    if (optimize_cross_file_func_vars && file_func_vars_optimizer.can_call_external_static (external_function)) {
                        return "%s;".printf (file_func_vars_optimizer.external_function_call_as_native_value (external_function, raw_args, php_line_number));
                    }
                    return "%s (%s);".printf (external_zval_call_name (external_function), convert_zval_argument_list (raw_args, php_line_number));
                }
            }
            string expression = convert_zval_expression (php_function_name + "(" + match.fetch (2).strip () + ")", php_line_number);
            expression = discard_unused_zval_wrapper (expression);
            return "%s;".printf (expression);
        }

        int fallback_equals_index = line.index_of ("=");
        if (fallback_equals_index > 0) {
            string lhs = line.substring (0, fallback_equals_index).strip ();
            if (lhs.has_prefix ("$")) {
                string name = replace_php_variables (lhs);
                if (is_identifier (name)) {
                    string storage_name = ensure_variable_alias_for_name (name);
                    string expression = convert_zval_expression (trim_trailing_semicolon (line.substring (fallback_equals_index + 1).strip ()), php_line_number);
                    if (!variable_types.has_key (storage_name)) {
                        variable_types[storage_name] = "Php.zval";
                        return @"Php.zval $storage_name = $expression;";
                    }
                    return @"$storage_name = $expression;";
                }
            }
        }

        throw unsupported_syntax (php_line_number, line);
    }

    private static string discard_unused_zval_wrapper (string expression) {
        string clean = expression.strip ();
        foreach (string prefix in new string[] {
            "Php.zval.integer (",
            "Php.zval.double_number (",
            "Php.zval.boolean (",
            "Php.zval.string_value ("
        }) {
            if (!clean.has_prefix (prefix) || !clean.has_suffix (")")) {
                continue;
            }
            int open_index = prefix.length - 1;
            int close_index = find_matching_paren (clean, open_index);
            if (close_index == clean.length - 1) {
                return clean.substring (open_index + 1, close_index - open_index - 1).strip ();
            }
        }
        return expression;
    }

    private string? transpile_zval_object_property_assignment (string line, int php_line_number) throws Error {
        if (!line.has_prefix ("$this->") || !line.has_suffix (";")) {
            return null;
        }
        int equals_index = find_top_level_assignment_index (line);
        if (equals_index < 0) {
            return null;
        }
        string property_access = line.substring (0, equals_index).strip ();
        string property_name = property_access.substring ("$this->".length).strip ();
        if (!is_identifier (property_name)) {
            throw unsupported_syntax (php_line_number, line);
        }
        string storage_name = "this." + property_name;
        string raw_expression = trim_trailing_semicolon (line.substring (equals_index + 1).strip ());
        if (current_class_typed_properties.has_key (property_name)) {
            string? typed_assignment = transpile_zval_typed_static_assignment (storage_name, raw_expression, php_line_number);
            if (typed_assignment != null) {
                return typed_assignment;
            }
        }

        string value = convert_zval_expression (raw_expression, php_line_number);
        return "Php.array_set (this.__php2vala_props, Php.zval.string_value (\"%s\"), %s);".printf (property_name, value);
    }

    private string? transpile_zval_inline_fwrite_fopen_call (string line, int php_line_number) throws Error {
        if (!(optimize_local_vars || optimize_file_func_vars)) {
            return null;
        }
        string clean = trim_trailing_semicolon (line.strip ());
        MatchInfo match;
        if (!function_expression_regex.match (clean, 0, out match) || match.fetch (1) != "fwrite") {
            return null;
        }

        var args = split_top_level (match.fetch (2).strip (), ',');
        if (args.size != 2) {
            return null;
        }

        string stream_expression = args[0].strip ();
        MatchInfo stream_match;
        if (!function_expression_regex.match (stream_expression, 0, out stream_match) || stream_match.fetch (1) != "fopen") {
            return null;
        }

        var fopen_args = split_top_level (stream_match.fetch (2).strip (), ',');
        if (fopen_args.size != 2) {
            return null;
        }

        string fopen_name = map_php_runtime_function_name ("fopen", "fopen_static", php_line_number);
        string fwrite_name = map_php_runtime_function_name ("fwrite", "fwrite_static", php_line_number);
        string stream_name = "__php2vala_stream_%d".printf (temp_variable_counter++);
        string path = convert_zval_expression (fopen_args[0].strip (), php_line_number);
        string mode = convert_zval_expression (fopen_args[1].strip (), php_line_number);
        string data = convert_zval_expression (args[1].strip (), php_line_number);
        string loc = source_location_args (php_line_number);
        string stream_type = fopen_name.has_prefix ("PhpNative.") ? "PhpNative.StreamResource" : "PhpEmbed.StreamResource";

        return "%s %s = %s (%s, %s%s);\n%s (%s, %s%s);".printf (
            stream_type,
            stream_name,
            fopen_name,
            convert_zval_arg_for_runtime_static_type (path, "string"),
            convert_zval_arg_for_runtime_static_type (mode, "string"),
            loc,
            fwrite_name,
            stream_name,
            convert_zval_arg_for_runtime_static_type (data, "string"),
            loc
        );
    }

    private string? transpile_zval_closure_assignment (string line, int php_line_number) throws Error {
        if (!line.has_prefix ("$") || line.index_of ("= function") < 0 || !line.has_suffix (";")) {
            return null;
        }

        int equals_index = find_top_level_assignment_index (line);
        if (equals_index <= 1) {
            return null;
        }
        string variable_name = line.substring (1, equals_index - 1).strip ();
        if (!is_identifier (variable_name)) {
            throw unsupported_syntax (php_line_number, line);
        }

        string closure = trim_trailing_semicolon (line.substring (equals_index + 1).strip ());
        if (!closure.has_prefix ("function")) {
            return null;
        }

        int open_paren = closure.index_of ("(");
        int close_paren = find_matching_paren (closure, open_paren);
        if (open_paren < 0 || close_paren < 0) {
            throw unsupported_syntax (php_line_number, line);
        }
        string raw_parameters = closure.substring (open_paren + 1, close_paren - open_paren - 1).strip ();
        if (raw_parameters != "") {
            throw unsupported_syntax (php_line_number, line);
        }

        int open_brace = closure.index_of ("{", close_paren);
        int close_brace = find_matching_brace (closure, open_brace);
        if (open_brace < 0 || close_brace < 0) {
            throw unsupported_syntax (php_line_number, line);
        }

        string body = closure.substring (open_brace + 1, close_brace - open_brace - 1);
        var statements = PhpToValaSourceParser.collect_statements (body, php_line_number + 1);
        var output = new StringBuilder ();
        output.append_printf ("__php2vala_zval_closure %s = () => {\n", variable_name);
        int block_level = 0;
        bool last_statement_terminates = false;
        for (int i = 0; i < statements.texts.size; i++) {
            string? vala_line = transpile_zval_line (statements.texts[i], statements.lines[i]);
            if (vala_line == null) {
                continue;
            }
            validate_no_untranslated_php (statements.texts[i], statements.lines[i], statements.columns[i], vala_line);
            block_level = append_generated_block (output, 1, vala_line, block_level);
            last_statement_terminates = zval_statement_terminates_function (vala_line);
        }
        if (!last_statement_terminates) {
            append_indented_line (output, 1, "return Php.zval.null_value ();");
        }
        output.append ("};");

        local_closure_variables[variable_name] = true;
        variable_types[variable_name] = "__php2vala_zval_closure";
        return output.str;
    }

    private string transpile_zval_throw (string line, int php_line_number) throws Error {
        string expression = trim_trailing_semicolon (line.substring ("throw ".length).strip ());
        if (expression.has_prefix ("new ")) {
            int open_index = expression.index_of ("(");
            int close_index = open_index >= 0 ? find_matching_paren (expression, open_index) : -1;
            if (open_index > 0 && close_index > open_index) {
                expression = expression.substring (open_index + 1, close_index - open_index - 1).strip ();
            } else if (open_index > 0) {
                string args_part = expression.substring (open_index + 1).strip ();
                if (args_part.has_suffix (")")) {
                    args_part = args_part.substring (0, args_part.length - 1).strip ();
                }
                if (args_part.has_prefix ("\"")) {
                    int quote_index = args_part.last_index_of ("\"");
                    if (quote_index > 0) {
                        args_part = args_part.substring (0, quote_index + 1);
                    }
                }
                expression = args_part;
            }
        }
        string message = expression == "" ? "Php.zval.string_value (\"PHP exception\")" : convert_zval_expression (expression, php_line_number);
        return "throw new IOError.FAILED (%s);".printf (convert_zval_arg_for_runtime_static_type (message, "string"));
    }

    private string? transpile_zval_const (string line, int php_line_number) throws Error {
        string clean = trim_trailing_semicolon (line.substring ("const ".length).strip ());
        int equals_index = find_top_level_assignment_index (clean);
        if (equals_index <= 0) {
            throw unsupported_syntax (php_line_number, line);
        }
        string name = clean.substring (0, equals_index).strip ();
        if (!is_identifier (name)) {
            throw unsupported_syntax (php_line_number, line);
        }
        string value = convert_zval_expression (clean.substring (equals_index + 1).strip (), php_line_number);
        defined_constants[name] = value;
        return null;
    }

    private string? transpile_zval_chained_assignment (string first_name, string raw_expression, int php_line_number) throws Error {
        var names = new ArrayList<string> ();
        names.add (first_name);

        string expression = raw_expression.strip ();
        while (true) {
            int equals_index = find_top_level_assignment_index (expression);
            if (equals_index < 0) {
                break;
            }

            string lhs = expression.substring (0, equals_index).strip ();
            if (!lhs.has_prefix ("$")) {
                return null;
            }

            string name = replace_php_variables (lhs);
            if (!is_identifier (name)) {
                return null;
            }

            names.add (name);
            expression = expression.substring (equals_index + 1).strip ();
        }

        if (names.size <= 1) {
            return null;
        }

        string current_value = convert_zval_expression (expression, php_line_number);
        var output = new StringBuilder ();
        for (int i = names.size - 1; i >= 0; i--) {
            string name = names[i];
            if (output.len > 0) {
                output.append ("\n");
            }
            if (!variable_types.has_key (name)) {
                variable_types[name] = "Php.zval";
                output.append_printf ("Php.zval %s = %s;", name, current_value);
            } else {
                output.append_printf ("%s = %s;", name, current_value);
            }
            current_value = name;
        }

        return output.str;
    }

    private string? transpile_zval_exec_call (string line, int php_line_number) throws Error {
        MatchInfo match;
        if (!function_call_regex.match (line, 0, out match) || match.fetch (1) != "exec") {
            return null;
        }
        var args = split_top_level (match.fetch (2).strip (), ',');
        if (args.size < 1 || args.size > 3) {
            return null;
        }

        if (args.size == 3 && args[1].strip ().has_prefix ("$") && args[2].strip ().has_prefix ("$")) {
            string output_name = replace_php_variables (args[1].strip ());
            string status_name = replace_php_variables (args[2].strip ());
            if (is_identifier (output_name) && is_identifier (status_name)) {
                string output_storage = ensure_variable_alias_for_name (output_name);
                string status_storage = ensure_variable_alias_for_name (status_name);
                var static_output = new StringBuilder ();
                if (!variable_types.has_key (output_storage)) {
                    variable_types[output_storage] = "Php.zval";
                    static_output.append_printf ("Php.zval %s = Php.array_new ();\n", output_storage);
                }
                if (!variable_types.has_key (status_storage)) {
                    variable_types[status_storage] = "Php.zval";
                    static_output.append_printf ("Php.zval %s = Php.zval.null_value ();\n", status_storage);
                }

                string status_temp = "__php2vala_exec_status_%d".printf (temp_variable_counter++);
                string static_name = map_php_runtime_function_name ("exec", "exec_output_status_static", php_line_number);
                static_output.append_printf ("int %s = 0;\n", status_temp);
                static_output.append_printf (
                    "%s (Php.zval_to_string (%s), out %s, out %s%s);\n",
                    static_name,
                    convert_zval_expression (args[0].strip (), php_line_number),
                    output_storage,
                    status_temp,
                    source_location_args (php_line_number)
                );
                static_output.append_printf ("%s = Php.zval.integer (%s);", status_storage, status_temp);
                return static_output.str;
            }
        }

        var output = new StringBuilder ();
        var converted_args = new ArrayList<string> ();
        converted_args.add (convert_zval_expression (args[0].strip (), php_line_number));
        for (int i = 1; i < args.size; i++) {
            string raw_arg = args[i].strip ();
            if (raw_arg.has_prefix ("$")) {
                string variable_name = replace_php_variables (raw_arg);
                if (is_identifier (variable_name)) {
                    string storage_name = ensure_variable_alias_for_name (variable_name);
                    if (!variable_types.has_key (storage_name)) {
                        variable_types[storage_name] = "Php.zval";
                        output.append_printf ("Php.zval %s = Php.zval.null_value ();\n", storage_name);
                    }
                    converted_args.add (storage_name);
                    continue;
                }
            }
            converted_args.add (convert_zval_expression (raw_arg, php_line_number));
        }

        output.append (embed_generated_zval_call ("exec", join_strings (converted_args, ", "), php_line_number));
        output.append (";");
        return output.str;
    }

    private static int find_top_level_assignment_index (string expression) {
        bool in_single_quote = false;
        bool in_double_quote = false;
        int depth = 0;

        for (int i = 0; i < expression.length; i++) {
            char c = expression[i];
            if (c == '\'' && !in_double_quote) {
                in_single_quote = !in_single_quote;
            } else if (c == '"' && !in_single_quote) {
                in_double_quote = !in_double_quote;
            } else if (!in_single_quote && !in_double_quote) {
                if (c == '(' || c == '[' || c == '{') {
                    depth++;
                } else if (c == ')' || c == ']' || c == '}') {
                    depth--;
                } else if (c == '=' && depth == 0) {
                    char previous = i > 0 ? expression[i - 1] : '\0';
                    char next = i + 1 < expression.length ? expression[i + 1] : '\0';
                    if (previous != '=' && previous != '!' && previous != '<' && previous != '>' && next != '=') {
                        return i;
                    }
                }
            }
        }

        return -1;
    }

    private string transpile_zval_echo (string expression, int php_line_number) throws Error {
        string clean_expression = trim_trailing_semicolon (expression);
        mark_function_used ("echo");
        var parts = split_top_level (clean_expression, ',');
        var output = new StringBuilder ();
        foreach (string raw_part in parts) {
            string part = raw_part.strip ();
            if (part == "") {
                continue;
            }
            if (output.len > 0) {
                output.append (" ");
            }
            string? converted_concat = convert_zval_binary_expression (part, '.', "Php.concat", php_line_number);
            string converted_part = converted_concat ?? convert_zval_expression (part, php_line_number);
            if (optimize_local_vars || optimize_file_func_vars) {
                string echo_name = map_php_runtime_function_name ("echo", "echo_static", php_line_number);
                output.append_printf (
                    "%s (%s%s);",
                    echo_name,
                    convert_zval_arg_for_runtime_static_type (converted_part, "string"),
                    source_location_args (php_line_number)
                );
            } else {
                string echo_name = map_php_runtime_function_name ("echo", "echo_zval", php_line_number);
                output.append_printf (
                    "%s (%s%s);",
                    echo_name,
                    converted_part,
                    source_location_args (php_line_number)
                );
            }
        }
        return output.str;
    }

    private string transpile_zval_inline_condition (string line, string vala_keyword, int php_line_number) throws Error {
        int open_index = line.index_of ("(");
        int close_index = find_matching_paren (line, open_index);
        if (open_index < 0 || close_index < 0) {
            throw unsupported_syntax (php_line_number, line);
        }
        string raw_condition = line.substring (open_index + 1, close_index - open_index - 1);
        string condition = convert_zval_condition (raw_condition, php_line_number);
        string rest = line.substring (close_index + 1).strip ();
        if (rest == "" || rest == "{") {
            return @"$vala_keyword ($condition) {";
        }
        string nested = transpile_zval_line (rest, php_line_number);
        return @"$vala_keyword ($condition) { $nested }";
    }

    private string transpile_zval_foreach (string line, int php_line_number) throws Error {
        int open_index = line.index_of ("(");
        int close_index = find_matching_paren (line, open_index);
        if (open_index < 0 || close_index < 0) {
            throw unsupported_syntax (php_line_number, line);
        }
        string inner = line.substring (open_index + 1, close_index - open_index - 1).strip ();
        var parts = inner.split (" as ");
        if (parts.length != 2) {
            throw unsupported_syntax (php_line_number, line);
        }
        string source = convert_zval_iterable_expression (parts[0].strip (), php_line_number);
        string target = parts[1].strip ();
        int key_sep = target.index_of ("=>");
        if (key_sep >= 0) {
            string key_name = replace_php_variables (target.substring (0, key_sep).strip ());
            string value_name = replace_php_variables (target.substring (key_sep + 2).strip ());
            variable_types[key_name] = "Php.zval";
            variable_types[value_name] = "Php.zval";
            string key_iter_name = "__php2vala_key_" + key_name;
            return @"foreach (string $key_iter_name in Php.array_keys ($source)) { Php.zval $key_name = Php.zval.string_value ($key_iter_name); Php.zval $value_name = Php.array_get ($source, $key_name);";
        }
        string item_name = replace_php_variables (target);
        if (is_native_string_list_expression (source)) {
            variable_types[item_name] = "string";
            return @"foreach (string $item_name in $source) {";
        }
        variable_types[item_name] = "Php.zval";
        return @"foreach (Php.zval $item_name in Php.array_values ($source)) {";
    }

    private string convert_zval_iterable_expression (string expression, int php_line_number) throws Error {
        string? native_local = convert_zval_function_expression_to_native_local (expression, php_line_number);
        if (native_local != null) {
            return native_local;
        }
        return convert_zval_expression (expression, php_line_number);
    }

    private static bool is_native_string_list_expression (string expression) {
        string clean = expression.strip ();
        return clean.has_prefix ("PhpEmbed.preg_split_string_list_static")
            || clean.has_prefix ("PhpEmbed.array_slice_string_list_static")
            || clean.has_prefix ("PhpNative.preg_split_string_list_static")
            || clean.has_prefix ("PhpNative.array_slice_string_list_static");
    }

    private string transpile_zval_switch (string line, int php_line_number) throws Error {
        int open_index = line.index_of ("(");
        int close_index = find_matching_paren (line, open_index);
        if (open_index < 0 || close_index < 0) {
            throw unsupported_syntax (php_line_number, line);
        }
        string expression = convert_zval_expression (line.substring (open_index + 1, close_index - open_index - 1), php_line_number);
        return "switch (Php.zval_to_string (%s)) {".printf (expression);
    }

    private string transpile_zval_case (string line, int php_line_number) throws Error {
        if (!line.has_suffix (":")) {
            throw unsupported_syntax (php_line_number, line);
        }
        string expression = line.substring ("case ".length, line.length - "case ".length - 1).strip ();
        if (is_string_literal (expression)) {
            return "case %s:".printf (convert_string_literal (expression));
        }
        return "case Php.zval_to_string (%s):".printf (convert_zval_expression (expression, php_line_number));
    }

    private string transpile_zval_for (string line, int php_line_number) throws Error {
        int open_index = line.index_of ("(");
        int close_index = find_matching_paren (line, open_index);
        if (open_index < 0 || close_index < 0) {
            throw unsupported_syntax (php_line_number, line);
        }
        var parts = split_top_level (line.substring (open_index + 1, close_index - open_index - 1).strip (), ';');
        if (parts.size != 3) {
            throw unsupported_syntax (php_line_number, line);
        }
        string init = convert_zval_for_init (parts[0].strip (), php_line_number);
        string condition = convert_zval_condition (parts[1].strip (), php_line_number);
        string increment = convert_zval_for_increment (parts[2].strip (), php_line_number);
        return @"for ($init; $condition; $increment) {";
    }

    private string transpile_zval_while (string line, int php_line_number) throws Error {
        int open_index = line.index_of ("(");
        int close_index = find_matching_paren (line, open_index);
        if (open_index < 0 || close_index < 0) {
            throw unsupported_syntax (php_line_number, line);
        }
        string condition = convert_zval_condition (line.substring (open_index + 1, close_index - open_index - 1), php_line_number);
        string rest = line.substring (close_index + 1).strip ();
        if (rest == "" || rest == "{") {
            return @"while ($condition) {";
        }
        string nested = transpile_zval_line (rest, php_line_number);
        return @"while ($condition) { $nested }";
    }

    private string transpile_zval_do_while_end (string line, int php_line_number) throws Error {
        int open_index = line.index_of ("(");
        int close_index = find_matching_paren (line, open_index);
        if (open_index < 0 || close_index < 0) {
            throw unsupported_syntax (php_line_number, line);
        }
        string condition = convert_zval_condition (line.substring (open_index + 1, close_index - open_index - 1), php_line_number);
        return "} while (%s);".printf (condition);
    }

    private string? transpile_zval_dynamic_this_call (string line, int php_line_number) throws Error {
        if (!line.has_prefix ("$this->$") || !line.has_suffix ("();")) {
            return null;
        }

        string variable_name = line.substring ("$this->$".length, line.length - "$this->$".length - "();".length).strip ();
        return build_zval_dynamic_function_dispatch (variable_name, php_line_number, line);
    }

    private string? transpile_zval_new_object_assignment (string line, int php_line_number) throws Error {
        if (!line.has_prefix ("$") || line.index_of ("= new ") < 0 || !line.has_suffix (";")) {
            return null;
        }

        int equals_index = line.index_of ("=");
        string variable_name = line.substring (1, equals_index - 1).strip ();
        string expression = trim_trailing_semicolon (line.substring (equals_index + 1).strip ());
        if (!is_identifier (variable_name) || !expression.has_prefix ("new ")) {
            return null;
        }

        string class_call = expression.substring ("new ".length).strip ();
        string class_name;
        string args = "";
        int open_index = class_call.index_of ("(");
        if (open_index >= 0) {
            if (!class_call.has_suffix (")")) {
                throw unsupported_syntax (php_line_number, line);
            }
            class_name = class_call.substring (0, open_index).strip ();
            args = class_call.substring (open_index + 1, class_call.length - open_index - 2).strip ();
        } else {
            class_name = class_call.strip ();
        }
        if (!is_php_class_name (class_name)) {
            throw unsupported_syntax (php_line_number, line);
        }

        if (!class_exists_for_php2vala (class_name)) {
            throw unsupported_function (php_line_number, class_name);
        }

        object_class_names[variable_name] = class_name;
        string vala_class_name = php_class_name_to_vala (class_name);
        string output = @"$vala_class_name $variable_name = new $vala_class_name ();";
        if (args != "") {
            LocalFunction? constructor = find_class_method (class_name, "__construct");
            if (constructor == null) {
                throw unsupported_function (php_line_number, class_name + "::__construct");
            }
            validate_php_stub_argument_count (constructor, args, php_line_number, class_name + "::__construct");
            output += "\n" + @"$variable_name.__construct ($(convert_zval_argument_list (args, php_line_number)));";
        }
        return output;
    }

    private string? transpile_zval_object_method_call (string line, int php_line_number) throws Error {
        if (!line.has_prefix ("$") || line.index_of ("->") < 0 || !line.has_suffix (");")) {
            return null;
        }

        if (line.has_prefix ("$this->")) {
            string call = line.substring ("$this->".length, line.length - "$this->".length - 1).strip ();
            int open_index = call.index_of ("(");
            if (open_index < 0 || !call.has_suffix (")")) {
                return null;
            }
            string method_name = call.substring (0, open_index).strip ();
            string args = call.substring (open_index + 1, call.length - open_index - 2).strip ();
            if (!is_identifier (method_name)) {
                throw unsupported_syntax (php_line_number, line);
            }
            return "this.%s (%s);".printf (method_name, convert_zval_argument_list (args, php_line_number));
        }

        int arrow_index = line.index_of ("->");
        string variable_name = line.substring (1, arrow_index - 1).strip ();
        string call = line.substring (arrow_index + 2, line.length - arrow_index - 3).strip ();
        int open_index = call.index_of ("(");
        if (!is_identifier (variable_name) || open_index < 0 || !call.has_suffix (")")) {
            return null;
        }

        string method_name = call.substring (0, open_index).strip ();
        string args = call.substring (open_index + 1, call.length - open_index - 2).strip ();
        if (!is_identifier (method_name)) {
            throw unsupported_syntax (php_line_number, line);
        }

        string? class_name = object_class_names[variable_name];
        if (class_name == null) {
            throw unsupported_syntax (php_line_number, line);
        }

        LocalFunction? method = find_class_method (class_name, method_name);
        if (method != null) {
            validate_php_stub_argument_count (method, args, php_line_number, class_name + "::" + method_name);
        }

        return @"$variable_name.$method_name ($(convert_zval_argument_list (args, php_line_number)));";
    }

    private string? transpile_zval_static_method_call (string line, int php_line_number) throws Error {
        int static_call_index = line.index_of ("::");
        if (static_call_index <= 0 || !line.has_suffix (");")) {
            return null;
        }

        string class_name = line.substring (0, static_call_index).strip ();
        string call = line.substring (static_call_index + 2, line.length - static_call_index - 3).strip ();
        int open_index = call.index_of ("(");
        if (!is_identifier (class_name) || open_index < 0 || !call.has_suffix (")")) {
            return null;
        }

        string method_name = call.substring (0, open_index).strip ();
        string args = call.substring (open_index + 1, call.length - open_index - 2).strip ();
        if (!is_identifier (method_name)) {
            throw unsupported_syntax (php_line_number, line);
        }

        LocalFunction? method = find_class_method (class_name, method_name);
        if (!class_exists_for_php2vala (class_name)) {
            throw unsupported_function (php_line_number, class_name);
        }
        if (method == null && class_exists_for_php2vala (class_name)) {
            throw unsupported_function (php_line_number, class_name + "::" + method_name);
        }
        if (method != null && !method.is_static) {
            throw unsupported_syntax (php_line_number, line);
        }
        if (method != null) {
            validate_php_stub_argument_count (method, args, php_line_number, class_name + "::" + method_name);
        }

        PhpClass? method_class = find_php_class (class_name);
        if (method != null && method_class != null && method_class.pwphpbytecode_loader_symbol != null) {
            string loader_symbol = (string) method_class.pwphpbytecode_loader_symbol;
            if (!is_identifier (loader_symbol)) {
                throw unsupported_syntax (php_line_number, loader_symbol);
            }
            mark_pwphpbytecode_method_used (method_class, method_name);
            string converted_args = convert_zval_argument_list (args, php_line_number);
            string extra_args = converted_args == "" ? "" : ", " + converted_args;
            string php_file = source_file_for_line (php_line_number);
            int php_line = source_original_line_for_line (php_line_number);
            return "PhpPwBytecode.ensure_initialized_or_throw (\"%s\", %d);\nif (PhpPwBytecode.begin_load_class (\"%s\", \"%s\", %d)) {\n    %s ();\n    PhpPwBytecode.finish_load_class (\"%s\");\n}\nPhpPwBytecode.call_static_method_zval_params (\"%s\", \"%s\", \"%s\", %d%s);".printf (
                escape_vala_string (php_file),
                php_line,
                escape_vala_string (loader_symbol),
                escape_vala_string (php_file),
                php_line,
                loader_symbol,
                escape_vala_string (loader_symbol),
                escape_vala_string (class_name),
                escape_vala_string (method_name),
                escape_vala_string (php_file),
                php_line,
                extra_args
            );
        }

        return @"$class_name.$method_name ($(convert_zval_argument_list (args, php_line_number)));";
    }

    private PhpClass? find_php_class (string class_name) {
        PhpClass? php_class = php_classes[class_name];
        if (php_class == null) {
            php_class = external_php_classes[class_name];
        }
        return php_class;
    }

    private LocalFunction? find_class_method (string class_name, string method_name) {
        PhpClass? php_class = find_php_class (class_name);
        return find_class_method_in_class (php_class, method_name);
    }

    private LocalFunction? find_class_method_in_class (PhpClass? php_class, string method_name) {
        if (php_class == null) {
            return null;
        }
        foreach (LocalFunction method in php_class.methods) {
            if (method.name == method_name) {
                return method;
            }
        }
        if (php_class.extends_name != "") {
            PhpClass? parent = php_classes[php_class.extends_name];
            if (parent == null) {
                parent = external_php_classes[php_class.extends_name];
            }
            if (parent == null) {
                parent = external_php_classes[resolve_relative_php_class_name (php_class, php_class.extends_name)];
            }
            return find_class_method_in_class (parent, method_name);
        }
        return null;
    }

    private string resolve_relative_php_class_name (PhpClass php_class, string relative_name) {
        foreach (string key in external_php_classes.keys) {
            if (external_php_classes[key] == php_class) {
                int slash = key.last_index_of ("\\");
                if (slash > 0) {
                    return key.substring (0, slash + 1) + relative_name;
                }
            }
        }
        return relative_name;
    }

    private bool class_exists_for_php2vala (string class_name) {
        return php_classes.has_key (class_name) || external_php_classes.has_key (class_name);
    }

    private static string extract_php_namespace (string source) {
        foreach (string raw_line in source.split ("\n")) {
            string line = raw_line.strip ();
            if (line.has_prefix ("namespace ") && line.has_suffix (";")) {
                return line.substring ("namespace ".length, line.length - "namespace ".length - 1).strip ();
            }
        }
        return "";
    }

    private static bool is_php_class_name (string class_name) {
        string clean = class_name.has_prefix ("\\") ? class_name.substring (1) : class_name;
        if (clean == "") {
            return false;
        }
        foreach (string part in clean.split ("\\")) {
            if (!is_identifier (part)) {
                return false;
            }
        }
        return true;
    }

    private static string php_class_name_to_vala (string class_name) {
        return (class_name.has_prefix ("\\") ? class_name.substring (1) : class_name).replace ("\\", ".");
    }

    private void validate_php_stub_argument_count (LocalFunction callable, string args, int php_line_number, string callable_name) throws Error {
        int provided = args.strip () == "" ? 0 : split_top_level (args, ',').size;
        int min_args = 0;
        foreach (LocalParameter parameter in callable.parameters) {
            if (parameter.default_value == null) {
                min_args++;
            }
        }
        int max_args = callable.parameters.size;
        if (provided < min_args || provided > max_args) {
            throw new PhpToValaError.UNSUPPORTED (
                "Invalid argument count for '%s' at %s:%d: expected %d..%d, got %d",
                callable_name,
                source_file_for_line (php_line_number),
                source_original_line_for_line (php_line_number),
                min_args,
                max_args,
                provided
            );
        }
    }

    private string external_zval_call_name (LocalFunction external_function) {
        string call_name = external_function.zval_call_name ?? external_function.name;
        mark_external_function_used (external_function, call_name);
        return call_name;
    }

    private string? transpile_zval_variable_function_call (string line, int php_line_number) throws Error {
        if (!line.has_prefix ("$") || !line.has_suffix ("();") || line.index_of ("->") >= 0 || line.index_of ("=") >= 0) {
            return null;
        }

        string variable_name = line.substring (1, line.length - 1 - "();".length).strip ();
        if (local_closure_variables.has_key (variable_name)) {
            return "%s ();".printf (variable_name);
        }
        return build_zval_dynamic_function_dispatch (variable_name, php_line_number, line);
    }

    private string build_zval_dynamic_function_dispatch (string variable_name, int php_line_number, string source_line) throws Error {
        if (!is_identifier (variable_name)) {
            throw unsupported_syntax (php_line_number, source_line);
        }

        var output = new StringBuilder ();
        string selector = "Php.zval_to_string (%s)".printf (replace_php_variables (variable_name));
        bool first = true;
        foreach (string function_name in local_functions.keys) {
            LocalFunction? local_function = local_functions[function_name];
            if (local_function == null || local_function.parameters.size != 0) {
                continue;
            }
            output.append_printf (
                "%s (%s == \"%s\") { %s (); }",
                first ? "if" : "else if",
                selector,
                function_name,
                function_name
            );
            first = false;
        }

        if (first) {
            throw unsupported_syntax (php_line_number, source_line);
        }

        output.append (" else { }");
        return output.str;
    }

    private string convert_zval_for_init (string expression, int php_line_number) throws Error {
        int equals_index = expression.index_of ("=");
        if (equals_index > 0) {
            string name = replace_php_variables (expression.substring (0, equals_index).strip ());
            string value = expression.substring (equals_index + 1).strip ();
            variable_types[name] = "int";
            string? native_value = convert_native_int_expression (value, php_line_number);
            return "int %s = %s".printf (
                name,
                native_value ?? "Php.zval_to_int (%s)".printf (convert_zval_expression (value, php_line_number))
            );
        }
        return expression;
    }

    private string convert_zval_for_increment (string expression, int php_line_number) throws Error {
        string converted = replace_php_variables (expression);
        return converted;
    }

    private string? transpile_zval_increment (string line, int php_line_number) throws Error {
        if (!line.has_suffix ("++;") && !line.has_suffix ("--;")) {
            return null;
        }
        string name = line.substring (0, line.length - 3).strip ();
        if (!name.has_prefix ("$")) {
            return null;
        }
        name = replace_php_variables (name);
        if (!is_identifier (name)) {
            throw unsupported_syntax (php_line_number, line);
        }
        if (!variable_types.has_key (name)) {
            variable_types[name] = "Php.zval";
            string initial = line.has_suffix ("++;") ? "Php.zval.integer (1)" : "Php.zval.integer (-1)";
            return @"Php.zval $name = $initial;";
        }
        string helper = line.has_suffix ("++;") ? "Php.add_zval" : "Php.sub_zval";
        return @"$name = $helper ($name, Php.zval.integer (1));";
    }

    private string? transpile_zval_list_assignment (string line, int php_line_number) throws Error {
        if (!line.has_prefix ("list(") && !line.has_prefix ("list (")) {
            return null;
        }
        int open_index = line.index_of ("(");
        int close_index = find_matching_paren (line, open_index);
        if (open_index < 0 || close_index < 0) {
            throw unsupported_syntax (php_line_number, line);
        }

        string rest = trim_trailing_semicolon (line.substring (close_index + 1).strip ());
        if (!rest.has_prefix ("=")) {
            throw unsupported_syntax (php_line_number, line);
        }

        string temp_name = "__php2vala_list_%d".printf (temp_variable_counter++);
        var output = new StringBuilder ();
        output.append_printf ("Php.zval %s = %s;", temp_name, convert_zval_expression (rest.substring (1).strip (), php_line_number));

        var targets = split_top_level (line.substring (open_index + 1, close_index - open_index - 1), ',');
        for (int i = 0; i < targets.size; i++) {
            string target = targets[i].strip ();
            if (target == "") {
                continue;
            }
            if (!target.has_prefix ("$")) {
                throw unsupported_syntax (php_line_number, line);
            }
            string name = replace_php_variables (target);
            if (!is_identifier (name)) {
                throw unsupported_syntax (php_line_number, line);
            }
            string value = "Php.array_get (%s, Php.zval.integer (%d))".printf (temp_name, i);
            output.append ("\n");
            if (!variable_types.has_key (name)) {
                variable_types[name] = "Php.zval";
                output.append_printf ("Php.zval %s = %s;", name, value);
            } else {
                output.append_printf ("%s = %s;", name, value);
            }
        }

        return output.str;
    }

    private string? transpile_zval_array_assignment (string line, int php_line_number) throws Error {
        if (!line.has_prefix ("$") || !line.has_suffix (";")) {
            return null;
        }
        int bracket_index = line.index_of ("[");
        int equals_index = line.index_of ("=");
        if (bracket_index <= 1 || equals_index <= bracket_index) {
            return null;
        }
        string name = line.substring (1, bracket_index - 1).strip ();
        if (!is_identifier (name)) {
            return null;
        }
        string storage_name = ensure_variable_alias_for_name (name);
        string access = line.substring (bracket_index, equals_index - bracket_index).strip ();
        var prelude = new StringBuilder ();
        string value = convert_zval_value_expression_with_prelude (trim_trailing_semicolon (line.substring (equals_index + 1).strip ()), php_line_number, prelude);
        var keys = parse_zval_array_keys (access, php_line_number);
        string init = "";
        if (!variable_types.has_key (storage_name)) {
            variable_types[storage_name] = "Php.zval";
            init = "Php.zval %s = Php.array_new ();\n".printf (storage_name);
        }
        string prefix = init + prelude.str;
        if (keys.size == 1) {
            if (keys[0] == "") {
                return prefix + @"Php.array_append ($storage_name, $value);";
            }
            return prefix + @"Php.array_set ($storage_name, $(keys[0]), $value);";
        }
        if (keys.size == 2) {
            if (keys[1] == "") {
                return prefix + @"Php.array_append (Php.array_ensure ($storage_name, $(keys[0])), $value);";
            }
            return prefix + @"Php.array_set (Php.array_ensure ($storage_name, $(keys[0])), $(keys[1]), $value);";
        }
        throw unsupported_syntax (php_line_number, line);
    }

    private ArrayList<string> parse_zval_array_keys (string access, int php_line_number) throws Error {
        var keys = new ArrayList<string> ();
        int index = 0;
        while (index < access.length) {
            if (access[index] != '[') {
                throw unsupported_syntax (php_line_number, access);
            }
            int end = find_matching_bracket (access, index);
            if (end < 0) {
                throw unsupported_syntax (php_line_number, access);
            }
            string key = access.substring (index + 1, end - index - 1).strip ();
            if (key == "") {
                keys.add ("");
            } else {
                string converted_key = convert_zval_expression (key, php_line_number);
                keys.add (simplify_zval_int_arithmetic_call (converted_key) ?? converted_key);
            }
            index = end + 1;
        }
        return keys;
    }

    private string? simplify_zval_int_arithmetic_call (string expression) {
        string clean = expression.strip ();
        int open_index = clean.index_of ("(");
        if (open_index <= 0 || find_matching_paren (clean, open_index) != clean.length - 1) {
            return null;
        }
        string call_name = clean.substring (0, open_index).strip ();
        string op;
        switch (call_name) {
        case "Php.add_zval":
            op = "+";
            break;
        case "Php.sub_zval":
            op = "-";
            break;
        case "Php.mul_zval":
            op = "*";
            break;
        default:
            return null;
        }
        string args = clean.substring (open_index + 1, clean.length - open_index - 2);
        var parts = split_top_level (args, ',');
        if (parts.size != 2) {
            return null;
        }
        string? left = unwrap_zval_constructor_arg (parts[0].strip (), "Php.zval.integer (");
        string? right = unwrap_zval_constructor_arg (parts[1].strip (), "Php.zval.integer (");
        if (left == null || right == null) {
            return null;
        }
        return "Php.zval.integer (%s %s %s)".printf (left, op, right);
    }

    private string? transpile_zval_compound_assignment (string line, int php_line_number) throws Error {
        if (!line.has_prefix ("$") || !line.has_suffix (";")) {
            return null;
        }
        string clean = trim_trailing_semicolon (line.substring (1));
        string[] operators = {".=", "/=", "*=", "+=", "-="};
        foreach (string op in operators) {
            int index = clean.index_of (op);
            if (index <= 0) {
                continue;
            }
            string lhs = replace_php_variables (clean.substring (0, index).strip ());
            string raw_rhs = clean.substring (index + op.length).strip ();
            if (op == "+=" && is_short_array_expression (raw_rhs) && is_identifier (lhs)) {
                return transpile_zval_short_array_merge (lhs, raw_rhs, php_line_number);
            }
            string rhs = convert_zval_expression (raw_rhs, php_line_number);
            int bracket_index = lhs.index_of ("[");
            if (bracket_index > 0) {
                string name = lhs.substring (0, bracket_index);
                var keys = parse_zval_array_keys (lhs.substring (bracket_index), php_line_number);
                if (keys.size == 1) {
                    string current = "Php.array_get (%s, %s)".printf (name, keys[0]);
                    string op_name = op == ".=" ? "Php.concat" : (op == "+=" ? "Php.add_zval" : (op == "-=" ? "Php.sub_zval" : "Php.mul_zval"));
                    if (op == "/=") {
                        mark_function_used ("div");
                        return @"Php.array_set ($name, $(keys[0]), Php.div_zval ($current, $rhs$(source_location_args (php_line_number))));";
                    }
                    return @"Php.array_set ($name, $(keys[0]), $op_name ($current, $rhs));";
                }
            }
            string current = convert_zval_expression ("$" + lhs, php_line_number);
            string op_name = op == ".=" ? "Php.concat" : (op == "+=" ? "Php.add_zval" : (op == "-=" ? "Php.sub_zval" : "Php.mul_zval"));
            if (op == "/=") {
                op_name = "php2vala_div";
            }
            if (op == ".=") {
                return @"Php.concat_assign ($current, $rhs);";
            }
            if (op == "/=") {
                mark_function_used ("div");
                return @"$lhs = Php.div_zval ($current, $rhs$(source_location_args (php_line_number)));";
            }
            return @"$lhs = $op_name ($current, $rhs);";
        }
        return null;
    }

    private string transpile_zval_exit (string expression, int php_line_number, string construct_name = "exit") throws Error {
        string clean_expression = trim_trailing_semicolon (expression).strip ();
        if (optimize_local_vars || optimize_file_func_vars) {
            string static_base = construct_name == "die" ? "die" : "exit";
            string? static_call = null;
            if (clean_expression == "") {
                string function_name = map_php_function_name (static_base + "_no_args", php_line_number);
                static_call = "%s (%s)".printf (function_name, source_location_args (php_line_number).substring (2));
            } else if (int_regex.match (clean_expression)) {
                string function_name = map_php_function_name (static_base + "_int", php_line_number);
                static_call = "%s (%s%s)".printf (function_name, clean_expression, source_location_args (php_line_number));
            } else if (is_string_literal (clean_expression)) {
                string function_name = map_php_function_name (static_base + "_string", php_line_number);
                static_call = "%s (Php.zval_to_string (%s)%s)".printf (
                    function_name,
                    convert_zval_expression (clean_expression, php_line_number),
                    source_location_args (php_line_number)
                );
            } else if (is_php_int_parameter_expression (clean_expression)) {
                string function_name = map_php_function_name (static_base + "_int", php_line_number);
                static_call = "%s (%s%s)".printf (
                    function_name,
                    convert_zval_arg_for_runtime_static_type (convert_zval_expression (clean_expression, php_line_number), "int"),
                    source_location_args (php_line_number)
                );
            }
            if (static_call != null) {
                return transpiling_local_function
                    ? "Process.exit (%s);".printf (static_call)
                    : "return %s;".printf (static_call);
            }
        }

        string function_name = map_php_function_name (construct_name, php_line_number);
        string converted_expression = clean_expression == "" ? "Php.zval.null_value ()" : convert_zval_expression (clean_expression, php_line_number);
        string call = "%s (%s%s)".printf (function_name, converted_expression, source_location_args (php_line_number));
        string status = "Php.zval_to_int (%s)".printf (call);
        return transpiling_local_function
            ? "Process.exit (%s);".printf (status)
            : "return %s;".printf (status);
    }

    private bool is_php_int_parameter_expression (string expression) {
        string clean = expression.strip ();
        if (!clean.has_prefix ("$")) {
            return false;
        }
        string name = replace_php_variables (clean);
        if (!is_identifier (name) || !current_parameter_php_types.has_key (name)) {
            return false;
        }
        string type_name = current_parameter_php_types[name].down ().strip ();
        return type_name == "int" || type_name == "integer";
    }

    private string convert_zval_expression_no_throw (string expression, int php_line_number) {
        try {
            return convert_zval_expression (expression, php_line_number);
        } catch (Error e) {
            return "Php.zval.null_value ()";
        }
    }

    private string convert_zval_return_expression (string expression, int php_line_number) throws Error {
        string converted = convert_zval_expression (expression, php_line_number);
        if (!transpiling_local_function || !local_functions.has_key (current_local_function_name)) {
            return converted;
        }

        LocalFunction? local_function = local_functions[current_local_function_name];
        if (local_function == null || local_function.return_type == null) {
            return converted;
        }

        switch (local_function.return_type.down ()) {
        case "int":
        case "integer":
            return "Php.zval.integer (Php.zval_to_int (%s))".printf (converted);
        case "float":
        case "double":
            return "Php.zval.double_number (Php.zval_to_double (%s))".printf (converted);
        case "string":
            return "Php.zval.string_value (Php.zval_to_string (%s))".printf (converted);
        case "bool":
        case "boolean":
            return "Php.zval.boolean (Php.truthy (%s))".printf (converted);
        default:
            return converted;
        }
    }

    private string convert_zval_expression (string expression, int php_line_number) throws Error {
        string result = trim_trailing_semicolon (expression.strip ());
        if (result == "") {
            return "Php.zval.null_value ()";
        }
        if (result.has_prefix ("@")) {
            return convert_zval_expression (result.substring (1).strip (), php_line_number);
        }
        if (result.has_prefix ("(") && result.has_suffix (")") && find_matching_paren (result, 0) == result.length - 1) {
            return convert_zval_expression (result.substring (1, result.length - 2), php_line_number);
        }
        string? object_method_expression = convert_zval_object_method_expression (result, php_line_number);
        if (object_method_expression != null) {
            return object_method_expression;
        }
        if (is_standalone_php_variable_access (result)) {
            return convert_zval_variable_or_access (result, php_line_number);
        }
        if (result.has_prefix ("[") && result.has_suffix ("]") && find_matching_bracket (result, 0) == result.length - 1) {
            return convert_zval_short_array_expression (result, php_line_number);
        }
        string? coalesce_expression = convert_zval_coalesce_expression (result, php_line_number);
        if (coalesce_expression != null) {
            return coalesce_expression;
        }
        string? ternary_expression = convert_zval_ternary_expression (result, php_line_number);
        if (ternary_expression != null) {
            return ternary_expression;
        }
        string? comparison_expression = convert_zval_comparison_expression (result, php_line_number);
        if (comparison_expression != null) {
            return comparison_expression;
        }
        string? cast_expression = convert_zval_cast_expression (result, php_line_number);
        if (cast_expression != null) {
            return cast_expression;
        }
        if (result == "TRUE" || result == "true") {
            return "Php.zval.boolean (true)";
        }
        if (result == "FALSE" || result == "false") {
            return "Php.zval.boolean (false)";
        }
        if (result == "NULL" || result == "null") {
            return "Php.zval.null_value ()";
        }
        if (result == "PHP_EOL") {
            return "Php.zval.string_value (\"\\n\")";
        }
        if (result == "TOKEN_PARSE") {
            return "Php.zval.integer (1)";
        }
        if (defined_constants.has_key (result)) {
            return defined_constants[result];
        }
        string? known_constant = convert_known_php_constant (result);
        if (known_constant != null) {
            return known_constant;
        }
        if (is_string_literal (result)) {
            return "Php.zval.string_value (%s)".printf (convert_string_literal (result));
        }
        if (int_regex.match (result)) {
            return "Php.zval.integer (%s)".printf (result);
        }
        if (double_regex.match (result)) {
            return "Php.zval.double_number (%s)".printf (result);
        }
        if (result.has_prefix ("-") && result.length > 1 && !double_regex.match (result) && !int_regex.match (result)) {
            return "Php.neg_zval (%s)".printf (convert_zval_expression (result.substring (1).strip (), php_line_number));
        }
        if (result.has_prefix ("++$")) {
            string name = replace_php_variables (result.substring (2).strip ());
            if (is_identifier (name)) {
                return "Php.zval.integer (++%s)".printf (variable_aliases.has_key (name) ? variable_aliases[name] : name);
            }
        }
        if (result.has_prefix ("++")) {
            string name = result.substring (2).strip ();
            if (is_identifier (name)) {
                return "Php.zval.integer (++%s)".printf (variable_aliases.has_key (name) ? variable_aliases[name] : name);
            }
        }
        if (result.has_prefix ("--$")) {
            string name = replace_php_variables (result.substring (2).strip ());
            if (is_identifier (name)) {
                return "Php.zval.integer (--%s)".printf (variable_aliases.has_key (name) ? variable_aliases[name] : name);
            }
        }
        if (result.has_prefix ("--")) {
            string name = result.substring (2).strip ();
            if (is_identifier (name)) {
                return "Php.zval.integer (--%s)".printf (variable_aliases.has_key (name) ? variable_aliases[name] : name);
            }
        }
        MatchInfo variable_call_match;
        if ((new Regex ("^\\$([A-Za-z_][A-Za-z0-9_]*)\\s*\\((.*)\\)$")).match (result, 0, out variable_call_match)) {
            string variable_name = variable_call_match.fetch (1);
            string args = variable_call_match.fetch (2).strip ();
            if (local_closure_variables.has_key (variable_name)) {
                if (args != "") {
                    throw unsupported_syntax (php_line_number, expression);
                }
                return "%s ()".printf (variable_name);
            }
        }
        string? binary = convert_zval_binary_expression (result, '.', "Php.concat", php_line_number);
        if (binary != null) return binary;
        binary = convert_zval_binary_expression (result, '+', "Php.add_zval", php_line_number);
        if (binary != null) return binary;
        binary = convert_zval_binary_expression (result, '-', "Php.sub_zval", php_line_number);
        if (binary != null) return binary;
        binary = convert_zval_binary_expression (result, '*', "Php.mul_zval", php_line_number);
        if (binary != null) return binary;
        binary = convert_zval_division_expression (result, php_line_number);
        if (binary != null) return binary;
        string? bitwise_or = convert_zval_bitwise_or_expression (result, php_line_number);
        if (bitwise_or != null) return bitwise_or;
        if (result.has_prefix ("$")) {
            return convert_zval_variable_or_access (result, php_line_number);
        }
        string? class_constant = convert_zval_class_constant (result);
        if (class_constant != null) {
            return class_constant;
        }
        MatchInfo match;
        if (function_expression_regex.match (result, 0, out match)) {
            string function_name = match.fetch (1);
            string args = match.fetch (2).strip ();
            if (local_functions.has_key (function_name)) {
                LocalFunction? local_function = local_functions[function_name];
                return (local_function != null ? zval_local_function_name (local_function) : function_name) + " (" + convert_zval_argument_list (args, php_line_number) + ")";
            }
            if (external_functions.has_key (function_name)) {
                LocalFunction? external_function = external_functions[function_name];
                if (external_function != null) {
                    validate_php_stub_argument_count (external_function, args, php_line_number, function_name);
                    if (optimize_cross_file_func_vars
                        && file_func_vars_optimizer.can_call_external_static (external_function)
                        && external_function.static_return_type != "void") {
                        return file_func_vars_optimizer.external_function_call_as_zval_value (external_function, args, php_line_number);
                    }
                }
                return external_function != null ? external_zval_call_name (external_function) + " (" + convert_zval_argument_list (args, php_line_number) + ")" : function_name + " (" + convert_zval_argument_list (args, php_line_number) + ")";
            }
            return convert_zval_function_call (function_name, args, php_line_number);
        }
        if (variable_types.has_key (result)) {
            return variable_types[result] == "int" ? "Php.zval.integer (%s)".printf (result) : result;
        }
        return result;
    }

    private string? convert_zval_ternary_expression (string expression, int php_line_number) throws Error {
        int question_index = find_top_level_char_index (expression, '?');
        if (question_index < 0) {
            return null;
        }
        int colon_index = find_top_level_char_index (expression.substring (question_index + 1), ':');
        if (colon_index < 0) {
            throw unsupported_syntax (php_line_number, expression);
        }
        colon_index += question_index + 1;

        string condition = convert_zval_condition (expression.substring (0, question_index).strip (), php_line_number);
        string true_value = convert_zval_expression (expression.substring (question_index + 1, colon_index - question_index - 1).strip (), php_line_number);
        string false_value = convert_zval_expression (expression.substring (colon_index + 1).strip (), php_line_number);
        return "(%s ? %s : %s)".printf (condition, true_value, false_value);
    }

    private string? convert_zval_coalesce_expression (string expression, int php_line_number) throws Error {
        int index = find_top_level_operator_index (expression, "??");
        if (index < 0) {
            return null;
        }
        string left = convert_zval_expression (expression.substring (0, index).strip (), php_line_number);
        string right = convert_zval_expression (expression.substring (index + 2).strip (), php_line_number);
        return "(%s.is_null () ? %s : %s)".printf (left, right, left);
    }

    private string? convert_zval_comparison_expression (string expression, int php_line_number) throws Error {
        foreach (string op in new string[] {"!==", "===", "!=", "==", ">=", "<=", ">", "<"}) {
            if (find_top_level_operator_index (expression, op) > 0) {
                return "Php.zval.boolean (%s)".printf (convert_zval_condition (expression, php_line_number));
            }
        }
        return null;
    }

    private string? convert_zval_class_constant (string expression) {
        int index = expression.index_of ("::");
        if (index <= 0) {
            return null;
        }
        string class_name = expression.substring (0, index).strip ();
        string constant_name = expression.substring (index + 2).strip ();
        if (!is_php_class_name (class_name) || !is_identifier (constant_name)) {
            return null;
        }
        if (!class_exists_for_php2vala (class_name)) {
            return null;
        }
        return "Php.zval.string_value (%s.%s)".printf (php_class_name_to_vala (class_name), constant_name);
    }

    private string? convert_known_php_constant (string expression) {
        switch (expression) {
        case "STREAM_CLIENT_CONNECT":
            return "Php.zval.integer (4)";
        case "PREG_SET_ORDER":
            return "Php.zval.integer (2)";
        case "JSON_PRETTY_PRINT":
            return "Php.zval.integer (128)";
        case "JSON_UNESCAPED_SLASHES":
            return "Php.zval.integer (64)";
        default:
            return null;
        }
    }

    private string? convert_zval_bitwise_or_expression (string expression, int php_line_number) throws Error {
        var parts = split_top_level (expression, '|');
        if (parts.size <= 1) {
            return null;
        }
        string result = convert_zval_arg_for_runtime_static_type (convert_zval_expression (parts[0].strip (), php_line_number), "int");
        for (int i = 1; i < parts.size; i++) {
            result = "%s | %s".printf (
                result,
                convert_zval_arg_for_runtime_static_type (convert_zval_expression (parts[i].strip (), php_line_number), "int")
            );
        }
        return "Php.zval.integer (%s)".printf (result);
    }

    private string? convert_zval_object_method_expression (string expression, int php_line_number) throws Error {
        int arrow_index = expression.index_of ("->");
        if (!expression.has_prefix ("$") || arrow_index <= 0) {
            return null;
        }
        string object_expression = expression.substring (0, arrow_index).strip ();
        string call = expression.substring (arrow_index + 2).strip ();
        int open_index = call.index_of ("(");
        if (open_index < 0 || !call.has_suffix (")") || find_matching_paren (call, open_index) != call.length - 1) {
            return null;
        }
        string method_name = call.substring (0, open_index).strip ();
        if (!is_identifier (method_name)) {
            return null;
        }
        string args = call.substring (open_index + 1, call.length - open_index - 2).strip ();
        string target;
        if (object_expression == "$this") {
            target = "this";
        } else {
            string variable_name = replace_php_variables (object_expression);
            if (!is_identifier (variable_name)) {
                return null;
            }
            target = variable_aliases.has_key (variable_name) ? variable_aliases[variable_name] : variable_name;
            string? class_name = object_class_names[variable_name];
            if (class_name != null) {
                LocalFunction? method = find_class_method (class_name, method_name);
                if (method != null) {
                    validate_php_stub_argument_count (method, args, php_line_number, class_name + "::" + method_name);
                }
            }
        }
        return "%s.%s (%s)".printf (target, method_name, convert_zval_argument_list (args, php_line_number));
    }

    private string? convert_zval_binary_expression (string expression, char op, string helper_name, int php_line_number) throws Error {
        var parts = split_top_level (expression, op);
        if (parts.size <= 1) {
            return null;
        }
        if (op == '.' && parts.size > 2) {
            var converted_parts = new StringBuilder ();
            for (int i = 0; i < parts.size; i++) {
                if (i > 0 && is_decimal_concat_false_positive (parts[i - 1], parts[i])) {
                    return null;
                }
                if (converted_parts.len > 0) {
                    converted_parts.append (", ");
                }
                converted_parts.append (convert_zval_expression (parts[i].strip (), php_line_number));
            }
            return "Php.concat_many (%s)".printf (converted_parts.str);
        }
        if (op == '+' || op == '-' || op == '*') {
            string? native_int = convert_zval_int_arithmetic_expression (parts, op, php_line_number);
            if (native_int != null) {
                return "Php.zval.integer (%s)".printf (native_int);
            }
        }
        string result = convert_zval_expression (parts[0].strip (), php_line_number);
        for (int i = 1; i < parts.size; i++) {
            if (op == '.' && is_decimal_concat_false_positive (parts[i - 1], parts[i])) {
                return null;
            }
            result = "%s (%s, %s)".printf (helper_name, result, convert_zval_expression (parts[i].strip (), php_line_number));
        }
        return result;
    }

    private string? convert_zval_int_arithmetic_expression (ArrayList<string> parts, char op, int php_line_number) throws Error {
        if (parts.size <= 1) {
            return null;
        }
        var converted_parts = new ArrayList<string> ();
        foreach (string raw_part in parts) {
            string? native_part = convert_native_int_expression (raw_part.strip (), php_line_number);
            if (native_part == null) {
                return null;
            }
            converted_parts.add (native_part);
        }
        return join_strings (converted_parts, " %c ".printf (op));
    }

    private string convert_zval_short_array_expression (string expression, int php_line_number) throws Error {
        string inner = expression.substring (1, expression.length - 2).strip ();
        if (inner == "") {
            return "Php.array_new ()";
        }

        var values = new ArrayList<string> ();
        foreach (string raw_part in split_top_level (inner, ',')) {
            string part = raw_part.strip ();
            if (part == "") {
                continue;
            }
            if (find_top_level_double_arrow_index (part) >= 0) {
                throw unsupported_syntax (php_line_number, expression);
            }
            values.add (convert_zval_expression (part, php_line_number));
        }

        return "Php.array_from_values_zval ({ %s })".printf (join_strings (values, ", "));
    }

    private string? transpile_zval_short_array_assignment (string variable_name, string raw_expression, int php_line_number) throws Error {
        string expression = trim_trailing_semicolon (raw_expression.strip ());
        if (!is_short_array_expression (expression)) {
            return null;
        }

        var single_values = split_top_level (short_array_inner (expression), ',');
        if (single_values.size == 1) {
            string raw_single_value = single_values[0].strip ();
            if (raw_single_value != "") {
                string single_value = convert_zval_expression (raw_single_value, php_line_number);
                if (is_runtime_object_type (infer_expression_type (single_value))) {
                    stream_select_read_resources[variable_name] = single_value;
                    bool declare_stream_array = !variable_types.has_key (variable_name);
                    variable_types[variable_name] = "Php.zval";
                    return declare_stream_array ? @"Php.zval $variable_name = Php.array_new ();" : @"$variable_name = Php.array_new ();";
                }
            }
        }

        bool declare = !variable_types.has_key (variable_name);
        variable_types[variable_name] = "Php.zval";
        var output = new StringBuilder ();
        output.append_printf (declare ? "Php.zval %s = Php.array_new ();" : "%s = Php.array_new ();", variable_name);
        emit_zval_short_array_entries (output, variable_name, short_array_inner (expression), php_line_number, false);
        return output.str;
    }

    private string? transpile_zval_stream_select_assignment (string storage_name, string raw_expression, int php_line_number) throws Error {
        string expression = trim_trailing_semicolon (raw_expression.strip ());
        if (expression.has_prefix ("@")) {
            expression = expression.substring (1).strip ();
        }
        MatchInfo match;
        if (!function_expression_regex.match (expression, 0, out match) || match.fetch (1) != "stream_select") {
            return null;
        }

        var args = split_top_level (match.fetch (2).strip (), ',');
        if (args.size != 5) {
            return null;
        }

        string read_name = replace_php_variables (args[0].strip ());
        if (!is_identifier (read_name)) {
            return null;
        }

        string? stream_resource = stream_select_read_resources[read_name];
        if (stream_resource == null && read_name == "read" && current_class_typed_properties.has_key ("fp")) {
            stream_resource = "this.fp";
        }
        if (stream_resource == null) {
            return null;
        }

        string stream = stream_resource;
        string seconds = convert_zval_expression (args[3].strip (), php_line_number);
        string microseconds = convert_zval_expression (args[4].strip (), php_line_number);
        string function_name = map_php_runtime_function_name ("stream_select", "stream_select_read_static", php_line_number);
        string call = "Php.zval.integer (%s (%s, %s, %s%s))".printf (
            function_name,
            stream,
            convert_zval_arg_for_runtime_static_type (seconds, "int"),
            convert_zval_arg_for_runtime_static_type (microseconds, "int"),
            source_location_args (php_line_number)
        );

        if (!variable_types.has_key (storage_name)) {
            variable_types[storage_name] = "Php.zval";
            return @"Php.zval $storage_name = $call;";
        }
        return @"$storage_name = $call;";
    }

    private string? transpile_zval_array_filter_assignment (string variable_name, string raw_expression, int php_line_number) throws Error {
        string expression = trim_trailing_semicolon (raw_expression.strip ());
        if (expression.has_prefix ("array_values")) {
            MatchInfo array_values_match;
            if (function_expression_regex.match (expression, 0, out array_values_match) && array_values_match.fetch (1) == "array_values") {
                var values_args = split_top_level (array_values_match.fetch (2).strip (), ',');
                if (values_args.size == 1) {
                    expression = values_args[0].strip ();
                }
            }
        }
        if (!expression.has_prefix ("array_filter")) {
            return null;
        }
        MatchInfo match;
        if (!function_expression_regex.match (expression, 0, out match) || match.fetch (1) != "array_filter") {
            return null;
        }

        var args = split_top_level (match.fetch (2).strip (), ',');
        if (args.size != 2) {
            return null;
        }
        string source = args[0].strip ();
        string predicate = args[1].strip ();

        bool declare = !variable_types.has_key (variable_name);
        variable_types[variable_name] = "Php.zval";
        var output = new StringBuilder ();
        output.append_printf (declare ? "Php.zval %s = Php.array_new ();" : "%s = Php.array_new ();", variable_name);
        if (is_short_array_expression (source) && is_null_filter_arrow (predicate)) {
            emit_zval_short_array_entries (output, variable_name, short_array_inner (source), php_line_number, true);
            return output.str;
        }

        string parameter_name;
        string predicate_body;
        if (!parse_arrow_predicate (predicate, out parameter_name, out predicate_body)) {
            return null;
        }
        variable_types[parameter_name] = "Php.zval";
        string source_expression = convert_zval_expression (source, php_line_number);
        string condition = convert_zval_condition (predicate_body, php_line_number);
        output.append_printf ("\nforeach (Php.zval %s in Php.array_values (%s)) {\n    if (%s) {\n        Php.array_append (%s, %s);\n    }\n}", parameter_name, source_expression, condition, variable_name, parameter_name);
        return output.str;
    }

    private string transpile_zval_short_array_merge (string variable_name, string raw_expression, int php_line_number) throws Error {
        string expression = trim_trailing_semicolon (raw_expression.strip ());
        if (!variable_types.has_key (variable_name)) {
            variable_types[variable_name] = "Php.zval";
        }
        var output = new StringBuilder ();
        emit_zval_short_array_entries (output, variable_name, short_array_inner (expression), php_line_number, false);
        return output.str;
    }

    private void emit_zval_short_array_entries (StringBuilder output, string array_name, string inner, int php_line_number, bool filter_null_values) throws Error {
        foreach (string raw_part in split_top_level (inner, ',')) {
            string part = raw_part.strip ();
            if (part == "") {
                continue;
            }

            int arrow_index = find_top_level_double_arrow_index (part);
            string key = "";
            string value_expression = part;
            if (arrow_index >= 0) {
                key = part.substring (0, arrow_index).strip ();
                value_expression = part.substring (arrow_index + 2).strip ();
            }

            var prelude = new StringBuilder ();
            string value = convert_zval_value_expression_with_prelude (value_expression, php_line_number, prelude);
            output.append ("\n");
            output.append (prelude.str);
            if (filter_null_values) {
                string temp_name = "__php2vala_array_value_%d".printf (temp_variable_counter++);
                output.append_printf ("Php.zval %s = %s;\n", temp_name, value);
                if (arrow_index >= 0) {
                    output.append_printf ("if (!%s.is_null ()) {\n    Php.array_set (%s, %s, %s);\n}", temp_name, array_name, convert_zval_expression (key, php_line_number), temp_name);
                } else {
                    output.append_printf ("if (!%s.is_null ()) {\n    Php.array_append (%s, %s);\n}", temp_name, array_name, temp_name);
                }
                continue;
            }

            if (arrow_index >= 0) {
                output.append_printf ("Php.array_set (%s, %s, %s);", array_name, convert_zval_expression (key, php_line_number), value);
            } else {
                output.append_printf ("Php.array_append (%s, %s);", array_name, value);
            }
        }
    }

    private string convert_zval_value_expression_with_prelude (string expression, int php_line_number, StringBuilder prelude) throws Error {
        string clean = trim_trailing_semicolon (expression.strip ());
        if (is_short_array_expression (clean) && is_associative_short_array_expression (clean)) {
            string temp_name = "__php2vala_array_%d".printf (temp_variable_counter++);
            prelude.append_printf ("Php.zval %s = Php.array_new ();\n", temp_name);
            emit_zval_short_array_entries (prelude, temp_name, short_array_inner (clean), php_line_number, false);
            prelude.append ("\n");
            return temp_name;
        }
        return convert_zval_expression (clean, php_line_number);
    }

    private bool is_null_filter_arrow (string expression) {
        string clean = expression.strip ();
        int arrow_index = find_top_level_double_arrow_index (clean);
        if (!clean.has_prefix ("fn") || arrow_index < 0) {
            return false;
        }
        string body = clean.substring (arrow_index + 2).strip ();
        return body.has_suffix ("!== null") || body.has_suffix ("!= null");
    }

    private bool parse_arrow_predicate (string expression, out string parameter_name, out string body) {
        parameter_name = "";
        body = "";
        string clean = expression.strip ();
        int arrow_index = find_top_level_double_arrow_index (clean);
        if (!clean.has_prefix ("fn") || arrow_index < 0) {
            return false;
        }
        int open_paren = clean.index_of ("(");
        int close_paren = find_matching_paren (clean, open_paren);
        if (open_paren < 0 || close_paren < 0 || close_paren > arrow_index) {
            return false;
        }
        string raw_parameter = clean.substring (open_paren + 1, close_paren - open_paren - 1).strip ();
        if (!raw_parameter.has_prefix ("$")) {
            return false;
        }
        parameter_name = replace_php_variables (raw_parameter);
        if (!is_identifier (parameter_name)) {
            return false;
        }
        body = clean.substring (arrow_index + 2).strip ();
        return body != "";
    }

    private bool is_short_array_expression (string expression) {
        string clean = trim_trailing_semicolon (expression.strip ());
        return clean.has_prefix ("[") && clean.has_suffix ("]") && find_matching_bracket (clean, 0) == clean.length - 1;
    }

    private string short_array_inner (string expression) {
        string clean = trim_trailing_semicolon (expression.strip ());
        return clean.substring (1, clean.length - 2).strip ();
    }

    private bool is_associative_short_array_expression (string expression) {
        if (!is_short_array_expression (expression)) {
            return false;
        }
        foreach (string raw_part in split_top_level (short_array_inner (expression), ',')) {
            if (find_top_level_double_arrow_index (raw_part.strip ()) >= 0) {
                return true;
            }
        }
        return false;
    }

    private string? convert_zval_cast_expression (string expression, int php_line_number) throws Error {
        string[] casts = {"float", "double", "int", "integer", "string", "bool", "boolean"};
        foreach (string cast_name in casts) {
            string prefix = "(" + cast_name + ")";
            if (!expression.has_prefix (prefix)) {
                continue;
            }

            string inner = convert_zval_expression (expression.substring (prefix.length).strip (), php_line_number);
            switch (cast_name) {
            case "float":
            case "double":
                return "Php.zval.double_number (Php.zval_to_double (%s))".printf (inner);
            case "int":
            case "integer":
                return "Php.zval.integer (Php.zval_to_int (%s))".printf (inner);
            case "string":
                return "Php.zval.string_value (Php.zval_to_string (%s))".printf (inner);
            case "bool":
            case "boolean":
                return "Php.zval.boolean (Php.truthy (%s))".printf (inner);
            default:
                return null;
            }
        }

        return null;
    }

    private static bool is_decimal_concat_false_positive (string left, string right) {
        string l = left.strip ();
        string r = right.strip ();
        return l.length > 0 && r.length > 0 && l[l.length - 1].isdigit () && r[0].isdigit ();
    }

    private string? convert_zval_division_expression (string expression, int php_line_number) throws Error {
        var parts = split_division (expression);
        if (parts.size <= 1) {
            return null;
        }
        mark_function_used ("div");
        string result = convert_zval_expression (parts[0].strip (), php_line_number);
        for (int i = 1; i < parts.size; i++) {
            result = "Php.div_zval (%s, %s%s)".printf (
                result,
                convert_zval_expression (parts[i].strip (), php_line_number),
                source_location_args (php_line_number)
            );
        }
        return result;
    }

    private string convert_zval_variable_or_access (string expression, int php_line_number) throws Error {
        if (expression.has_prefix ("$this->")) {
            string property_name = expression.substring ("$this->".length).strip ();
            int bracket_index = property_name.index_of ("[");
            string access = "";
            if (bracket_index >= 0) {
                access = property_name.substring (bracket_index);
                property_name = property_name.substring (0, bracket_index);
            }
            if (!is_identifier (property_name)) {
                throw unsupported_syntax (php_line_number, expression);
            }
            if (access == "" && current_class_typed_properties.has_key (property_name)) {
                return "this." + property_name;
            }
            string current = "Php.array_get (this.__php2vala_props, Php.zval.string_value (\"%s\"))".printf (property_name);
            if (access != "") {
                var keys = parse_zval_array_keys (access, php_line_number);
                foreach (string key in keys) {
                    current = "Php.array_get (%s, %s)".printf (current, key == "" ? "Php.zval.null_value ()" : key);
                }
            }
            return current;
        }
        string no_dollar = replace_php_variables (expression);
        foreach (string name in variable_aliases.keys) {
            no_dollar = replace_identifier_token (no_dollar, name, variable_aliases[name]);
        }
        int bracket_index = no_dollar.index_of ("[");
        if (bracket_index < 0) {
            if (variable_types.has_key (no_dollar)) {
                switch (normalize_runtime_type (variable_types[no_dollar])) {
                case "int":
                case "int64":
                case "long":
                    return "Php.zval.integer (%s)".printf (no_dollar);
                case "double":
                case "float":
                    return "Php.zval.double_number (%s)".printf (no_dollar);
                case "string":
                    return "Php.zval.string_value (%s)".printf (no_dollar);
                case "bool":
                case "boolean":
                    return "Php.zval.boolean (%s)".printf (no_dollar);
                default:
                    break;
                }
            }
            return no_dollar;
        }
        string name = no_dollar.substring (0, bracket_index);
            string current = name;
            if (variable_types.has_key (name) && variable_types[name] == "Gee.ArrayList<string>") {
                var keys = parse_zval_array_keys (no_dollar.substring (bracket_index), php_line_number);
                if (keys.size == 1) {
                    return "Php.zval.string_value (%s.get (%s))".printf (name, convert_zval_arg_for_runtime_static_type (keys[0], "int"));
                }
            }
            var keys = parse_zval_array_keys (no_dollar.substring (bracket_index), php_line_number);
            foreach (string key in keys) {
                current = "Php.array_get (%s, %s)".printf (current, key == "" ? "Php.zval.null_value ()" : key);
            }
            return current;
    }

    private string convert_zval_function_call (string function_name, string args, int php_line_number) throws Error {
        if (optimize_local_vars || optimize_file_func_vars) {
            string? static_call = convert_zval_function_call_via_static_embed (function_name, args, php_line_number);
            if (static_call != null) {
                return static_call;
            }
        }

        switch (function_name) {
        case "array":
            if (args.strip () == "") {
                return "Php.array_new ()";
            }
            return convert_zval_array_function_expression (args, php_line_number);
        case "shell_exec":
            return embed_generated_zval_call ("shell_exec", convert_zval_argument_list (args, php_line_number), php_line_number);
        case "file_get_contents":
            return embed_generated_zval_call ("file_get_contents", convert_zval_argument_list (args, php_line_number), php_line_number);
        case "var_dump":
            return embed_generated_zval_call ("var_dump", convert_zval_argument_list (args, php_line_number), php_line_number);
        case "phpinfo":
            if (args.strip () != "") {
                throw unsupported_function (php_line_number, function_name);
            }
            return embed_generated_zval_call ("phpinfo", "", php_line_number);
        case "json_last_error_msg":
            if (args.strip () != "") {
                throw unsupported_function (php_line_number, function_name);
            }
            return embed_generated_zval_call ("json_last_error_msg", "", php_line_number);
        case "token_get_all":
            var token_args = split_top_level (args, ',');
            if (token_args.size < 1 || token_args.size > 2) {
                throw unsupported_function (php_line_number, function_name);
            }
            string token_source = convert_zval_expression (token_args[0].strip (), php_line_number);
            string token_parse = token_args.size == 2 ? "Php.truthy (%s)".printf (convert_zval_expression (token_args[1].strip (), php_line_number)) : "false";
            return embed_generated_zval_call ("token_get_all", "%s, Php.zval.boolean (%s)".printf (token_source, token_parse), php_line_number);
        case "token_name":
            return embed_generated_zval_call ("token_name", convert_zval_argument_list (args, php_line_number), php_line_number);
        case "explode":
            return embed_generated_zval_call ("explode", convert_zval_argument_list (args, php_line_number), php_line_number);
        case "implode":
            return embed_generated_zval_call ("implode", convert_zval_argument_list (args, php_line_number), php_line_number);
        case "trim":
            return embed_generated_zval_call ("trim", convert_zval_argument_list (args, php_line_number), php_line_number);
        case "str_replace":
            return embed_generated_zval_call ("str_replace", convert_zval_argument_list (args, php_line_number), php_line_number);
        case "strlen":
            return embed_generated_zval_call ("strlen", convert_zval_argument_list (args, php_line_number), php_line_number);
        case "stripos":
            return embed_generated_zval_call ("stripos", convert_zval_argument_list (args, php_line_number), php_line_number);
        case "stream_select":
            return embed_generated_zval_call ("stream_select", convert_zval_argument_list (args, php_line_number), php_line_number);
        case "floor":
            return embed_generated_zval_call ("floor", convert_zval_argument_list (args, php_line_number), php_line_number);
        case "count":
            return embed_generated_zval_call ("count", convert_zval_argument_list (args, php_line_number), php_line_number);
        case "ctype_digit":
            var ctype_digit_args = split_top_level (args, ',');
            if (ctype_digit_args.size != 1) {
                throw unsupported_function (php_line_number, function_name);
            }
            return "Php.zval.boolean ((new Regex (\"^[0-9]+$\")).match (Php.zval_to_string (%s)))".printf (convert_zval_expression (ctype_digit_args[0].strip (), php_line_number));
        case "isset":
            mark_function_used ("isset");
            return "Php.isset_value_zval (%s%s)".printf (convert_zval_argument_list (args, php_line_number), source_location_args (php_line_number));
        case "flush":
            return embed_generated_zval_call ("flush", "", php_line_number);
        case "usleep":
            return embed_generated_zval_call ("usleep", convert_zval_argument_list (args, php_line_number), php_line_number);
        case "empty":
            mark_function_used ("empty");
            return "Php.empty_value_zval (%s%s)".printf (convert_zval_argument_list (args, php_line_number), source_location_args (php_line_number));
        }
        var raw_args = split_top_level (args, ',');
        var zargs = new ArrayList<string> ();
        foreach (string raw_arg in raw_args) {
            string clean_arg = raw_arg.strip ();
            if (clean_arg != "") {
                zargs.add (convert_zval_expression (clean_arg, php_line_number));
            }
        }
        string runtime_function_name;
        FunctionBinding? binding = resolve_zval_runtime_binding_for_arg_count (function_name, zargs.size, out runtime_function_name);
        if (binding != null) {
            mark_function_used (runtime_function_name);
            return zval_runtime_call_from_zargs (binding, runtime_function_name, zargs, php_line_number);
        }
        throw unsupported_function (php_line_number, function_name);
    }

    private string embed_generated_zval_call (string function_name, string converted_args, int php_line_number) throws Error {
        var zargs = new ArrayList<string> ();
        foreach (string raw_arg in split_top_level (converted_args, ',')) {
            string clean_arg = raw_arg.strip ();
            if (clean_arg != "") {
                zargs.add (clean_arg);
            }
        }
        string runtime_function_name;
        FunctionBinding? binding = resolve_zval_runtime_binding_for_arg_count (function_name, zargs.size, out runtime_function_name);
        if (binding == null) {
            throw unsupported_function (php_line_number, function_name);
        }
        mark_function_used (runtime_function_name);
        return zval_runtime_call_from_zargs (binding, runtime_function_name, zargs, php_line_number);
    }

    private string zval_runtime_call_from_zargs (FunctionBinding binding, string runtime_function_name, ArrayList<string> zargs, int php_line_number) {
        string call_name = binding.call_name (runtime_function_name);
        string argument_list = build_zval_runtime_argument_list (binding, zargs, php_line_number);
        return "%s (%s)".printf (call_name, argument_list);
    }

    private string build_zval_runtime_argument_list (FunctionBinding binding, ArrayList<string> zargs, int php_line_number) {
        var output = new StringBuilder ();
        if (binding.source_location_args_first ()) {
            output.append (source_location_args (php_line_number).substring (2));
        }

        int call_arg_count = zargs.size > binding.runtime_parameter_count () ? zargs.size : binding.runtime_parameter_count ();
        for (int i = 0; i < call_arg_count; i++) {
            if (output.len > 0) {
                output.append (", ");
            }
            string parameter_type = runtime_parameter_type_for_arg (binding, i);
            if (i >= zargs.size) {
                int parameter_index = binding.runtime_parameter_index (i);
                output.append (binding.parameter_defaults[parameter_index] ?? "");
                continue;
            }
            if (is_out_runtime_parameter_type (parameter_type)) {
                string zarg = zargs[i].strip ();
                string out_name = zval_wrapped_identifier (zarg) ?? zarg;
                output.append ("out " + out_name);
            } else {
                output.append (zargs[i]);
            }
        }

        if (binding.source_location_args && !binding.source_location_args_first ()) {
            if (output.len > 0) {
                output.append (", ");
            }
            output.append (source_location_args (php_line_number).substring (2));
        }
        return output.str;
    }

    private string? convert_zval_function_call_via_static_embed (string function_name, string args, int php_line_number) throws Error {
        string loc = source_location_args (php_line_number);
        string runtime_function_name = function_name + "_static";
        var raw_args = split_top_level (args, ',');
        var zargs = new ArrayList<string> ();
        foreach (string raw_arg in raw_args) {
            string clean_arg = raw_arg.strip ();
            if (clean_arg != "") {
                zargs.add (convert_zval_expression (clean_arg, php_line_number));
            }
        }

        if (function_name == "count" && zargs.size == 1) {
            string count_type = normalize_runtime_type (infer_expression_type (zargs[0]));
            if (count_type.has_prefix ("Gee.ArrayList<") || count_type.has_prefix ("Gee.HashMap<")) {
                return "Php.zval.integer (%s.size)".printf (zargs[0]);
            }
        }

        string? discovered_static_call = convert_zval_function_call_via_discovered_static (function_name, zargs, php_line_number);
        if (discovered_static_call != null) {
            return discovered_static_call;
        }

        // These PHP functions operate on mixed/zval values. Do not route them
        // through *_static while optimizing; that would reintroduce zvals in the
        // static call path and erase the point of the static ABI.
        switch (function_name) {
        case "json_encode":
        case "serialize":
        case "print_r":
        case "var_dump":
        case "var_export":
        case "count":
        case "is_array":
        case "is_object":
        case "is_null":
        case "is_string":
        case "is_int":
        case "is_integer":
        case "is_long":
        case "is_float":
        case "is_double":
        case "is_real":
        case "is_bool":
        case "gettype":
        case "explode":
        case "strval":
        case "intval":
        case "boolval":
            return null;
        }

        switch (function_name) {
        case "shell_exec":
        case "exec":
        case "file_get_contents":
            if (zargs.size != 1) {
                return null;
            }
            string? static_name_string = resolve_static_call_name (function_name);
            if (static_name_string == null) {
                return null;
            }
            mark_function_used (runtime_function_name);
            return "Php.zval.string_value (%s (Php.zval_to_string (%s)%s))".printf (static_name_string, zargs[0], loc);
        case "trim":
        case "ltrim":
        case "strval":
            if (zargs.size != 1) {
                return null;
            }
            string? static_name_string_1 = resolve_static_call_name (function_name);
            if (static_name_string_1 == null) {
                return null;
            }
            mark_function_used (runtime_function_name);
            return "Php.zval.string_value (%s (Php.zval_to_string (%s)%s))".printf (static_name_string_1, zargs[0], loc);
        case "strlen":
        case "intval":
            if (zargs.size != 1) {
                return null;
            }
            string? static_name_int = resolve_static_call_name (function_name);
            if (static_name_int == null) {
                return null;
            }
            mark_function_used (runtime_function_name);
            return "Php.zval.integer (%s (%s%s))".printf (
                static_name_int,
                function_name == "strlen" ? "Php.zval_to_string (%s)".printf (zargs[0]) : zargs[0],
                loc
            );
        case "boolval":
        case "is_array":
        case "is_object":
        case "is_null":
        case "is_string":
        case "is_int":
        case "is_integer":
        case "is_long":
        case "is_float":
        case "is_double":
        case "is_real":
        case "is_bool":
        case "is_numeric":
            if (zargs.size != 1) {
                return null;
            }
            string? static_name_bool = resolve_static_call_name (function_name);
            if (static_name_bool == null) {
                return null;
            }
            mark_function_used (runtime_function_name);
            return "Php.zval.boolean (%s (%s%s))".printf (static_name_bool, zargs[0], loc);
        case "gettype":
        case "json_encode":
        case "serialize":
            if (zargs.size != 1) {
                return null;
            }
            string? static_name_mixed = resolve_static_call_name (function_name);
            if (static_name_mixed == null) {
                return null;
            }
            mark_function_used (runtime_function_name);
            return "Php.zval.string_value (%s (%s%s))".printf (static_name_mixed, zargs[0], loc);
        case "json_last_error_msg":
        case "phpinfo":
            if (zargs.size != 0) {
                return null;
            }
            string? static_name_no_args = resolve_static_call_name (function_name);
            if (static_name_no_args == null) {
                return null;
            }
            mark_function_used (runtime_function_name);
            return "Php.zval.string_value (%s (%s))".printf (static_name_no_args, loc.substring (2));
        case "token_get_all":
            if (zargs.size < 1 || zargs.size > 2) {
                return null;
            }
            note_embed_zval_fallback ("token_get_all", "token_get_all_zval");
            mark_function_used ("token_get_all_zval");
            string token_parse = zargs.size == 2 ? "Php.truthy (%s)".printf (zargs[1]) : "false";
            return "PhpEmbed.token_get_all_zval (%s, %s, Php.zval.boolean (%s))".printf (loc.substring (2), zargs[0], token_parse);
        case "token_name":
            if (zargs.size != 1) {
                return null;
            }
            string? token_name_static = resolve_static_call_name ("token_name");
            if (token_name_static == null) {
                return null;
            }
            mark_function_used ("token_name");
            return "Php.zval.string_value (%s (Php.zval_to_int (%s)%s))".printf (token_name_static, zargs[0], loc);
        case "floor":
            if (zargs.size != 1) {
                return null;
            }
            string floor_name = map_php_function_name ("floor", php_line_number);
            return "Php.zval.double_number (%s (Php.zval_to_double (%s)%s))".printf (floor_name, zargs[0], loc);
        case "microtime":
            if (zargs.size == 1 && zargs[0] == "Php.zval.boolean (true)") {
                string microtime_float = map_php_runtime_function_name ("microtime", "microtime_float_static", php_line_number);
                return "Php.zval.double_number (%s (%s))".printf (microtime_float, loc.substring (2));
            }
            if (zargs.size != 1) {
                return null;
            }
            FunctionBinding? microtime_binding = resolve_zval_runtime_binding ("microtime", "microtime_zval");
            if (microtime_binding == null) {
                return null;
            }
            mark_function_used ("microtime");
            string microtime_call = microtime_binding.call_name ("microtime_zval");
            if (microtime_binding.namespace_name == "PhpEmbed") {
                return "%s (%s, %s)".printf (microtime_call, loc.substring (2), zargs[0]);
            }
            return "%s (%s%s)".printf (microtime_call, zargs[0], loc);
        case "count":
            var count_args = split_top_level (args, ',');
            if (count_args.size == 1) {
                string count_arg = convert_zval_expression (count_args[0].strip (), php_line_number);
                string count_type = infer_expression_type (count_arg);
                if (count_type.has_prefix ("Gee.ArrayList<") || count_type.has_prefix ("Gee.HashMap<")) {
                    return "Php.zval.integer (%s.size)".printf (count_arg);
                }
            }
            if (zargs.size != 1) {
                return null;
            }
            string? count_static = resolve_static_call_name ("count");
            if (count_static == null) {
                return null;
            }
            mark_function_used ("count");
            return "Php.zval.integer (%s (%s%s))".printf (count_static, zargs[0], loc);
        case "isset":
            if (zargs.size != 1) {
                return null;
            }
            mark_function_used ("isset");
            return "Php.zval.boolean (Php.isset_value_static (%s%s))".printf (zargs[0], loc);
        case "flush":
            if (zargs.size != 0) {
                return null;
            }
            string flush_name = map_php_function_name ("flush", php_line_number);
            return "Php.zval.boolean (%s (%s))".printf (flush_name, loc.substring (2));
        case "empty":
            if (zargs.size != 1) {
                return null;
            }
            mark_function_used ("empty");
            return "Php.zval.boolean (Php.empty_value_static (%s%s))".printf (zargs[0], loc);
        case "str_replace":
            if (zargs.size != 3) {
                return null;
            }
            string? str_replace_static = resolve_static_call_name ("str_replace");
            if (str_replace_static == null) {
                return null;
            }
            mark_function_used ("str_replace");
            return "Php.zval.string_value (%s (Php.zval_to_string (%s), Php.zval_to_string (%s), Php.zval_to_string (%s)%s))".printf (str_replace_static, zargs[0], zargs[1], zargs[2], loc);
        case "stripos":
            if (zargs.size != 2) {
                return null;
            }
            string? stripos_static = resolve_static_call_name ("stripos");
            if (stripos_static == null) {
                return null;
            }
            mark_function_used ("stripos");
            return "Php.zval.integer (%s (Php.zval_to_string (%s), Php.zval_to_string (%s)%s))".printf (stripos_static, zargs[0], zargs[1], loc);
        }

        return null;
    }

    private string? convert_zval_function_call_via_discovered_static (string function_name, ArrayList<string> zargs, int php_line_number) {
        string runtime_function_name;

        FunctionBinding? dynamic_static = resolve_dynamic_static_binding_from_vapi (
            function_name,
            zargs.size,
            out runtime_function_name
        );
        if (dynamic_static != null) {
            string? call = build_dynamic_static_zval_call (
                dynamic_static,
                runtime_function_name,
                zargs,
                php_line_number
            );
            if (call != null) {
                mark_function_used (runtime_function_name);
                return call;
            }
        }

        return null;
    }

    private FunctionBinding? resolve_dynamic_static_binding_from_vapi (
        string function_name,
        int arg_count,
        out string runtime_function_name
    ) {
        runtime_function_name = "";
        if (is_php_construct_runtime_pseudo_function (function_name)) {
            return null;
        }

        if (runtime_name == "native-runtime") {
            FunctionBinding? native_static = find_runtime_static_binding_for_arg_count (
                native_function_bindings,
                "PhpNative",
                function_name,
                arg_count,
                out runtime_function_name
            );
            if (native_static != null) {
                return native_static;
            }
            info_resolver_step_once (
                function_name,
                "native-static",
                "info: TODO in php2vala: static implementation in valaphp-runtime-* (native) missing for PHP function '%s'; trying native zval".printf (function_name)
            );
            return null;
        }

        if (runtime_name == "embed-runtime") {
            FunctionBinding? embed_static = find_runtime_static_binding_for_arg_count (
                embed_function_bindings,
                "PhpEmbed",
                function_name,
                arg_count,
                out runtime_function_name
            );
            if (embed_static != null) {
                return embed_static;
            }
            warn_missing_embed_static_once (function_name);
            return null;
        }

        if (runtime_name == "both-runtimes") {
            FunctionBinding? native_static = find_runtime_static_binding_for_arg_count (
                native_function_bindings,
                "PhpNative",
                function_name,
                arg_count,
                out runtime_function_name
            );
            if (native_static != null) {
                return native_static;
            }

            info_resolver_step_once (
                function_name,
                "native-static",
                "info: TODO in php2vala: static implementation in valaphp-runtime-* (native) missing for PHP function '%s'; trying native zval".printf (function_name)
            );

            if (find_native_runtime_binding (function_name + "_zval") != null) {
                return null;
            }

            info_resolver_step_once (
                function_name,
                "native-zval",
                "info: TODO in php2vala: zval implementation in valaphp-runtime-* (native) missing for PHP function '%s'; trying embed static".printf (function_name)
            );

            FunctionBinding? embed_static = find_runtime_static_binding_for_arg_count (
                embed_function_bindings,
                "PhpEmbed",
                function_name,
                arg_count,
                out runtime_function_name
            );
            if (embed_static != null) {
                return embed_static;
            }

            warn_missing_embed_static_once (function_name);
            return null;
        }

        return null;
    }

    private FunctionBinding? resolve_dynamic_static_binding_for_native_local (
        string function_name,
        int arg_count,
        out string runtime_function_name
    ) {
        runtime_function_name = "";
        if (is_php_construct_runtime_pseudo_function (function_name)) {
            return null;
        }

        if (runtime_name == "native-runtime") {
            return find_runtime_static_binding_for_arg_count_native_local (
                native_function_bindings,
                "PhpNative",
                function_name,
                arg_count,
                out runtime_function_name
            );
        }

        if (runtime_name == "embed-runtime") {
            return find_runtime_static_binding_for_arg_count_native_local (
                embed_function_bindings,
                "PhpEmbed",
                function_name,
                arg_count,
                out runtime_function_name
            );
        }

        if (runtime_name == "both-runtimes") {
            FunctionBinding? native_static = find_runtime_static_binding_for_arg_count_native_local (
                native_function_bindings,
                "PhpNative",
                function_name,
                arg_count,
                out runtime_function_name
            );
            if (native_static != null) {
                return native_static;
            }

            return find_runtime_static_binding_for_arg_count_native_local (
                embed_function_bindings,
                "PhpEmbed",
                function_name,
                arg_count,
                out runtime_function_name
            );
        }

        return null;
    }

    private FunctionBinding? find_runtime_static_binding_for_arg_count (
        HashMap<string, FunctionBinding> bindings,
        string namespace_name,
        string function_name,
        int arg_count,
        out string runtime_function_name
    ) {
        runtime_function_name = "";
        FunctionBinding? base_binding = bindings.get (function_name);
        if (base_binding != null
            && base_binding.namespace_name == namespace_name
            && base_binding.mapped_function_name != null
            && base_binding.mapped_function_name.has_suffix ("_static")
            && base_binding.accepts_runtime_arg_count (arg_count)
            && can_convert_runtime_static_binding (base_binding)) {
            runtime_function_name = base_binding.mapped_function_name;
            return base_binding;
        }

        FunctionBinding? exact = bindings.get (function_name + "_static");
        if (exact != null
            && exact.namespace_name == namespace_name
            && exact.accepts_runtime_arg_count (arg_count)
            && can_convert_runtime_static_binding (exact)) {
            runtime_function_name = function_name + "_static";
            return exact;
        }

        foreach (string candidate_name in bindings.keys) {
            if (!is_static_variant_for_php_function (function_name, candidate_name)) {
                continue;
            }
            FunctionBinding? candidate = bindings.get (candidate_name);
            if (candidate == null
                || candidate.namespace_name != namespace_name
                || !candidate.accepts_runtime_arg_count (arg_count)
                || !can_convert_runtime_static_binding (candidate)) {
                continue;
            }

            runtime_function_name = candidate_name;
            return candidate;
        }

        return null;
    }

    private static bool is_php_construct_runtime_pseudo_function (string function_name) {
        return function_name == "isset" || function_name == "empty";
    }

    private FunctionBinding? find_runtime_static_binding_for_arg_count_native_local (
        HashMap<string, FunctionBinding> bindings,
        string namespace_name,
        string function_name,
        int arg_count,
        out string runtime_function_name
    ) {
        runtime_function_name = "";
        FunctionBinding? base_binding = bindings.get (function_name);
        if (base_binding != null
            && base_binding.namespace_name == namespace_name
            && base_binding.mapped_function_name != null
            && base_binding.mapped_function_name.has_suffix ("_static")
            && base_binding.accepts_runtime_arg_count (arg_count)
            && can_convert_runtime_static_binding (base_binding)
            && can_use_runtime_static_return_as_native_local (base_binding.return_type)) {
            runtime_function_name = base_binding.mapped_function_name;
            return base_binding;
        }

        foreach (string candidate_name in bindings.keys) {
            if (!is_static_variant_for_php_function (function_name, candidate_name)) {
                continue;
            }
            FunctionBinding? candidate = bindings.get (candidate_name);
            if (candidate == null
                || candidate.namespace_name != namespace_name
                || !candidate.accepts_runtime_arg_count (arg_count)
                || !can_convert_runtime_static_binding (candidate)
                || !can_use_runtime_static_return_as_native_local (candidate.return_type)) {
                continue;
            }

            runtime_function_name = candidate_name;
            return candidate;
        }

        FunctionBinding? exact = bindings.get (function_name + "_static");
        if (exact != null
            && exact.namespace_name == namespace_name
            && exact.accepts_runtime_arg_count (arg_count)
            && can_convert_runtime_static_binding (exact)
            && can_use_runtime_static_return_as_native_local (exact.return_type)) {
            runtime_function_name = function_name + "_static";
            return exact;
        }

        return null;
    }

    private bool can_convert_runtime_static_binding (FunctionBinding binding) {
        if (!can_convert_runtime_static_return_type (binding.return_type)) {
            return false;
        }
        for (int i = 0; i < binding.runtime_parameter_count (); i++) {
            string parameter_type = binding.parameter_types[binding.runtime_parameter_index (i)];
            if (!can_convert_runtime_static_parameter_type (parameter_type)) {
                return false;
            }
        }
        return true;
    }

    private bool can_convert_runtime_static_return_type (string type_name) {
        switch (normalize_runtime_type (type_name)) {
        case "bool":
        case "boolean":
        case "int":
        case "int64":
        case "long":
        case "double":
        case "float":
        case "string":
        case "Php.zval":
            return true;
        default:
            if (normalize_runtime_type (type_name).has_suffix (".RoundMode")) {
                return true;
            }
            return is_runtime_object_type (type_name);
        }
    }

    private bool can_convert_runtime_static_parameter_type (string type_name) {
        switch (normalize_runtime_type (type_name)) {
        case "bool":
        case "boolean":
        case "int":
        case "int64":
        case "long":
        case "double":
        case "float":
        case "string":
        case "Php.zval":
            return true;
        default:
            if (normalize_runtime_type (type_name).has_suffix (".RoundMode")) {
                return true;
            }
            return is_runtime_object_type (type_name);
        }
    }

    private bool can_convert_runtime_static_arguments (FunctionBinding binding, ArrayList<string> zargs) {
        for (int i = 0; i < zargs.size; i++) {
            string parameter_type = binding.parameter_types[binding.runtime_parameter_index (i)];
            if (is_out_runtime_parameter_type (parameter_type)) {
                string? out_name = zval_wrapped_identifier (zargs[i]);
                if (out_name == null || !is_identifier (out_name)) {
                    return false;
                }
            }
            if (is_runtime_object_type (parameter_type)
                && normalize_runtime_type (parameter_type) != normalize_runtime_type (infer_expression_type (zargs[i]))) {
                return false;
            }
        }
        return true;
    }

    private string? build_dynamic_static_zval_call (
        FunctionBinding binding,
        string runtime_function_name,
        ArrayList<string> zargs,
        int php_line_number
    ) {
        if (!can_convert_runtime_static_arguments (binding, zargs)) {
            return null;
        }

        string function_call = "%s (%s)".printf (
            binding.call_name (runtime_function_name),
            build_dynamic_static_argument_list (binding, zargs, php_line_number)
        );

        switch (normalize_runtime_type (binding.return_type)) {
        case "bool":
        case "boolean":
            return "Php.zval.boolean (%s)".printf (function_call);
        case "int":
        case "int64":
        case "long":
            return "Php.zval.integer (%s)".printf (function_call);
        case "double":
        case "float":
            return "Php.zval.double_number (%s)".printf (function_call);
        case "string":
            return "Php.zval.string_value (%s)".printf (function_call);
        case "Php.zval":
            return function_call;
        default:
            return null;
        }
    }

    private string? transpile_zval_typed_static_assignment (string storage_name, string raw_expression, int php_line_number) throws Error {
        if (!(optimize_local_vars || optimize_file_func_vars)) {
            return null;
        }
        string expression = trim_trailing_semicolon (raw_expression.strip ());
        MatchInfo match;
        if (!function_expression_regex.match (expression, 0, out match)) {
            return null;
        }

        string function_name = match.fetch (1);
        string args = match.fetch (2).strip ();
        var zargs = new ArrayList<string> ();
        foreach (string raw_arg in split_top_level (args, ',')) {
            string clean_arg = raw_arg.strip ();
            if (clean_arg != "") {
                zargs.add (convert_zval_expression (clean_arg, php_line_number));
            }
        }

        string runtime_function_name;
        FunctionBinding? binding = resolve_dynamic_static_binding_for_native_local (
            function_name,
            zargs.size,
            out runtime_function_name
        );
        if (binding == null || !can_use_runtime_static_return_as_native_local (binding.return_type)) {
            return null;
        }
        if (!can_convert_runtime_static_arguments (binding, zargs)) {
            return null;
        }

        string type_name = normalize_runtime_type (binding.return_type);
        string prelude;
        string postlude;
        string call = build_dynamic_static_call_with_zval_outs (binding, runtime_function_name, zargs, php_line_number, out prelude, out postlude);
        mark_function_used (runtime_function_name);

        if (variable_types.has_key (storage_name)) {
            if (normalize_runtime_type (variable_types[storage_name]) != type_name) {
                return null;
            }
            return @"$prelude$storage_name = $call;$postlude";
        }

        variable_types[storage_name] = type_name;
        return @"$prelude$type_name $storage_name = $call;$postlude";
    }

    private string? transpile_zval_typed_native_assignment (string storage_name, string raw_expression, int php_line_number) throws Error {
        if (!(optimize_local_vars || optimize_file_func_vars)) {
            return null;
        }

        string? type_name = typed_native_assignment_return_type (raw_expression);
        if (type_name == null) {
            return null;
        }

        string? value = convert_typed_native_expression (raw_expression, php_line_number, type_name);
        if (value == null) {
            return null;
        }

        if (variable_types.has_key (storage_name)) {
            if (normalize_runtime_type (variable_types[storage_name]) != type_name) {
                return null;
            }
            return @"$storage_name = $value;";
        }

        variable_types[storage_name] = type_name;
        return @"$type_name $storage_name = $value;";
    }

    private string? typed_native_assignment_return_type (string raw_expression) {
        string expression = trim_trailing_semicolon (raw_expression.strip ());
        if (is_integer_literal (expression)) {
            return "int";
        }
        if (can_convert_to_native_double_expression (expression)) {
            return "double";
        }
        return null;
    }

    private bool can_convert_to_native_double_expression (string expression) {
        string clean = trim_trailing_semicolon (expression.strip ());
        if (is_microtime_true_expression (clean) || is_numeric_literal (clean)) {
            return true;
        }

        if (clean.has_prefix ("$")) {
            string variable_name = replace_php_variables (clean);
            foreach (string name in variable_aliases.keys) {
                variable_name = replace_identifier_token (variable_name, name, variable_aliases[name]);
            }
            return variable_types.has_key (variable_name);
        }

        foreach (string op in new string[] {"+", "-"}) {
            int index = find_top_level_operator_index (clean, op);
            if (index > 0) {
                return can_convert_to_native_double_expression (clean.substring (0, index).strip ())
                    && can_convert_to_native_double_expression (clean.substring (index + op.length).strip ());
            }
        }

        return false;
    }

    private static bool is_microtime_true_expression (string expression) {
        string clean = expression.replace (" ", "").replace ("\t", "");
        return clean == "microtime(true)";
    }

    private static bool is_numeric_literal (string expression) {
        string clean = expression.strip ();
        if (clean == "") {
            return false;
        }

        int start = 0;
        if (clean[0] == '-' || clean[0] == '+') {
            start = 1;
        }
        if (start >= clean.length) {
            return false;
        }

        bool has_digit = false;
        bool has_dot = false;
        for (int i = start; i < clean.length; i++) {
            char c = clean[i];
            if (c >= '0' && c <= '9') {
                has_digit = true;
                continue;
            }
            if (c == '.' && !has_dot) {
                has_dot = true;
                continue;
            }
            return false;
        }

        return has_digit;
    }

    private static bool is_integer_literal (string expression) {
        string clean = expression.strip ();
        if (clean == "") {
            return false;
        }
        int start = 0;
        if (clean[0] == '-' || clean[0] == '+') {
            start = 1;
        }
        if (start >= clean.length) {
            return false;
        }
        for (int i = start; i < clean.length; i++) {
            char c = clean[i];
            if (c < '0' || c > '9') {
                return false;
            }
        }
        return true;
    }

    private string? convert_typed_native_expression (string raw_expression, int php_line_number, string type_name) throws Error {
        string expression = trim_trailing_semicolon (raw_expression.strip ());
        if (type_name == "int") {
            return convert_native_int_expression (expression, php_line_number);
        }
        if (type_name == "double") {
            return convert_native_double_expression (expression, php_line_number);
        }
        return null;
    }

    private string? convert_native_int_expression (string expression, int php_line_number) throws Error {
        string clean = trim_trailing_semicolon (expression.strip ());
        if (is_integer_literal (clean)) {
            return clean;
        }
        if (clean.has_prefix ("$")) {
            string converted = convert_zval_expression (clean, php_line_number);
            string variable_name = replace_php_variables (clean);
            foreach (string name in variable_aliases.keys) {
                variable_name = replace_identifier_token (variable_name, name, variable_aliases[name]);
            }
            if (variable_types.has_key (variable_name)) {
                string type_name = normalize_runtime_type (variable_types[variable_name]);
                if (type_name == "int" || type_name == "long" || type_name == "int64") {
                    return variable_name;
                }
            }
            return "Php.zval_to_int (%s)".printf (converted);
        }
        string? count_expression = convert_native_count_expression (clean, php_line_number);
        if (count_expression != null) {
            return count_expression;
        }
        return null;
    }

    private string? convert_native_double_expression (string expression, int php_line_number) throws Error {
        string clean = trim_trailing_semicolon (expression.strip ());
        if (is_microtime_true_expression (clean)) {
            return "%s (%s)".printf (
                map_php_runtime_function_name ("microtime", "microtime_float_static", php_line_number),
                source_location_args (php_line_number).substring (2)
            );
        }
        if (is_numeric_literal (clean)) {
            return clean;
        }
        string? count_expression = convert_native_count_expression (clean, php_line_number);
        if (count_expression != null) {
            return count_expression;
        }
        if (clean.has_prefix ("$")) {
            string converted = convert_zval_expression (clean, php_line_number);
            string variable_name = replace_php_variables (clean);
            foreach (string name in variable_aliases.keys) {
                variable_name = replace_identifier_token (variable_name, name, variable_aliases[name]);
            }
            if (variable_types.has_key (variable_name)) {
                string type_name = normalize_runtime_type (variable_types[variable_name]);
                if (type_name == "double" || type_name == "float" || type_name == "int" || type_name == "long" || type_name == "int64") {
                    return variable_name;
                }
            }
            return "Php.zval_to_double (%s)".printf (converted);
        }
        foreach (string op in new string[] {"+", "-"}) {
            int index = find_top_level_operator_index (clean, op);
            if (index > 0) {
                string? left = convert_native_double_expression (clean.substring (0, index).strip (), php_line_number);
                string? right = convert_native_double_expression (clean.substring (index + op.length).strip (), php_line_number);
                if (left == null || right == null) {
                    return null;
                }
                return "(%s %s %s)".printf (left, op, right);
            }
        }

        return null;
    }

    private string? convert_native_count_expression (string expression, int php_line_number) throws Error {
        MatchInfo match;
        if (!function_expression_regex.match (expression, 0, out match) || match.fetch (1) != "count") {
            return null;
        }
        var args = split_top_level (match.fetch (2).strip (), ',');
        if (args.size != 1) {
            return null;
        }
        string zarg = convert_zval_expression (args[0].strip (), php_line_number);
        string arg_type = normalize_runtime_type (infer_expression_type (zarg));
        if (arg_type.has_prefix ("Gee.ArrayList<") || arg_type.has_prefix ("Gee.HashMap<")) {
            return "%s.size".printf (zarg);
        }
        if (arg_type == "Php.zval") {
            string function_name = map_php_runtime_function_name ("count", "count_static", php_line_number);
            return "%s (%s%s)".printf (function_name, zarg, source_location_args (php_line_number));
        }
        return null;
    }

    private string build_dynamic_static_call_with_zval_outs (
        FunctionBinding binding,
        string runtime_function_name,
        ArrayList<string> zargs,
        int php_line_number,
        out string prelude,
        out string postlude
    ) {
        prelude = "";
        postlude = "";
        var output = new StringBuilder ();
        if (binding.source_location_args_first ()) {
            output.append (source_location_args (php_line_number).substring (2));
        }

        int call_arg_count = zargs.size > binding.runtime_parameter_count () ? zargs.size : binding.runtime_parameter_count ();
        for (int i = 0; i < call_arg_count; i++) {
            if (output.len > 0) {
                output.append (", ");
            }

            string parameter_type = binding.parameter_types[binding.runtime_parameter_index (i)];
            if (i >= zargs.size) {
                int parameter_index = binding.runtime_parameter_index (i);
                output.append (binding.parameter_defaults[parameter_index] ?? "");
                continue;
            }
            if (is_out_runtime_parameter_type (parameter_type)) {
                string temp_name = "__php2vala_out_%d".printf (temp_variable_counter++);
                string value_type = normalize_runtime_type (parameter_type);
                string zarg = zargs[i].strip ();
                string out_name = zval_wrapped_identifier (zarg) ?? zarg;
                string temp_type = value_type == "string" ? "string" : value_type;
                prelude += "%s %s = %s;\n".printf (temp_type, temp_name, convert_zval_arg_for_runtime_static_type (zarg, value_type));
                output.append ("out " + temp_name);
                if (variable_types.has_key (out_name) && normalize_runtime_type (variable_types[out_name]) != "Php.zval") {
                    postlude += "\n%s = %s;".printf (out_name, temp_name);
                } else {
                    postlude += "\n%s = %s;".printf (out_name, convert_runtime_static_out_arg_to_zval (temp_name, value_type));
                }
                continue;
            }

            output.append (convert_zval_arg_for_runtime_static_type (zargs[i], parameter_type));
        }

        if (binding.source_location_args && !binding.source_location_args_first ()) {
            if (output.len == 0) {
                output.append (source_location_args (php_line_number).substring (2));
            } else {
                output.append (source_location_args (php_line_number));
            }
        }

        return "%s (%s)".printf (binding.call_name (runtime_function_name), output.str);
    }

    private static string? zval_wrapped_identifier (string expression) {
        string clean = expression.strip ();
        if (is_identifier (clean)) {
            return clean;
        }
        foreach (string prefix in new string[] {
            "Php.zval.integer (",
            "Php.zval.double_number (",
            "Php.zval.boolean (",
            "Php.zval.string_value ("
        }) {
            if (!clean.has_prefix (prefix) || !clean.has_suffix (")")) {
                continue;
            }
            int open_index = prefix.length - 1;
            int close_index = find_matching_paren (clean, open_index);
            if (close_index != clean.length - 1) {
                continue;
            }
            string inner = clean.substring (open_index + 1, close_index - open_index - 1).strip ();
            if (is_identifier (inner)) {
                return inner;
            }
        }
        return null;
    }

    private string convert_runtime_static_out_arg_to_zval (string expression, string type_name) {
        switch (normalize_runtime_type (type_name)) {
        case "bool":
        case "boolean":
            return "Php.zval.boolean (%s)".printf (expression);
        case "int":
        case "int64":
        case "long":
            return "Php.zval.integer (%s)".printf (expression);
        case "double":
        case "float":
            return "Php.zval.double_number (%s)".printf (expression);
        case "string":
            return "Php.zval.string_value (%s)".printf (expression);
        default:
            return expression;
        }
    }

    private string? typed_static_assignment_return_type (string raw_expression) {
        string expression = trim_trailing_semicolon (raw_expression.strip ());
        MatchInfo match;
        if (!function_expression_regex.match (expression, 0, out match)) {
            return null;
        }

        string function_name = match.fetch (1);
        var args = split_top_level (match.fetch (2).strip (), ',');
        int arg_count = 0;
        foreach (string raw_arg in args) {
            if (raw_arg.strip () != "") {
                arg_count++;
            }
        }

        string runtime_function_name;
        FunctionBinding? binding = resolve_dynamic_static_binding_for_native_local (
            function_name,
            arg_count,
            out runtime_function_name
        );
        if (binding == null || !can_use_runtime_static_return_as_native_local (binding.return_type)) {
            return null;
        }
        return normalize_runtime_type (binding.return_type);
    }

    private bool can_use_runtime_static_return_as_native_local (string type_name) {
        string clean = normalize_runtime_type (type_name);
        if (clean == "Php.zval") {
            return false;
        }
        return can_convert_runtime_static_return_type (clean);
    }

    public string? convert_zval_function_expression_to_native_local (string expression, int php_line_number) throws Error {
        string clean = trim_trailing_semicolon (expression.strip ());
        MatchInfo match;
        if (!function_expression_regex.match (clean, 0, out match)) {
            return null;
        }
        string function_name = match.fetch (1);
        var raw_args = split_top_level (match.fetch (2).strip (), ',');
        var zargs = new ArrayList<string> ();
        foreach (string raw_arg in raw_args) {
            string clean_arg = raw_arg.strip ();
            if (clean_arg != "") {
                zargs.add (convert_zval_expression (clean_arg, php_line_number));
            }
        }

        string runtime_function_name;
        FunctionBinding? binding = resolve_dynamic_static_binding_for_native_local (
            function_name,
            zargs.size,
            out runtime_function_name
        );
        if (binding == null || !can_convert_runtime_static_arguments (binding, zargs)) {
            return null;
        }
        for (int i = 0; i < binding.runtime_parameter_count (); i++) {
            if (is_out_runtime_parameter_type (binding.parameter_types[binding.runtime_parameter_index (i)])) {
                return null;
            }
        }

        mark_function_used (runtime_function_name);
        return "%s (%s)".printf (
            binding.call_name (runtime_function_name),
            build_dynamic_static_argument_list (binding, zargs, php_line_number)
        );
    }

    public string? infer_native_static_function_expression_return_type (string expression) {
        string clean = trim_trailing_semicolon (expression.strip ());
        MatchInfo match;
        if (!function_expression_regex.match (clean, 0, out match)) {
            return null;
        }

        string function_name = match.fetch (1);
        int arg_count = 0;
        foreach (string raw_arg in split_top_level (match.fetch (2).strip (), ',')) {
            if (raw_arg.strip () != "") {
                arg_count++;
            }
        }

        string runtime_function_name;
        FunctionBinding? binding = resolve_dynamic_static_binding_for_native_local (
            function_name,
            arg_count,
            out runtime_function_name
        );
        if (binding == null || !can_use_runtime_static_return_as_native_local (binding.return_type)) {
            return null;
        }
        return normalize_runtime_type (binding.return_type);
    }

    private string build_dynamic_static_argument_list (FunctionBinding binding, ArrayList<string> zargs, int php_line_number) {
        var output = new StringBuilder ();
        if (binding.source_location_args_first ()) {
            output.append (source_location_args (php_line_number).substring (2));
        }

        int call_arg_count = zargs.size > binding.runtime_parameter_count () ? zargs.size : binding.runtime_parameter_count ();
        for (int i = 0; i < call_arg_count; i++) {
            if (output.len > 0) {
                output.append (", ");
            }
            string parameter_type = binding.parameter_types[binding.runtime_parameter_index (i)];
            if (i >= zargs.size) {
                int parameter_index = binding.runtime_parameter_index (i);
                output.append (binding.parameter_defaults[parameter_index] ?? "");
                continue;
            }
            if (is_out_runtime_parameter_type (parameter_type)) {
                string? out_name = zval_wrapped_identifier (zargs[i]);
                output.append ("out " + (out_name ?? zargs[i]));
            } else {
                output.append (convert_zval_arg_for_runtime_static_type (zargs[i], parameter_type));
            }
        }

        if (binding.source_location_args && !binding.source_location_args_first ()) {
            if (output.len == 0) {
                output.append (source_location_args (php_line_number).substring (2));
            } else {
                output.append (source_location_args (php_line_number));
            }
        }

        return output.str;
    }

    private string convert_zval_arg_for_runtime_static_type (string zarg, string type_name) {
        switch (normalize_runtime_type (type_name)) {
        case "bool":
        case "boolean":
            {
                string? inner = unwrap_zval_constructor_arg (zarg, "Php.zval.boolean (");
                if (inner != null) {
                    return inner;
                }
            }
            return "Php.truthy (%s)".printf (zarg);
        case "int":
        case "int64":
        case "long":
            {
                string? inner = unwrap_zval_constructor_arg (zarg, "Php.zval.integer (");
                if (inner != null) {
                    return inner;
                }
            }
            return "Php.zval_to_int (%s)".printf (zarg);
        case "double":
        case "float":
            {
                string? inner = unwrap_zval_constructor_arg (zarg, "Php.zval.double_number (");
                if (inner != null) {
                    return inner;
                }
                inner = unwrap_zval_constructor_arg (zarg, "Php.zval.integer (");
                if (inner != null) {
                    return inner;
                }
            }
            return "Php.zval_to_double (%s)".printf (zarg);
        case "string":
            {
                string? string_concat = convert_concat_many_to_native_string (zarg);
                if (string_concat != null) {
                    return string_concat;
                }
                string_concat = convert_concat_to_native_string (zarg);
                if (string_concat != null) {
                    return string_concat;
                }
                string? inner = unwrap_zval_constructor_arg (zarg, "Php.zval.string_value (");
                if (inner != null && can_pass_unwrapped_string_to_static_arg (inner)) {
                    return inner;
                }
            }
            return "Php.zval_to_string (%s)".printf (zarg);
        case "Php.zval":
            return zarg;
        default:
            return zarg;
        }
    }

    private string? convert_concat_many_to_native_string (string expression) {
        string clean = expression.strip ();
        string prefix = "Php.concat_many (";
        if (!clean.has_prefix (prefix) || !clean.has_suffix (")")) {
            return null;
        }
        int open_index = prefix.length - 1;
        int close_index = find_matching_paren (clean, open_index);
        if (close_index != clean.length - 1) {
            return null;
        }

        var native_parts = new ArrayList<string> ();
        string args = clean.substring (open_index + 1, close_index - open_index - 1);
        foreach (string raw_arg in split_top_level (args, ',')) {
            string arg = raw_arg.strip ();
            if (arg == "") {
                continue;
            }
            string? inner = unwrap_zval_constructor_arg (arg, "Php.zval.string_value (");
            if (inner == null || !can_pass_unwrapped_string_to_static_arg (inner)) {
                return null;
            }
            native_parts.add (inner);
        }

        if (native_parts.size == 0) {
            return "\"\"";
        }
        return join_strings (native_parts, " + ");
    }

    private string? convert_concat_to_native_string (string expression) {
        string clean = expression.strip ();
        string prefix = "Php.concat (";
        if (!clean.has_prefix (prefix) || !clean.has_suffix (")")) {
            return null;
        }
        int open_index = prefix.length - 1;
        int close_index = find_matching_paren (clean, open_index);
        if (close_index != clean.length - 1) {
            return null;
        }
        var args = split_top_level (clean.substring (open_index + 1, close_index - open_index - 1), ',');
        if (args.size != 2) {
            return null;
        }
        var native_parts = new ArrayList<string> ();
        foreach (string raw_arg in args) {
            string? inner = unwrap_zval_constructor_arg (raw_arg.strip (), "Php.zval.string_value (");
            if (inner == null || !can_pass_unwrapped_string_to_static_arg (inner)) {
                return null;
            }
            native_parts.add (inner);
        }
        return join_strings (native_parts, " + ");
    }

    private static string? unwrap_zval_constructor_arg (string expression, string prefix) {
        string clean = expression.strip ();
        if (!clean.has_prefix (prefix) || !clean.has_suffix (")")) {
            return null;
        }
        int open_index = prefix.length - 1;
        int close_index = find_matching_paren (clean, open_index);
        if (close_index != clean.length - 1) {
            return null;
        }
        return clean.substring (open_index + 1, close_index - open_index - 1).strip ();
    }

    private bool can_pass_unwrapped_string_to_static_arg (string expression) {
        string clean = expression.strip ();
        if (is_string_literal (clean)) {
            return true;
        }
        if (is_identifier (clean)) {
            return true;
        }
        if (variable_types.has_key (clean) && normalize_runtime_type (variable_types[clean]) == "string") {
            return true;
        }

        string call_name = extract_vala_call_name (clean);
        if (call_name == "") {
            return false;
        }
        foreach (string function_name in function_bindings.keys) {
            FunctionBinding? binding = function_bindings.get (function_name);
            if (binding == null || binding.call_name (function_name) != call_name) {
                continue;
            }
            return normalize_runtime_type (binding.return_type) == "string";
        }
        return false;
    }

    private static string extract_vala_call_name (string expression) {
        string clean = expression.strip ();
        int open_index = clean.index_of ("(");
        if (open_index <= 0) {
            return "";
        }
        int close_index = find_matching_paren (clean, open_index);
        if (close_index != clean.length - 1) {
            return "";
        }
        return clean.substring (0, open_index).strip ();
    }

    private string convert_zval_array_function_expression (string args, int php_line_number) throws Error {
        var values = new ArrayList<string> ();
        foreach (string raw_part in split_top_level (args, ',')) {
            string part = raw_part.strip ();
            if (part == "") {
                continue;
            }
            values.add (convert_zval_expression (part, php_line_number));
        }
        return "Php.array_from_values_zval ({ %s })".printf (join_strings (values, ", "));
    }

    private static string join_strings (ArrayList<string> values, string separator) {
        var output = new StringBuilder ();
        foreach (string value in values) {
            if (output.len > 0) {
                output.append (separator);
            }
            output.append (value);
        }
        return output.str;
    }

    private string convert_zval_argument_list (string args, int php_line_number) throws Error {
        if (args.strip () == "") {
            return "";
        }
        var converted = new StringBuilder ();
        foreach (string arg in split_top_level (args, ',')) {
            if (converted.len > 0) {
                converted.append (", ");
            }
            converted.append (convert_zval_expression (arg.strip (), php_line_number));
        }
        return converted.str;
    }

    public string convert_zval_argument_list_public (string args, int php_line_number) throws Error {
        return convert_zval_argument_list (args, php_line_number);
    }

    private string convert_zval_condition (string expression, int php_line_number) throws Error {
        string clean = expression.strip ();
        var or_parts = split_top_level_operator (clean, "||");
        if (or_parts.size > 1) {
            return join_zval_conditions (or_parts, " || ", php_line_number);
        }
        var and_parts = split_top_level_operator (clean, "&&");
        if (and_parts.size > 1) {
            return join_zval_conditions (and_parts, " && ", php_line_number);
        }
        if (clean.has_prefix ("!")) {
            return "!(%s)".printf (convert_zval_condition (clean.substring (1).strip (), php_line_number));
        }
        string[] operators = {"!==", "===", "!=", "==", ">=", "<=", ">", "<"};
        foreach (string op in operators) {
            int index = find_top_level_operator_index (clean, op);
            if (index > 0) {
                if (op == ">" || op == "<" || op == ">=" || op == "<=") {
                    string? native_left = convert_native_double_expression (clean.substring (0, index).strip (), php_line_number);
                    string? native_right = convert_native_double_expression (clean.substring (index + op.length).strip (), php_line_number);
                    if (native_left != null && native_right != null) {
                        return "%s %s %s".printf (native_left, op, native_right);
                    }
                }
                string left = convert_zval_expression (clean.substring (0, index).strip (), php_line_number);
                string right = convert_zval_expression (clean.substring (index + op.length).strip (), php_line_number);
                switch (op) {
                case "!==":
                    return "Php.strict_ne (%s, %s)".printf (left, right);
                case "!=":
                    return "Php.ne (%s, %s)".printf (left, right);
                case "===":
                    return "Php.strict_eq (%s, %s)".printf (left, right);
                case "==":
                    return "Php.eq (%s, %s)".printf (left, right);
                case ">":
                    return "Php.zval_to_double (%s) > Php.zval_to_double (%s)".printf (left, right);
                case "<":
                    return "Php.zval_to_double (%s) < Php.zval_to_double (%s)".printf (left, right);
                case ">=":
                    return "Php.zval_to_double (%s) >= Php.zval_to_double (%s)".printf (left, right);
                case "<=":
                    return "Php.zval_to_double (%s) <= Php.zval_to_double (%s)".printf (left, right);
                }
            }
        }
        string converted = convert_zval_expression (clean, php_line_number);
        if (is_runtime_object_type (infer_expression_type (converted))) {
            return "%s != null".printf (converted);
        }
        return "Php.truthy (%s)".printf (converted);
    }

    private string join_zval_conditions (ArrayList<string> parts, string op, int php_line_number) throws Error {
        var output = new StringBuilder ();
        foreach (string part in parts) {
            if (output.len > 0) {
                output.append (op);
            }
            output.append ("(" + convert_zval_condition (part.strip (), php_line_number) + ")");
        }
        return output.str;
    }

    private ArrayList<string> split_top_level_operator (string expression, string op) {
        var parts = new ArrayList<string> ();
        var current = new StringBuilder ();
        bool in_single_quote = false;
        bool in_double_quote = false;
        int depth = 0;
        int i = 0;
        while (i < expression.length) {
            char c = expression[i];
            if (c == '\'' && !in_double_quote) {
                in_single_quote = !in_single_quote;
            } else if (c == '"' && !in_single_quote) {
                in_double_quote = !in_double_quote;
            } else if (!in_single_quote && !in_double_quote) {
                if (c == '(' || c == '[' || c == '{') {
                    depth++;
                } else if (c == ')' || c == ']' || c == '}') {
                    depth--;
                } else if (depth == 0 && expression.substring (i).has_prefix (op)) {
                    parts.add (current.str);
                    current.truncate (0);
                    i += op.length;
                    continue;
                }
            }
            current.append_c (c);
            i++;
        }
        parts.add (current.str);
        return parts;
    }

    private int find_top_level_operator_index (string expression, string op) {
        bool in_single_quote = false;
        bool in_double_quote = false;
        int depth = 0;
        for (int i = 0; i < expression.length; i++) {
            char c = expression[i];
            if (c == '\'' && !in_double_quote) {
                in_single_quote = !in_single_quote;
            } else if (c == '"' && !in_single_quote) {
                in_double_quote = !in_double_quote;
            } else if (!in_single_quote && !in_double_quote) {
                if (c == '(' || c == '[' || c == '{') {
                    depth++;
                } else if (c == ')' || c == ']' || c == '}') {
                    depth--;
                } else if (depth == 0 && expression.substring (i).has_prefix (op)) {
                    if (op == ">" && i > 0 && (expression[i - 1] == '=' || expression[i - 1] == '-')) {
                        continue;
                    }
                    if (op == "-" && i + 1 < expression.length && expression[i + 1] == '>') {
                        continue;
                    }
                    return i;
                }
            }
        }
        return -1;
    }

    private bool is_standalone_php_variable_access (string expression) {
        string clean = expression.strip ();
        if (!clean.has_prefix ("$")) {
            return false;
        }
        bool in_single_quote = false;
        bool in_double_quote = false;
        int paren_depth = 0;
        int bracket_depth = 0;
        for (int i = 0; i < clean.length; i++) {
            char c = clean[i];
            if (c == '\'' && !in_double_quote) {
                in_single_quote = !in_single_quote;
            } else if (c == '"' && !in_single_quote) {
                in_double_quote = !in_double_quote;
            } else if (!in_single_quote && !in_double_quote) {
                if (c == '(') {
                    paren_depth++;
                } else if (c == ')') {
                    paren_depth--;
                } else if (c == '[') {
                    bracket_depth++;
                } else if (c == ']') {
                    bracket_depth--;
                } else if (paren_depth == 0 && bracket_depth == 0) {
                    if (c == '-' && i + 1 < clean.length && clean[i + 1] == '>') {
                        i++;
                        continue;
                    }
                    if (c == '=' || c == '!' || c == '<' || c == '>' || c == '+' || c == '-' || c == '*' || c == '/' || c == '.' || c == '?' || c == ':') {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    private int find_top_level_double_arrow_index (string expression) {
        bool in_single_quote = false;
        bool in_double_quote = false;
        int depth = 0;
        for (int i = 0; i < expression.length - 1; i++) {
            char c = expression[i];
            if (c == '\'' && !in_double_quote) {
                in_single_quote = !in_single_quote;
            } else if (c == '"' && !in_single_quote) {
                in_double_quote = !in_double_quote;
            } else if (!in_single_quote && !in_double_quote) {
                if (c == '(' || c == '[' || c == '{') {
                    depth++;
                } else if (c == ')' || c == ']' || c == '}') {
                    depth--;
                } else if (depth == 0 && c == '=' && expression[i + 1] == '>') {
                    return i;
                }
            }
        }
        return -1;
    }

    private int find_top_level_char_index (string expression, char needle) {
        bool in_single_quote = false;
        bool in_double_quote = false;
        int depth = 0;
        for (int i = 0; i < expression.length; i++) {
            char c = expression[i];
            if (c == '\'' && !in_double_quote) {
                in_single_quote = !in_single_quote;
            } else if (c == '"' && !in_single_quote) {
                in_double_quote = !in_double_quote;
            } else if (!in_single_quote && !in_double_quote) {
                if (c == '(' || c == '[' || c == '{') {
                    depth++;
                } else if (c == ')' || c == ']' || c == '}') {
                    depth--;
                } else if (depth == 0 && c == needle) {
                    return i;
                }
            }
        }
        return -1;
    }
    public string? transpile_line (string line, int php_line_number) throws Error {
        if (line.has_prefix ("//")) {
            return line;
        }
        if (line.has_prefix ("#")) {
            return "//" + line.substring (1);
        }
        if (line.has_prefix ("/*")) {
            return line.replace ("\n", "\n        ");
        }
        if (line.has_prefix ("@")) {
            return transpile_line (line.substring (1).strip (), php_line_number);
        }
        if (line.has_suffix (";flush();")) {
            string first = line.substring (0, line.length - "flush();".length).strip ();
            string flush_name = map_php_function_name ("flush", php_line_number);
            return transpile_line (first, php_line_number) + "\n" + flush_name + " (" + source_location_args (php_line_number).substring (2) + ");";
        }

        if (line == "{" || line == "}") {
            return line;
        }
        if (line == "else{") {
            return "else {";
        }
        if (line.has_prefix ("} else")) {
            return "} else {";
        }

        if (line.has_prefix ("echo ")) {
            return transpile_echo (line.substring (5).strip (), php_line_number);
        }

        if (line.has_prefix ("if(") || line.has_prefix ("if (")) {
            return transpile_inline_condition (line, "if", php_line_number);
        }

        if (line.has_prefix ("foreach(") || line.has_prefix ("foreach (")) {
            return transpile_foreach (line, php_line_number);
        }

        if (line.has_prefix ("for(") || line.has_prefix ("for (")) {
            return transpile_for (line, php_line_number);
        }

        if (line.has_prefix ("elseif(") || line.has_prefix ("elseif (")) {
            return transpile_inline_condition (line, "else if", php_line_number);
        }

        if (line.has_prefix ("else ")) {
            string nested = transpile_line (line.substring (5).strip (), php_line_number);
            return @"else { $nested }";
        }

        if (line.has_prefix ("return")) {
            return transpile_return (line, php_line_number);
        }

        if (line == "exit;" || line == "die;") {
            return transpile_exit ("", php_line_number);
        }
        if (line == "continue;") {
            return "continue;";
        }
        if (line == "break;") {
            return "break;";
        }

        MatchInfo match;
        string? compound_assignment = transpile_compound_assignment (line, php_line_number);
        if (compound_assignment != null) {
            return compound_assignment;
        }

        string? array_assignment = transpile_array_assignment (line, php_line_number);
        if (array_assignment != null) {
            return array_assignment;
        }

        string? assignment_name = parse_assignment_name (line);
        if (assignment_name != null) {
            string raw_expression = parse_assignment_expression (line);
            string? array_init = build_array_initialization (assignment_name);
            if (array_init != null && raw_expression.strip () == "array()") {
                return array_init;
            }
            string expression = convert_expression (raw_expression, php_line_number);
            string type_name = infer_type (expression);
            if (local_functions.has_key (assignment_name) && !variable_aliases.has_key (assignment_name)) {
                string alias_name = assignment_name + "_result";
                variable_aliases[assignment_name] = alias_name;
                variable_types[alias_name] = type_name;
                return @"$type_name $alias_name = $expression;";
            }

            string storage_name = variable_aliases.has_key (assignment_name) ? variable_aliases[assignment_name] : assignment_name;
            if (storage_name != assignment_name && variable_types.has_key (storage_name) && variable_types[storage_name] == type_name) {
                return @"$storage_name = $expression;";
            }
            if (variable_types.has_key (assignment_name)) {
                string old_type = variable_types[assignment_name];
                if (old_type != type_name && type_name != "var") {
                    string alias_name = unique_alias_name (assignment_name, type_name);
                    variable_aliases[assignment_name] = alias_name;
                    variable_types[alias_name] = type_name;
                    return @"$type_name $alias_name = $expression;";
                }
                return @"$storage_name = $expression;";
            }

            variable_types[assignment_name] = type_name;
            return @"$type_name $assignment_name = $expression;";
        }

        if (assignment_regex.match (line, 0, out match)) {
            string name = match.fetch (1);
            string raw_expression = match.fetch (2).strip ();
            string? array_init = build_array_initialization (name);
            if (array_init != null && raw_expression == "array()") {
                return array_init;
            }
            string expression = convert_expression (raw_expression, php_line_number);
            string type_name = infer_type (expression);
            if (local_functions.has_key (name) && !variable_aliases.has_key (name)) {
                string alias_name = name + "_result";
                variable_aliases[name] = alias_name;
                variable_types[alias_name] = type_name;
                return @"$type_name $alias_name = $expression;";
            }

            string storage_name = variable_aliases.has_key (name) ? variable_aliases[name] : name;
            if (storage_name != name && variable_types.has_key (storage_name) && variable_types[storage_name] == type_name) {
                return @"$storage_name = $expression;";
            }
            if (variable_types.has_key (name)) {
                string old_type = variable_types[name];
                if (old_type != type_name && type_name != "var") {
                    string alias_name = unique_alias_name (name, type_name);
                    variable_aliases[name] = alias_name;
                    variable_types[alias_name] = type_name;
                    return @"$type_name $alias_name = $expression;";
                }
                return @"$storage_name = $expression;";
            }

            variable_types[name] = type_name;
            return @"$type_name $name = $expression;";
        }

        if (function_call_regex.match (line, 0, out match)) {
            string php_function_name = match.fetch (1);
            if (php_function_name == "exit" || php_function_name == "die") {
                return transpile_exit (match.fetch (2).strip (), php_line_number);
            }

            if (local_functions.has_key (php_function_name)) {
                string args = convert_function_argument_list (match.fetch (2).strip (), php_line_number);
                return @"$php_function_name ($args);";
            }

            string function_name = map_php_function_name (php_function_name, php_line_number);
            string args = convert_function_args (php_function_name, match.fetch (2).strip (), php_line_number);
            if (php_function_name == "phpinfo" || php_function_name == "var_dump") {
                return "stdout.printf (\"%s\", " + function_name + " (" + args + "));";
            }
            return @"$function_name ($args);";
        }

        int static_call_index = line.index_of ("::");
        if (static_call_index > 0 && line.has_suffix (");")) {
            return "// skipped PHP static call: " + line;
        }

        throw unsupported_syntax (php_line_number, line);
    }

    private static string? parse_assignment_name (string line) {
        if (!line.has_prefix ("$") || !line.has_suffix (";")) {
            return null;
        }

        int equals_index = line.index_of ("=");
        if (equals_index <= 1) {
            return null;
        }

        string name = line.substring (1, equals_index - 1).strip ();
        if (!is_identifier (name)) {
            return null;
        }

        return name;
    }

    private static string parse_assignment_expression (string line) {
        int equals_index = line.index_of ("=");
        string expression = line.substring (equals_index + 1).strip ();
        return trim_trailing_semicolon (expression);
    }

    private string? build_array_initialization (string name) {
        if (name == "matrix") {
            variable_types[name] = "Gee.ArrayList<Gee.ArrayList<string>>";
            return @"Gee.ArrayList<Gee.ArrayList<string>> $name = new Gee.ArrayList<Gee.ArrayList<string>> ();";
        }
        if (current_local_function_name == "get_total_mem_zram" && name == "ret") {
            variable_types[name] = "Gee.HashMap<string, Gee.HashMap<string, string>>";
            return @"Gee.HashMap<string, Gee.HashMap<string, string>> $name = new Gee.HashMap<string, Gee.HashMap<string, string>> ();";
        }
        if (name == "ret" || name == "ret2") {
            variable_types[name] = "Gee.HashMap<string, double?>";
            return @"Gee.HashMap<string, double?> $name = new Gee.HashMap<string, double?> ();";
        }
        return null;
    }

    private string unique_alias_name (string name, string type_name) {
        string suffix = type_name.index_of ("ArrayList") >= 0 ? "list" : "value";
        string candidate = name + "_" + suffix;
        int index = 2;
        while (variable_types.has_key (candidate)) {
            candidate = "%s_%s%d".printf (name, suffix, index);
            index++;
        }
        return candidate;
    }

    private string? transpile_array_assignment (string line, int php_line_number) throws Error {
        if (!line.has_prefix ("$") || !line.has_suffix (";")) {
            return null;
        }

        int bracket_index = line.index_of ("[");
        int equals_index = line.index_of ("=");
        if (bracket_index <= 1 || equals_index <= bracket_index) {
            return null;
        }

        string name = line.substring (1, bracket_index - 1).strip ();
        if (!is_identifier (name)) {
            return null;
        }

        string access = line.substring (bracket_index, equals_index - bracket_index).strip ();
        string value = convert_expression (trim_trailing_semicolon (line.substring (equals_index + 1).strip ()), php_line_number);
        var keys = parse_array_keys (access, php_line_number);
        if (keys.size == 0) {
            return null;
        }

        string init = "";
        if (!variable_types.has_key (name)) {
            variable_types[name] = "Gee.HashMap<string, string>";
            init = "Gee.HashMap<string, string> %s = new Gee.HashMap<string, string> ();\n".printf (name);
        }

        if (keys.size == 1) {
            string type_name = variable_types.has_key (name) ? variable_types[name] : "";
            if (type_name == "Gee.HashMap<string, double?>") {
                return init + @"$name.set ($(keys[0]), $(numeric_expression (value)));";
            }
            return init + @"$name.set ($(keys[0]), $value);";
        }

        if (keys.size == 2) {
            string type_name = variable_types.has_key (name) ? variable_types[name] : "";
            if (name == "matrix" || type_name == "Gee.ArrayList<Gee.ArrayList<string>>") {
                return init
                    + @"while ($name.size <= $(keys[0])) { $name.add (new Gee.ArrayList<string> ()); }" + "\n"
                    + "var %s_row = %s.get (%s);\n".printf (name, name, keys[0])
                    + "while (%s_row.size <= %s) { %s_row.add (\"\"); }\n".printf (name, keys[1], name)
                    + "%s_row.set (%s, %s);".printf (name, keys[1], value);
            }
            if (type_name == "Gee.HashMap<string, Gee.HashMap<string, string>>") {
                if (keys[1] == "") {
                    return init + "// skipped PHP append to nested mixed array";
                }
                return init
                    + @"if (!$name.has_key ($(keys[0])) || $name.get ($(keys[0])) == null) { $name.set ($(keys[0]), new Gee.HashMap<string, string> ()); }" + "\n"
                    + @"$name.get ($(keys[0])).set ($(keys[1]), $value);";
            }
            if (keys[1] == "") {
                return init + "// skipped PHP append to nested mixed array";
            }
            return init + @"$name.get ($(keys[0])).set ($(keys[1]), $value);";
        }

        throw unsupported_syntax (php_line_number, line);
    }

    private string numeric_expression (string expression) {
        string clean = expression.strip ();
        if (clean.index_of ("*") >= 0) {
            var parts = split_top_level (clean, '*');
            if (parts.size == 2) {
                return "%s * %s".printf (numeric_expression (parts[0].strip ()), parts[1].strip ());
            }
        }
        if (clean.has_prefix ("PhpEmbed.trim_static(")
            || clean.has_prefix ("PhpEmbed.str_replace_static(")
            || (variable_types.has_key (clean) && variable_types[clean] == "string")) {
            return "double.parse (%s)".printf (clean);
        }
        return clean;
    }

    private ArrayList<string> parse_array_keys (string access, int php_line_number) throws Error {
        var keys = new ArrayList<string> ();
        int index = 0;
        while (index < access.length) {
            if (access[index] != '[') {
                throw unsupported_syntax (php_line_number, access);
            }
            int end = find_matching_bracket (access, index);
            if (end < 0) {
                throw unsupported_syntax (php_line_number, access);
            }
            string key = access.substring (index + 1, end - index - 1).strip ();
            keys.add (convert_expression (key, php_line_number));
            index = end + 1;
        }
        return keys;
    }

    private static int find_matching_bracket (string value, int start_index) {
        bool in_single_quote = false;
        bool in_double_quote = false;
        int depth = 0;
        for (int i = start_index + 1; i < value.length; i++) {
            char c = value[i];
            if (c == '\'' && !in_double_quote) {
                in_single_quote = !in_single_quote;
            } else if (c == '"' && !in_single_quote) {
                in_double_quote = !in_double_quote;
            } else if (!in_single_quote && !in_double_quote) {
                if (c == '[') {
                    depth++;
                } else if (c == ']') {
                    if (depth == 0) {
                        return i;
                    }
                    depth--;
                }
            }
        }
        return -1;
    }

    private static bool is_identifier (string name) {
        if (name == "") {
            return false;
        }

        for (int i = 0; i < name.length; i++) {
            char c = name[i];
            if (i == 0) {
                if (!(c.isalpha () || c == '_')) {
                    return false;
                }
            } else if (!is_identifier_char (c)) {
                return false;
            }
        }

        return true;
    }

    private string transpile_echo (string expression, int php_line_number) throws Error {
        string clean_expression = trim_trailing_semicolon (expression);

        var parts = split_top_level (clean_expression, ',');
        var output = new StringBuilder ();

        foreach (string raw_part in parts) {
            var concat_parts = split_concat (raw_part.strip ());

            foreach (string raw_concat_part in concat_parts) {
                string part = convert_expression (raw_concat_part.strip (), php_line_number);
                string format = format_for_expression (part);
                string value = value_for_format (part);

                if (output.len > 0) {
                    output.append (" ");
                }

                output.append_printf ("stdout.printf (\"%s\", %s);", format, value);
            }
        }

        return output.str;
    }

    private string transpile_inline_condition (string line, string vala_keyword, int php_line_number) throws Error {
        int open_index = line.index_of ("(");
        int close_index = find_matching_paren (line, open_index);
        if (open_index < 0 || close_index < 0) {
            throw unsupported_syntax (php_line_number, line);
        }

        string condition = convert_expression (line.substring (open_index + 1, close_index - open_index - 1), php_line_number);
        string rest = line.substring (close_index + 1).strip ();
        if (rest == "" || rest == "{") {
            return @"$vala_keyword ($condition) {";
        }

        string nested = transpile_line (rest, php_line_number);
        return @"$vala_keyword ($condition) { $nested }";
    }

    private string transpile_foreach (string line, int php_line_number) throws Error {
        int open_index = line.index_of ("(");
        int close_index = find_matching_paren (line, open_index);
        if (open_index < 0 || close_index < 0) {
            throw unsupported_syntax (php_line_number, line);
        }
        string inner = line.substring (open_index + 1, close_index - open_index - 1).strip ();
        var parts = inner.split (" as ");
        if (parts.length != 2) {
            throw unsupported_syntax (php_line_number, line);
        }
        string source = convert_expression (parts[0].strip (), php_line_number);
        string target = parts[1].strip ();
        int key_sep = target.index_of ("=>");
        if (key_sep >= 0) {
            string key_name = replace_php_variables (target.substring (0, key_sep).strip ());
            string value_name = replace_php_variables (target.substring (key_sep + 2).strip ());
            string source_type = infer_expression_type (source);
            if (source_type.has_prefix ("Gee.HashMap<")) {
                string key_type = hash_key_type (source_type);
                string value_type = hash_value_type (source_type);
                variable_types[key_name] = key_type;
                variable_types[value_name] = value_type;
                return @"foreach ($key_type $key_name in $source.keys) { $value_type $value_name = $source.get ($key_name);";
            }
            variable_types[key_name] = "int";
            variable_types[value_name] = "string";
            return @"for (int $key_name = 0; $key_name < $source.size; $key_name++) { string $value_name = $source.get ($key_name);";
        }
        string item_name = replace_php_variables (target);
        variable_types[item_name] = "string";
        return @"foreach (var $item_name in $source) {";
    }

    private string transpile_for (string line, int php_line_number) throws Error {
        int open_index = line.index_of ("(");
        int close_index = find_matching_paren (line, open_index);
        if (open_index < 0 || close_index < 0) {
            throw unsupported_syntax (php_line_number, line);
        }
        string inner = line.substring (open_index + 1, close_index - open_index - 1).strip ();
        var parts = split_top_level (inner, ';');
        if (parts.size == 3) {
            string init = convert_for_part (parts[0].strip (), true, php_line_number);
            string condition = convert_for_part (parts[1].strip (), false, php_line_number);
            string increment = convert_for_part (parts[2].strip (), false, php_line_number);
            return @"for ($init; $condition; $increment) {";
        }
        inner = convert_expression (inner, php_line_number);
        return @"for ($inner) {";
    }

    private string convert_for_part (string expression, bool is_init, int php_line_number) throws Error {
        string converted = convert_expression (expression, php_line_number);
        if (is_init) {
            int equals_index = converted.index_of ("=");
            if (equals_index > 0) {
                string name = converted.substring (0, equals_index).strip ();
                if (is_identifier (name)) {
                    variable_types[name] = "int";
                    return "int " + converted;
                }
            }
        }
        return converted;
    }

    private string transpile_return (string line, int php_line_number) throws Error {
        string expression = line.substring ("return".length).strip ();
        expression = trim_trailing_semicolon (expression);
        if (expression.has_prefix ("(") && expression.has_suffix (")")) {
            expression = expression.substring (1, expression.length - 2).strip ();
        }
        if (expression == "") {
            return "return;";
        }
        return "return %s;".printf (convert_expression (expression, php_line_number));
    }

    private string? transpile_compound_assignment (string line, int php_line_number) throws Error {
        if (!line.has_prefix ("$") || !line.has_suffix (";")) {
            return null;
        }

        string clean = trim_trailing_semicolon (line.substring (1));
        string[] operators = {"/=", "*=", "+=", "-="};
        foreach (string op in operators) {
            int index = clean.index_of (op);
            if (index <= 0) {
                continue;
            }
            string lhs = replace_php_variables (clean.substring (0, index).strip ());
            string rhs = convert_expression (clean.substring (index + op.length).strip (), php_line_number);
            string vala_op = op.substring (0, 1);
            if (is_identifier (lhs) && variable_types.has_key (lhs) && variable_types[lhs] == "string" && (op == "*=" || op == "/=")) {
                string alias_name = unique_alias_name (lhs, "double");
                variable_aliases[lhs] = alias_name;
                variable_types[alias_name] = "double";
                return @"double $alias_name = double.parse ($lhs) $vala_op $rhs;";
            }
            int bracket_index = lhs.index_of ("[");
            if (bracket_index > 0) {
                string name = lhs.substring (0, bracket_index);
                var keys = parse_array_keys (lhs.substring (bracket_index), php_line_number);
                if (keys.size == 1) {
                    string key = keys[0];
                    return @"$name.set ($key, (($name.has_key ($key) && $name.get ($key) != null) ? $name.get ($key) : 0.0) $vala_op double.parse ($rhs));";
                }
            }
            return @"$lhs $vala_op= $rhs;";
        }
        return null;
    }

    public static int find_matching_paren (string value, int start_index) {
        if (start_index < 0) {
            return -1;
        }
        bool in_single_quote = false;
        bool in_double_quote = false;
        int depth = 0;
        for (int i = start_index; i < value.length; i++) {
            char c = value[i];
            if (c == '\'' && !in_double_quote) {
                in_single_quote = !in_single_quote;
            } else if (c == '"' && !in_single_quote) {
                in_double_quote = !in_double_quote;
            } else if (!in_single_quote && !in_double_quote) {
                if (c == '(') {
                    depth++;
                } else if (c == ')') {
                    depth--;
                    if (depth == 0) {
                        return i;
                    }
                }
            }
        }
        return -1;
    }

    public static int find_matching_brace (string value, int start_index) {
        if (start_index < 0) {
            return -1;
        }
        bool in_single_quote = false;
        bool in_double_quote = false;
        int depth = 0;
        for (int i = start_index; i < value.length; i++) {
            char c = value[i];
            if (c == '\'' && !in_double_quote) {
                in_single_quote = !in_single_quote;
            } else if (c == '"' && !in_single_quote) {
                in_double_quote = !in_double_quote;
            } else if (!in_single_quote && !in_double_quote) {
                if (c == '{') {
                    depth++;
                } else if (c == '}') {
                    depth--;
                    if (depth == 0) {
                        return i;
                    }
                }
            }
        }
        return -1;
    }

    private string transpile_exit (string expression, int php_line_number) throws Error {
        string clean_expression = trim_trailing_semicolon (expression).strip ();
        if (clean_expression == "") {
            string function_name = map_php_function_name ("exit_no_args", php_line_number);
            if (transpiling_local_function) {
                return "Process.exit (%s (%s));".printf (function_name, source_location_args (php_line_number).substring (2));
            }
            return "return %s (%s);".printf (function_name, source_location_args (php_line_number).substring (2));
        }

        string converted_expression = convert_expression (clean_expression, php_line_number);
        if (is_string_exit_expression (converted_expression)) {
            string function_name = map_php_function_name ("exit_string", php_line_number);
            if (transpiling_local_function) {
                return "Process.exit (%s (%s%s));".printf (function_name, value_for_format (converted_expression), source_location_args (php_line_number));
            }
            return "return %s (%s%s);".printf (function_name, value_for_format (converted_expression), source_location_args (php_line_number));
        }

        string function_name = map_php_function_name ("exit_int", php_line_number);
        if (transpiling_local_function) {
            return "Process.exit (%s ((int) (%s)%s));".printf (function_name, converted_expression, source_location_args (php_line_number));
        }
        return "return %s ((int) (%s)%s);".printf (function_name, converted_expression, source_location_args (php_line_number));
    }

    public string convert_expression (string expression, int php_line_number) throws Error {
        string result = expression.strip ();
        result = trim_trailing_semicolon (result);
        result = normalize_php_boolean_comparisons (result);
        result = result.replace ("!==", "!=").replace ("===", "==");
        result = convert_string_literal (result);
        result = replace_php_variables_with_aliases (result);
        result = replace_division_operator (result, php_line_number);
        result = replace_concat_operator (result);
        result = replace_known_function_names (result, php_line_number);
        result = replace_inline_known_function_names (result, php_line_number);
        result = convert_array_accesses (result);

        if (result == "TRUE") {
            return "true";
        }
        if (result == "FALSE") {
            return "false";
        }
        if (result == "NULL") {
            return "null";
        }
        if (result == "TOKEN_PARSE") {
            return "1";
        }

        return result;
    }

    private static string normalize_php_boolean_comparisons (string expression) {
        return expression
            .replace ("!==false", "!=-1")
            .replace ("!== false", "!= -1")
            .replace ("!=false", "!=-1")
            .replace ("!= false", "!= -1")
            .replace ("===false", "==-1")
            .replace ("=== false", "== -1")
            .replace ("==false", "==-1")
            .replace ("== false", "== -1");
    }

    private static string convert_string_literal (string expression) {
        if (expression.length < 2) {
            return expression;
        }

        bool single_quoted = expression.has_prefix ("'") && expression.has_suffix ("'");
        bool double_quoted = expression.has_prefix ("\"") && expression.has_suffix ("\"");
        if (!single_quoted && !double_quoted) {
            return expression;
        }

        string value = expression.substring (1, expression.length - 2);
        if (single_quoted) {
            return "\"%s\"".printf (escape_vala_string (value));
        }

        return "\"%s\"".printf (escape_vala_string (decode_php_double_quoted_string (value)));
    }

    public string convert_string_literal_public (string expression) {
        return convert_string_literal (expression);
    }

    private static string decode_php_double_quoted_string (string value) {
        return value
            .replace ("\\r", "\r")
            .replace ("\\n", "\n")
            .replace ("\\t", "\t")
            .replace ("\\\"", "\"")
            .replace ("\\\\", "\\");
    }

    private string replace_known_function_names (string expression, int php_line_number) throws Error {
        string? special = convert_special_function_expression (expression, php_line_number);
        if (special != null) {
            return special;
        }

        foreach (string function_name in local_functions.keys) {
            string php_prefix = function_name + "(";
            if (expression.has_prefix (php_prefix) && expression.has_suffix (")") && find_matching_paren (expression, php_prefix.length - 1) == expression.length - 1) {
                string inner_args = expression.substring (php_prefix.length, expression.length - php_prefix.length - 1);
                LocalFunction? local_function = local_functions[function_name];
                if (optimize_file_func_vars
                    && transpiling_local_function
                    && local_function != null
                    && file_func_vars_optimizer.can_emit (local_function)) {
                    return file_func_vars_optimizer.local_function_call_as_native_value (local_function, inner_args, php_line_number);
                }
                if (optimize_local_vars
                    && transpiling_local_function
                    && local_function != null
                    && local_vars_optimizer.can_emit (local_function)) {
                    return local_vars_optimizer.local_function_call_as_native_value (local_function, inner_args, php_line_number);
                }
                return (local_function != null ? zval_local_function_name (local_function) : function_name) + "(" + convert_function_argument_list (inner_args, php_line_number) + ")";
            }
        }

        foreach (string function_name in external_functions.keys) {
            string php_prefix = function_name + "(";
            if (expression.has_prefix (php_prefix) && expression.has_suffix (")") && find_matching_paren (expression, php_prefix.length - 1) == expression.length - 1) {
                string inner_args = expression.substring (php_prefix.length, expression.length - php_prefix.length - 1);
                LocalFunction? external_function = external_functions[function_name];
                if (optimize_cross_file_func_vars
                    && external_function != null
                    && file_func_vars_optimizer.can_call_external_static (external_function)
                    && external_function.static_return_type != "void") {
                    validate_php_stub_argument_count (external_function, inner_args, php_line_number, function_name);
                    return file_func_vars_optimizer.external_function_call_as_native_value (external_function, inner_args, php_line_number);
                }
                if (external_function != null) {
                    validate_php_stub_argument_count (external_function, inner_args, php_line_number, function_name);
                }
                return external_function != null ? external_zval_call_name (external_function) + "(" + convert_function_argument_list (inner_args, php_line_number) + ")" : function_name + "(" + convert_function_argument_list (inner_args, php_line_number) + ")";
            }
        }

        foreach (string function_name in function_bindings.keys) {
            string php_prefix = function_name + "(";
            if (expression.has_prefix (php_prefix) && expression.has_suffix (")") && find_matching_paren (expression, php_prefix.length - 1) == expression.length - 1) {
                string inner_args = expression.substring (php_prefix.length, expression.length - php_prefix.length - 1);
                return map_php_function_name (function_name, php_line_number) + "(" + convert_function_args (function_name, inner_args, php_line_number) + ")";
            }
        }

        MatchInfo match;
        if (function_expression_regex.match (expression, 0, out match)) {
            string function_name = match.fetch (1);
            if (function_name.has_prefix ("__php2vala_file_func_")) {
                return expression;
            }
            int open_index = expression.index_of ("(");
            if (open_index >= 0 && find_matching_paren (expression, open_index) == expression.length - 1) {
                throw unsupported_function (php_line_number, function_name);
            }
        }

        return expression;
    }

    private string? convert_special_function_expression (string expression, int php_line_number) throws Error {
        MatchInfo match;
        if (!function_expression_regex.match (expression, 0, out match)) {
            return null;
        }

        string function_name = match.fetch (1);
        string args = match.fetch (2).strip ();
        if (function_name == "isset") {
            string converted_args = convert_function_argument_list (args, php_line_number);
            return "%s(%s%s)".printf (
                map_php_function_name ("isset", php_line_number),
                converted_args,
                source_location_args (php_line_number)
            );
        }
        if (function_name == "count") {
            string converted_args = convert_function_argument_list (args, php_line_number);
            string type_name = infer_expression_type (converted_args);
            if (type_name.has_prefix ("Gee.ArrayList<") || type_name.has_prefix ("Gee.HashMap<")) {
                return converted_args + ".size";
            }
        }
        if (function_name == "empty") {
            string converted_args = convert_function_argument_list (args, php_line_number);
            string type_name = infer_expression_type (converted_args);
            if (type_name == "string") {
                return converted_args + " == \"\"";
            }
            if (type_name.has_prefix ("Gee.ArrayList<") || type_name.has_prefix ("Gee.HashMap<")) {
                return converted_args + ".size == 0";
            }
        }
        return null;
    }

    public string map_php_function_name (string function_name, int php_line_number) throws Error {
        FunctionBinding? binding = function_bindings.get (function_name);
        if (binding == null) {
            throw unsupported_function (php_line_number, function_name);
        }

        if (is_embed_runtime_binding (binding)) {
            info_missing_native_runtime_once (function_name, binding.mapped_function_name ?? function_name);
        }
        mark_function_used (function_name);
        return binding.call_name (function_name);
    }

    public string map_php_runtime_function_name (string php_function_name, string runtime_function_name, int php_line_number) throws Error {
        FunctionBinding? binding = function_bindings.get (runtime_function_name);
        if (binding != null) {
            if (is_embed_runtime_binding (binding)) {
                info_missing_native_runtime_once (php_function_name, runtime_function_name);
            }
            mark_function_used (runtime_function_name);
            return binding.call_name (runtime_function_name);
        }

        FunctionBinding? php_binding = function_bindings.get (php_function_name);
        if (php_binding != null && is_embed_runtime_binding (php_binding)) {
            info_missing_native_runtime_once (php_function_name, runtime_function_name);
            mark_function_used (runtime_function_name);
            return php_binding.namespace_name + "." + runtime_function_name;
        }

        throw unsupported_function (php_line_number, php_function_name);
    }

    private void note_embed_zval_fallback (string function_name, string runtime_function_name) {
        if (!has_native_runtime_candidate (function_name)) {
            info_missing_native_runtime_once (function_name, runtime_function_name);
        }
    }

    private string? resolve_static_call_name (string function_name) {
        string runtime_function_name = function_name + "_static";

        if (runtime_name == "native-runtime") {
            FunctionBinding? native_static = find_native_runtime_binding (runtime_function_name);
            if (native_static != null) {
                return native_static.call_name (runtime_function_name);
            }
            info_resolver_step_once (
                function_name,
                "native-static",
                "info: TODO in php2vala: static implementation in valaphp-runtime-* (native) missing for PHP function '%s'; trying native zval".printf (function_name)
            );
            return null;
        }

        if (runtime_name == "embed-runtime") {
            FunctionBinding? embed_static = find_embed_runtime_binding (runtime_function_name);
            if (embed_static != null) {
                return embed_static.call_name (runtime_function_name);
            }
            warn_missing_embed_static_once (function_name);
            return null;
        }

        if (runtime_name == "both-runtimes") {
            FunctionBinding? native_static = find_native_runtime_binding (runtime_function_name);
            if (native_static != null) {
                return native_static.call_name (runtime_function_name);
            }

            info_resolver_step_once (
                function_name,
                "native-static",
                "info: TODO in php2vala: static implementation in valaphp-runtime-* (native) missing for PHP function '%s'; trying native zval".printf (function_name)
            );

            if (find_native_runtime_binding (function_name + "_zval") != null) {
                return null;
            }

            info_resolver_step_once (
                function_name,
                "native-zval",
                "info: TODO in php2vala: zval implementation in valaphp-runtime-* (native) missing for PHP function '%s'; trying embed static".printf (function_name)
            );

            FunctionBinding? embed_static = find_embed_runtime_binding (runtime_function_name);
            if (embed_static != null) {
                return embed_static.call_name (runtime_function_name);
            }

            warn_missing_embed_static_once (function_name);
            return null;
        }

        return null;
    }

    private static string normalize_runtime_type (string type_name) {
        return type_name
            .replace ("owned ", "")
            .replace ("unowned ", "")
            .replace ("out ", "")
            .replace ("?", "")
            .strip ();
    }

    private static bool is_out_runtime_parameter_type (string type_name) {
        return type_name.strip ().has_prefix ("out ");
    }

    private static bool is_runtime_object_type (string type_name) {
        string clean = normalize_runtime_type (type_name);
        return clean.index_of (".") > 0
            && clean != "Php.zval"
            && !clean.has_suffix ("[]");
    }

    private static bool is_static_variant_for_php_function (string function_name, string runtime_function_name) {
        if (!runtime_function_name.has_suffix ("_static")) {
            return false;
        }

        string stem = runtime_function_name.substring (0, runtime_function_name.length - "_static".length);
        if (stem == function_name) {
            return true;
        }
        if (!stem.has_prefix (function_name + "_")) {
            return false;
        }

        string shape = stem.substring (function_name.length + 1);
        foreach (string token in shape.split ("_")) {
            if (!is_static_shape_token (token)) {
                return false;
            }
        }
        return true;
    }

    private static bool is_zval_variant_for_php_function (string function_name, string runtime_function_name) {
        if (!runtime_function_name.has_suffix ("_zval")) {
            return false;
        }

        string stem = runtime_function_name.substring (0, runtime_function_name.length - "_zval".length);
        if (stem == function_name) {
            return true;
        }
        if (!stem.has_prefix (function_name + "_")) {
            return false;
        }

        string shape = stem.substring (function_name.length + 1);
        foreach (string token in shape.split ("_")) {
            if (!is_static_shape_token (token)) {
                return false;
            }
        }
        return true;
    }

    private bool runtime_binding_accepts_arg_count (FunctionBinding binding, int arg_count) {
        int offset = binding.runtime_parameter_offset ();
        int limit = binding.runtime_parameter_limit ();
        if (limit < offset) {
            return false;
        }
        return binding.accepts_runtime_arg_count (arg_count);
    }

    private bool can_use_runtime_zval_binding (FunctionBinding binding) {
        if (normalize_runtime_type (binding.return_type) != "Php.zval") {
            return false;
        }
        int offset = binding.runtime_parameter_offset ();
        int limit = binding.runtime_parameter_limit ();
        for (int i = offset; i < limit; i++) {
            string parameter_type = normalize_runtime_type (binding.parameter_types[i]);
            if (parameter_type == "Php.zval" || parameter_type == "Php.zval[]") {
                continue;
            }
            if (is_out_runtime_parameter_type (binding.parameter_types[i])
                && normalize_runtime_type (binding.parameter_types[i]) == "Php.zval") {
                continue;
            }
            return false;
        }
        return true;
    }

    private FunctionBinding? find_runtime_zval_binding_for_arg_count (
        HashMap<string, FunctionBinding> bindings,
        string namespace_name,
        string function_name,
        int arg_count,
        out string runtime_function_name
    ) {
        runtime_function_name = "";
        FunctionBinding? exact = bindings.get (function_name + "_zval");
        if (exact != null
            && exact.namespace_name == namespace_name
            && runtime_binding_accepts_arg_count (exact, arg_count)
            && can_use_runtime_zval_binding (exact)) {
            runtime_function_name = function_name + "_zval";
            return exact;
        }

        foreach (string candidate_name in bindings.keys) {
            if (!is_zval_variant_for_php_function (function_name, candidate_name)) {
                continue;
            }
            FunctionBinding? candidate = bindings.get (candidate_name);
            if (candidate == null
                || candidate.namespace_name != namespace_name
                || !runtime_binding_accepts_arg_count (candidate, arg_count)
                || !can_use_runtime_zval_binding (candidate)) {
                continue;
            }

            runtime_function_name = candidate_name;
            return candidate;
        }

        return null;
    }

    private FunctionBinding? resolve_zval_runtime_binding_for_arg_count (string function_name, int arg_count, out string runtime_function_name) {
        runtime_function_name = "";

        if (runtime_name == "native-runtime") {
            FunctionBinding? native_zval = find_runtime_zval_binding_for_arg_count (
                native_function_bindings,
                "PhpNative",
                function_name,
                arg_count,
                out runtime_function_name
            );
            if (native_zval != null) {
                return native_zval;
            }
            return null;
        }

        if (runtime_name == "both-runtimes") {
            FunctionBinding? native_zval = find_runtime_zval_binding_for_arg_count (
                native_function_bindings,
                "PhpNative",
                function_name,
                arg_count,
                out runtime_function_name
            );
            if (native_zval != null) {
                return native_zval;
            }
            info_resolver_step_once (
                function_name,
                "native-zval",
                "info: TODO in php2vala: zval implementation in valaphp-runtime-* (native) missing for PHP function '%s'; trying embed zval".printf (function_name)
            );
        }

        FunctionBinding? embed_zval = find_runtime_zval_binding_for_arg_count (
            embed_function_bindings,
            "PhpEmbed",
            function_name,
            arg_count,
            out runtime_function_name
        );
        if (embed_zval != null) {
            return embed_zval;
        }

        return null;
    }

    private FunctionBinding? resolve_zval_runtime_binding_for_arg_count_silent (string function_name, int arg_count, out string runtime_function_name) {
        runtime_function_name = "";

        if (runtime_name == "native-runtime") {
            return find_runtime_zval_binding_for_arg_count (
                native_function_bindings,
                "PhpNative",
                function_name,
                arg_count,
                out runtime_function_name
            );
        }

        if (runtime_name == "both-runtimes") {
            FunctionBinding? native_zval = find_runtime_zval_binding_for_arg_count (
                native_function_bindings,
                "PhpNative",
                function_name,
                arg_count,
                out runtime_function_name
            );
            if (native_zval != null) {
                return native_zval;
            }
        }

        return find_runtime_zval_binding_for_arg_count (
            embed_function_bindings,
            "PhpEmbed",
            function_name,
            arg_count,
            out runtime_function_name
        );
    }

    private static bool is_static_shape_token (string token) {
        switch (token) {
        case "array":
        case "bool":
        case "boolean":
        case "char":
        case "chars":
        case "double":
        case "float":
        case "int":
        case "integer":
        case "list":
        case "long":
        case "numeric":
        case "string":
        case "values":
            return true;
        default:
            return false;
        }
    }

    private FunctionBinding? resolve_zval_runtime_binding (string function_name, string runtime_function_name) {
        if (runtime_name == "both-runtimes") {
            if (optimize_local_vars || optimize_file_func_vars) {
                if (find_native_runtime_binding (function_name + "_static") == null) {
                    info_resolver_step_once (
                        function_name,
                        "native-static",
                        "info: TODO in php2vala: static implementation in valaphp-runtime-* (native) missing for PHP function '%s'; trying native zval".printf (function_name)
                    );
                }
            }

            FunctionBinding? native_zval = find_native_runtime_binding (runtime_function_name);
            if (native_zval != null) {
                return native_zval;
            }

            info_resolver_step_once (
                function_name,
                "native-zval",
                (optimize_local_vars || optimize_file_func_vars)
                    ? "info: TODO in php2vala: zval implementation in valaphp-runtime-* (native) missing for PHP function '%s'; trying embed zval".printf (function_name)
                    : "info: TODO in php2vala: zval implementation in valaphp-runtime-* (native) missing for PHP function '%s'; trying embed zval".printf (function_name)
            );

            FunctionBinding? embed_zval = find_embed_runtime_binding (runtime_function_name);
            if (embed_zval != null) {
                return embed_zval;
            }
            return null;
        }

        if (runtime_name == "native-runtime") {
            if (optimize_local_vars || optimize_file_func_vars) {
                if (find_native_runtime_binding (function_name + "_static") == null) {
                    info_resolver_step_once (
                        function_name,
                        "native-static",
                        "info: TODO in php2vala: static implementation in valaphp-runtime-* (native) missing for PHP function '%s'; trying native zval".printf (function_name)
                    );
                }
            }
            return find_native_runtime_binding (runtime_function_name);
        }

        if (optimize_local_vars || optimize_file_func_vars) {
            if (runtime_function_name.has_suffix ("_static")) {
                warn_missing_embed_static_once (function_name);
            }
        }
        FunctionBinding? embed_binding = find_embed_runtime_binding (runtime_function_name);
        if (embed_binding != null) {
            return embed_binding;
        }
        return null;
    }

    private void info_resolver_step_once (string function_name, string step_name, string message) {
        string key = function_name + "|" + step_name;
        if (informed_missing_native_runtime.has_key (key)) {
            return;
        }

        informed_missing_native_runtime[key] = true;
        stderr.printf ("%s\n", message);
    }

    private void warn_missing_embed_static_once (string function_name) {
        if (!(optimize_local_vars || optimize_file_func_vars)) {
            return;
        }
        if (has_embed_static_binding (function_name)) {
            return;
        }
        if (warned_missing_embed_static.has_key (function_name)) {
            return;
        }

        warned_missing_embed_static[function_name] = true;
        stderr.printf (
            "info: TODO in php2vala: static implementation in valaphp-runtime-embed missing for PHP function '%s'; falling back to zval\n",
            function_name
        );
    }

    private void info_missing_native_runtime_once (string function_name, string runtime_function_name) {
        if (runtime_name != "both-runtimes") {
            return;
        }
        string key = function_name + "|" + runtime_function_name;
        if (informed_missing_native_runtime.has_key (key)) {
            return;
        }

        informed_missing_native_runtime[key] = true;
        string embed_fallback_kind = "embed fallback";
        if (runtime_function_name.has_suffix ("_static")) {
            embed_fallback_kind = "embed static fallback";
        } else if (runtime_function_name.has_suffix ("_zval")) {
            embed_fallback_kind = "embed zval fallback";
        }
        stderr.printf (
            "info: TODO in php2vala: native implementation in valaphp-runtime-* missing for PHP function '%s' via '%s'; using %s\n",
            function_name,
            runtime_function_name,
            embed_fallback_kind
        );
    }

    private bool has_embed_static_binding (string function_name) {
        FunctionBinding? binding = find_embed_runtime_binding (function_name + "_static");
        return binding != null && is_embed_runtime_binding (binding);
    }

    private bool has_native_runtime_binding (string runtime_function_name) {
        return find_native_runtime_binding (runtime_function_name) != null;
    }

    private bool has_native_runtime_candidate (string function_name) {
        return has_native_runtime_binding (function_name)
            || has_native_runtime_binding (function_name + "_static")
            || has_native_runtime_binding (function_name + "_zval");
    }

    private bool is_embed_runtime_binding (FunctionBinding binding) {
        return binding.namespace_name == "PhpEmbed"
            && PhpToValaRuntimeBindings.is_runtime_package (binding.package_name, "valaphp-runtime-embed");
    }

    private FunctionBinding? find_native_runtime_binding (string runtime_function_name) {
        FunctionBinding? binding = native_function_bindings.get (runtime_function_name);
        if (binding == null || binding.namespace_name != "PhpNative") {
            return null;
        }
        return binding;
    }

    private FunctionBinding? find_embed_runtime_binding (string runtime_function_name) {
        FunctionBinding? binding = embed_function_bindings.get (runtime_function_name);
        if (binding == null || !is_embed_runtime_binding (binding)) {
            return null;
        }
        return binding;
    }

    private string replace_inline_known_function_names (string expression, int php_line_number) {
        string result = expression;
        result = replace_inline_core_construct_call (result, "isset", "isset_value_static", php_line_number);
        foreach (string function_name in function_bindings.keys) {
            FunctionBinding? binding = function_bindings.get (function_name);
            if (binding == null) {
                continue;
            }
            result = replace_inline_function_call (result, function_name, php_line_number);
        }
        foreach (string function_name in local_functions.keys) {
            LocalFunction? local_function = local_functions[function_name];
            if (optimize_file_func_vars
                && transpiling_local_function
                && local_function != null
                && file_func_vars_optimizer.can_emit (local_function)) {
                result = file_func_vars_optimizer.replace_inline_local_function_call (result, local_function, php_line_number);
            } else if (optimize_local_vars
                && transpiling_local_function
                && local_function != null
                && local_vars_optimizer.can_emit (local_function)) {
                result = local_vars_optimizer.replace_inline_local_function_call (result, local_function, php_line_number);
            } else {
                result = replace_function_token (result, function_name, local_function != null ? zval_local_function_name (local_function) : function_name);
            }
        }
        return result;
    }

    private string replace_inline_core_construct_call (string expression, string function_name, string mapped_function_name, int php_line_number) {
        var output = new StringBuilder ();
        int i = 0;
        while (i < expression.length) {
            if (expression.substring (i).has_prefix (function_name + "(")
                && (i == 0 || (!is_identifier_char (expression[i - 1]) && expression[i - 1] != '.'))) {
                int open_index = i + function_name.length;
                int close_index = find_matching_paren (expression, open_index);
                if (close_index > open_index) {
                    string inner_args = expression.substring (open_index + 1, close_index - open_index - 1);
                    try {
                        mark_function_used (function_name);
                        output.append ("Php.");
                        output.append (mapped_function_name);
                        output.append ("(");
                        output.append (convert_function_argument_list (inner_args, php_line_number));
                        output.append (source_location_args (php_line_number));
                        output.append (")");
                    } catch (Error e) {
                        output.append (expression.substring (i, close_index - i + 1));
                    }
                    i = close_index + 1;
                    continue;
                }
            }

            output.append_c (expression[i]);
            i++;
        }
        return output.str;
    }

    private string replace_inline_function_call (string expression, string function_name, int php_line_number) {
        FunctionBinding? binding = function_bindings.get (function_name);
        if (binding == null) {
            return expression;
        }

        var output = new StringBuilder ();
        int i = 0;
        while (i < expression.length) {
            if (expression.substring (i).has_prefix (function_name + "(")
                && (i == 0 || (!is_identifier_char (expression[i - 1]) && expression[i - 1] != '.'))) {
                int open_index = i + function_name.length;
                int close_index = find_matching_paren (expression, open_index);
                if (close_index > open_index) {
                    string inner_args = expression.substring (open_index + 1, close_index - open_index - 1);
                    try {
                        string? special = convert_special_function_expression (function_name + "(" + inner_args + ")", php_line_number);
                        if (special != null) {
                            output.append (special);
                            i = close_index + 1;
                            continue;
                        }
                        mark_function_used (function_name);
                        output.append (binding.call_name (function_name));
                        output.append ("(");
                        output.append (convert_function_args (function_name, inner_args, php_line_number));
                        output.append (")");
                        i = close_index + 1;
                        continue;
                    } catch (Error e) {
                    }
                }
            }
            output.append_c (expression[i]);
            i++;
        }
        return output.str;
    }

    private static string replace_function_token (string expression, string function_name, string replacement_name) {
        var output = new StringBuilder ();
        int i = 0;
        while (i < expression.length) {
            if (expression.substring (i).has_prefix (function_name + "(")
                && (i == 0 || (!is_identifier_char (expression[i - 1]) && expression[i - 1] != '.'))) {
                output.append (replacement_name);
                output.append_c ('(');
                i += function_name.length + 1;
                continue;
            }
            output.append_c (expression[i]);
            i++;
        }
        return output.str;
    }

    private void mark_function_used (string function_name) {
        if (!used_function_lookup.has_key (function_name)) {
            used_function_lookup[function_name] = true;
            used_function_names.add (function_name);
        }
    }

    private string convert_function_args (string function_name, string args, int php_line_number) throws Error {
        string converted_args = convert_function_argument_list (args, php_line_number);
        FunctionBinding? binding = function_bindings.get (function_name);
        bool add_source_location = binding != null && binding.source_location_args;

        switch (function_name) {
        case "json_decode":
            if (converted_args == "") {
                return add_source_location ? source_location_args (php_line_number).substring (2) : "";
            }
            if (split_top_level (converted_args, ',').size == 1) {
                converted_args += ", false";
            }
            return add_source_location ? converted_args + source_location_args (php_line_number) : converted_args;
        case "json_last_error_msg":
        case "phpinfo":
            return add_source_location ? source_location_args (php_line_number).substring (2) : "";
        case "token_get_all":
            var token_parts = split_top_level (converted_args, ',');
            if (token_parts.size == 1) {
                converted_args += ", false";
            } else if (token_parts.size == 2) {
                string parse_arg = token_parts[1].strip ();
                if (parse_arg != "true" && parse_arg != "false") {
                    converted_args = "%s, %s != 0".printf (token_parts[0].strip (), parse_arg);
                }
            }
            return add_source_location ? converted_args + source_location_args (php_line_number) : converted_args;
        case "var_dump":
            return add_source_location ? converted_args + source_location_args (php_line_number) : converted_args;
        case "serialize":
            return add_source_location ? converted_args + source_location_args (php_line_number) : converted_args;
        default:
            string? generic_args = convert_function_args_from_binding (binding, converted_args, php_line_number);
            if (generic_args != null) {
                return generic_args;
            }
            return add_source_location ? converted_args + source_location_args (php_line_number) : converted_args;
        }
    }

    private string? convert_function_args_from_binding (FunctionBinding? binding, string converted_args, int php_line_number) {
        if (binding == null) {
            return null;
        }

        var args = new ArrayList<string> ();
        if (converted_args.strip () != "") {
            foreach (string arg in split_top_level (converted_args, ',')) {
                args.add (arg.strip ());
            }
        }

        int runtime_parameter_count = binding.runtime_parameter_count ();
        while (args.size < runtime_parameter_count) {
            int parameter_index = binding.runtime_parameter_index (args.size);
            if (parameter_index >= binding.parameter_defaults.size || binding.parameter_defaults[parameter_index] == null) {
                return null;
            }
            args.add (binding.parameter_defaults[parameter_index]);
        }

        var output = new StringBuilder ();
        if (binding.source_location_args_first ()) {
            output.append (source_location_args (php_line_number).substring (2));
        }
        for (int i = 0; i < args.size; i++) {
            if (output.len > 0) {
                output.append (", ");
            }
            string parameter_type = runtime_parameter_type_for_arg (binding, i);
            if (is_out_runtime_parameter_type (parameter_type)) {
                string? out_name = zval_wrapped_identifier (args[i]);
                output.append ("out " + (out_name ?? args[i]));
            } else {
                output.append (args[i]);
            }
        }

        if (binding.source_location_args && !binding.source_location_args_first ()) {
            if (output.len == 0) {
                output.append (source_location_args (php_line_number).substring (2));
            } else {
                output.append (source_location_args (php_line_number));
            }
        }

        return output.str;
    }

    public string enum_prefix_for_function (string function_name) {
        FunctionBinding? binding = function_bindings.get (function_name);
        if (binding == null) {
            return "PhpEmbed";
        }
        return binding.namespace_name;
    }

    private string convert_function_argument_list (string args, int php_line_number) throws Error {
        if (args.strip () == "") {
            return "";
        }

        var converted = new StringBuilder ();
        foreach (string arg in split_top_level (args, ',')) {
            if (converted.len > 0) {
                converted.append (", ");
            }
            converted.append (convert_expression (arg.strip (), php_line_number));
        }

        return converted.str;
    }

    public string source_location_args (int php_line_number) {
        string mapped_source_path = source_path;
        int mapped_line_number = php_line_number;
        int index = php_line_number - 1;
        if (index >= 0 && index < source_line_files.size && index < source_line_numbers.size) {
            mapped_source_path = source_line_files[index];
            mapped_line_number = source_line_numbers[index];
        }
        return @", \"$(escape_vala_string (mapped_source_path))\", $mapped_line_number";
    }

    private string source_file_for_line (int php_line_number) {
        int index = php_line_number - 1;
        if (index >= 0 && index < source_line_files.size) {
            return source_line_files[index];
        }
        return source_path;
    }

    private int source_original_line_for_line (int php_line_number) {
        int index = php_line_number - 1;
        if (index >= 0 && index < source_line_numbers.size) {
            return source_line_numbers[index];
        }
        return php_line_number;
    }

    private static string escape_vala_string (string value) {
        return value
            .replace ("\\", "\\\\")
            .replace ("\"", "\\\"")
            .replace ("\r", "\\r")
            .replace ("\n", "\\n")
            .replace ("\t", "\\t");
    }

    public static string trim_trailing_semicolon (string value) {
        string result = value.strip ();
        if (result.has_suffix (";")) {
            return result.substring (0, result.length - 1).strip ();
        }
        return result;
    }

    private static string replace_php_variables (string expression) {
        var output = new StringBuilder ();
        bool in_single_quote = false;
        bool in_double_quote = false;

        for (int i = 0; i < expression.length; i++) {
            char c = expression[i];

            if (c == '\'' && !in_double_quote) {
                in_single_quote = !in_single_quote;
                output.append_c (c);
                continue;
            }

            if (c == '"' && !in_single_quote) {
                in_double_quote = !in_double_quote;
                output.append_c (c);
                continue;
            }

            if (c == '$' && !in_single_quote && !in_double_quote) {
                continue;
            }

            output.append_c (c);
        }

        return output.str;
    }

    private string ensure_variable_alias_for_name (string name) {
        if (variable_aliases.has_key (name)) {
            return variable_aliases[name];
        }
        if (!is_vala_reserved_identifier (name)) {
            return name;
        }
        string candidate = name + "_value";
        int index = 2;
        while (variable_types.has_key (candidate) || variable_aliases.has_key (candidate)) {
            candidate = "%s_value%d".printf (name, index++);
        }
        variable_aliases[name] = candidate;
        return candidate;
    }

    private static bool is_vala_reserved_identifier (string name) {
        switch (name) {
        case "abstract":
        case "as":
        case "async":
        case "base":
        case "bool":
        case "break":
        case "case":
        case "catch":
        case "char":
        case "class":
        case "const":
        case "construct":
        case "continue":
        case "default":
        case "delegate":
        case "delete":
        case "do":
        case "double":
        case "dynamic":
        case "else":
        case "enum":
        case "errordomain":
        case "extern":
        case "false":
        case "finally":
        case "float":
        case "for":
        case "foreach":
        case "get":
        case "if":
        case "in":
        case "inline":
        case "int":
        case "interface":
        case "internal":
        case "is":
        case "lock":
        case "namespace":
        case "new":
        case "null":
        case "out":
        case "override":
        case "owned":
        case "private":
        case "protected":
        case "public":
        case "ref":
        case "return":
        case "set":
        case "signal":
        case "sizeof":
        case "static":
        case "string":
        case "struct":
        case "switch":
        case "this":
        case "throw":
        case "throws":
        case "true":
        case "try":
        case "typeof":
        case "unowned":
        case "using":
        case "var":
        case "virtual":
        case "void":
        case "volatile":
        case "weak":
        case "while":
        case "yield":
            return true;
        default:
            return false;
        }
    }

    private string replace_php_variables_with_aliases (string expression) {
        string result = replace_php_variables (expression);
        foreach (string name in variable_aliases.keys) {
            result = replace_identifier_token (result, name, variable_aliases[name]);
        }
        return result;
    }

    private static string replace_identifier_token (string expression, string name, string replacement) {
        var output = new StringBuilder ();
        int i = 0;
        while (i < expression.length) {
            if (expression.substring (i).has_prefix (name)
                && (i == 0 || !is_identifier_char (expression[i - 1]))
                && (i + name.length >= expression.length || !is_identifier_char (expression[i + name.length]))) {
                output.append (replacement);
                i += name.length;
                continue;
            }
            output.append_c (expression[i]);
            i++;
        }
        return output.str;
    }

    private string convert_array_accesses (string expression) {
        string result = expression;
        bool changed = true;
        while (changed) {
            changed = false;
            foreach (string name in variable_types.keys) {
                string type_name = variable_types[name];
                string prefix = name + "[";
                int index = result.index_of (prefix);
                if (index < 0) {
                    continue;
                }
                int open = index + name.length;
                int close = find_matching_bracket (result, open);
                if (close < 0) {
                    continue;
                }
                string key = result.substring (open + 1, close - open - 1).strip ();
                string replacement = array_access_expression (name, type_name, key);
                result = result.substring (0, index) + replacement + result.substring (close + 1);
                changed = true;
                break;
            }
        }
        result = convert_get_bracket_accesses (result);
        return result;
    }

    private static string convert_get_bracket_accesses (string expression) {
        string result = expression;
        bool changed = true;
        while (changed) {
            changed = false;
            int bracket = result.index_of (")[");
            if (bracket < 0) {
                break;
            }
            int open = bracket + 1;
            int close = find_matching_bracket (result, open);
            if (close < 0) {
                break;
            }
            string key = result.substring (open + 1, close - open - 1).strip ();
            result = result.substring (0, bracket + 1) + ".get (" + key + ")" + result.substring (close + 1);
            changed = true;
        }
        return result;
    }

    private string array_access_expression (string name, string type_name, string key) {
        if (type_name == "Gee.HashMap<string, double?>") {
            return @"($name.get ($key) ?? 0.0)";
        }
        if (type_name == "Gee.HashMap<string, string>") {
            return @"($name.get ($key) ?? \"\")";
        }
        if (type_name == "Gee.HashMap<string, Gee.HashMap<string, string>>") {
            return @"$name.get ($key)";
        }
        if (type_name.has_prefix ("Gee.ArrayList<")) {
            return @"$name.get ($key)";
        }
        return @"$name.get ($key)";
    }

    private static string replace_concat_operator (string expression) {
        var output = new StringBuilder ();
        bool in_single_quote = false;
        bool in_double_quote = false;

        for (int i = 0; i < expression.length; i++) {
            char c = expression[i];

            if (c == '\'' && !in_double_quote) {
                in_single_quote = !in_single_quote;
                output.append_c (c);
                continue;
            }

            if (c == '"' && !in_single_quote) {
                in_double_quote = !in_double_quote;
                output.append_c (c);
                continue;
            }

            if (c == '.' && !in_single_quote && !in_double_quote
                && !is_decimal_point (expression, i)
                && !is_qualified_name_dot (expression, i)) {
                output.append ("+");
                continue;
            }

            output.append_c (c);
        }

        return output.str;
    }

    private static bool is_decimal_point (string expression, int index) {
        if (index <= 0 || index >= expression.length - 1) {
            return false;
        }

        return expression[index - 1].isdigit () && expression[index + 1].isdigit ();
    }

    private static bool is_qualified_name_dot (string expression, int index) {
        if (index <= 0 || index >= expression.length - 1) {
            return false;
        }

        return is_identifier_char (expression[index - 1])
            && is_identifier_char (expression[index + 1]);
    }

    public static bool is_identifier_char (char c) {
        return c.isalnum () || c == '_';
    }

    private string replace_division_operator (string expression, int php_line_number) throws Error {
        var parts = split_division (expression);
        if (parts.size <= 1) {
            return expression;
        }

        string result = parts[0].strip ();
        for (int i = 1; i < parts.size; i++) {
            string function_name = map_php_function_name ("div", php_line_number);
            FunctionBinding? binding = function_bindings.get ("div");
            string location_args = binding != null && binding.source_location_args ? source_location_args (php_line_number) : "";
            result = @"$function_name($result, $(parts[i].strip ())$location_args)";
        }

        return result;
    }

    private Error unsupported_function (int php_line_number, string function_name) {
        if (runtime_name == "native-runtime" && PhpToValaRuntimeBindings.has_embed_runtime_binding (function_name)) {
            return new PhpToValaError.UNSUPPORTED (
                "Unsupported PHP function or construct '%s' at %s:%d: no native-runtime binding exists, but an embed-runtime binding is available; use --both-runtimes to allow embed fallback",
                function_name,
                source_path,
                php_line_number
            );
        }

        return new PhpToValaError.UNSUPPORTED (
            "Unsupported PHP function or construct '%s' at %s:%d",
            function_name,
            source_path,
            php_line_number
        );
    }

    private Error unsupported_syntax (int php_line_number, string line) {
        string mapped_source_path = source_path;
        int mapped_line_number = php_line_number;
        int index = php_line_number - 1;
        if (index >= 0 && index < source_line_files.size && index < source_line_numbers.size) {
            mapped_source_path = source_line_files[index];
            mapped_line_number = source_line_numbers[index];
        }
        string token_context = PhpToValaSourceParser.debug_tokens_for_source_line (source_path, last_resolved_source, php_line_number);
        if (mapped_source_path != source_path || mapped_line_number != php_line_number) {
            token_context = token_context.replace (
                "PHP tokens near line %d:".printf (php_line_number),
                "PHP tokens near expanded source line %d (mapped to %s:%d):".printf (php_line_number, mapped_source_path, mapped_line_number)
            );
        }
        return new PhpToValaError.UNSUPPORTED (
            "Unsupported PHP syntax at %s:%d: %s\n%s",
            mapped_source_path,
            mapped_line_number,
            line,
            token_context
        );
    }

    public ArrayList<string> split_top_level (string expression, char separator) {
        var parts = new ArrayList<string> ();
        var current = new StringBuilder ();
        bool in_single_quote = false;
        bool in_double_quote = false;
        int depth = 0;

        for (int i = 0; i < expression.length; i++) {
            char c = expression[i];

            if (c == '\'' && !in_double_quote) {
                in_single_quote = !in_single_quote;
            } else if (c == '"' && !in_single_quote) {
                in_double_quote = !in_double_quote;
            } else if (!in_single_quote && !in_double_quote) {
                if (c == '(' || c == '[' || c == '{') {
                    depth++;
                } else if (c == ')' || c == ']' || c == '}') {
                    depth--;
                } else if (c == separator && depth == 0) {
                    if (separator == '-' && i + 1 < expression.length && expression[i + 1] == '>') {
                        current.append_c (c);
                        continue;
                    }
                    if ((separator == '-' || separator == '+') && is_unary_sign_position (expression, i)) {
                        current.append_c (c);
                        continue;
                    }
                    parts.add (current.str);
                    current.truncate (0);
                    continue;
                }
            }

            current.append_c (c);
        }

        parts.add (current.str);
        return parts;
    }

    private static bool is_unary_sign_position (string expression, int index) {
        if (index <= 0) {
            return true;
        }
        for (int i = index - 1; i >= 0; i--) {
            char previous = expression[i];
            if (previous.isspace ()) {
                continue;
            }
            return previous == '('
                || previous == '['
                || previous == '{'
                || previous == ','
                || previous == '+'
                || previous == '-'
                || previous == '*'
                || previous == '/'
                || previous == '%'
                || previous == '.'
                || previous == '='
                || previous == '<'
                || previous == '>'
                || previous == '!'
                || previous == '&'
                || previous == '|'
                || previous == '?'
                || previous == ':';
        }
        return true;
    }

    private ArrayList<string> split_concat (string expression) {
        var parts = new ArrayList<string> ();
        var current = new StringBuilder ();
        bool in_single_quote = false;
        bool in_double_quote = false;
        int depth = 0;

        for (int i = 0; i < expression.length; i++) {
            char c = expression[i];

            if (c == '\'' && !in_double_quote) {
                in_single_quote = !in_single_quote;
            } else if (c == '"' && !in_single_quote) {
                in_double_quote = !in_double_quote;
            } else if (!in_single_quote && !in_double_quote) {
                if (c == '(' || c == '[' || c == '{') {
                    depth++;
                } else if (c == ')' || c == ']' || c == '}') {
                    depth--;
                } else if (c == '.' && depth == 0 && !is_decimal_point (expression, i)) {
                    parts.add (current.str);
                    current.truncate (0);
                    continue;
                }
            }

            current.append_c (c);
        }

        parts.add (current.str);
        return parts;
    }

    private static ArrayList<string> split_division (string expression) {
        var parts = new ArrayList<string> ();
        var current = new StringBuilder ();
        bool in_single_quote = false;
        bool in_double_quote = false;
        int depth = 0;

        for (int i = 0; i < expression.length; i++) {
            char c = expression[i];

            if (c == '\'' && !in_double_quote) {
                in_single_quote = !in_single_quote;
            } else if (c == '"' && !in_single_quote) {
                in_double_quote = !in_double_quote;
            } else if (!in_single_quote && !in_double_quote) {
                if (c == '(' || c == '[' || c == '{') {
                    depth++;
                } else if (c == ')' || c == ']' || c == '}') {
                    depth--;
                } else if (c == '/' && depth == 0) {
                    parts.add (current.str);
                    current.truncate (0);
                    continue;
                }
            }

            current.append_c (c);
        }

        parts.add (current.str);
        return parts;
    }

    private string infer_type (string expression) {
        string expression_type = infer_expression_type (expression);
        if (expression_type != "") {
            return expression_type;
        }
        if (is_string_literal (expression)) {
            return "string";
        }
        if (expression.index_of ("?? \"\"") >= 0) {
            return "string";
        }
        if (int_regex.match (expression)) {
            return "int";
        }
        if (double_regex.match (expression)) {
            return "double";
        }
        if (is_function_call_expression (expression, "round")) {
            return "double";
        }
        if (is_function_call_expression (expression, "div")) {
            return "double";
        }
        if (is_function_call_expression (expression, "json_decode")
            || is_function_call_expression (expression, "unserialize")) {
            FunctionBinding? binding = function_bindings.get (is_function_call_expression (expression, "json_decode") ? "json_decode" : "unserialize");
            return binding != null ? binding.return_type : "Php.zval";
        }
        if (is_function_call_expression (expression, "strlen")
            || is_function_call_expression (expression, "count")) {
            return "int";
        }
        if (is_function_call_expression (expression, "is_array")
            || is_function_call_expression (expression, "is_object")
            || is_function_call_expression (expression, "is_null")
            || is_function_call_expression (expression, "is_string")
            || is_function_call_expression (expression, "is_int")
            || is_function_call_expression (expression, "is_integer")
            || is_function_call_expression (expression, "is_long")
            || is_function_call_expression (expression, "is_float")
            || is_function_call_expression (expression, "is_double")
            || is_function_call_expression (expression, "is_real")
            || is_function_call_expression (expression, "is_bool")
            || is_function_call_expression (expression, "is_numeric")
            || is_function_call_expression (expression, "isset")
            || is_function_call_expression (expression, "empty")) {
            return "bool";
        }
        if (is_function_call_expression (expression, "json_last_error_msg")
            || is_function_call_expression (expression, "json_encode")
            || is_function_call_expression (expression, "phpinfo")
            || is_function_call_expression (expression, "print_r")
            || is_function_call_expression (expression, "serialize")
            || is_function_call_expression (expression, "var_dump")
            || is_function_call_expression (expression, "var_export")
            || is_function_call_expression (expression, "gettype")
            || is_function_call_expression (expression, "trim")
            || is_function_call_expression (expression, "substr")
            || is_function_call_expression (expression, "exec")
            || is_function_call_expression (expression, "shell_exec")
            || is_function_call_expression (expression, "file_get_contents")) {
            return "string";
        }
        if (expression == "true" || expression == "false") {
            return "bool";
        }
        return "var";
    }

    private string infer_expression_type (string expression) {
        string clean = expression.strip ();
        if (variable_types.has_key (clean)) {
            return variable_types[clean];
        }
        if (clean.has_prefix ("PhpEmbed.explode_static(")) {
            return "Gee.ArrayList<string>";
        }
        foreach (string function_name in function_bindings.keys) {
            FunctionBinding? binding = function_bindings.get (function_name);
            if (binding != null && clean.has_prefix (binding.call_name (function_name) + "(")) {
                return binding.return_type;
            }
        }
        return "";
    }

    public string? infer_bound_call_return_type (string expression) {
        string clean = expression.strip ();
        foreach (string function_name in function_bindings.keys) {
            FunctionBinding? binding = function_bindings.get (function_name);
            if (binding != null && clean.has_prefix (binding.call_name (function_name) + "(")) {
                return binding.return_type;
            }
            if (binding != null && clean.has_prefix (binding.call_name (function_name) + " (")) {
                return binding.return_type;
            }
        }
        return null;
    }

    private static string hash_key_type (string type_name) {
        var inner = generic_inner (type_name, "Gee.HashMap<");
        var parts = split_generic_pair (inner);
        return parts[0];
    }

    private static string hash_value_type (string type_name) {
        var inner = generic_inner (type_name, "Gee.HashMap<");
        var parts = split_generic_pair (inner);
        return parts[1];
    }

    private static string generic_inner (string type_name, string prefix) {
        if (!type_name.has_prefix (prefix) || !type_name.has_suffix (">")) {
            return "";
        }
        return type_name.substring (prefix.length, type_name.length - prefix.length - 1);
    }

    private static string[] split_generic_pair (string inner) {
        int depth = 0;
        for (int i = 0; i < inner.length; i++) {
            char c = inner[i];
            if (c == '<') {
                depth++;
            } else if (c == '>') {
                depth--;
            } else if (c == ',' && depth == 0) {
                string[] result = { inner.substring (0, i).strip (), inner.substring (i + 1).strip () };
                return result;
            }
        }
        string[] fallback = { "string", "var" };
        return fallback;
    }

    private string format_for_expression (string expression) {
        string inferred_type = infer_expression_type (expression);
        if (inferred_type != "") {
            return format_for_type (inferred_type);
        }
        if (is_string_literal (expression)) {
            return "%s";
        }
        if (int_regex.match (expression)) {
            return "%d";
        }
        if (double_regex.match (expression)) {
            return "%g";
        }
        if (is_function_call_expression (expression, "round")) {
            return "%g";
        }
        if (is_function_call_expression (expression, "div")) {
            return "%g";
        }
        if (expression.index_of ("/") >= 0 || expression.has_prefix ("Php.div_static(") || expression.has_prefix ("Php.div_static (")) {
            return "%g";
        }
        if (is_function_call_expression (expression, "strlen")
            || is_function_call_expression (expression, "count")) {
            return "%d";
        }
        if (is_function_call_expression (expression, "is_array")
            || is_function_call_expression (expression, "is_object")
            || is_function_call_expression (expression, "is_null")
            || is_function_call_expression (expression, "is_string")
            || is_function_call_expression (expression, "is_int")
            || is_function_call_expression (expression, "is_integer")
            || is_function_call_expression (expression, "is_long")
            || is_function_call_expression (expression, "is_float")
            || is_function_call_expression (expression, "is_double")
            || is_function_call_expression (expression, "is_real")
            || is_function_call_expression (expression, "is_bool")
            || is_function_call_expression (expression, "is_numeric")
            || is_function_call_expression (expression, "isset")
            || is_function_call_expression (expression, "empty")) {
            return "%s";
        }
        if (is_function_call_expression (expression, "json_last_error_msg")
            || is_function_call_expression (expression, "json_encode")
            || is_function_call_expression (expression, "phpinfo")
            || is_function_call_expression (expression, "print_r")
            || is_function_call_expression (expression, "serialize")
            || is_function_call_expression (expression, "var_dump")
            || is_function_call_expression (expression, "var_export")
            || is_function_call_expression (expression, "gettype")
            || is_function_call_expression (expression, "trim")
            || is_function_call_expression (expression, "substr")
            || is_function_call_expression (expression, "exec")
            || is_function_call_expression (expression, "shell_exec")
            || is_function_call_expression (expression, "file_get_contents")) {
            return "%s";
        }
        if (expression == "true" || expression == "false") {
            return "%s";
        }
        if (variable_types.has_key (expression)) {
            string type_name = variable_types[expression];
            if (type_name == "int") {
                return "%d";
            }
            if (type_name == "double") {
                return "%g";
            }
            return "%s";
        }
        return "%s";
    }

    private static string format_for_type (string type_name) {
        if (type_name == "int") {
            return "%d";
        }
        if (type_name == "double") {
            return "%g";
        }
        return "%s";
    }

    private string value_for_format (string expression) {
        if (expression == "true" || expression == "false") {
            return @"$expression ? \"1\" : \"\"";
        }
        string inferred_type = infer_expression_type (expression);
        if (inferred_type == "bool") {
            return @"$expression ? \"1\" : \"\"";
        }
        if (inferred_type == "Php.zval") {
            return "Php.zval_to_string (%s)".printf (expression);
        }
        if (is_function_call_expression (expression, "is_array")
            || is_function_call_expression (expression, "is_object")
            || is_function_call_expression (expression, "is_null")
            || is_function_call_expression (expression, "is_string")
            || is_function_call_expression (expression, "is_int")
            || is_function_call_expression (expression, "is_integer")
            || is_function_call_expression (expression, "is_long")
            || is_function_call_expression (expression, "is_float")
            || is_function_call_expression (expression, "is_double")
            || is_function_call_expression (expression, "is_real")
            || is_function_call_expression (expression, "is_bool")
            || is_function_call_expression (expression, "is_numeric")
            || is_function_call_expression (expression, "isset")
            || is_function_call_expression (expression, "empty")) {
            return @"$expression ? \"1\" : \"\"";
        }
        if (variable_types.has_key (expression) && variable_types[expression] == "bool") {
            return @"$expression ? \"1\" : \"\"";
        }
        return expression;
    }

    private bool is_string_exit_expression (string expression) {
        return is_string_literal (expression)
            || (variable_types.has_key (expression) && variable_types[expression] == "string");
    }

    private static bool is_string_literal (string expression) {
        if (expression.length < 2) {
            return false;
        }

        char quote = expression[0];
        if (quote != '\"' && quote != '\'') {
            return false;
        }

        bool escaped = false;
        for (int i = 1; i < expression.length; i++) {
            char c = expression[i];
            if (escaped) {
                escaped = false;
                continue;
            }
            if (c == '\\') {
                escaped = true;
                continue;
            }
            if (c == quote) {
                return i == expression.length - 1;
            }
        }

        return false;
    }


    private bool is_function_call_expression (string expression, string function_name) {
        FunctionBinding? binding = function_bindings.get (function_name);
        if (binding == null) {
            return false;
        }

        return expression.has_prefix (binding.call_name (function_name) + "(");
    }

}
