//! Semantic Builder

use std::{cell::RefCell, path::PathBuf, rc::Rc, sync::Arc};

use itertools::Itertools;
#[allow(clippy::wildcard_imports)]
use oxc_ast::{ast::*, AstKind, Trivias, TriviasMap, Visit};
use oxc_diagnostics::Error;
use oxc_span::{Atom, SourceType, Span};
use oxc_syntax::{
    module_record::ModuleRecord,
    node::{AstNodeId, NodeFlags},
    operator::AssignmentOperator,
};
use rustc_hash::FxHashMap;

use crate::{
    binder::Binder,
    checker::{EarlyErrorJavaScript, EarlyErrorTypeScript},
    class::ClassTableBuilder,
    control_flow::{
        AssignmentValue, ControlFlowGraph, EdgeType, Register, StatementControlFlowType,
    },
    diagnostics::Redeclaration,
    jsdoc::JSDocBuilder,
    label::LabelBuilder,
    module_record::ModuleRecordBuilder,
    node::{AstNode, AstNodes},
    pg::replicate_tree_to_leaves,
    reference::{Reference, ReferenceFlag, ReferenceId},
    scope::{ScopeFlags, ScopeId, ScopeTree},
    symbol::{SymbolFlags, SymbolId, SymbolTable},
    Semantic,
};

#[derive(Debug, Clone)]
pub struct VariableInfo {
    pub span: Span,
    pub symbol_id: SymbolId,
}

#[derive(Debug)]
pub struct RedeclareVariables {
    pub variables: Vec<VariableInfo>,
}

pub struct SemanticBuilder<'a> {
    pub source_text: &'a str,

    pub source_type: SourceType,

    trivias: Rc<TriviasMap>,

    /// Semantic early errors such as redeclaration errors.
    errors: RefCell<Vec<Error>>,

    // states
    pub current_node_id: AstNodeId,
    pub current_node_flags: NodeFlags,
    pub current_symbol_flags: SymbolFlags,
    pub current_scope_id: ScopeId,
    // To make a namespace/module value like
    // we need the to know the modules we are inside
    // and when we reach a value declaration we set it
    // to value like
    pub namespace_stack: Vec<SymbolId>,
    /// If true, the current node is in the type definition
    in_type_definition: bool,

    // builders
    pub nodes: AstNodes<'a>,
    pub scope: ScopeTree,
    pub symbols: SymbolTable,

    pub(crate) module_record: Arc<ModuleRecord>,

    pub label_builder: LabelBuilder<'a>,

    jsdoc: JSDocBuilder<'a>,

    check_syntax_error: bool,

    redeclare_variables: RedeclareVariables,

    pub cfg: ControlFlowGraph,

    pub class_table_builder: ClassTableBuilder,
}

pub struct SemanticBuilderReturn<'a> {
    pub semantic: Semantic<'a>,
    pub errors: Vec<Error>,
}

impl<'a> SemanticBuilder<'a> {
    pub fn new(source_text: &'a str, source_type: SourceType) -> Self {
        let scope = ScopeTree::default();
        let current_scope_id = scope.root_scope_id();

        let trivias = Rc::new(TriviasMap::default());
        Self {
            source_text,
            source_type,
            trivias: Rc::clone(&trivias),
            errors: RefCell::new(vec![]),
            current_node_id: AstNodeId::new(0),
            current_node_flags: NodeFlags::empty(),
            current_symbol_flags: SymbolFlags::empty(),
            in_type_definition: false,
            current_scope_id,
            namespace_stack: vec![],
            nodes: AstNodes::default(),
            scope,
            symbols: SymbolTable::default(),
            module_record: Arc::new(ModuleRecord::default()),
            label_builder: LabelBuilder::default(),
            jsdoc: JSDocBuilder::new(source_text, &trivias),
            check_syntax_error: false,
            redeclare_variables: RedeclareVariables { variables: vec![] },
            cfg: ControlFlowGraph::new(),
            class_table_builder: ClassTableBuilder::new(),
        }
    }

    #[must_use]
    pub fn with_trivias(mut self, trivias: Trivias) -> Self {
        self.trivias = Rc::new(TriviasMap::from(trivias));
        self.jsdoc = JSDocBuilder::new(self.source_text, &self.trivias);
        self
    }

    #[must_use]
    pub fn with_check_syntax_error(mut self, yes: bool) -> Self {
        self.check_syntax_error = yes;
        self
    }

    /// Get the built module record from `build_module_record`
    pub fn module_record(&self) -> Arc<ModuleRecord> {
        Arc::clone(&self.module_record)
    }

    /// Build the module record with a shallow AST visit
    #[must_use]
    pub fn build_module_record(
        mut self,
        resolved_absolute_path: PathBuf,
        program: &'a Program<'a>,
    ) -> Self {
        let mut module_record_builder = ModuleRecordBuilder::new(resolved_absolute_path);
        module_record_builder.visit(program);
        self.module_record = Arc::new(module_record_builder.build());
        self
    }

    pub fn build(mut self, program: &Program<'a>) -> SemanticBuilderReturn<'a> {
        if self.source_type.is_typescript_definition() {
            self.scope.add_scope(None, ScopeFlags::Top);
        } else {
            self.visit_program(program);

            // Checking syntax error on module record requires scope information from the previous AST pass
            if self.check_syntax_error {
                EarlyErrorJavaScript::check_module_record(&self);
            }
        }

        let semantic = Semantic {
            source_text: self.source_text,
            source_type: self.source_type,
            trivias: self.trivias,
            nodes: self.nodes,
            scopes: self.scope,
            symbols: self.symbols,
            classes: self.class_table_builder.build(),
            module_record: Arc::clone(&self.module_record),
            jsdoc: self.jsdoc.build(),
            unused_labels: self.label_builder.unused_node_ids,
            redeclare_variables: self.redeclare_variables.variables,
            cfg: self.cfg,
        };
        SemanticBuilderReturn { semantic, errors: self.errors.into_inner() }
    }

    pub fn build2(self) -> Semantic<'a> {
        Semantic {
            source_text: self.source_text,
            source_type: self.source_type,
            trivias: self.trivias,
            nodes: self.nodes,
            scopes: self.scope,
            symbols: self.symbols,
            classes: self.class_table_builder.build(),
            module_record: Arc::new(ModuleRecord::default()),
            jsdoc: self.jsdoc.build(),
            unused_labels: self.label_builder.unused_node_ids,
            redeclare_variables: self.redeclare_variables.variables,
            cfg: self.cfg,
        }
    }

    /// Push a Syntax Error
    pub fn error<T: Into<Error>>(&self, error: T) {
        self.errors.borrow_mut().push(error.into());
    }

    fn create_ast_node(&mut self, kind: AstKind<'a>) {
        let mut flags = self.current_node_flags;
        if self.jsdoc.retrieve_jsdoc_comment(kind) {
            flags |= NodeFlags::JSDoc;
        }
        let ast_node = AstNode::new(kind, self.current_scope_id, flags);
        let parent_node_id =
            if matches!(kind, AstKind::Program(_)) { None } else { Some(self.current_node_id) };
        self.current_node_id = self.nodes.add_node(ast_node, parent_node_id);
    }

    fn pop_ast_node(&mut self) {
        if let Some(parent_id) = self.nodes.parent_id(self.current_node_id) {
            self.current_node_id = parent_id;
        }
    }

    pub fn current_scope_flags(&self) -> ScopeFlags {
        self.scope.get_flags(self.current_scope_id)
    }

    pub fn strict_mode(&self) -> bool {
        self.current_scope_flags().is_strict_mode()
            || self.current_node_flags.contains(NodeFlags::Class)
    }

    pub fn set_function_node_flag(&mut self, flag: NodeFlags) {
        if self.current_scope_flags().is_function() {
            *self.nodes.get_node_mut(self.scope.get_node_id(self.current_scope_id)).flags_mut() |=
                flag;
        }
    }

    /// Declares a `Symbol` for the node, adds it to symbol table, and binds it to the scope.
    ///
    /// includes: the `SymbolFlags` that node has in addition to its declaration type (eg: export, ambient, etc.)
    /// excludes: the flags which node cannot be declared alongside in a symbol table. Used to report forbidden declarations.
    ///
    /// Reports errors for conflicting identifier names.
    pub fn declare_symbol_on_scope(
        &mut self,
        span: Span,
        name: &Atom,
        scope_id: ScopeId,
        includes: SymbolFlags,
        excludes: SymbolFlags,
    ) -> SymbolId {
        if let Some(symbol_id) = self.check_redeclaration(scope_id, span, name, excludes, true) {
            self.symbols.union_flag(symbol_id, includes);
            if includes.is_function_scoped_declaration() {
                self.add_redeclared_variables(VariableInfo { span, symbol_id });
            }
            return symbol_id;
        }

        let includes = includes | self.current_symbol_flags;
        let symbol_id = self.symbols.create_symbol(span, name.clone(), includes, scope_id);
        self.symbols.add_declaration(self.current_node_id);
        self.scope.add_binding(scope_id, name.clone(), symbol_id);
        symbol_id
    }

    pub fn declare_symbol(
        &mut self,
        span: Span,
        name: &Atom,
        includes: SymbolFlags,
        excludes: SymbolFlags,
    ) -> SymbolId {
        self.declare_symbol_on_scope(span, name, self.current_scope_id, includes, excludes)
    }

    pub fn check_redeclaration(
        &mut self,
        scope_id: ScopeId,
        span: Span,
        name: &Atom,
        excludes: SymbolFlags,
        report_error: bool,
    ) -> Option<SymbolId> {
        let symbol_id = self.scope.get_binding(scope_id, name)?;
        if report_error && self.symbols.get_flag(symbol_id).intersects(excludes) {
            let symbol_span = self.symbols.get_span(symbol_id);
            self.error(Redeclaration(name.clone(), symbol_span, span));
        }
        Some(symbol_id)
    }

    pub fn declare_reference(&mut self, reference: Reference) -> ReferenceId {
        let reference_name = reference.name().clone();
        let reference_id = self.symbols.create_reference(reference);
        self.scope.add_unresolved_reference(self.current_scope_id, reference_name, reference_id);
        reference_id
    }

    /// Declares a `Symbol` for the node, shadowing previous declarations in the same scope.
    pub fn declare_shadow_symbol(
        &mut self,
        name: &Atom,
        span: Span,
        scope_id: ScopeId,
        includes: SymbolFlags,
    ) -> SymbolId {
        let includes = includes | self.current_symbol_flags;
        let symbol_id =
            self.symbols.create_symbol(span, name.clone(), includes, self.current_scope_id);
        self.symbols.add_declaration(self.current_node_id);
        self.scope.get_bindings_mut(scope_id).insert(name.clone(), symbol_id);
        symbol_id
    }

    fn resolve_references_for_current_scope(&mut self) {
        let all_references = self
            .scope
            .unresolved_references_mut(self.current_scope_id)
            .drain()
            .collect::<Vec<(Atom, Vec<ReferenceId>)>>();

        let mut unresolved_references: FxHashMap<Atom, Vec<ReferenceId>> = FxHashMap::default();
        let mut resolved_references: Vec<(SymbolId, Vec<ReferenceId>)> = vec![];

        for (name, reference_ids) in all_references {
            if let Some(symbol_id) = self.scope.get_binding(self.current_scope_id, &name) {
                resolved_references.push((symbol_id, reference_ids));
            } else {
                unresolved_references.insert(name, reference_ids);
            }
        }

        let scope_id =
            self.scope.get_parent_id(self.current_scope_id).unwrap_or(self.current_scope_id);

        for (name, reference_ids) in unresolved_references {
            self.scope.extend_unresolved_reference(scope_id, name, reference_ids);
        }

        for (symbol_id, reference_ids) in resolved_references {
            for reference_id in reference_ids {
                self.symbols.references[reference_id].set_symbol_id(symbol_id);
                self.symbols.resolved_references[symbol_id].push(reference_id);
            }
        }
    }

    pub fn add_redeclared_variables(&mut self, variable: VariableInfo) {
        self.redeclare_variables.variables.push(variable);
    }
}

impl<'a> Visit<'a> for SemanticBuilder<'a> {
    fn enter_scope(&mut self, flags: ScopeFlags) {
        let parent_scope_id =
            if flags.contains(ScopeFlags::Top) { None } else { Some(self.current_scope_id) };

        let mut flags = flags;
        // Inherit strict mode for functions
        // https://tc39.es/ecma262/#sec-strict-mode-code
        if let Some(parent_scope_id) = parent_scope_id {
            let mut strict_mode = self.scope.root_flags().is_strict_mode();
            let parent_scope_flags = self.scope.get_flags(parent_scope_id);

            if !strict_mode
                && parent_scope_flags.is_function()
                && parent_scope_flags.is_strict_mode()
            {
                strict_mode = true;
            }

            // inherit flags for non-function scopes
            if !flags.contains(ScopeFlags::Function) {
                flags |= parent_scope_flags & ScopeFlags::Modifiers;
            };

            if strict_mode {
                flags |= ScopeFlags::StrictMode;
            }
        }

        self.current_scope_id = self.scope.add_scope(parent_scope_id, flags);
    }

    fn leave_scope(&mut self) {
        self.resolve_references_for_current_scope();
        if let Some(parent_id) = self.scope.get_parent_id(self.current_scope_id) {
            self.current_scope_id = parent_id;
        }
    }

    // Setup all the context for the binder.
    // The order is important here.
    fn enter_node(&mut self, kind: AstKind<'a>) {
        self.create_ast_node(kind);
        self.enter_kind(kind);
    }

    fn leave_node(&mut self, kind: AstKind<'a>) {
        if self.check_syntax_error {
            let node = self.nodes.get_node(self.current_node_id);
            EarlyErrorJavaScript::run(node, self);
            EarlyErrorTypeScript::run(node, self);
        }
        self.leave_kind(kind);
        self.pop_ast_node();
    }

    fn visit_program(&mut self, program: &Program<'a>) {
        let kind = AstKind::Program(self.alloc(program));
        self.enter_scope({
            let mut flags = ScopeFlags::Top;
            if program.is_strict() {
                flags |= ScopeFlags::StrictMode;
            }
            flags
        });
        self.enter_node(kind);

        /* cfg */
        let _program_basic_block = self.cfg.new_basic_block();
        /* cfg - must be above directives as directives are in cfg */

        for directive in &program.directives {
            self.visit_directive(directive);
        }

        self.visit_statements(&program.body);
        self.leave_node(kind);
        self.leave_scope();
    }

    fn visit_block_statement(&mut self, stmt: &BlockStatement<'a>) {
        let kind = AstKind::BlockStatement(self.alloc(stmt));
        self.enter_scope(ScopeFlags::empty());
        self.enter_node(kind);

        /* cfg */
        let statement_state = self
            .cfg
            .before_statement(self.current_node_id, StatementControlFlowType::DoesNotUseContinue);
        /* cfg */

        self.visit_statements(&stmt.body);

        /* cfg */
        self.cfg.after_statement(
            &statement_state,
            self.current_node_id,
            self.cfg.current_node_ix,
            None,
        );
        /* cfg */

        self.leave_node(kind);
        self.leave_scope();
    }

    fn visit_break_statement(&mut self, stmt: &BreakStatement) {
        let kind = AstKind::BreakStatement(self.alloc(stmt));
        self.enter_node(kind);

        /* cfg */
        let statement_state = self
            .cfg
            .before_statement(self.current_node_id, StatementControlFlowType::DoesNotUseContinue);
        /* cfg */

        if let Some(break_target) = &stmt.label {
            self.visit_label_identifier(break_target);

            /* cfg */
            if let Some(label_found) =
                self.cfg.label_to_ast_node_ix.iter().rev().find(|x| x.0 == break_target.name)
            {
                let (_, break_, _) = self.cfg.ast_node_to_break_continue.iter().rev().find(|x| x.0 == label_found.1)
                                                .expect("expected a corresponding break/continue array for a found label owning ast node");
                self.cfg.basic_blocks_with_breaks[*break_].push(self.cfg.current_node_ix);
            } else {
                self.cfg
                    .basic_blocks_with_breaks
                    .last_mut()
                    .expect(
                        "expected there to be a stack of control flows that a break can belong to",
                    )
                    .push(self.cfg.current_node_ix);
            }
            /* cfg */
        }
        /* cfg */
        else {
            self.cfg
                .basic_blocks_with_breaks
                .last_mut()
                .expect("expected there to be a stack of control flows that a break can belong to")
                .push(self.cfg.current_node_ix);
        }
        self.cfg.put_unreachable();

        self.cfg.after_statement(
            &statement_state,
            self.current_node_id,
            self.cfg.current_node_ix,
            None,
        );
        /* cfg */

        self.leave_node(kind);
    }

    fn visit_continue_statement(&mut self, stmt: &ContinueStatement) {
        let kind = AstKind::ContinueStatement(self.alloc(stmt));
        self.enter_node(kind);

        /* cfg */
        let statement_state = self
            .cfg
            .before_statement(self.current_node_id, StatementControlFlowType::DoesNotUseContinue);
        /* cfg */

        if let Some(continue_target) = &stmt.label {
            self.visit_label_identifier(continue_target);
            /* cfg */
            if let Some(label_found) =
                self.cfg.label_to_ast_node_ix.iter().rev().find(|x| x.0 == continue_target.name)
            {
                let (_, _, continue_) = self.cfg.ast_node_to_break_continue.iter().rev().find(|x| x.0 == label_found.1)
                                        .expect("expected a corresponding break/continue array for a found label owning ast node");
                if let Some(continue_) = continue_ {
                    self.cfg.basic_blocks_with_breaks[*continue_].push(self.cfg.current_node_ix);
                } else {
                    self.cfg
                    .basic_blocks_with_breaks
                    .last_mut()
                    .expect(
                        "expected there to be a stack of control flows that a break can belong to",
                    )
                    .push(self.cfg.current_node_ix);
                }
            } else {
                self.cfg
                    .basic_blocks_with_breaks
                    .last_mut()
                    .expect(
                        "expected there to be a stack of control flows that a break can belong to",
                    )
                    .push(self.cfg.current_node_ix);
            }
            /* cfg */
        }
        /* cfg */
        else {
            self.cfg
                .basic_blocks_with_breaks
                .last_mut()
                .expect("expected there to be a stack of control flows that a break can belong to")
                .push(self.cfg.current_node_ix);
        }
        self.cfg.put_unreachable();
        /* cfg */

        /* cfg */
        let current_node_ix = self.cfg.current_node_ix;
        // todo: assert on this instead when continues which
        // aren't in iterations are nonrecoverable errors
        if let Some(continues) = self.cfg.basic_blocks_with_continues.last_mut() {
            continues.push(current_node_ix);
        }
        self.cfg.put_unreachable();

        self.cfg.after_statement(
            &statement_state,
            self.current_node_id,
            self.cfg.current_node_ix,
            None,
        );
        /* cfg */

        self.leave_node(kind);
    }

    fn visit_debugger_statement(&mut self, stmt: &DebuggerStatement) {
        let kind = AstKind::DebuggerStatement(self.alloc(stmt));
        self.enter_node(kind);

        /* cfg */
        // just take the next_label since it should be taken by the next
        // statement regardless of whether the statement can use it or not
        self.cfg.next_label.take();
        /* cfg */

        self.leave_node(kind);
    }

    fn visit_do_while_statement(&mut self, stmt: &DoWhileStatement<'a>) {
        let kind = AstKind::DoWhileStatement(self.alloc(stmt));
        self.enter_node(kind);

        /* cfg */
        let before_do_while_stmt_graph_ix = self.cfg.current_node_ix;
        let start_body_graph_ix = self.cfg.new_basic_block();
        let statement_state =
            self.cfg.before_statement(self.current_node_id, StatementControlFlowType::UsesContinue);
        /* cfg */

        self.visit_statement(&stmt.body);

        /* cfg - condition basic block */
        let start_of_condition_graph_ix = self.cfg.new_basic_block();
        /* cfg */

        self.visit_expression(&stmt.test);

        /* cfg */
        let end_of_condition_graph_ix = self.cfg.current_node_ix;

        let end_do_while_graph_ix = self.cfg.new_basic_block();

        // before do while to start of condition basic block
        self.cfg.add_edge(
            before_do_while_stmt_graph_ix,
            start_of_condition_graph_ix,
            EdgeType::Normal,
        );
        // body of do-while to start of condition
        self.cfg.add_edge(start_body_graph_ix, start_of_condition_graph_ix, EdgeType::Backedge);
        // end of condition to after do while
        self.cfg.add_edge(end_of_condition_graph_ix, end_do_while_graph_ix, EdgeType::Normal);
        // end of condition to after start of body
        self.cfg.add_edge(end_of_condition_graph_ix, start_body_graph_ix, EdgeType::Normal);

        self.cfg.after_statement(
            &statement_state,
            self.current_node_id,
            // all basic blocks are break here so we connect them to the
            // basic block after the do-while statement
            end_do_while_graph_ix,
            // all basic blocks are continues here so we connect them to the
            // basic block of the condition
            Some(start_of_condition_graph_ix),
        );
        /* cfg */

        self.leave_node(kind);
    }

    fn visit_expression_statement(&mut self, stmt: &ExpressionStatement<'a>) {
        let kind = AstKind::ExpressionStatement(self.alloc(stmt));
        self.enter_node(kind);

        /* cfg */
        let statement_state = self
            .cfg
            .before_statement(self.current_node_id, StatementControlFlowType::DoesNotUseContinue);
        /* cfg */

        self.visit_expression(&stmt.expression);

        /* cfg */
        self.cfg.after_statement(
            &statement_state,
            self.current_node_id,
            self.cfg.current_node_ix,
            None,
        );
        /* cfg */

        self.leave_node(kind);
    }

    fn visit_for_statement(&mut self, stmt: &ForStatement<'a>) {
        let kind = AstKind::ForStatement(self.alloc(stmt));
        let is_lexical_declaration =
            stmt.init.as_ref().is_some_and(ForStatementInit::is_lexical_declaration);
        if is_lexical_declaration {
            self.enter_scope(ScopeFlags::empty());
        }
        self.enter_node(kind);
        if let Some(init) = &stmt.init {
            self.visit_for_statement_init(init);
        }
        /* cfg */
        let before_for_graph_ix = self.cfg.current_node_ix;
        let test_graph_ix = self.cfg.new_basic_block();
        /* cfg */
        if let Some(test) = &stmt.test {
            self.visit_expression(test);
        }

        /* cfg */
        let update_graph_ix = self.cfg.new_basic_block();
        /* cfg */

        if let Some(update) = &stmt.update {
            self.visit_expression(update);
        }

        /* cfg */
        let body_graph_ix = self.cfg.new_basic_block();
        let statement_state =
            self.cfg.before_statement(self.current_node_id, StatementControlFlowType::UsesContinue);
        /* cfg */

        self.visit_statement(&stmt.body);

        /* cfg */
        let after_for_stmt = self.cfg.new_basic_block();
        self.cfg.add_edge(before_for_graph_ix, test_graph_ix, EdgeType::Normal);
        self.cfg.add_edge(test_graph_ix, body_graph_ix, EdgeType::Normal);
        self.cfg.add_edge(body_graph_ix, update_graph_ix, EdgeType::Backedge);
        self.cfg.add_edge(update_graph_ix, test_graph_ix, EdgeType::Backedge);
        self.cfg.add_edge(test_graph_ix, after_for_stmt, EdgeType::Normal);

        self.cfg.after_statement(
            &statement_state,
            self.current_node_id,
            // all basic blocks are break here so we connect them to the
            // basic block after the for statement
            self.cfg.current_node_ix,
            // all basic blocks are continues here so we connect them to the
            // basic block of the condition
            Some(test_graph_ix),
        );

        /* cfg */
        self.leave_node(kind);
        if is_lexical_declaration {
            self.leave_scope();
        }
    }

    fn visit_for_statement_init(&mut self, init: &ForStatementInit<'a>) {
        let kind = AstKind::ForStatementInit(self.alloc(init));
        self.enter_node(kind);
        match init {
            ForStatementInit::UsingDeclaration(decl) => {
                self.visit_using_declaration(decl);
            }
            ForStatementInit::VariableDeclaration(decl) => {
                self.visit_variable_declaration(decl);
            }
            ForStatementInit::Expression(expr) => self.visit_expression(expr),
        }
        self.leave_node(kind);
    }

    fn visit_for_in_statement(&mut self, stmt: &ForInStatement<'a>) {
        let kind = AstKind::ForInStatement(self.alloc(stmt));
        let is_lexical_declaration = stmt.left.is_lexical_declaration();
        if is_lexical_declaration {
            self.enter_scope(ScopeFlags::empty());
        }
        self.enter_node(kind);
        self.visit_for_statement_left(&stmt.left);

        /* cfg */
        let before_for_stmt_graph_ix = self.cfg.current_node_ix;
        let start_prepare_cond_graph_ix = self.cfg.new_basic_block();
        /* cfg */

        self.visit_expression(&stmt.right);

        /* cfg */
        let end_of_prepare_cond_graph_ix = self.cfg.current_node_ix;
        // this basic block is always empty since there's no update condition in a for-in loop.
        let basic_block_with_backedge_graph_ix = self.cfg.new_basic_block();
        let body_graph_ix = self.cfg.new_basic_block();
        let statement_state =
            self.cfg.before_statement(self.current_node_id, StatementControlFlowType::UsesContinue);
        /* cfg */

        self.visit_statement(&stmt.body);

        /* cfg */
        let end_of_body_graph_ix = self.cfg.current_node_ix;
        let after_for_graph_ix = self.cfg.new_basic_block();
        // connect before for statement to the iterable expression
        self.cfg.add_edge(before_for_stmt_graph_ix, start_prepare_cond_graph_ix, EdgeType::Normal);
        // connect the end of the iterable expression to the basic block with back edge
        self.cfg.add_edge(
            end_of_prepare_cond_graph_ix,
            basic_block_with_backedge_graph_ix,
            EdgeType::Normal,
        );
        // connect the basic block with back edge to the start of the body
        self.cfg.add_edge(basic_block_with_backedge_graph_ix, body_graph_ix, EdgeType::Normal);
        // connect the end of the body back to the basic block
        // with back edge for the next iteration
        self.cfg.add_edge(
            end_of_body_graph_ix,
            basic_block_with_backedge_graph_ix,
            EdgeType::Backedge,
        );
        // connect the basic block with back edge to the basic block after the for loop
        // for when there are no more iterations left in the iterable
        self.cfg.add_edge(basic_block_with_backedge_graph_ix, after_for_graph_ix, EdgeType::Normal);

        self.cfg.after_statement(
            &statement_state,
            self.current_node_id,
            // all basic blocks are break here so we connect them to the
            // basic block after the for-in statement
            self.cfg.current_node_ix,
            // all basic blocks are continues here so we connect them to the
            // basic block of the condition
            Some(basic_block_with_backedge_graph_ix),
        );
        /* cfg */

        self.leave_node(kind);
        if is_lexical_declaration {
            self.leave_scope();
        }
    }

    fn visit_for_of_statement(&mut self, stmt: &ForOfStatement<'a>) {
        let kind = AstKind::ForOfStatement(self.alloc(stmt));
        let is_lexical_declaration = stmt.left.is_lexical_declaration();
        if is_lexical_declaration {
            self.enter_scope(ScopeFlags::empty());
        }
        self.enter_node(kind);
        self.visit_for_statement_left(&stmt.left);

        /* cfg */
        let before_for_stmt_graph_ix = self.cfg.current_node_ix;
        let start_prepare_cond_graph_ix = self.cfg.new_basic_block();
        /* cfg */

        self.visit_expression(&stmt.right);

        /* cfg */
        let end_of_prepare_cond_graph_ix = self.cfg.current_node_ix;
        // this basic block is always empty since there's no update condition in a for-of loop.
        let basic_block_with_backedge_graph_ix = self.cfg.new_basic_block();
        let body_graph_ix = self.cfg.new_basic_block();
        let statement_state =
            self.cfg.before_statement(self.current_node_id, StatementControlFlowType::UsesContinue);
        /* cfg */

        self.visit_statement(&stmt.body);

        /* cfg */
        let end_of_body_graph_ix = self.cfg.current_node_ix;
        let after_for_graph_ix = self.cfg.new_basic_block();
        // connect before for statement to the iterable expression
        self.cfg.add_edge(before_for_stmt_graph_ix, start_prepare_cond_graph_ix, EdgeType::Normal);
        // connect the end of the iterable expression to the basic block with back edge
        self.cfg.add_edge(
            end_of_prepare_cond_graph_ix,
            basic_block_with_backedge_graph_ix,
            EdgeType::Normal,
        );
        // connect the basic block with back edge to the start of the body
        self.cfg.add_edge(basic_block_with_backedge_graph_ix, body_graph_ix, EdgeType::Normal);
        // connect the end of the body back to the basic block
        // with back edge for the next iteration
        self.cfg.add_edge(
            end_of_body_graph_ix,
            basic_block_with_backedge_graph_ix,
            EdgeType::Backedge,
        );
        // connect the basic block with back edge to the basic block after the for loop
        // for when there are no more iterations left in the iterable
        self.cfg.add_edge(basic_block_with_backedge_graph_ix, after_for_graph_ix, EdgeType::Normal);

        self.cfg.after_statement(
            &statement_state,
            self.current_node_id,
            // all basic blocks are break here so we connect them to the
            // basic block after the for-of statement
            self.cfg.current_node_ix,
            // all basic blocks are continues here so we connect them to the
            // basic block of the condition
            Some(basic_block_with_backedge_graph_ix),
        );
        /* cfg */

        self.leave_node(kind);
        if is_lexical_declaration {
            self.leave_scope();
        }
    }

    fn visit_if_statement(&mut self, stmt: &IfStatement<'a>) {
        let kind = AstKind::IfStatement(self.alloc(stmt));
        self.enter_node(kind);

        self.visit_expression(&stmt.test);

        /* cfg */
        let statement_state = self
            .cfg
            .before_statement(self.current_node_id, StatementControlFlowType::DoesNotUseContinue);

        let before_if_stmt_graph_ix = self.cfg.current_node_ix;

        // if statement basic block
        let before_consequent_stmt_graph_ix = self.cfg.new_basic_block();
        /* cfg */

        self.visit_statement(&stmt.consequent);

        /* cfg */
        let after_consequent_stmt_graph_ix = self.cfg.current_node_ix;
        /* cfg */

        let else_graph_ix = if let Some(alternate) = &stmt.alternate {
            /* cfg */
            let else_graph_ix = self.cfg.new_basic_block();
            /* cfg */

            self.visit_statement(alternate);

            Some((else_graph_ix, self.cfg.current_node_ix))
        } else {
            None
        };

        /* cfg - bb after if statement joins consequent and alternate */
        let after_if_graph_ix = self.cfg.new_basic_block();

        if stmt.alternate.is_some() {
            self.cfg.put_unreachable();
        }
        //  else {
        self.cfg.add_edge(after_consequent_stmt_graph_ix, after_if_graph_ix, EdgeType::Normal);
        // }

        self.cfg.add_edge(
            before_if_stmt_graph_ix,
            before_consequent_stmt_graph_ix,
            EdgeType::Normal,
        );

        if let Some((start_of_alternate_stmt_graph_ix, after_alternate_stmt_graph_ix)) =
            else_graph_ix
        {
            self.cfg.add_edge(
                before_if_stmt_graph_ix,
                start_of_alternate_stmt_graph_ix,
                EdgeType::Normal,
            );
            self.cfg.add_edge(after_alternate_stmt_graph_ix, after_if_graph_ix, EdgeType::Normal);
        } else {
            self.cfg.add_edge(before_if_stmt_graph_ix, after_if_graph_ix, EdgeType::Normal);
        }

        self.cfg.after_statement(&statement_state, self.current_node_id, after_if_graph_ix, None);
        /* cfg */

        self.leave_node(kind);
    }

    fn visit_labeled_statement(&mut self, stmt: &LabeledStatement<'a>) {
        let kind = AstKind::LabeledStatement(self.alloc(stmt));
        self.enter_node(kind);

        /* cfg */
        let statement_state = self
            .cfg
            .before_statement(self.current_node_id, StatementControlFlowType::DoesNotUseContinue);
        /* cfg */

        self.visit_label_identifier(&stmt.label);

        /* cfg */
        self.cfg.next_label = Some(stmt.label.name.clone());
        /* cfg */

        self.visit_statement(&stmt.body);

        /* cfg */
        self.cfg.after_statement(
            &statement_state,
            self.current_node_id,
            self.cfg.current_node_ix,
            None,
        );

        /* cfg */

        self.leave_node(kind);
    }

    fn visit_return_statement(&mut self, stmt: &ReturnStatement<'a>) {
        let kind = AstKind::ReturnStatement(self.alloc(stmt));
        self.enter_node(kind);

        /* cfg */
        let statement_state = self
            .cfg
            .before_statement(self.current_node_id, StatementControlFlowType::DoesNotUseContinue);

        // returning something is an assignment to the return register
        self.cfg.use_this_register = Some(Register::Return);
        /* cfg */

        if let Some(arg) = &stmt.argument {
            self.visit_expression(arg);
            /* cfg */
            self.cfg.put_x_in_register(AssignmentValue::NotImplicitUndefined);
            /* cfg */
        }
        /* cfg - put implicit undefined as return arg  */
        else {
            self.cfg.put_undefined();
        }
        /* cfg */

        /* cfg - put unreachable after return */
        self.cfg.put_unreachable();

        self.cfg.after_statement(
            &statement_state,
            self.current_node_id,
            self.cfg.current_node_ix,
            None,
        );
        /* cfg */

        self.leave_node(kind);
    }

    fn visit_switch_statement(&mut self, stmt: &SwitchStatement<'a>) {
        let kind = AstKind::SwitchStatement(self.alloc(stmt));
        self.enter_scope(ScopeFlags::empty());
        self.enter_node(kind);
        self.visit_expression(&stmt.discriminant);

        /* cfg */
        let discriminant_graph_ix = self.cfg.current_node_ix;
        self.cfg.switch_case_conditions.push(vec![]);
        let statement_state = self
            .cfg
            .before_statement(self.current_node_id, StatementControlFlowType::DoesNotUseContinue);
        let mut ends_of_switch_cases = vec![];
        /* cfg */

        for case in &stmt.cases {
            self.visit_switch_case(case);
            ends_of_switch_cases.push(self.cfg.current_node_ix);
        }

        /* cfg */
        let switch_case_conditions = self.cfg.switch_case_conditions.pop().expect(
            "there must be a corresponding previous_switch_case_last_block in a switch statement",
        );

        // for each switch case
        for i in 0..switch_case_conditions.len() {
            let switch_case_condition_graph_ix = switch_case_conditions[i];

            // every switch case condition can be skipped,
            // so there's a possible jump from it to the next switch case condition
            for y in switch_case_conditions.iter().skip(i + 1) {
                self.cfg.add_edge(switch_case_condition_graph_ix, *y, EdgeType::Normal);
            }

            // connect the end of each switch statement to
            // the condition of the next switch statement
            if switch_case_conditions.len() > i + 1 {
                let end_of_switch_case = ends_of_switch_cases[i];
                let next_switch_statement_condition = switch_case_conditions[i + 1];

                self.cfg.add_edge(
                    end_of_switch_case,
                    next_switch_statement_condition,
                    EdgeType::Normal,
                );
            }

            self.cfg.add_edge(
                discriminant_graph_ix,
                switch_case_condition_graph_ix,
                EdgeType::Normal,
            );
        }

        if let Some(last) = switch_case_conditions.last() {
            self.cfg.add_edge(*last, self.cfg.current_node_ix, EdgeType::Normal);
        }

        self.cfg.after_statement(
            &statement_state,
            self.current_node_id,
            self.cfg.current_node_ix,
            None,
        );
        /* cfg */

        self.leave_node(kind);
        self.leave_scope();
    }

    fn visit_switch_case(&mut self, case: &SwitchCase<'a>) {
        let kind = AstKind::SwitchCase(self.alloc(case));
        self.enter_node(kind);

        /* cfg */
        // make a new basic block so that we can jump to it later from the switch
        //   discriminant and the switch cases above it (if they don't test ss true)
        let switch_cond_graph_ix = self.cfg.new_basic_block();
        self.cfg
            .switch_case_conditions
            .last_mut()
            .expect("there must be a switch_case_conditions while in a switch case")
            .push(switch_cond_graph_ix);
        /* cfg */

        if let Some(expr) = &case.test {
            self.visit_expression(expr);
        }

        /* cfg */
        let statements_in_switch_graph_ix = self.cfg.new_basic_block();
        self.cfg.add_edge(switch_cond_graph_ix, statements_in_switch_graph_ix, EdgeType::Normal);
        /* cfg */

        self.visit_statements(&case.consequent);

        self.leave_node(kind);
    }

    fn visit_throw_statement(&mut self, stmt: &ThrowStatement<'a>) {
        let kind = AstKind::ThrowStatement(self.alloc(stmt));
        self.enter_node(kind);

        /* cfg */
        let statement_state = self
            .cfg
            .before_statement(self.current_node_id, StatementControlFlowType::DoesNotUseContinue);
        let throw_expr = self.cfg.new_register();
        self.cfg.use_this_register = Some(throw_expr);
        /* cfg */

        self.visit_expression(&stmt.argument);
        // todo - put unreachable after throw statement

        /* cfg */
        self.cfg.put_throw(throw_expr);
        self.cfg.after_statement(
            &statement_state,
            self.current_node_id,
            self.cfg.current_node_ix,
            None,
        );
        /* cfg */

        self.leave_node(kind);
    }

    fn visit_try_statement(&mut self, stmt: &TryStatement<'a>) {
        let kind = AstKind::TryStatement(self.alloc(stmt));
        self.enter_node(kind);

        /* cfg */
        let statement_state = self
            .cfg
            .before_statement(self.current_node_id, StatementControlFlowType::DoesNotUseContinue);

        let before_try_stmt_graph_ix = self.cfg.current_node_ix;
        let catch_block_graph_ix = self.cfg.new_basic_block();
        let catch_block_basic_block_id = self.cfg.current_basic_block;
        // every statement created with this active adds an edge from that node to this
        // catch block node
        //
        // NOTE: we oversimplify here, realistically even in between basic blocks we
        // do throwsy things which could cause problems, but for the most part simply
        // pointing the end of every basic block to the catch block is enough
        self.cfg.after_throw_block = Some(catch_block_graph_ix);
        let start_of_try_block_graph_ix = self.cfg.new_basic_block();
        /* cfg */

        self.visit_block_statement(&stmt.block);

        /* cfg */
        self.cfg.after_throw_block = None; // only active during try block

        let end_of_try_block_graph_ix = self.cfg.current_node_ix;
        /* cfg */

        let optional_catch_block_graph_ix = if let Some(handler) = &stmt.handler {
            /* cfg */
            self.cfg.current_node_ix = catch_block_graph_ix;
            self.cfg.current_basic_block = catch_block_basic_block_id;
            /* cfg */

            self.visit_catch_clause(handler);

            Some((catch_block_graph_ix, self.cfg.current_node_ix))
        } else {
            /* cfg */
            let preserved_node_ix = self.cfg.current_node_ix;
            // each statement that connects the would be catch block should just point
            // to an unreachable() as if any statement throws this function would end early
            self.cfg.current_node_ix = catch_block_graph_ix;
            self.cfg.put_unreachable();
            self.cfg.current_node_ix = preserved_node_ix;
            /* cfg */

            None
        };
        let finally = if let Some(finalizer) = &stmt.finalizer {
            /* cfg */
            let start_of_finally_block_graph_ix = self.cfg.new_basic_block();
            /* cfg */

            self.visit_finally_clause(finalizer);

            /* cfg */
            Some((start_of_finally_block_graph_ix, self.cfg.current_node_ix))
            /* cfg */
        } else {
            None
        };

        /* cfg */
        let after_try_stmt_graph_ix = self.cfg.new_basic_block();

        self.cfg.add_edge(before_try_stmt_graph_ix, start_of_try_block_graph_ix, EdgeType::Normal);
        match (finally, optional_catch_block_graph_ix) {
            (
                Some((nothrow_finally_start, nothrow_finally_end)),
                Some((catch_start, catch_end)),
            ) => {
                //                            <try>
                //                              |
                //                         (1)/  \(2)
                //                 <nothrow fin>  |
                //                     |          |
                //             <fn continues>  <catch>
                //                                |
                //                           <throw fin>
                //                                |
                //                           <unreachable>
                //                                |
                //       todo: should continue to upper function's catch
                let duplicated = replicate_tree_to_leaves(nothrow_finally_start, &mut self.cfg);
                let throw_finally_start = duplicated[&nothrow_finally_start];

                // 1
                self.cfg.add_edge(end_of_try_block_graph_ix, catch_start, EdgeType::Normal);
                self.cfg.add_edge(catch_end, throw_finally_start, EdgeType::Normal);
                // 2
                self.cfg.add_edge(
                    end_of_try_block_graph_ix,
                    nothrow_finally_start,
                    EdgeType::Normal,
                );
                self.cfg.add_edge(nothrow_finally_end, after_try_stmt_graph_ix, EdgeType::Normal);

                // throw can happen before a single statement finishes in try block - essentially skipping try block
                self.cfg.add_edge(before_try_stmt_graph_ix, catch_start, EdgeType::Normal);
            }
            (Some((nothrow_finally_start, nothrow_finally_end)), None) => {
                //                            <try>
                //                              |
                //                         (1)/  \(2)
                //                 <nothrow fin>  |
                //                     |          |
                //             <fn continues>  <catch>
                //                                |
                //                           <throw fin>
                //                                |
                //                           <unreachable>
                //                                |
                //       todo: should continue to upper function's catch

                let duplicated = replicate_tree_to_leaves(nothrow_finally_start, &mut self.cfg);
                let throw_finally_start = duplicated[&nothrow_finally_start];
                // 1
                self.cfg.add_edge(
                    end_of_try_block_graph_ix,
                    nothrow_finally_start,
                    EdgeType::Normal,
                );
                self.cfg.add_edge(nothrow_finally_end, after_try_stmt_graph_ix, EdgeType::Normal);

                // 2

                // even though we don't explicitly have a catch block, we still make a catch
                // block in the cfg which immediately points to the throw catch block.
                self.cfg.add_edge(
                    end_of_try_block_graph_ix,
                    catch_block_graph_ix,
                    EdgeType::Normal,
                );
                self.cfg.add_edge(catch_block_graph_ix, throw_finally_start, EdgeType::Normal);
                // throw can happen before a single statement finishes in try block - essentially skipping try block
                self.cfg.add_edge(before_try_stmt_graph_ix, throw_finally_start, EdgeType::Normal);
            }
            (None, Some((catch_start, catch_end))) => {
                self.cfg.add_edge(end_of_try_block_graph_ix, catch_start, EdgeType::Normal);
                self.cfg.add_edge(catch_end, after_try_stmt_graph_ix, EdgeType::Normal);
                // if nothing throws
                self.cfg.add_edge(
                    end_of_try_block_graph_ix,
                    after_try_stmt_graph_ix,
                    EdgeType::Normal,
                );
                // throw can happen before a single statement finishes in try block - essentially skipping try block
                self.cfg.add_edge(before_try_stmt_graph_ix, catch_start, EdgeType::Normal);
            }
            (None, None) => {
                // todo: support unwinding finally/catch blocks that aren't in this function
                // even if something throws
                self.cfg.add_edge(
                    end_of_try_block_graph_ix,
                    after_try_stmt_graph_ix,
                    EdgeType::Normal,
                );
            }
        }

        self.cfg.after_statement(
            &statement_state,
            self.current_node_id,
            after_try_stmt_graph_ix,
            None,
        );
        /* cfg */

        self.leave_node(kind);
    }

    fn visit_while_statement(&mut self, stmt: &WhileStatement<'a>) {
        let kind = AstKind::WhileStatement(self.alloc(stmt));
        self.enter_node(kind);

        /* cfg - condition basic block */
        let before_while_stmt_graph_ix = self.cfg.current_node_ix;
        let condition_graph_ix = self.cfg.new_basic_block();
        /* cfg */

        self.visit_expression(&stmt.test);

        /* cfg - body basic block */
        let body_graph_ix = self.cfg.new_basic_block();
        let statement_state =
            self.cfg.before_statement(self.current_node_id, StatementControlFlowType::UsesContinue);
        /* cfg */

        self.visit_statement(&stmt.body);

        /* cfg - after body basic block */
        let after_body_graph_ix = self.cfg.new_basic_block();

        self.cfg.add_edge(before_while_stmt_graph_ix, condition_graph_ix, EdgeType::Normal);
        self.cfg.add_edge(condition_graph_ix, body_graph_ix, EdgeType::Normal);
        self.cfg.add_edge(body_graph_ix, after_body_graph_ix, EdgeType::Normal);
        self.cfg.add_edge(body_graph_ix, condition_graph_ix, EdgeType::Backedge);
        self.cfg.add_edge(condition_graph_ix, after_body_graph_ix, EdgeType::Normal);

        self.cfg.after_statement(
            &statement_state,
            self.current_node_id,
            // all basic blocks are break here so we connect them to the
            // basic block after the while statement
            after_body_graph_ix,
            // all basic blocks are continues here so we connect them to the
            // basic block of the condition
            Some(condition_graph_ix),
        );
        /* cfg */
        self.leave_node(kind);
    }

    fn visit_with_statement(&mut self, stmt: &WithStatement<'a>) {
        let kind = AstKind::WithStatement(self.alloc(stmt));
        self.enter_node(kind);

        /* cfg - condition basic block */
        let before_with_stmt_graph_ix = self.cfg.current_node_ix;
        let statement_state = self
            .cfg
            .before_statement(self.current_node_id, StatementControlFlowType::DoesNotUseContinue);
        let condition_graph_ix = self.cfg.new_basic_block();
        /* cfg */

        self.visit_expression(&stmt.object);

        /* cfg - body basic block */
        let body_graph_ix = self.cfg.new_basic_block();
        /* cfg */

        self.visit_statement(&stmt.body);

        /* cfg - after body basic block */
        let after_body_graph_ix = self.cfg.new_basic_block();

        self.cfg.add_edge(before_with_stmt_graph_ix, condition_graph_ix, EdgeType::Normal);
        self.cfg.add_edge(condition_graph_ix, body_graph_ix, EdgeType::Normal);
        self.cfg.add_edge(body_graph_ix, after_body_graph_ix, EdgeType::Normal);
        self.cfg.add_edge(condition_graph_ix, after_body_graph_ix, EdgeType::Normal);

        self.cfg.after_statement(
            &statement_state,
            self.current_node_id,
            self.cfg.current_node_ix,
            None,
        );
        /* cfg */

        self.leave_node(kind);
    }

    fn visit_function(&mut self, func: &Function<'a>, flags: Option<ScopeFlags>) {
        let kind = AstKind::Function(self.alloc(func));
        self.enter_scope({
            let mut flags = flags.unwrap_or(ScopeFlags::empty()) | ScopeFlags::Function;
            if func.is_strict() {
                flags |= ScopeFlags::StrictMode;
            }
            flags
        });
        self.enter_node(kind);

        /* cfg */
        let preserved = self.cfg.preserve_expression_state();

        let before_function_graph_ix = self.cfg.current_node_ix;
        let function_graph_ix = self.cfg.new_basic_block_for_function();
        self.cfg.function_to_node_ix.insert(self.current_node_id, function_graph_ix);
        self.cfg.add_edge(before_function_graph_ix, function_graph_ix, EdgeType::NewFunction);
        /* cfg */

        if let Some(ident) = &func.id {
            self.visit_binding_identifier(ident);
        }
        self.visit_formal_parameters(&func.params);
        if let Some(body) = &func.body {
            self.visit_function_body(body);
        }

        /* cfg */
        self.cfg.restore_expression_state(preserved);
        let after_function_graph_ix = self.cfg.new_basic_block();
        self.cfg.add_edge(before_function_graph_ix, after_function_graph_ix, EdgeType::Normal);
        // self.cfg.put_x_in_register(AssignmentValue::Function(self.current_node_id));
        /* cfg */

        if let Some(parameters) = &func.type_parameters {
            self.visit_ts_type_parameter_declaration(parameters);
        }
        if let Some(annotation) = &func.return_type {
            self.visit_ts_type_annotation(annotation);
        }
        self.leave_node(kind);
        self.leave_scope();
    }

    fn visit_class(&mut self, class: &Class<'a>) {
        // Class level decorators are transpiled as functions outside of the class taking the class
        // itself as argument. They should be visited before class is entered. E.g., they inherit
        // strict mode from the enclosing scope rather than from class.
        for decorator in &class.decorators {
            self.visit_decorator(decorator);
        }
        let kind = AstKind::Class(self.alloc(class));

        // FIXME(don): Should we enter a scope when visiting class declarations?
        let is_class_expr = class.r#type == ClassType::ClassExpression;
        if is_class_expr {
            // Class expressions create a temporary scope with the class name as its only variable
            // E.g., `let c = class A { foo() { console.log(A) } }`
            self.enter_scope(ScopeFlags::empty());
        }

        self.enter_node(kind);

        /* cfg */
        let preserved = self.cfg.preserve_expression_state();
        self.cfg.store_final_assignments_into_this_array.push(vec![]);
        /* cfg */

        if let Some(id) = &class.id {
            self.visit_binding_identifier(id);
        }
        if let Some(parameters) = &class.type_parameters {
            self.visit_ts_type_parameter_declaration(parameters);
        }

        if let Some(super_class) = &class.super_class {
            self.visit_class_heritage(super_class);
        }
        if let Some(super_parameters) = &class.super_type_parameters {
            self.visit_ts_type_parameter_instantiation(super_parameters);
        }
        self.visit_class_body(&class.body);

        /* cfg */
        let _elements = self.cfg.store_final_assignments_into_this_array.pop().expect(
            "expected there to be atleast one vec in the store_final_assignments_into_this_arrays",
        );
        self.cfg.restore_expression_state(preserved);
        self.cfg.spread_indices.push(vec![]);
        // self.cfg.put_collection_in_register(self.current_node_id, CollectionType::Class, elements);
        /* cfg */

        self.leave_node(kind);
        if is_class_expr {
            self.leave_scope();
        }
    }

    fn visit_arrow_expression(&mut self, expr: &ArrowExpression<'a>) {
        let kind = AstKind::ArrowExpression(self.alloc(expr));
        self.enter_scope(ScopeFlags::Function | ScopeFlags::Arrow);
        self.enter_node(kind);

        self.visit_formal_parameters(&expr.params);

        /* cfg */
        let preserved = self.cfg.preserve_expression_state();

        let current_basic_block_ix = self.cfg.current_basic_block;
        let current_node_ix = self.cfg.current_node_ix;
        let function_graph_ix = self.cfg.new_basic_block_for_function();
        self.cfg.function_to_node_ix.insert(self.current_node_id, function_graph_ix);
        self.cfg.add_edge(current_node_ix, function_graph_ix, EdgeType::NewFunction);
        if expr.expression {
            self.cfg.store_assignments_into_this_array.push(vec![]);
            self.cfg.use_this_register = Some(Register::Return);
        }
        /* cfg */
        self.visit_function_body(&expr.body);

        /* cfg */
        self.cfg.restore_expression_state(preserved);
        self.cfg.current_basic_block = current_basic_block_ix;
        self.cfg.current_node_ix = current_node_ix;
        // self.cfg.put_x_in_register(AssignmentValue::Function(self.current_node_id));
        /* cfg */
        if let Some(parameters) = &expr.type_parameters {
            self.visit_ts_type_parameter_declaration(parameters);
        }
        self.leave_node(kind);
        self.leave_scope();
    }
}

impl<'a> SemanticBuilder<'a> {
    fn enter_kind(&mut self, kind: AstKind<'a>) {
        match kind {
            AstKind::ModuleDeclaration(decl) => {
                self.current_symbol_flags |= Self::symbol_flag_from_module_declaration(decl);
                decl.bind(self);
            }
            AstKind::VariableDeclarator(decl) => {
                decl.bind(self);
                self.make_all_namespaces_valuelike();
            }
            AstKind::StaticBlock(_) => self.label_builder.enter_function_or_static_block(),
            AstKind::Function(func) => {
                func.bind(self);
                self.label_builder.enter_function_or_static_block();
                self.add_current_node_id_to_current_scope();
                self.make_all_namespaces_valuelike();
            }
            AstKind::ArrowExpression(_) => {
                self.add_current_node_id_to_current_scope();
                self.make_all_namespaces_valuelike();
            }
            AstKind::Class(class) => {
                self.current_node_flags |= NodeFlags::Class;
                class.bind(self);
                self.make_all_namespaces_valuelike();
            }
            AstKind::ClassBody(body) => {
                self.class_table_builder.declare_class_body(
                    body,
                    self.current_node_id,
                    &self.nodes,
                );
            }
            AstKind::PrivateIdentifier(ident) => {
                self.class_table_builder.add_private_identifier_reference(
                    ident,
                    self.current_node_id,
                    &self.nodes,
                );
            }
            AstKind::BindingRestElement(element) => {
                element.bind(self);
            }
            AstKind::FormalParameter(param) => {
                param.bind(self);
            }
            AstKind::CatchClause(clause) => {
                clause.bind(self);
            }
            AstKind::TSModuleDeclaration(module_declaration) => {
                module_declaration.bind(self);
                let symbol_id = self
                    .scope
                    .get_bindings(self.current_scope_id)
                    .get(module_declaration.id.name());
                self.namespace_stack.push(*symbol_id.unwrap());
                self.in_type_definition = true;
            }
            AstKind::TSTypeAliasDeclaration(type_alias_declaration) => {
                type_alias_declaration.bind(self);
                self.in_type_definition = true;
            }
            AstKind::TSInterfaceDeclaration(interface_declaration) => {
                interface_declaration.bind(self);
                self.in_type_definition = true;
            }
            AstKind::TSEnumDeclaration(enum_declaration) => {
                enum_declaration.bind(self);
                // TODO: const enum?
                self.make_all_namespaces_valuelike();
                self.in_type_definition = true;
            }
            AstKind::TSTypeParameterInstantiation(_) | AstKind::TSTypeAnnotation(_) => {
                self.in_type_definition = true;
            }
            AstKind::TSEnumMember(enum_member) => {
                enum_member.bind(self);
            }
            AstKind::TSTypeParameter(type_parameter) => {
                type_parameter.bind(self);
            }
            AstKind::IdentifierReference(ident) => {
                self.reference_identifier(ident);
            }
            AstKind::JSXElementName(elem) => {
                self.reference_jsx_element_name(elem);
            }
            AstKind::LabeledStatement(stmt) => {
                self.label_builder.enter(stmt, self.current_node_id);
            }
            AstKind::ContinueStatement(ContinueStatement { label, .. })
            | AstKind::BreakStatement(BreakStatement { label, .. }) => {
                if let Some(label) = &label {
                    self.label_builder.mark_as_used(label);
                }
            }
            AstKind::YieldExpression(_) => {
                self.set_function_node_flag(NodeFlags::HasYield);
            }
            _ => {}
        }
    }

    #[allow(clippy::single_match)]
    fn leave_kind(&mut self, kind: AstKind<'a>) {
        match kind {
            AstKind::Class(_) => {
                self.current_node_flags -= NodeFlags::Class;
                self.class_table_builder.pop_class();
            }
            AstKind::ModuleDeclaration(decl) => {
                self.current_symbol_flags -= Self::symbol_flag_from_module_declaration(decl);
            }
            AstKind::LabeledStatement(_) => self.label_builder.leave(),
            AstKind::StaticBlock(_) | AstKind::Function(_) => {
                self.label_builder.leave_function_or_static_block();
            }
            AstKind::TSModuleBlock(_) => {
                self.namespace_stack.pop();
            }
            AstKind::TSEnumDeclaration(_)
            | AstKind::TSTypeAliasDeclaration(_)
            | AstKind::TSInterfaceDeclaration(_)
            | AstKind::TSModuleDeclaration(_)
            | AstKind::TSTypeParameterInstantiation(_)
            | AstKind::TSTypeAnnotation(_) => {
                self.in_type_definition = false;
            }
            _ => {}
        }
    }

    fn add_current_node_id_to_current_scope(&mut self) {
        self.scope.add_node_id(self.current_scope_id, self.current_node_id);
    }

    fn make_all_namespaces_valuelike(&mut self) {
        for symbol_id in &self.namespace_stack {
            // Ambient modules cannot be value modules
            if self.symbols.get_flag(*symbol_id).intersects(SymbolFlags::Ambient) {
                continue;
            }
            self.symbols.union_flag(*symbol_id, SymbolFlags::ValueModule);
        }
    }

    fn reference_identifier(&mut self, ident: &IdentifierReference) {
        let flag = self.resolve_reference_usages();
        let reference = Reference::new(ident.span, ident.name.clone(), self.current_node_id, flag);
        let reference_id = self.declare_reference(reference);
        ident.reference_id.set(Some(reference_id));
    }

    /// Resolve reference flags for the current ast node.
    fn resolve_reference_usages(&self) -> ReferenceFlag {
        if self.in_type_definition {
            return ReferenceFlag::Type;
        }

        let mut flags = ReferenceFlag::None;

        if self.nodes.parent_id(self.current_node_id).is_none() {
            return ReferenceFlag::Read;
        }

        // This func should only get called when an IdentifierReference is
        // reached
        debug_assert!(matches!(
            self.nodes.get_node(self.current_node_id).kind(),
            AstKind::IdentifierReference(_)
        ));

        for (curr, parent) in self
            .nodes
            .iter_parents(self.current_node_id)
            .tuple_windows::<(&AstNode<'a>, &AstNode<'a>)>()
        {
            match (curr.kind(), parent.kind()) {
                // lhs of assignment expression
                (AstKind::SimpleAssignmentTarget(_), AstKind::AssignmentExpression(_)) => {
                    debug_assert!(!flags.is_read());
                    flags = ReferenceFlag::write();
                    // a lhs expr will not propagate upwards into a rhs
                    // expression, sow e can safely break
                    break;
                }
                (AstKind::AssignmentTarget(_), AstKind::AssignmentExpression(expr)) => {
                    flags |= if expr.operator == AssignmentOperator::Assign {
                        ReferenceFlag::write()
                    } else {
                        ReferenceFlag::read_write()
                    };
                    break;
                }
                (_, AstKind::SimpleAssignmentTarget(_) | AstKind::AssignmentTarget(_)) => {
                    flags |= ReferenceFlag::write();
                    // continue up tree
                }
                (_, AstKind::UpdateExpression(_)) => {
                    flags |= ReferenceFlag::Write;
                    // continue up tree
                }
                (
                    AstKind::AssignmentTarget(_),
                    AstKind::ForInStatement(_) | AstKind::ForOfStatement(_),
                ) => {
                    break;
                }
                (_, AstKind::ParenthesizedExpression(_)) => {
                    // continue up tree
                }
                _ => {
                    flags |= ReferenceFlag::Read;
                    break;
                }
            }
        }

        debug_assert!(flags != ReferenceFlag::None);

        flags
    }

    fn reference_jsx_element_name(&mut self, elem: &JSXElementName) {
        if matches!(
            self.nodes.parent_kind(self.current_node_id),
            Some(AstKind::JSXOpeningElement(_))
        ) {
            if let Some(ident) = match elem {
                JSXElementName::Identifier(ident)
                    if ident.name.chars().next().is_some_and(char::is_uppercase) =>
                {
                    Some(ident)
                }
                JSXElementName::MemberExpression(expr) => Some(expr.get_object_identifier()),
                _ => None,
            } {
                let reference = Reference::new(
                    ident.span,
                    ident.name.clone(),
                    self.current_node_id,
                    ReferenceFlag::read(),
                );
                self.declare_reference(reference);
            }
        }
    }

    fn symbol_flag_from_module_declaration(module: &ModuleDeclaration) -> SymbolFlags {
        if matches!(module, ModuleDeclaration::ImportDeclaration(_)) {
            SymbolFlags::Import
        } else {
            SymbolFlags::Export
        }
    }
}
