diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/flow/bind_analyze/engine.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/flow/bind_analyze/engine.rs new file mode 100644 index 000000000..b9b224aad --- /dev/null +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/flow/bind_analyze/engine.rs @@ -0,0 +1,1295 @@ +//! Bind engine: converts the AST recursive traversal of the flow binding phase (`bind_analyze`) into an explicit task stack +//! +//! In the original implementation, functions like `bind_expr`/`bind_block`/`bind_condition_expr` recurse into each other, +//! and the recursion depth is determined by the nesting depth of user code (member chains, call chains, parens, nested blocks, etc.), +//! so deeply nested inputs directly overflow the native stack. This engine converts all recursive call sites into a heap-based +//! task stack plus continuations, keeping the native call stack depth constant. +//! +//! Key constraint: the execution order of all side effects (`create_node`/`add_antecedent`/`bind_syntax_node`, etc.) +//! must be exactly identical to the original recursive implementation, because flow ids depend on the node creation order. + +use emmylua_parser::{ + BinaryOperator, LuaAssignStat, LuaAst, LuaAstNode, LuaAstToken, LuaBlock, LuaCallExprStat, + LuaElseIfClauseStat, LuaExpr, LuaForRangeStat, LuaForStat, LuaFuncStat, LuaIfStat, + LuaIndexExpr, LuaLocalStat, LuaRepeatStat, LuaVarExpr, LuaWhileStat, UnaryOperator, +}; + +use super::{ + exprs::is_binary_logical, + finish_flow_label, + stats::{ + bind_multi_return_refs, check_local_immutable, check_value_expr_is_check_expr, + finish_entered_loop_post_flow, get_local_decl_ids, get_var_decl_ids, + static_literal_truthiness, static_number_value, + }, +}; +use crate::{ + FlowId, FlowNodeKind, LuaClosureId, LuaDeclId, compilation::analyzer::flow::binder::FlowBinder, +}; + +/// Task: carries the input current and produces one FlowId delivered to the continuation on the stack +enum Task { + /// Bind a plain expression (the result always equals the input current) + Expr(LuaExpr, FlowId), + /// `bind_condition_expr`: bind the condition expression and create condition nodes + Cond(LuaExpr, FlowId, FlowId, FlowId), + /// `finish_flow_label` + Finish(FlowId, FlowId), + /// `bind_node` + Node(LuaAst, FlowId), + /// `bind_block` + Block(LuaBlock, FlowId), + /// Pass a value through + Pass(FlowId), +} + +/// Suspended parent task state +enum Continuation { + /// Execute the remaining tasks in order, passing the result through + Seq { pending: Vec }, + /// Condition node creation phase (restore the targets, then create True/False condition nodes) + CondNodes { + expr: LuaExpr, + current: FlowId, + true_target: FlowId, + false_target: FlowId, + old_true: FlowId, + old_false: FlowId, + }, + /// Create a Finish task after receiving a value + ThenFinish { + label: FlowId, + default: Option, + }, + /// Create a Cond task after receiving a value (the value is the current of the condition expression) + ThenCond { + expr: LuaExpr, + true_target: FlowId, + false_target: FlowId, + }, + /// Create a Block task after receiving a value + ThenBlock { block: LuaBlock }, + /// Safe index: bind child nodes after the prefix condition completes + SafeIndexDone { index_expr: LuaIndexExpr }, + /// Unary not: restore the condition targets + UnaryNotDone { old_true: FlowId, old_false: FlowId }, + /// assert args: condition binding completed + AssertCond { + args: Vec, + idx: usize, + labels: Vec, + false_target: FlowId, + }, + /// assert args: label merge completed + AssertFinish { + args: Vec, + idx: usize, + labels: Vec, + false_target: FlowId, + }, + /// local statement: create the decl node after the value expressions are bound + LocalDone { + local_stat: LuaLocalStat, + current: FlowId, + }, + /// assignment statement: create the node after the values/variables are bound + AssignDone { + assign_stat: LuaAssignStat, + current: FlowId, + }, + /// return statement completed + ReturnDone { current: FlowId }, + /// call statement completed + CallStatDone { + call_expr_stat: LuaCallExprStat, + current: FlowId, + kind: CallStatKind, + }, + /// function definition statement completed + FuncDone { + func_stat: LuaFuncStat, + current: FlowId, + }, + /// local function statement completed + LocalFuncDone { current: FlowId }, + /// while loop: post-process after the loop body completes + WhileDone { + after_label: FlowId, + loop_enters: bool, + has_block: bool, + current: FlowId, + old_loop_label: FlowId, + old_break_target_label: FlowId, + }, + /// repeat loop: post-process after the loop completes + RepeatDone { + post_label: FlowId, + old_loop_label: FlowId, + old_break_target_label: FlowId, + }, + /// for loop: post-process after the loop body completes + ForDone { + post_label: FlowId, + loop_enters: bool, + has_block: bool, + current: FlowId, + old_loop_label: FlowId, + old_break_target_label: FlowId, + }, + /// for range: post-process after completion + ForRangeDone { + current: FlowId, + old_loop_label: FlowId, + old_break_target_label: FlowId, + }, + /// for loop: create the ForIStat node after the iteration expressions complete + ThenForNode { + for_stat: LuaForStat, + pre_label: FlowId, + block: Option, + }, + /// for range: create the decl node after the iteration expressions complete + ThenForRangeDecl { + for_range_stat: LuaForRangeStat, + pre_label: FlowId, + }, + /// if statement: process the remaining branches after one branch block completes + IfBranchDone { + clauses: Vec, + idx: usize, + else_label: FlowId, + post_if: FlowId, + current: FlowId, + else_block: Option, + has_else_clause: bool, + }, + /// if statement: finalize after the else block completes + IfFinal { post_if: FlowId, else_label: FlowId }, + /// block: bind child nodes in order + BlockIter { + children: Vec, + idx: usize, + current: FlowId, + can_change_flow: bool, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum CallStatKind { + Normal, + Error, +} + +enum Step { + Task(Task), + Done(FlowId), + Resume(Continuation, FlowId), +} + +/// Bind engine +pub(super) struct BindEngine<'a, 'b> { + binder: &'a mut FlowBinder<'b>, + stack: Vec, +} + +impl<'a, 'b> BindEngine<'a, 'b> { + fn new(binder: &'a mut FlowBinder<'b>) -> Self { + Self { + binder, + stack: Vec::new(), + } + } + + fn run(mut self, root: Task) -> FlowId { + let mut step = Step::Task(root); + loop { + step = match step { + Step::Task(task) => self.evaluate(task), + Step::Done(value) => match self.stack.pop() { + Some(continuation) => Step::Resume(continuation, value), + None => return value, + }, + Step::Resume(continuation, value) => self.resume(continuation, value), + }; + } + } + + fn evaluate(&mut self, task: Task) -> Step { + match task { + Task::Pass(value) => Step::Done(value), + Task::Finish(label, default) => { + Step::Done(finish_flow_label(self.binder, label, default)) + } + Task::Expr(expr, current) => self.evaluate_expr(expr, current), + Task::Cond(expr, current, true_target, false_target) => { + let old_true = self.binder.true_target; + let old_false = self.binder.false_target; + self.binder.true_target = true_target; + self.binder.false_target = false_target; + self.stack.push(Continuation::CondNodes { + expr: expr.clone(), + current, + true_target, + false_target, + old_true, + old_false, + }); + Step::Task(Task::Expr(expr, current)) + } + Task::Node(node, current) => self.evaluate_node(node, current), + Task::Block(block, current) => { + let children = block.children::().collect::>(); + if children.is_empty() { + Step::Done(current) + } else { + let first = children[0].clone(); + self.stack.push(Continuation::BlockIter { + children, + idx: 0, + current, + can_change_flow: true, + }); + Step::Task(Task::Node(first, current)) + } + } + } + } + + fn evaluate_expr(&mut self, expr: LuaExpr, current: FlowId) -> Step { + match expr { + LuaExpr::NameExpr(name_expr) => { + self.binder + .bind_syntax_node(name_expr.get_syntax_id(), current); + Step::Done(current) + } + LuaExpr::LiteralExpr(_) => Step::Done(current), + LuaExpr::ParenExpr(paren_expr) => match paren_expr.get_expr() { + Some(inner) => Step::Task(Task::Expr(inner, current)), + None => Step::Done(current), + }, + LuaExpr::ClosureExpr(closure_expr) => { + self.spawn_children(LuaAst::LuaClosureExpr(closure_expr), current) + } + LuaExpr::CallExpr(call_expr) => { + self.spawn_children(LuaAst::LuaCallExpr(call_expr), current) + } + LuaExpr::TableExpr(table_expr) => { + self.spawn_children(LuaAst::LuaTableExpr(table_expr), current) + } + LuaExpr::IndexExpr(index_expr) => { + self.binder + .bind_syntax_node(index_expr.get_syntax_id(), current); + if index_expr.is_safe_index() { + let pre_access = self.binder.create_branch_label(); + let Some(prefix_expr) = index_expr.get_prefix_expr() else { + return Step::Done(current); + }; + self.stack.push(Continuation::SafeIndexDone { index_expr }); + self.stack.push(Continuation::ThenFinish { + label: pre_access, + default: None, + }); + Step::Task(Task::Cond( + prefix_expr, + current, + pre_access, + self.binder.false_target, + )) + } else { + self.spawn_children(LuaAst::LuaIndexExpr(index_expr), current) + } + } + LuaExpr::BinaryExpr(binary_expr) => { + let Some(op_token) = binary_expr.get_op_token() else { + return Step::Done(current); + }; + let Some((left, right)) = binary_expr.get_exprs() else { + return Step::Done(current); + }; + match op_token.get_op() { + BinaryOperator::OpAnd => { + let pre_right = self.binder.create_branch_label(); + self.stack.push(Continuation::ThenCond { + expr: right, + true_target: self.binder.true_target, + false_target: self.binder.false_target, + }); + self.stack.push(Continuation::ThenFinish { + label: pre_right, + default: None, + }); + Step::Task(Task::Cond( + left, + current, + pre_right, + self.binder.false_target, + )) + } + BinaryOperator::OpOr | BinaryOperator::OpNilCoalescing => { + let pre_right = self.binder.create_branch_label(); + self.stack.push(Continuation::ThenCond { + expr: right, + true_target: self.binder.true_target, + false_target: self.binder.false_target, + }); + self.stack.push(Continuation::ThenFinish { + label: pre_right, + default: None, + }); + Step::Task(Task::Cond( + left, + current, + self.binder.true_target, + pre_right, + )) + } + _ => self.spawn_children(LuaAst::LuaBinaryExpr(binary_expr), current), + } + } + LuaExpr::UnaryExpr(unary_expr) => { + let is_not = unary_expr + .get_op_token() + .is_some_and(|op| op.get_op() == UnaryOperator::OpNot); + if !is_not { + return self.spawn_children(LuaAst::LuaUnaryExpr(unary_expr), current); + } + let Some(inner_expr) = unary_expr.get_expr() else { + return Step::Done(current); + }; + // not swaps the condition targets; restore them after the inner binding completes + let old_true = self.binder.true_target; + let old_false = self.binder.false_target; + self.binder.true_target = old_false; + self.binder.false_target = old_true; + self.stack.push(Continuation::UnaryNotDone { + old_true, + old_false, + }); + Step::Task(Task::Expr(inner_expr, current)) + } + LuaExpr::TernaryExpr(ternary_expr) => { + let Some(condition) = ternary_expr.get_condition_expr() else { + return Step::Done(current); + }; + let Some((true_expr, false_expr)) = ternary_expr.get_true_false_exprs() else { + return Step::Done(current); + }; + let true_branch_label = self.binder.create_branch_label(); + let false_branch_label = self.binder.create_branch_label(); + let unreachable = self.binder.unreachable; + let true_target = self.binder.true_target; + let false_target = self.binder.false_target; + self.stack.push(Continuation::ThenCond { + expr: false_expr, + true_target, + false_target, + }); + self.stack.push(Continuation::ThenFinish { + label: false_branch_label, + default: Some(unreachable), + }); + self.stack.push(Continuation::ThenCond { + expr: true_expr, + true_target, + false_target, + }); + self.stack.push(Continuation::ThenFinish { + label: true_branch_label, + default: Some(unreachable), + }); + Step::Task(Task::Cond( + condition, + current, + true_branch_label, + false_branch_label, + )) + } + } + } + + /// Bind all children of an AST node in order (results are ignored; current is passed through) + fn spawn_children(&mut self, node: LuaAst, current: FlowId) -> Step { + let children = node.children::().collect::>(); + if children.is_empty() { + return Step::Done(current); + } + let mut pending = Vec::with_capacity(children.len() - 1); + for child in children.iter().skip(1).rev() { + pending.push(Task::Node(child.clone(), current)); + } + self.stack.push(Continuation::Seq { pending }); + Step::Task(Task::Node(children[0].clone(), current)) + } + + fn evaluate_node(&mut self, node: LuaAst, current: FlowId) -> Step { + match node { + LuaAst::LuaBlock(block) => Step::Task(Task::Block(block, current)), + LuaAst::LuaAssignStat(assign_stat) => { + let (vars, values) = assign_stat.get_var_and_expr_list(); + let mut pending = Vec::new(); + // Bind the values first, then the variables (pop order = original recursive binding order) + for var in vars.iter().rev() { + if let Some(ast) = LuaAst::cast(var.syntax().clone()) { + pending.push(Task::Node(ast, current)); + } + } + for expr in values.iter().rev() { + if let Some(ast) = LuaAst::cast(expr.syntax().clone()) { + pending.push(Task::Node(ast, current)); + } + } + self.stack.push(Continuation::AssignDone { + assign_stat, + current, + }); + if pending.is_empty() { + Step::Task(Task::Pass(current)) + } else { + self.stack.push(Continuation::Seq { pending }); + Step::Task(Task::Pass(current)) + } + } + LuaAst::LuaLocalStat(local_stat) => { + let local_names = local_stat.get_local_name_list().collect::>(); + let values = local_stat.get_value_exprs().collect::>(); + let min_len = local_names.len().min(values.len()); + for i in 0..min_len { + let name = &local_names[i]; + let value = &values[i]; + let decl_id = LuaDeclId::new(self.binder.file_id, name.get_position()); + if check_local_immutable(self.binder, decl_id) + && check_value_expr_is_check_expr(value.clone()) + { + self.binder + .decl_bind_expr_ref + .insert(decl_id, value.to_ptr()); + } + } + self.stack.push(Continuation::LocalDone { + local_stat, + current, + }); + self.spawn_expr_sequence(&values, current) + } + LuaAst::LuaReturnStat(return_stat) => { + let exprs = return_stat.get_expr_list().collect::>(); + self.stack.push(Continuation::ReturnDone { current }); + self.spawn_expr_sequence(&exprs, current) + } + LuaAst::LuaCallExprStat(call_expr_stat) => { + self.evaluate_call_expr_stat(call_expr_stat, current) + } + LuaAst::LuaLabelStat(label_stat) => { + let Some(label_name_token) = label_stat.get_label_name_token() else { + return Step::Done(current); + }; + let label_name = label_name_token.get_name_text(); + let closure_id = LuaClosureId::from_node(label_stat.syntax()); + self.binder + .db + .get_reference_index_mut() + .add_label_declaration( + self.binder.file_id, + closure_id, + label_name, + label_name_token.get_range(), + ); + let name_label = self.binder.create_name_label(label_name, closure_id); + self.binder.add_antecedent(name_label, current); + Step::Done(name_label) + } + LuaAst::LuaBreakStat(break_stat) => { + let break_flow_id = self.binder.create_break(); + if let Some(loop_flow) = self.binder.get_flow(self.binder.loop_label) + && loop_flow.kind.is_unreachable() + { + self.binder.report_error(crate::AnalyzeError::new( + crate::DiagnosticCode::SyntaxError, + &t!("Break outside loop"), + break_stat.get_range(), + )); + return Step::Done(current); + } + self.binder.add_antecedent(break_flow_id, current); + self.binder + .add_antecedent(self.binder.break_target_label, break_flow_id); + Step::Done(break_flow_id) + } + LuaAst::LuaContinueStat(continue_stat) => { + let continue_flow_id = self.binder.create_continue(); + if let Some(loop_flow) = self.binder.get_flow(self.binder.loop_label) + && loop_flow.kind.is_unreachable() + { + self.binder.report_error(crate::AnalyzeError::new( + crate::DiagnosticCode::SyntaxError, + &t!("Continue outside loop"), + continue_stat.get_range(), + )); + return Step::Done(current); + } + self.binder.add_antecedent(continue_flow_id, current); + self.binder + .add_antecedent(self.binder.loop_label, continue_flow_id); + Step::Done(continue_flow_id) + } + LuaAst::LuaGotoStat(goto_stat) => { + let closure_id = LuaClosureId::from_node(goto_stat.syntax()); + let Some(label_token) = goto_stat.get_label_name_token() else { + return Step::Done(current); + }; + let label_name = label_token.get_name_text(); + self.binder + .db + .get_reference_index_mut() + .add_label_reference( + self.binder.file_id, + closure_id, + label_name, + label_token.get_range(), + ); + let return_flow_id = self.binder.create_return(); + self.binder.cache_goto_flow( + closure_id, + label_token.clone(), + label_name, + return_flow_id, + ); + self.binder.add_antecedent(return_flow_id, current); + Step::Done(return_flow_id) + } + LuaAst::LuaDoStat(do_stat) => match do_stat.get_block() { + Some(block) => Step::Task(Task::Block(block, current)), + None => Step::Done(current), + }, + LuaAst::LuaWhileStat(while_stat) => self.evaluate_while_stat(while_stat, current), + LuaAst::LuaRepeatStat(repeat_stat) => self.evaluate_repeat_stat(repeat_stat, current), + LuaAst::LuaIfStat(if_stat) => self.evaluate_if_stat(if_stat, current), + LuaAst::LuaForStat(for_stat) => self.evaluate_for_stat(for_stat, current), + LuaAst::LuaForRangeStat(for_range_stat) => { + self.evaluate_for_range_stat(for_range_stat, current) + } + LuaAst::LuaFuncStat(func_stat) => { + if func_stat.get_func_name().is_none() { + return Step::Done(current); + } + self.stack.push(Continuation::FuncDone { + func_stat: func_stat.clone(), + current, + }); + self.spawn_children(LuaAst::LuaFuncStat(func_stat), current) + } + LuaAst::LuaLocalFuncStat(local_func_stat) => { + self.stack.push(Continuation::LocalFuncDone { current }); + self.spawn_children(LuaAst::LuaLocalFuncStat(local_func_stat), current) + } + LuaAst::LuaComment(comment) => { + Step::Done(super::comment::bind_comment(self.binder, comment, current)) + } + // exprs + LuaAst::LuaNameExpr(_) + | LuaAst::LuaIndexExpr(_) + | LuaAst::LuaTableExpr(_) + | LuaAst::LuaBinaryExpr(_) + | LuaAst::LuaUnaryExpr(_) + | LuaAst::LuaParenExpr(_) + | LuaAst::LuaCallExpr(_) + | LuaAst::LuaLiteralExpr(_) + | LuaAst::LuaClosureExpr(_) => match LuaExpr::cast(node.syntax().clone()) { + Some(expr) => Step::Task(Task::Expr(expr, current)), + None => Step::Done(current), + }, + LuaAst::LuaTableField(_) + | LuaAst::LuaParamList(_) + | LuaAst::LuaParamName(_) + | LuaAst::LuaCallArgList(_) + | LuaAst::LuaLocalName(_) => self.spawn_children(node, current), + _ => Step::Done(current), + } + } + + /// Bind a sequence of value expressions in order (current is passed through) + fn spawn_expr_sequence(&mut self, exprs: &[LuaExpr], current: FlowId) -> Step { + if exprs.is_empty() { + return Step::Task(Task::Pass(current)); + } + let mut pending = Vec::with_capacity(exprs.len() - 1); + for expr in exprs.iter().skip(1).rev() { + pending.push(Task::Expr(expr.clone(), current)); + } + self.stack.push(Continuation::Seq { pending }); + Step::Task(Task::Expr(exprs[0].clone(), current)) + } + + fn evaluate_call_expr_stat( + &mut self, + call_expr_stat: LuaCallExprStat, + current: FlowId, + ) -> Step { + let Some(call_expr) = call_expr_stat.get_call_expr() else { + return Step::Done(current); + }; + + if call_expr.is_assert() { + let Some(arg_list) = call_expr.get_args_list() else { + return Step::Done(current); + }; + let args = arg_list.get_args().collect::>(); + if args.is_empty() { + return Step::Done(current); + } + let false_target = self.binder.unreachable; + let labels = args + .iter() + .map(|_| self.binder.create_branch_label()) + .collect::>(); + let first_arg = args[0].clone(); + let first_label = labels[0]; + self.stack.push(Continuation::AssertFinish { + args, + idx: 0, + labels, + false_target, + }); + Step::Task(Task::Cond(first_arg, current, first_label, false_target)) + } else { + let kind = if call_expr.is_error() { + CallStatKind::Error + } else { + CallStatKind::Normal + }; + self.stack.push(Continuation::CallStatDone { + call_expr_stat, + current, + kind, + }); + match LuaAst::cast(call_expr.syntax().clone()) { + Some(ast) => self.spawn_children(ast, current), + None => Step::Task(Task::Pass(current)), + } + } + } + + fn evaluate_while_stat(&mut self, while_stat: LuaWhileStat, current: FlowId) -> Step { + let pre_while_label = self.binder.create_loop_label(); + let after_while_label = self.binder.create_branch_label(); + let pre_block_label = self.binder.create_branch_label(); + self.binder.add_antecedent(pre_while_label, current); + let Some(condition_expr) = while_stat.get_condition_expr() else { + return Step::Done(current); + }; + + let old_loop_label = self.binder.loop_label; + let old_break_target_label = self.binder.break_target_label; + self.binder.loop_label = pre_while_label; + self.binder.break_target_label = after_while_label; + + let has_block = while_stat.get_block().is_some(); + match static_literal_truthiness(&condition_expr) { + Some(false) => { + self.binder.loop_label = old_loop_label; + self.binder.break_target_label = old_break_target_label; + Step::Done(current) + } + Some(true) => { + self.stack.push(Continuation::WhileDone { + after_label: after_while_label, + loop_enters: true, + has_block, + current, + old_loop_label, + old_break_target_label, + }); + match while_stat.get_block() { + Some(block) => Step::Task(Task::Block(block, current)), + None => Step::Task(Task::Pass(current)), + } + } + None => { + self.stack.push(Continuation::WhileDone { + after_label: after_while_label, + loop_enters: false, + has_block, + current, + old_loop_label, + old_break_target_label, + }); + if let Some(block) = while_stat.get_block() { + self.stack.push(Continuation::ThenBlock { block }); + } + self.stack.push(Continuation::ThenFinish { + label: pre_block_label, + default: None, + }); + Step::Task(Task::Cond( + condition_expr, + current, + pre_block_label, + after_while_label, + )) + } + } + } + + fn evaluate_repeat_stat(&mut self, repeat_stat: LuaRepeatStat, current: FlowId) -> Step { + let pre_repeat_label = self.binder.create_loop_label(); + let post_repeat_label = self.binder.create_branch_label(); + self.binder.add_antecedent(pre_repeat_label, current); + + let old_loop_label = self.binder.loop_label; + let old_break_target_label = self.binder.break_target_label; + self.binder.loop_label = pre_repeat_label; + self.binder.break_target_label = post_repeat_label; + + self.stack.push(Continuation::RepeatDone { + post_label: post_repeat_label, + old_loop_label, + old_break_target_label, + }); + if let Some(condition_expr) = repeat_stat.get_condition_expr() { + self.stack.push(Continuation::ThenCond { + expr: condition_expr, + true_target: post_repeat_label, + false_target: pre_repeat_label, + }); + } + if let Some(block) = repeat_stat.get_block() { + self.stack.push(Continuation::ThenBlock { block }); + } + Step::Task(Task::Finish(pre_repeat_label, current)) + } + + fn evaluate_if_stat(&mut self, if_stat: LuaIfStat, current: FlowId) -> Step { + let post_if_label = self.binder.create_branch_label(); + let else_label = self.binder.create_branch_label(); + let then_label = self.binder.create_branch_label(); + let clauses = if_stat.get_else_if_clause_list().collect::>(); + let else_clause = if_stat.get_else_clause(); + let has_else_clause = else_clause.is_some(); + let else_block = else_clause.and_then(|clause| clause.get_block()); + + self.stack.push(Continuation::IfBranchDone { + clauses, + idx: 0, + else_label, + post_if: post_if_label, + current, + else_block, + has_else_clause, + }); + if let Some(then_block) = if_stat.get_block() { + self.stack + .push(Continuation::ThenBlock { block: then_block }); + } + self.stack.push(Continuation::ThenFinish { + label: then_label, + default: Some(current), + }); + match if_stat.get_condition_expr() { + Some(condition_expr) => { + Step::Task(Task::Cond(condition_expr, current, then_label, else_label)) + } + None => Step::Task(Task::Pass(current)), + } + } + + fn evaluate_for_stat(&mut self, for_stat: LuaForStat, current: FlowId) -> Step { + let pre_for_label = self.binder.create_loop_label(); + let post_for_label = self.binder.create_branch_label(); + self.binder.add_antecedent(pre_for_label, current); + + let iter_exprs = for_stat.get_iter_expr().collect::>(); + let loop_enters = match iter_exprs.as_slice() { + [start_expr, stop_expr] => match ( + static_number_value(start_expr), + static_number_value(stop_expr), + ) { + (Some(start), Some(stop)) => start <= stop, + _ => false, + }, + [start_expr, stop_expr, step_expr, ..] => match ( + static_number_value(start_expr), + static_number_value(stop_expr), + static_number_value(step_expr), + ) { + (Some(start), Some(stop), Some(step)) => { + (step > 0.0 && start <= stop) || (step < 0.0 && start >= stop) + } + _ => false, + }, + _ => false, + }; + + let old_loop_label = self.binder.loop_label; + let old_break_target_label = self.binder.break_target_label; + self.binder.loop_label = pre_for_label; + self.binder.break_target_label = post_for_label; + + let block = for_stat.get_block(); + self.stack.push(Continuation::ForDone { + post_label: post_for_label, + loop_enters, + has_block: block.is_some(), + current, + old_loop_label, + old_break_target_label, + }); + self.stack.push(Continuation::ThenForNode { + for_stat, + pre_label: pre_for_label, + block, + }); + self.spawn_expr_sequence(&iter_exprs, current) + } + + fn evaluate_for_range_stat( + &mut self, + for_range_stat: LuaForRangeStat, + current: FlowId, + ) -> Step { + let pre_for_range_label = self.binder.create_loop_label(); + let post_for_range_label = self.binder.create_branch_label(); + self.binder.add_antecedent(pre_for_range_label, current); + + let old_loop_label = self.binder.loop_label; + let old_break_target_label = self.binder.break_target_label; + self.binder.loop_label = pre_for_range_label; + self.binder.break_target_label = post_for_range_label; + + let exprs = for_range_stat.get_expr_list().collect::>(); + self.stack.push(Continuation::ForRangeDone { + current, + old_loop_label, + old_break_target_label, + }); + if let Some(block) = for_range_stat.get_block() { + self.stack.push(Continuation::ThenBlock { block }); + } + self.stack.push(Continuation::ThenForRangeDecl { + for_range_stat, + pre_label: pre_for_range_label, + }); + self.spawn_expr_sequence(&exprs, current) + } + + fn resume(&mut self, continuation: Continuation, value: FlowId) -> Step { + match continuation { + Continuation::Seq { mut pending } => match pending.pop() { + Some(task) => { + self.stack.push(Continuation::Seq { pending }); + Step::Task(task) + } + None => Step::Done(value), + }, + Continuation::CondNodes { + expr, + current, + true_target, + false_target, + old_true, + old_false, + } => { + self.binder.true_target = old_true; + self.binder.false_target = old_false; + if !is_binary_logical(&expr) { + let true_condition = self + .binder + .create_node(FlowNodeKind::TrueCondition(expr.to_ptr())); + self.binder.add_antecedent(true_condition, current); + self.binder.add_antecedent(true_target, true_condition); + + let false_condition = self + .binder + .create_node(FlowNodeKind::FalseCondition(expr.to_ptr())); + self.binder.add_antecedent(false_condition, current); + self.binder.add_antecedent(false_target, false_condition); + } + Step::Done(current) + } + Continuation::ThenFinish { label, default } => { + Step::Task(Task::Finish(label, default.unwrap_or(value))) + } + Continuation::ThenCond { + expr, + true_target, + false_target, + } => Step::Task(Task::Cond(expr, value, true_target, false_target)), + Continuation::ThenBlock { block } => Step::Task(Task::Block(block, value)), + Continuation::SafeIndexDone { index_expr } => { + self.spawn_children(LuaAst::LuaIndexExpr(index_expr), value) + } + Continuation::UnaryNotDone { + old_true, + old_false, + } => { + self.binder.true_target = old_true; + self.binder.false_target = old_false; + Step::Done(value) + } + Continuation::AssertCond { + args, + idx, + labels, + false_target, + } => { + if idx >= args.len() { + Step::Done(value) + } else { + let arg = args[idx].clone(); + let label = labels[idx]; + self.stack.push(Continuation::AssertFinish { + args, + idx, + labels, + false_target, + }); + Step::Task(Task::Cond(arg, value, label, false_target)) + } + } + Continuation::AssertFinish { + args, + idx, + labels, + false_target, + } => { + let label = labels[idx]; + self.stack.push(Continuation::AssertCond { + args, + idx: idx + 1, + labels, + false_target, + }); + Step::Task(Task::Finish(label, value)) + } + Continuation::LocalDone { + local_stat, + current, + } => { + let local_flow_id = self.binder.create_decl(local_stat.get_position()); + self.binder.add_antecedent(local_flow_id, current); + let local_names = local_stat.get_local_name_list().collect::>(); + let values = local_stat.get_value_exprs().collect::>(); + bind_multi_return_refs( + self.binder, + &get_local_decl_ids(self.binder, &local_names), + &values, + local_stat.get_position(), + local_flow_id, + ); + Step::Done(local_flow_id) + } + Continuation::AssignDone { + assign_stat, + current, + } => { + let assignment_kind = FlowNodeKind::Assignment(assign_stat.to_ptr()); + let flow_id = self.binder.create_node(assignment_kind); + self.binder.add_antecedent(flow_id, current); + let (vars, values) = assign_stat.get_var_and_expr_list(); + bind_multi_return_refs( + self.binder, + &get_var_decl_ids(self.binder, &vars), + &values, + assign_stat.get_position(), + flow_id, + ); + Step::Done(flow_id) + } + Continuation::ReturnDone { current } => { + let return_flow_id = self.binder.create_return(); + self.binder.add_antecedent(return_flow_id, current); + Step::Done(return_flow_id) + } + Continuation::CallStatDone { + call_expr_stat, + current, + kind, + } => match kind { + CallStatKind::Normal => { + let flow_id = self + .binder + .create_node(FlowNodeKind::CallExprStat(call_expr_stat.to_ptr())); + self.binder.add_antecedent(flow_id, current); + Step::Done(flow_id) + } + CallStatKind::Error => { + let return_flow_id = self.binder.create_return(); + self.binder.add_antecedent(return_flow_id, current); + Step::Done(return_flow_id) + } + }, + Continuation::FuncDone { func_stat, current } => match func_stat.get_func_name() { + Some(LuaVarExpr::NameExpr(_)) => { + let func_kind = FlowNodeKind::ImplFunc(func_stat.to_ptr()); + let flow_id = self.binder.create_node(func_kind); + self.binder.add_antecedent(flow_id, current); + Step::Done(flow_id) + } + _ => Step::Done(current), + }, + Continuation::LocalFuncDone { current } => Step::Done(current), + Continuation::WhileDone { + after_label, + loop_enters, + has_block, + current, + old_loop_label, + old_break_target_label, + } => { + self.binder.loop_label = old_loop_label; + self.binder.break_target_label = old_break_target_label; + if loop_enters && has_block { + Step::Done(finish_entered_loop_post_flow( + self.binder, + after_label, + value, + )) + } else { + Step::Done(current) + } + } + Continuation::RepeatDone { + post_label, + old_loop_label, + old_break_target_label, + } => { + self.binder.loop_label = old_loop_label; + self.binder.break_target_label = old_break_target_label; + Step::Done(finish_flow_label(self.binder, post_label, value)) + } + Continuation::ForDone { + post_label, + loop_enters, + has_block, + current, + old_loop_label, + old_break_target_label, + } => { + self.binder.loop_label = old_loop_label; + self.binder.break_target_label = old_break_target_label; + if loop_enters && has_block { + Step::Done(finish_entered_loop_post_flow( + self.binder, + post_label, + value, + )) + } else { + Step::Done(current) + } + } + Continuation::ForRangeDone { + current, + old_loop_label, + old_break_target_label, + } => { + self.binder.loop_label = old_loop_label; + self.binder.break_target_label = old_break_target_label; + Step::Done(current) + } + Continuation::ThenForNode { + for_stat, + pre_label, + block, + } => { + let for_node = self + .binder + .create_node(FlowNodeKind::ForIStat(for_stat.to_ptr())); + self.binder.add_antecedent(for_node, pre_label); + match block { + Some(block) => Step::Task(Task::Block(block, for_node)), + None => Step::Task(Task::Pass(for_node)), + } + } + Continuation::ThenForRangeDecl { + for_range_stat, + pre_label, + } => { + let decl_flow = self.binder.create_decl(for_range_stat.get_position()); + self.binder.add_antecedent(decl_flow, pre_label); + Step::Task(Task::Finish(pre_label, value)) + } + Continuation::IfBranchDone { + clauses, + idx, + else_label, + post_if, + current, + else_block, + has_else_clause, + } => { + self.binder.add_antecedent(post_if, value); + if idx >= clauses.len() { + // else branch + match else_block { + Some(block) => { + self.stack.push(Continuation::IfFinal { + post_if, + else_label, + }); + Step::Task(Task::Block(block, else_label)) + } + None => { + if !has_else_clause { + self.binder.add_antecedent(post_if, else_label); + } + Step::Done(finalize_if(self.binder, post_if, else_label)) + } + } + } else { + // process one elseif clause + let clause = clauses[idx].clone(); + let elseif_then_label = self.binder.create_branch_label(); + let post_elseif_label = self.binder.create_branch_label(); + self.stack.push(Continuation::IfBranchDone { + clauses, + idx: idx + 1, + else_label: post_elseif_label, + post_if, + current, + else_block, + has_else_clause, + }); + if let Some(block) = clause.get_block() { + self.stack.push(Continuation::ThenBlock { block }); + } + self.stack.push(Continuation::ThenFinish { + label: elseif_then_label, + default: Some(current), + }); + if let Some(condition_expr) = clause.get_condition_expr() { + self.stack.push(Continuation::ThenCond { + expr: condition_expr, + true_target: elseif_then_label, + false_target: post_elseif_label, + }); + } + Step::Task(Task::Finish(else_label, current)) + } + } + Continuation::IfFinal { + post_if, + else_label, + } => { + self.binder.add_antecedent(post_if, value); + Step::Done(finalize_if(self.binder, post_if, else_label)) + } + Continuation::BlockIter { + children, + mut idx, + mut current, + mut can_change_flow, + } => { + if can_change_flow { + current = value; + } + if let Some(flow_node) = self.binder.get_flow(current) { + match &flow_node.kind { + FlowNodeKind::Return | FlowNodeKind::Break | FlowNodeKind::Continue => { + current = self.binder.unreachable; + can_change_flow = false; + } + _ => {} + } + } + idx += 1; + if idx < children.len() { + let next = children[idx].clone(); + self.stack.push(Continuation::BlockIter { + children, + idx, + current, + can_change_flow, + }); + Step::Task(Task::Node(next, current)) + } else { + Step::Done(current) + } + } + } + } +} + +fn finalize_if(binder: &mut FlowBinder<'_>, post_if: FlowId, else_label: FlowId) -> FlowId { + if let Some(flow_node) = binder.get_flow(post_if) + && flow_node.antecedent.is_none() + { + return binder.unreachable; + } + + finish_flow_label(binder, post_if, else_label) +} + +/// Entry: bind a block and return the final flow id +pub(super) fn run_bind_block(binder: &mut FlowBinder, block: LuaBlock, current: FlowId) -> FlowId { + BindEngine::new(binder).run(Task::Block(block, current)) +} + +/// Entry: bind an expression +pub(super) fn run_bind_expr(binder: &mut FlowBinder, expr: LuaExpr, current: FlowId) -> FlowId { + BindEngine::new(binder).run(Task::Expr(expr, current)) +} + +#[cfg(test)] +mod tests { + use crate::VirtualWorkspace; + + #[test] + fn test_flow_bind_deep_index_chain() { + let mut ws = VirtualWorkspace::new(); + ws.def("---@type { x: integer }\nlocal t"); + + let mut expr = "t".to_string(); + for _ in 0..1_500 { + expr.push_str(".x"); + } + let _ = ws.expr_ty(&expr); + } + + #[test] + fn test_flow_bind_deep_call_chain() { + let mut ws = VirtualWorkspace::new(); + ws.def("---@type fun(): any\nlocal f"); + + let mut expr = "f".to_string(); + for _ in 0..2_000 { + expr.push_str("()"); + } + let _ = ws.expr_ty(&expr); + } + + #[test] + fn test_flow_bind_deep_paren_chain() { + let mut ws = VirtualWorkspace::new(); + let mut expr = "1".to_string(); + for _ in 0..150 { + expr.insert(0, '('); + expr.push(')'); + } + let _ = ws.expr_ty(&expr); + } + + #[test] + fn test_flow_bind_deep_logical_chain() { + let mut ws = VirtualWorkspace::new(); + let mut expr = "1".to_string(); + for _ in 0..1_500 { + expr.push_str(" and 1"); + } + let ty = ws.expr_ty(&expr); + assert!(ty.is_integer()); + } + + // Deeply nested if blocks + #[test] + fn test_flow_bind_deep_nested_blocks() { + let mut ws = VirtualWorkspace::new(); + let mut code = String::new(); + for _ in 0..100 { + code.push_str("if true then "); + } + code.push_str("local x = 1"); + for _ in 0..100 { + code.push_str(" end"); + } + ws.def(&code); + } +} diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/flow/bind_analyze/exprs/bind_binary_expr.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/flow/bind_analyze/exprs/bind_binary_expr.rs index 7817ba93c..16f930a1b 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/flow/bind_analyze/exprs/bind_binary_expr.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/flow/bind_analyze/exprs/bind_binary_expr.rs @@ -1,70 +1,4 @@ -use emmylua_parser::{BinaryOperator, LuaAst, LuaBinaryExpr, LuaExpr, UnaryOperator}; - -use crate::{ - FlowId, - compilation::analyzer::flow::{ - bind_analyze::{bind_each_child, exprs::bind_condition_expr, finish_flow_label}, - binder::FlowBinder, - }, -}; - -pub fn bind_binary_expr( - binder: &mut FlowBinder, - binary_expr: LuaBinaryExpr, - current: FlowId, -) -> Option<()> { - let op_token = binary_expr.get_op_token()?; - - match op_token.get_op() { - BinaryOperator::OpAnd => bind_and_expr(binder, binary_expr, current), - BinaryOperator::OpOr => bind_or_expr(binder, binary_expr, current), - BinaryOperator::OpNilCoalescing => bind_or_expr(binder, binary_expr, current), - _ => { - bind_each_child(binder, LuaAst::LuaBinaryExpr(binary_expr.clone()), current); - Some(()) - } - } -} - -fn bind_and_expr( - binder: &mut FlowBinder, - binary_expr: LuaBinaryExpr, - current: FlowId, -) -> Option<()> { - let (left, right) = binary_expr.get_exprs()?; - - let pre_right = binder.create_branch_label(); - bind_condition_expr(binder, left, current, pre_right, binder.false_target); - let current = finish_flow_label(binder, pre_right, current); - bind_condition_expr( - binder, - right, - current, - binder.true_target, - binder.false_target, - ); - - Some(()) -} - -fn bind_or_expr( - binder: &mut FlowBinder, - binary_expr: LuaBinaryExpr, - current: FlowId, -) -> Option<()> { - let (left, right) = binary_expr.get_exprs()?; - let pre_right = binder.create_branch_label(); - bind_condition_expr(binder, left, current, binder.true_target, pre_right); - let current = finish_flow_label(binder, pre_right, current); - bind_condition_expr( - binder, - right, - current, - binder.true_target, - binder.false_target, - ); - Some(()) -} +use emmylua_parser::{BinaryOperator, LuaExpr, UnaryOperator}; pub fn is_binary_logical(expr: &LuaExpr) -> bool { match expr { diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/flow/bind_analyze/exprs/mod.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/flow/bind_analyze/exprs/mod.rs index f43e71caa..e75eb123c 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/flow/bind_analyze/exprs/mod.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/flow/bind_analyze/exprs/mod.rs @@ -1,212 +1,12 @@ mod bind_binary_expr; -use emmylua_parser::{ - LuaAst, LuaAstNode, LuaCallExpr, LuaClosureExpr, LuaExpr, LuaIndexExpr, LuaNameExpr, - LuaTableExpr, LuaTernaryExpr, LuaUnaryExpr, UnaryOperator, -}; +use emmylua_parser::LuaExpr; -use crate::{ - FlowId, FlowNodeKind, - compilation::analyzer::flow::{ - bind_analyze::{ - bind_each_child, exprs::bind_binary_expr::is_binary_logical, finish_flow_label, - }, - binder::FlowBinder, - }, -}; -pub use bind_binary_expr::bind_binary_expr; +use crate::{FlowId, compilation::analyzer::flow::binder::FlowBinder}; -pub fn bind_condition_expr( - binder: &mut FlowBinder, - condition_expr: LuaExpr, - current: FlowId, - true_target: FlowId, - false_target: FlowId, -) { - let old_true_target = binder.true_target; - let old_false_target = binder.false_target; - - binder.true_target = true_target; - binder.false_target = false_target; - bind_expr(binder, condition_expr.clone(), current); - binder.true_target = old_true_target; - binder.false_target = old_false_target; - - if !is_binary_logical(&condition_expr) { - let true_condition = - binder.create_node(FlowNodeKind::TrueCondition(condition_expr.to_ptr())); - binder.add_antecedent(true_condition, current); - binder.add_antecedent(true_target, true_condition); - - let false_condition = - binder.create_node(FlowNodeKind::FalseCondition(condition_expr.to_ptr())); - binder.add_antecedent(false_condition, current); - binder.add_antecedent(false_target, false_condition); - } -} +pub use bind_binary_expr::is_binary_logical; +/// Bind an expression (explicit task stack engine; the result always equals the input current) pub fn bind_expr(binder: &mut FlowBinder, expr: LuaExpr, current: FlowId) -> FlowId { - match expr { - LuaExpr::NameExpr(name_expr) => bind_name_expr(binder, name_expr, current), - LuaExpr::CallExpr(call_expr) => bind_call_expr(binder, call_expr, current), - LuaExpr::TableExpr(table_expr) => bind_table_expr(binder, table_expr, current), - LuaExpr::LiteralExpr(_) => Some(()), // Literal expressions do not need binding - LuaExpr::ClosureExpr(closure_expr) => bind_closure_expr(binder, closure_expr, current), - LuaExpr::ParenExpr(paren_expr) => bind_paren_expr(binder, paren_expr, current), - LuaExpr::IndexExpr(index_expr) => bind_index_expr(binder, index_expr, current), - LuaExpr::BinaryExpr(binary_expr) => bind_binary_expr(binder, binary_expr, current), - LuaExpr::UnaryExpr(unary_expr) => bind_unary_expr(binder, unary_expr, current), - LuaExpr::TernaryExpr(ternary_expr) => bind_ternary_expr(binder, ternary_expr, current), - }; - - current -} - -pub fn bind_name_expr( - binder: &mut FlowBinder, - name_expr: LuaNameExpr, - current: FlowId, -) -> Option<()> { - binder.bind_syntax_node(name_expr.get_syntax_id(), current); - Some(()) -} - -pub fn bind_table_expr( - binder: &mut FlowBinder, - table_expr: LuaTableExpr, - current: FlowId, -) -> Option<()> { - bind_each_child(binder, LuaAst::LuaTableExpr(table_expr), current); - Some(()) -} - -pub fn bind_closure_expr( - binder: &mut FlowBinder, - closure_expr: LuaClosureExpr, - current: FlowId, -) -> Option<()> { - bind_each_child(binder, LuaAst::LuaClosureExpr(closure_expr), current); - Some(()) -} - -pub fn bind_index_expr( - binder: &mut FlowBinder, - index_expr: LuaIndexExpr, - current: FlowId, -) -> Option<()> { - binder.bind_syntax_node(index_expr.get_syntax_id(), current); - if index_expr.is_safe_index() { - return bind_safe_index_expr(binder, index_expr, current); - } - bind_each_child(binder, LuaAst::LuaIndexExpr(index_expr.clone()), current); - Some(()) -} - -fn bind_safe_index_expr( - binder: &mut FlowBinder, - index_expr: LuaIndexExpr, - current: FlowId, -) -> Option<()> { - let prefix_expr = index_expr.get_prefix_expr()?; - - let pre_access = binder.create_branch_label(); - bind_condition_expr( - binder, - prefix_expr, - current, - pre_access, - binder.false_target, - ); - let current = finish_flow_label(binder, pre_access, current); - - bind_each_child(binder, LuaAst::LuaIndexExpr(index_expr), current); - Some(()) -} - -pub fn bind_paren_expr( - binder: &mut FlowBinder, - paren_expr: emmylua_parser::LuaParenExpr, - current: FlowId, -) -> Option<()> { - let inner_expr = paren_expr.get_expr()?; - - bind_expr(binder, inner_expr, current); - Some(()) -} - -pub fn bind_unary_expr( - binder: &mut FlowBinder, - unary_expr: LuaUnaryExpr, - current: FlowId, -) -> Option<()> { - let inner_expr = unary_expr.get_expr()?; - - if unary_expr - .get_op_token() - .is_some_and(|op| op.get_op() == UnaryOperator::OpNot) - { - let old_true_target = binder.true_target; - let old_false_target = binder.false_target; - - // not 会反转条件出口, 内层 and/or 的短路分支也要落到反转后的路径. - binder.true_target = old_false_target; - binder.false_target = old_true_target; - bind_expr(binder, inner_expr, current); - binder.true_target = old_true_target; - binder.false_target = old_false_target; - - return Some(()); - } - - bind_expr(binder, inner_expr, current); - Some(()) -} - -pub fn bind_call_expr( - binder: &mut FlowBinder, - call_expr: LuaCallExpr, - current: FlowId, -) -> Option<()> { - bind_each_child(binder, LuaAst::LuaCallExpr(call_expr.clone()), current); - Some(()) -} - -fn bind_ternary_expr( - binder: &mut FlowBinder, - ternary_expr: LuaTernaryExpr, - current: FlowId, -) -> Option<()> { - let condition = ternary_expr.get_condition_expr()?; - let (true_expr, false_expr) = ternary_expr.get_true_false_exprs()?; - - let true_branch_label = binder.create_branch_label(); - let false_branch_label = binder.create_branch_label(); - - bind_condition_expr( - binder, - condition, - current, - true_branch_label, - false_branch_label, - ); - - let true_branch_start = finish_flow_label(binder, true_branch_label, binder.unreachable); - bind_condition_expr( - binder, - true_expr, - true_branch_start, - binder.true_target, - binder.false_target, - ); - - let false_branch_start = finish_flow_label(binder, false_branch_label, binder.unreachable); - bind_condition_expr( - binder, - false_expr, - false_branch_start, - binder.true_target, - binder.false_target, - ); - - Some(()) + super::engine::run_bind_expr(binder, expr, current) } diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/flow/bind_analyze/mod.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/flow/bind_analyze/mod.rs index 1ff6ee324..a249ca162 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/flow/bind_analyze/mod.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/flow/bind_analyze/mod.rs @@ -1,124 +1,22 @@ mod check_goto; mod comment; +mod engine; mod exprs; mod stats; -use emmylua_parser::{LuaAst, LuaAstNode, LuaBlock, LuaChunk, LuaExpr}; +use emmylua_parser::LuaChunk; -use crate::{ - FlowAntecedent, FlowId, FlowNodeKind, - compilation::analyzer::flow::{ - bind_analyze::{ - comment::bind_comment, - exprs::bind_expr, - stats::{ - bind_assign_stat, bind_break_stat, bind_call_expr_stat, bind_continue_stat, - bind_do_stat, bind_for_range_stat, bind_for_stat, bind_func_stat, bind_goto_stat, - bind_if_stat, bind_label_stat, bind_local_func_stat, bind_local_stat, - bind_repeat_stat, bind_return_stat, bind_while_stat, - }, - }, - binder::FlowBinder, - }, -}; +use crate::{FlowAntecedent, FlowId, compilation::analyzer::flow::binder::FlowBinder}; pub use check_goto::check_goto_label; pub fn bind_analyze(binder: &mut FlowBinder, chunk: LuaChunk) -> Option<()> { let block = chunk.get_block()?; let start = binder.start; - bind_block(binder, block, start); + engine::run_bind_block(binder, block, start); Some(()) } -fn bind_block(binder: &mut FlowBinder, block: LuaBlock, current: FlowId) -> FlowId { - let mut return_flow_id = current; - let mut can_change_flow = true; - for node in block.children::() { - let node_flow_id = bind_node(binder, node, return_flow_id); - if can_change_flow { - return_flow_id = node_flow_id; - } - - if let Some(flow_node) = binder.get_flow(return_flow_id) { - match &flow_node.kind { - FlowNodeKind::Return | FlowNodeKind::Break | FlowNodeKind::Continue => { - return_flow_id = binder.unreachable; - can_change_flow = false; - } - _ => {} - } - } - } - - return_flow_id -} - -fn bind_each_child(binder: &mut FlowBinder, ast_node: LuaAst, mut current: FlowId) -> FlowId { - for node in ast_node.children::() { - current = bind_node(binder, node, current); - } - - current -} - -fn bind_node(binder: &mut FlowBinder, node: LuaAst, current: FlowId) -> FlowId { - match node { - LuaAst::LuaBlock(block) => bind_block(binder, block, current), - // stat - LuaAst::LuaAssignStat(assign_stat) => bind_assign_stat(binder, assign_stat, current), - LuaAst::LuaLocalStat(local_stat) => bind_local_stat(binder, local_stat, current), - LuaAst::LuaCallExprStat(call_expr_stat) => { - bind_call_expr_stat(binder, call_expr_stat, current) - } - LuaAst::LuaLabelStat(label_stat) => bind_label_stat(binder, label_stat, current), - LuaAst::LuaBreakStat(break_stat) => bind_break_stat(binder, break_stat, current), - LuaAst::LuaContinueStat(continue_stat) => { - bind_continue_stat(binder, continue_stat, current) - } - LuaAst::LuaGotoStat(goto_stat) => bind_goto_stat(binder, goto_stat, current), - LuaAst::LuaReturnStat(return_stat) => bind_return_stat(binder, return_stat, current), - LuaAst::LuaDoStat(do_stat) => bind_do_stat(binder, do_stat, current), - LuaAst::LuaWhileStat(while_stat) => bind_while_stat(binder, while_stat, current), - LuaAst::LuaRepeatStat(repeat_stat) => bind_repeat_stat(binder, repeat_stat, current), - LuaAst::LuaIfStat(if_stat) => bind_if_stat(binder, if_stat, current), - LuaAst::LuaForStat(for_stat) => bind_for_stat(binder, for_stat, current), - LuaAst::LuaForRangeStat(for_range_stat) => { - bind_for_range_stat(binder, for_range_stat, current) - } - LuaAst::LuaFuncStat(func_stat) => bind_func_stat(binder, func_stat, current), - LuaAst::LuaLocalFuncStat(local_func_stat) => { - bind_local_func_stat(binder, local_func_stat, current) - } - // LuaAst::LuaElseIfClauseStat(else_if_clause_stat) => todo!(), - // LuaAst::LuaElseClauseStat(else_clause_stat) => todo!(), - - // exprs - LuaAst::LuaNameExpr(_) - | LuaAst::LuaIndexExpr(_) - | LuaAst::LuaTableExpr(_) - | LuaAst::LuaBinaryExpr(_) - | LuaAst::LuaUnaryExpr(_) - | LuaAst::LuaParenExpr(_) - | LuaAst::LuaCallExpr(_) - | LuaAst::LuaLiteralExpr(_) - | LuaAst::LuaClosureExpr(_) => bind_expr( - binder, - LuaExpr::cast(node.syntax().clone()).expect("cast always succeedss"), - current, - ), - - LuaAst::LuaComment(comment) => bind_comment(binder, comment, current), - LuaAst::LuaTableField(_) - | LuaAst::LuaParamList(_) - | LuaAst::LuaParamName(_) - | LuaAst::LuaCallArgList(_) - | LuaAst::LuaLocalName(_) => bind_each_child(binder, node, current), - - _ => current, - } -} - -fn finish_flow_label(binder: &mut FlowBinder, label: FlowId, default: FlowId) -> FlowId { +pub(super) fn finish_flow_label(binder: &mut FlowBinder, label: FlowId, default: FlowId) -> FlowId { if let Some(flow_node) = binder.get_flow(label) { if let Some(antecedent) = &flow_node.antecedent { if let FlowAntecedent::Single(existing_id) = antecedent { diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/flow/bind_analyze/stats.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/flow/bind_analyze/stats.rs index fb042c91d..c5e6274a7 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/flow/bind_analyze/stats.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/flow/bind_analyze/stats.rs @@ -1,59 +1,15 @@ use emmylua_parser::{ - BinaryOperator, LuaAssignStat, LuaAst, LuaAstNode, LuaAstToken, LuaBlock, LuaBreakStat, - LuaCallArgList, LuaCallExprStat, LuaContinueStat, LuaDoStat, LuaExpr, LuaForRangeStat, - LuaForStat, LuaFuncStat, LuaGotoStat, LuaIfStat, LuaLabelStat, LuaLiteralToken, LuaLocalName, - LuaLocalStat, LuaRepeatStat, LuaReturnStat, LuaVarExpr, LuaWhileStat, NumberResult, + BinaryOperator, LuaAstNode, LuaExpr, LuaLiteralToken, LuaLocalName, LuaVarExpr, NumberResult, UnaryOperator, }; +use rowan::TextSize; use crate::{ - AnalyzeError, DeclMultiReturnRef, DeclMultiReturnRefAt, DiagnosticCode, FlowId, FlowNodeKind, - LuaClosureId, LuaDeclId, - compilation::analyzer::flow::{ - bind_analyze::{ - bind_block, bind_each_child, bind_node, - exprs::{bind_condition_expr, bind_expr}, - finish_flow_label, - }, - binder::FlowBinder, - }, + DeclMultiReturnRef, DeclMultiReturnRefAt, FlowId, LuaDeclId, + compilation::analyzer::flow::binder::FlowBinder, }; -pub fn bind_local_stat( - binder: &mut FlowBinder, - local_stat: LuaLocalStat, - current: FlowId, -) -> FlowId { - let local_names = local_stat.get_local_name_list().collect::>(); - let values = local_stat.get_value_exprs().collect::>(); - let min_len = local_names.len().min(values.len()); - for i in 0..min_len { - let name = &local_names[i]; - let value = &values[i]; - let decl_id = LuaDeclId::new(binder.file_id, name.get_position()); - if check_local_immutable(binder, decl_id) && check_value_expr_is_check_expr(value.clone()) { - binder.decl_bind_expr_ref.insert(decl_id, value.to_ptr()); - } - } - - for value in &values { - // If there are more values than names, we still need to bind the values - bind_expr(binder, value.clone(), current); - } - - let local_flow_id = binder.create_decl(local_stat.get_position()); - binder.add_antecedent(local_flow_id, current); - bind_multi_return_refs( - binder, - &get_local_decl_ids(binder, &local_names), - &values, - local_stat.get_position(), - local_flow_id, - ); - local_flow_id -} - -fn check_local_immutable(binder: &mut FlowBinder, decl_id: LuaDeclId) -> bool { +pub(super) fn check_local_immutable(binder: &mut FlowBinder, decl_id: LuaDeclId) -> bool { let Some(decl_ref) = binder .db .get_reference_index() @@ -65,7 +21,7 @@ fn check_local_immutable(binder: &mut FlowBinder, decl_id: LuaDeclId) -> bool { !decl_ref.mutable } -fn check_value_expr_is_check_expr(value_expr: LuaExpr) -> bool { +pub(super) fn check_value_expr_is_check_expr(value_expr: LuaExpr) -> bool { match value_expr { LuaExpr::BinaryExpr(binary_expr) => { let Some(op) = binary_expr.get_op_token() else { @@ -79,7 +35,7 @@ fn check_value_expr_is_check_expr(value_expr: LuaExpr) -> bool { } } -fn get_local_decl_ids( +pub(super) fn get_local_decl_ids( binder: &FlowBinder<'_>, local_names: &[LuaLocalName], ) -> Vec> { @@ -89,7 +45,10 @@ fn get_local_decl_ids( .collect() } -fn get_var_decl_ids(binder: &FlowBinder<'_>, vars: &[LuaVarExpr]) -> Vec> { +pub(super) fn get_var_decl_ids( + binder: &FlowBinder<'_>, + vars: &[LuaVarExpr], +) -> Vec> { vars.iter() .map(|var| { binder @@ -100,44 +59,11 @@ fn get_var_decl_ids(binder: &FlowBinder<'_>, vars: &[LuaVarExpr]) -> Vec FlowId { - let (vars, values) = assign_stat.get_var_and_expr_list(); - // First bind the right-hand side expressions - for expr in &values { - if let Some(ast) = LuaAst::cast(expr.syntax().clone()) { - bind_node(binder, ast, current); - } - } - - for var in &vars { - if let Some(ast) = LuaAst::cast(var.syntax().clone()) { - bind_node(binder, ast, current); - } - } - - let assignment_kind = FlowNodeKind::Assignment(assign_stat.to_ptr()); - let flow_id = binder.create_node(assignment_kind); - binder.add_antecedent(flow_id, current); - bind_multi_return_refs( - binder, - &get_var_decl_ids(binder, &vars), - &values, - assign_stat.get_position(), - flow_id, - ); - - flow_id -} - -fn bind_multi_return_refs( +pub(super) fn bind_multi_return_refs( binder: &mut FlowBinder, decl_ids: &[Option], values: &[LuaExpr], - position: rowan::TextSize, + position: TextSize, flow_id: FlowId, ) { let tail_call = values.last().and_then(|value| match value { @@ -173,445 +99,7 @@ fn bind_multi_return_refs( } } -pub fn bind_call_expr_stat( - binder: &mut FlowBinder, - call_expr_stat: LuaCallExprStat, - current: FlowId, -) -> FlowId { - let call_expr = match call_expr_stat.get_call_expr() { - Some(expr) => expr, - None => return current, // If there's no call expression, just return the current flow - }; - - if call_expr.is_assert() { - let Some(arg_list) = call_expr.get_args_list() else { - return current; // If there's no argument list, just return the current flow - }; - - bind_assert_stat(binder, arg_list, current) - } else if call_expr.is_error() { - if let Some(ast) = LuaAst::cast(call_expr.syntax().clone()) { - bind_each_child(binder, ast, current); - } - let return_flow_id = binder.create_return(); - binder.add_antecedent(return_flow_id, current); - return_flow_id - } else { - if let Some(ast) = LuaAst::cast(call_expr.syntax().clone()) { - bind_each_child(binder, ast, current); - } - let flow_id = binder.create_node(FlowNodeKind::CallExprStat(call_expr_stat.to_ptr())); - binder.add_antecedent(flow_id, current); - flow_id - } -} - -fn bind_assert_stat(binder: &mut FlowBinder, arg_list: LuaCallArgList, current: FlowId) -> FlowId { - let false_target = binder.unreachable; - - let mut pre_arg = current; - for arg in arg_list.get_args() { - let pre_next_arg = binder.create_branch_label(); - bind_condition_expr(binder, arg, pre_arg, pre_next_arg, false_target); - pre_arg = finish_flow_label(binder, pre_next_arg, pre_arg); - } - - pre_arg -} - -pub fn bind_label_stat( - binder: &mut FlowBinder, - label_stat: LuaLabelStat, - current: FlowId, -) -> FlowId { - let Some(label_name_token) = label_stat.get_label_name_token() else { - return current; // If there's no label token, just return the current flow - }; - let label_name = label_name_token.get_name_text(); - let closure_id = LuaClosureId::from_node(label_stat.syntax()); - binder.db.get_reference_index_mut().add_label_declaration( - binder.file_id, - closure_id, - label_name, - label_name_token.get_range(), - ); - let name_label = binder.create_name_label(label_name, closure_id); - binder.add_antecedent(name_label, current); - - name_label -} - -pub fn bind_break_stat( - binder: &mut FlowBinder, - break_stat: LuaBreakStat, - current: FlowId, -) -> FlowId { - let break_flow_id = binder.create_break(); - if let Some(loop_flow) = binder.get_flow(binder.loop_label) - && loop_flow.kind.is_unreachable() - { - // report a error if we are trying to break outside a loop - binder.report_error(AnalyzeError::new( - DiagnosticCode::SyntaxError, - &t!("Break outside loop"), - break_stat.get_range(), - )); - return current; - } - - binder.add_antecedent(break_flow_id, current); - binder.add_antecedent(binder.break_target_label, break_flow_id); - break_flow_id -} - -pub fn bind_continue_stat( - binder: &mut FlowBinder, - continue_stat: LuaContinueStat, - current: FlowId, -) -> FlowId { - let continue_flow_id = binder.create_continue(); - if let Some(loop_flow) = binder.get_flow(binder.loop_label) - && loop_flow.kind.is_unreachable() - { - // report a error if we are trying to continue outside a loop - binder.report_error(AnalyzeError::new( - DiagnosticCode::SyntaxError, - &t!("Continue outside loop"), - continue_stat.get_range(), - )); - return current; - } - - binder.add_antecedent(continue_flow_id, current); - binder.add_antecedent(binder.loop_label, continue_flow_id); - continue_flow_id -} - -pub fn bind_goto_stat(binder: &mut FlowBinder, goto_stat: LuaGotoStat, current: FlowId) -> FlowId { - // Goto statements are handled separately in the flow analysis - // They will be processed when we analyze the labels - // For now, we just return None to indicate no flow node is created - let closure_id = LuaClosureId::from_node(goto_stat.syntax()); - let Some(label_token) = goto_stat.get_label_name_token() else { - return current; // If there's no label token, just return the current flow - }; - - let label_name = label_token.get_name_text(); - binder.db.get_reference_index_mut().add_label_reference( - binder.file_id, - closure_id, - label_name, - label_token.get_range(), - ); - let return_flow_id = binder.create_return(); - binder.cache_goto_flow(closure_id, label_token.clone(), label_name, return_flow_id); - binder.add_antecedent(return_flow_id, current); - return_flow_id -} - -pub fn bind_return_stat( - binder: &mut FlowBinder, - return_stat: LuaReturnStat, - current: FlowId, -) -> FlowId { - // If there are expressions in the return statement, bind them - for expr in return_stat.get_expr_list() { - bind_expr(binder, expr.clone(), current); - } - - // Return statements are typically used to exit a function - // We can treat them as a flow node that indicates the end of the current flow - let return_flow_id = binder.create_return(); - binder.add_antecedent(return_flow_id, current); - - return_flow_id -} - -pub fn bind_do_stat(binder: &mut FlowBinder, do_stat: LuaDoStat, mut current: FlowId) -> FlowId { - // Do statements are typically used for blocks of code - // We can treat them as a block and bind their contents - if let Some(do_block) = do_stat.get_block() { - current = bind_block(binder, do_block, current); - } - - current -} - -fn bind_iter_block( - binder: &mut FlowBinder, - iter_block: LuaBlock, - current: FlowId, - loop_label: FlowId, - break_target_label: FlowId, -) -> FlowId { - let old_loop_label = binder.loop_label; - let old_loop_post_label = binder.break_target_label; - - binder.loop_label = loop_label; - binder.break_target_label = break_target_label; - // Bind the block of code inside the iterator - let flow_id = bind_block(binder, iter_block, current); - - // Restore the previous loop labels - binder.loop_label = old_loop_label; - binder.break_target_label = old_loop_post_label; - - flow_id -} - -pub fn bind_while_stat( - binder: &mut FlowBinder, - while_stat: LuaWhileStat, - current: FlowId, -) -> FlowId { - let pre_while_label = binder.create_loop_label(); - let after_while_label = binder.create_branch_label(); - let pre_block_label = binder.create_branch_label(); - binder.add_antecedent(pre_while_label, current); - let Some(condition_expr) = while_stat.get_condition_expr() else { - return current; - }; - - let loop_enters = match static_literal_truthiness(&condition_expr) { - Some(true) => true, - Some(false) => return current, - None => { - bind_condition_expr( - binder, - condition_expr.clone(), - current, - pre_block_label, - after_while_label, - ); - false - } - }; - let block_current = if loop_enters { - current - } else { - finish_flow_label(binder, pre_block_label, current) - }; - - if let Some(iter_block) = while_stat.get_block() { - // Bind the block of code inside the while loop - let block_flow = bind_iter_block( - binder, - iter_block, - block_current, - pre_while_label, - after_while_label, - ); - if loop_enters { - return finish_entered_loop_post_flow(binder, after_while_label, block_flow); - } - } - - current -} - -pub fn bind_repeat_stat( - binder: &mut FlowBinder, - repeat_stat: LuaRepeatStat, - current: FlowId, -) -> FlowId { - let pre_repeat_label = binder.create_loop_label(); - let post_repeat_label = binder.create_branch_label(); - binder.add_antecedent(pre_repeat_label, current); - - let block_entry = finish_flow_label(binder, pre_repeat_label, current); - let mut block_flow_id = block_entry; - // Bind the block of code inside the repeat statement - if let Some(iter_block) = repeat_stat.get_block() { - block_flow_id = bind_iter_block( - binder, - iter_block, - block_entry, - pre_repeat_label, - post_repeat_label, - ); - } - - // Bind the condition expression as a condition node - if let Some(condition_expr) = repeat_stat.get_condition_expr() { - bind_condition_expr( - binder, - condition_expr, - block_flow_id, - post_repeat_label, - pre_repeat_label, - ); - } - - finish_flow_label(binder, post_repeat_label, block_flow_id) -} - -pub fn bind_if_stat(binder: &mut FlowBinder, if_stat: LuaIfStat, current: FlowId) -> FlowId { - let post_if_label = binder.create_branch_label(); - let mut else_label = binder.create_branch_label(); - let then_label = binder.create_branch_label(); - if let Some(condition_expr) = if_stat.get_condition_expr() { - bind_condition_expr(binder, condition_expr, current, then_label, else_label); - } - - if let Some(then_block) = if_stat.get_block() { - let then_label = finish_flow_label(binder, then_label, current); - let block_id = bind_block(binder, then_block, then_label); - binder.add_antecedent(post_if_label, block_id); - } else { - let then_label = finish_flow_label(binder, then_label, current); - // If there's no then block, we still need to add the antecedent - binder.add_antecedent(post_if_label, then_label); - } - - for elseif_clause in if_stat.get_else_if_clause_list() { - let pre_elseif_label = finish_flow_label(binder, else_label, current); - let elseif_then_label = binder.create_branch_label(); - let post_elseif_label = binder.create_branch_label(); - if let Some(condition_expr) = elseif_clause.get_condition_expr() { - bind_condition_expr( - binder, - condition_expr, - pre_elseif_label, - elseif_then_label, - post_elseif_label, - ); - } - // 后续 elseif/else 必须从当前 elseif 的 false 分支进入. - // 这里保留 label, 让下一段条件回溯时还能看到当前条件为 false 的事实. - else_label = post_elseif_label; - if let Some(elseif_block) = elseif_clause.get_block() { - let current = finish_flow_label(binder, elseif_then_label, current); - let block_id = bind_block(binder, elseif_block, current); - binder.add_antecedent(post_if_label, block_id); - } else { - let current = finish_flow_label(binder, elseif_then_label, current); - binder.add_antecedent(post_if_label, current); - } - } - - if let Some(else_clause) = if_stat.get_else_clause() { - let else_block = else_clause.get_block(); - if let Some(else_block) = else_block { - let block_id = bind_block(binder, else_block, else_label); - binder.add_antecedent(post_if_label, block_id); - } - } else { - binder.add_antecedent(post_if_label, else_label); - } - - if let Some(flow_node) = binder.get_flow(post_if_label) - && flow_node.antecedent.is_none() - { - return binder.unreachable; - } - - finish_flow_label(binder, post_if_label, else_label) -} - -pub fn bind_func_stat(binder: &mut FlowBinder, func_stat: LuaFuncStat, current: FlowId) -> FlowId { - let Some(func_name) = func_stat.get_func_name() else { - return current; // If there's no function name, just return the current flow - }; - - bind_each_child(binder, LuaAst::LuaFuncStat(func_stat.clone()), current); - let LuaVarExpr::NameExpr(_) = func_name else { - return current; // If the function name is not a simple name, just return the current flow - }; - - let func_kind = FlowNodeKind::ImplFunc(func_stat.to_ptr()); - let flow_id = binder.create_node(func_kind); - binder.add_antecedent(flow_id, current); - - flow_id -} - -pub fn bind_local_func_stat( - binder: &mut FlowBinder, - local_func_stat: emmylua_parser::LuaLocalFuncStat, - current: FlowId, -) -> FlowId { - bind_each_child(binder, LuaAst::LuaLocalFuncStat(local_func_stat), current); - current -} - -pub fn bind_for_range_stat( - binder: &mut FlowBinder, - for_range_stat: LuaForRangeStat, - current: FlowId, -) -> FlowId { - let pre_for_range_label = binder.create_loop_label(); - let post_for_range_label = binder.create_branch_label(); - binder.add_antecedent(pre_for_range_label, current); - - for expr in for_range_stat.get_expr_list() { - bind_expr(binder, expr.clone(), current); - } - - let decl_flow = binder.create_decl(for_range_stat.get_position()); - binder.add_antecedent(decl_flow, pre_for_range_label); - - let block_entry = finish_flow_label(binder, pre_for_range_label, current); - if let Some(iter_block) = for_range_stat.get_block() { - // Bind the block of code inside the for loop - bind_iter_block( - binder, - iter_block, - block_entry, - pre_for_range_label, - post_for_range_label, - ); - } - - current -} - -pub fn bind_for_stat(binder: &mut FlowBinder, for_stat: LuaForStat, current: FlowId) -> FlowId { - let pre_for_label = binder.create_loop_label(); - let post_for_label = binder.create_branch_label(); - binder.add_antecedent(pre_for_label, current); - - let iter_exprs = for_stat.get_iter_expr().collect::>(); - let loop_enters = match iter_exprs.as_slice() { - [start_expr, stop_expr] => match ( - static_number_value(start_expr), - static_number_value(stop_expr), - ) { - (Some(start), Some(stop)) => start <= stop, - _ => false, - }, - [start_expr, stop_expr, step_expr, ..] => match ( - static_number_value(start_expr), - static_number_value(stop_expr), - static_number_value(step_expr), - ) { - (Some(start), Some(stop), Some(step)) => { - (step > 0.0 && start <= stop) || (step < 0.0 && start >= stop) - } - _ => false, - }, - _ => false, - }; - - for var_expr in &iter_exprs { - bind_expr(binder, var_expr.clone(), current); - } - - let for_node = binder.create_node(FlowNodeKind::ForIStat(for_stat.to_ptr())); - binder.add_antecedent(for_node, pre_for_label); - - if let Some(iter_block) = for_stat.get_block() { - // Bind the block of code inside the for loop - let block_flow = - bind_iter_block(binder, iter_block, for_node, pre_for_label, post_for_label); - if loop_enters { - return finish_entered_loop_post_flow(binder, post_for_label, block_flow); - } - } - - current -} - -fn finish_entered_loop_post_flow( +pub(super) fn finish_entered_loop_post_flow( binder: &mut FlowBinder, after_loop_label: FlowId, block_flow: FlowId, @@ -632,7 +120,7 @@ fn finish_entered_loop_post_flow( /// /// 它不是完整的常量求值或路径推断, 动态表达式和复杂常量表达式会返回 unknown, /// 后续按不能确认进入循环处理. -fn static_literal_truthiness(expr: &LuaExpr) -> Option { +pub(super) fn static_literal_truthiness(expr: &LuaExpr) -> Option { match expr { LuaExpr::LiteralExpr(literal_expr) => match literal_expr.get_literal()? { LuaLiteralToken::Bool(bool_token) => Some(bool_token.is_true()), @@ -652,7 +140,7 @@ fn static_literal_truthiness(expr: &LuaExpr) -> Option { } } -fn static_number_value(expr: &LuaExpr) -> Option { +pub(super) fn static_number_value(expr: &LuaExpr) -> Option { match expr { LuaExpr::LiteralExpr(literal_expr) => match literal_expr.get_literal()? { LuaLiteralToken::Number(number_token) => match number_token.get_number_value() { diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/lua/func_body.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/lua/func_body.rs index 668d6cac5..dcda77d9a 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/lua/func_body.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/lua/func_body.rs @@ -384,7 +384,9 @@ where let condition_type = match infer_expr_type(&condition) { Ok(condition_type) => condition_type, - Err(InferFailReason::None | InferFailReason::RecursiveInfer) => { + Err( + InferFailReason::None | InferFailReason::RecursiveInfer | InferFailReason::DepthLimit, + ) => { return Ok(ConditionState::Dynamic); } Err(reason) => return Err(reason), diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/check_reason.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/check_reason.rs index f05c7c02a..36a02ca9d 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/check_reason.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/check_reason.rs @@ -19,7 +19,8 @@ pub fn check_reach_reason( InferFailReason::None | InferFailReason::FieldNotFound | InferFailReason::UnResolveOperatorCall - | InferFailReason::RecursiveInfer => Some(true), + | InferFailReason::RecursiveInfer + | InferFailReason::DepthLimit => Some(true), InferFailReason::UnResolveDeclType(decl_id) => { let decl = db.get_decl_index().get_decl(decl_id)?; let typ = db.get_type_index().get_type_cache(&(*decl_id).into()); @@ -70,7 +71,8 @@ pub fn resolve_as_any(db: &mut DbIndex, reason: &InferFailReason, loop_count: us | InferFailReason::FieldNotFound | InferFailReason::UnResolveTypeDecl(_) | InferFailReason::UnResolveOperatorCall - | InferFailReason::RecursiveInfer => { + | InferFailReason::RecursiveInfer + | InferFailReason::DepthLimit => { return Some(()); } InferFailReason::UnResolveDeclType(decl_id) => { diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/mod.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/mod.rs index 8ca86583f..39520ee55 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/mod.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/mod.rs @@ -217,7 +217,11 @@ fn try_resolve( Ok(_) => { changed = true; } - Err(InferFailReason::None | InferFailReason::RecursiveInfer) => {} + Err( + InferFailReason::None + | InferFailReason::RecursiveInfer + | InferFailReason::DepthLimit, + ) => {} Err(InferFailReason::FieldNotFound) => { if !cache.get_config().analysis_phase.is_force() { retain_unresolve.push((unresolve, InferFailReason::FieldNotFound)); diff --git a/crates/emmylua_code_analysis/src/semantic/cache/mod.rs b/crates/emmylua_code_analysis/src/semantic/cache/mod.rs index ec274413a..5745ffec3 100644 --- a/crates/emmylua_code_analysis/src/semantic/cache/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic/cache/mod.rs @@ -79,6 +79,8 @@ pub struct LuaInferCache { file_id: FileId, config: CacheOptions, no_flow_mode: bool, + /// Native recursion nesting depth counter; returns `InferFailReason::DepthLimit` when the limit is exceeded + pub(in crate::semantic) infer_depth: u32, pub expr_cache: HashMap>, pub(in crate::semantic) expr_no_flow_cache: HashMap>>, pub call_cache: @@ -102,6 +104,7 @@ impl LuaInferCache { file_id, config, no_flow_mode: false, + infer_depth: 0, expr_cache: HashMap::new(), expr_no_flow_cache: HashMap::new(), call_cache: HashMap::new(), diff --git a/crates/emmylua_code_analysis/src/semantic/infer/engine.rs b/crates/emmylua_code_analysis/src/semantic/infer/engine.rs new file mode 100644 index 000000000..16e72fea4 --- /dev/null +++ b/crates/emmylua_code_analysis/src/semantic/infer/engine.rs @@ -0,0 +1,609 @@ +//! Inference engine: replaces native recursion with an explicit task stack +//! +//! Recursion risk mainly comes from "linear structural chains": paren nesting, member chains, call chains, binary chains, +//! ternary chains, etc. The depth is fully determined by user code and can reach tens of thousands of levels. This engine converts expression inference +//! into a dispatch loop plus an explicit continuation stack, keeping the native call stack depth constant. +//! +//! The parts that still use native recursion (member lookup, call resolution internals, generic instantiation, etc.) are bounded +//! by the `LuaInferCache::infer_depth` budget; when exceeded, they return +//! `InferFailReason::DepthLimit` to degrade gracefully instead of crashing with a stack overflow. + +use emmylua_parser::{ + LuaAstNode, LuaBinaryExpr, LuaCallExpr, LuaExpr, LuaIndexExpr, LuaIndexKey, LuaIndexMemberExpr, + LuaSyntaxId, LuaTernaryExpr, LuaUnaryExpr, +}; + +use crate::{ + CacheEntry, DbIndex, InferGuard, LuaInferCache, TypeOps, + db_index::LuaType, + semantic::infer::{ + infer_binary::infer_binary_expr_result, + infer_call::{ + check_can_infer, infer_call_expr_func, infer_require_call, infer_setmetatable_call, + }, + infer_index::{infer_index_expr_with_member, infer_member, infer_member_by_key_type}, + infer_unary::infer_unary_expr_result, + narrow::get_type_at_call_expr_inline_cast, + }, +}; + +use super::{ + InferFailReason, InferResult, infer_closure_expr, infer_literal_expr, infer_name_expr, + infer_table_expr, prepare_expr_cache, +}; + +/// Maximum depth of the explicit stack, preventing pathological inputs from consuming too much memory +pub(super) const MAX_ENGINE_STACK_DEPTH: usize = 65536; +/// Maximum native recursion nesting depth (all inference entry points share the same budget) +pub(super) const MAX_INFER_DEPTH: u32 = 1024; + +/// Engine scheduler step +enum Step { + /// Dispatch a subtask; push the continuation onto the stack first if present + Task(Task, Option), + /// Task completed, the result propagates upward (pop the stack to resume the parent task, or return it as the final result) + Complete(InferResult), +} + +/// Task: currently only "infer one expression"; the rest of the logic runs natively in resume +enum Task { + Expr(LuaExpr), +} + +/// Suspended parent task state +enum Continuation { + /// Paren expression: write back to the parent cache after the inner expression completes + ExprFinalize { syntax_id: LuaSyntaxId }, + /// Call expression: continue once the prefix type is known + CallPrefix { call_expr: LuaCallExpr }, + /// Index expression: continue once the prefix type is known + MemberPrefix { + index_expr: LuaIndexExpr, + pass_flow: bool, + }, + /// Index expression: continue once the key expression type is known + IndexKey { + index_expr: LuaIndexExpr, + prefix_type: LuaType, + pass_flow: bool, + }, + /// Binary operation: left operand completed + BinaryLeft { binary_expr: LuaBinaryExpr }, + /// Binary operation: right operand completed + BinaryRight { + binary_expr: LuaBinaryExpr, + left_type: LuaType, + }, + /// Unary operation: operand completed + UnaryInner { unary_expr: LuaUnaryExpr }, + /// Ternary operation: true branch completed + TernaryTrue { ternary_expr: LuaTernaryExpr }, + /// Ternary operation: false branch completed + TernaryFalse { + ternary_expr: LuaTernaryExpr, + true_type: LuaType, + }, +} + +/// Inference engine +pub(super) struct InferEngine<'a> { + db: &'a DbIndex, + cache: &'a mut LuaInferCache, + stack: Vec, +} + +impl<'a> InferEngine<'a> { + pub(super) fn new(db: &'a DbIndex, cache: &'a mut LuaInferCache) -> Self { + Self { + db, + cache, + stack: Vec::new(), + } + } + + /// Run inference until the root task completes + pub(super) fn run(&mut self, expr: LuaExpr) -> InferResult { + let mut step = Step::Task(Task::Expr(expr), None); + loop { + step = match step { + Step::Task(task, continuation) => { + if let Some(continuation) = continuation { + if self.stack.len() >= MAX_ENGINE_STACK_DEPTH { + // Depth limit: degrade all pending tasks with DepthLimit + let err = InferFailReason::DepthLimit; + while let Some(pending) = self.stack.pop() { + let _ = self.resume(pending, Err(err.clone())); + } + return Err(err); + } + self.stack.push(continuation); + } + self.evaluate(task) + } + Step::Complete(result) => match self.stack.pop() { + Some(continuation) => self.resume(continuation, result), + None => return result, + }, + }; + } + } + + fn evaluate(&mut self, task: Task) -> Step { + match task { + Task::Expr(expr) => self.evaluate_expr(expr), + } + } + + fn evaluate_expr(&mut self, expr: LuaExpr) -> Step { + let no_flow = self.cache.is_no_flow(); + let syntax_id = expr.get_syntax_id(); + match prepare_expr_cache(self.db, self.cache, syntax_id) { + Ok(Some(ty)) => return Step::Complete(Ok(ty)), + Ok(None) => {} + Err(err) => return Step::Complete(Err(err)), + } + + if no_flow + && matches!(expr, LuaExpr::TableExpr(_)) + && !self.cache.no_flow_table_exprs.contains(&syntax_id) + { + self.cache + .expr_no_flow_cache + .insert(syntax_id, CacheEntry::Cache(None)); + return Step::Complete(Err(InferFailReason::None)); + } + + match expr { + LuaExpr::CallExpr(call_expr) => self.evaluate_call_expr(syntax_id, call_expr), + LuaExpr::TableExpr(table_expr) => { + let result = infer_table_expr(self.db, self.cache, table_expr); + self.complete_expr(syntax_id, result) + } + LuaExpr::LiteralExpr(literal_expr) => { + let result = infer_literal_expr(self.db, self.cache, literal_expr); + self.complete_expr(syntax_id, result) + } + LuaExpr::BinaryExpr(binary_expr) => { + let Some(op_token) = binary_expr.get_op_token() else { + return self.complete_expr(syntax_id, Err(InferFailReason::None)); + }; + let _ = op_token.get_op(); + let Some((left, _)) = binary_expr.get_exprs() else { + return self.complete_expr(syntax_id, Err(InferFailReason::None)); + }; + Step::Task( + Task::Expr(left), + Some(Continuation::BinaryLeft { binary_expr }), + ) + } + LuaExpr::UnaryExpr(unary_expr) => { + let Some(op_token) = unary_expr.get_op_token() else { + return self.complete_expr(syntax_id, Err(InferFailReason::None)); + }; + let _ = op_token.get_op(); + let Some(inner_expr) = unary_expr.get_expr() else { + return self.complete_expr(syntax_id, Err(InferFailReason::None)); + }; + Step::Task( + Task::Expr(inner_expr), + Some(Continuation::UnaryInner { unary_expr }), + ) + } + LuaExpr::ClosureExpr(closure_expr) => { + let result = infer_closure_expr(self.db, self.cache, closure_expr); + self.complete_expr(syntax_id, result) + } + LuaExpr::ParenExpr(paren_expr) => { + let Some(inner_expr) = paren_expr.get_expr() else { + return self.complete_expr(syntax_id, Err(InferFailReason::None)); + }; + Step::Task( + Task::Expr(inner_expr), + Some(Continuation::ExprFinalize { syntax_id }), + ) + } + LuaExpr::NameExpr(name_expr) => { + let result = infer_name_expr(self.db, self.cache, name_expr); + self.complete_expr(syntax_id, result) + } + LuaExpr::IndexExpr(index_expr) => { + let Some(prefix_expr) = index_expr.get_prefix_expr() else { + return self.complete_expr(syntax_id, Err(InferFailReason::None)); + }; + Step::Task( + Task::Expr(prefix_expr), + Some(Continuation::MemberPrefix { + index_expr, + pass_flow: !no_flow, + }), + ) + } + LuaExpr::TernaryExpr(ternary_expr) => { + let Some((true_expr, _)) = ternary_expr.get_true_false_exprs() else { + return self.complete_expr(syntax_id, Err(InferFailReason::None)); + }; + Step::Task( + Task::Expr(true_expr), + Some(Continuation::TernaryTrue { ternary_expr }), + ) + } + } + } + + fn evaluate_call_expr(&mut self, syntax_id: LuaSyntaxId, call_expr: LuaCallExpr) -> Step { + if call_expr.is_require() { + let result = infer_require_call(self.db, self.cache, call_expr); + return self.complete_expr(syntax_id, result); + } + if call_expr.is_setmetatable() { + let result = infer_setmetatable_call(self.db, self.cache, call_expr); + return self.complete_expr(syntax_id, result); + } + if let Err(err) = check_can_infer(self.db, self.cache, &call_expr) { + return self.complete_expr(syntax_id, Err(err)); + } + let Some(prefix_expr) = call_expr.get_prefix_expr() else { + return self.complete_expr(syntax_id, Err(InferFailReason::None)); + }; + Step::Task( + Task::Expr(prefix_expr), + Some(Continuation::CallPrefix { call_expr }), + ) + } + + fn evaluate_call_expr_with_prefix( + &mut self, + call_expr: LuaCallExpr, + prefix_type: LuaType, + ) -> Step { + let syntax_id = call_expr.get_syntax_id(); + let is_safe_call = call_expr.has_safe_navigation(); + let ret_type = match infer_call_expr_func( + self.db, + self.cache, + call_expr.clone(), + prefix_type.clone(), + &InferGuard::new(), + None, + ) { + Ok(func_ty) => func_ty.get_ret().clone(), + Err(err) => return self.complete_expr(syntax_id, Err(err)), + }; + let ret_type = if is_safe_call && prefix_type.is_nullable() { + TypeOps::Union.apply(self.db, &ret_type, &LuaType::Nil) + } else { + ret_type + }; + let ret_type = if !self.cache.is_no_flow() + && let Some(tree) = self + .db + .get_flow_index() + .get_flow_tree(&self.cache.get_file_id()) + && let Some(flow_id) = tree.get_flow_id(call_expr.get_syntax_id()) + && let Some(flow_ret_type) = get_type_at_call_expr_inline_cast( + self.db, + self.cache, + tree, + call_expr, + flow_id, + ret_type.clone(), + ) { + flow_ret_type + } else { + ret_type + }; + self.complete_expr(syntax_id, Ok(ret_type)) + } + + fn evaluate_index_member( + &mut self, + index_expr: LuaIndexExpr, + prefix_type: LuaType, + pass_flow: bool, + key_type: Option, + ) -> Step { + let syntax_id = index_expr.get_syntax_id(); + let index_member_expr = LuaIndexMemberExpr::IndexExpr(index_expr.clone()); + let member_type = match key_type { + Some(key_type) => infer_member_by_key_type( + self.db, + self.cache, + &prefix_type, + index_member_expr, + &key_type, + &InferGuard::new(), + ), + None => infer_member( + self.db, + self.cache, + &prefix_type, + index_member_expr, + &InferGuard::new(), + ), + }; + let member_type = match member_type { + Ok(ty) => ty, + Err(err) => return self.complete_expr(syntax_id, Err(err)), + }; + let result = infer_index_expr_with_member( + self.db, + self.cache, + index_expr, + prefix_type, + member_type, + pass_flow, + ); + self.complete_expr(syntax_id, result) + } + + /// Write the result back to the cache after the expression completes and propagate it upward + fn complete_expr(&mut self, syntax_id: LuaSyntaxId, result: InferResult) -> Step { + Step::Complete(self.finalize_expr(syntax_id, result)) + } + + /// Write the expression inference result back to the cache, consistent with the error handling semantics of the original recursive implementation + fn finalize_expr(&mut self, syntax_id: LuaSyntaxId, result_type: InferResult) -> InferResult { + let no_flow = self.cache.is_no_flow(); + match &result_type { + Ok(result_type) => { + if no_flow { + self.cache + .expr_no_flow_cache + .insert(syntax_id, CacheEntry::Cache(Some(result_type.clone()))); + } else { + self.cache + .expr_cache + .insert(syntax_id, CacheEntry::Cache(result_type.clone())); + } + } + Err(InferFailReason::None) + | Err(InferFailReason::RecursiveInfer) + | Err(InferFailReason::DepthLimit) => { + if no_flow { + self.cache + .expr_no_flow_cache + .insert(syntax_id, CacheEntry::Cache(None)); + } else { + self.cache + .expr_cache + .insert(syntax_id, CacheEntry::Cache(LuaType::Unknown)); + return Ok(LuaType::Unknown); + } + } + Err(InferFailReason::FieldNotFound) => { + if no_flow { + self.cache.expr_no_flow_cache.remove(&syntax_id); + } else if self.cache.get_config().analysis_phase.is_force() { + self.cache + .expr_cache + .insert(syntax_id, CacheEntry::Cache(LuaType::Nil)); + return Ok(LuaType::Nil); + } else { + self.cache.expr_cache.remove(&syntax_id); + } + } + _ => { + if no_flow { + self.cache.expr_no_flow_cache.remove(&syntax_id); + } else { + self.cache.expr_cache.remove(&syntax_id); + } + } + } + + result_type + } + + /// Resume a suspended parent task + fn resume(&mut self, continuation: Continuation, result: InferResult) -> Step { + match continuation { + Continuation::ExprFinalize { syntax_id } => self.complete_expr(syntax_id, result), + Continuation::CallPrefix { call_expr } => { + let syntax_id = call_expr.get_syntax_id(); + let prefix_type = match result { + Ok(ty) => ty, + Err(err) => return self.complete_expr(syntax_id, Err(err)), + }; + self.evaluate_call_expr_with_prefix(call_expr, prefix_type) + } + Continuation::MemberPrefix { + index_expr, + pass_flow, + } => { + let syntax_id = index_expr.get_syntax_id(); + let no_flow = self.cache.is_no_flow(); + if no_flow + && let Some(prefix_expr) = index_expr.get_prefix_expr() + && is_declined_index_prefix(&prefix_expr) + { + // Consistent with `try_infer_expr_for_index`: closure prefixes are declined for inference in no_flow mode + return self.complete_expr(syntax_id, Err(InferFailReason::None)); + } + let prefix_type = match result { + Ok(ty) => ty, + Err(err) => { + if no_flow { + // In no_flow mode, `try_infer_expr_for_index` maps all failures to None + return self.complete_expr(syntax_id, Err(InferFailReason::None)); + } + return self.complete_expr(syntax_id, Err(err)); + } + }; + match index_expr.get_index_key() { + Some(LuaIndexKey::Expr(key_expr)) => { + if no_flow && is_declined_index_prefix(&key_expr) { + return self.complete_expr(syntax_id, Err(InferFailReason::None)); + } + Step::Task( + Task::Expr(key_expr), + Some(Continuation::IndexKey { + index_expr, + prefix_type, + pass_flow, + }), + ) + } + _ => self.evaluate_index_member(index_expr, prefix_type, pass_flow, None), + } + } + Continuation::IndexKey { + index_expr, + prefix_type, + pass_flow, + } => { + let syntax_id = index_expr.get_syntax_id(); + let key_type = match result { + Ok(ty) => ty, + Err(err) => { + if self.cache.is_no_flow() { + return self.complete_expr(syntax_id, Err(InferFailReason::None)); + } + return self.complete_expr(syntax_id, Err(err)); + } + }; + self.evaluate_index_member(index_expr, prefix_type, pass_flow, Some(key_type)) + } + Continuation::BinaryLeft { binary_expr } => { + let syntax_id = binary_expr.get_syntax_id(); + let left_type = match result { + Ok(ty) => ty, + Err(err) => return self.complete_expr(syntax_id, Err(err)), + }; + let Some((_, right)) = binary_expr.get_exprs() else { + return self.complete_expr(syntax_id, Err(InferFailReason::None)); + }; + Step::Task( + Task::Expr(right), + Some(Continuation::BinaryRight { + binary_expr, + left_type, + }), + ) + } + Continuation::BinaryRight { + binary_expr, + left_type, + } => { + let syntax_id = binary_expr.get_syntax_id(); + let right_type = match result { + Ok(ty) => ty, + Err(err) => return self.complete_expr(syntax_id, Err(err)), + }; + let result = infer_binary_expr_result(self.db, binary_expr, left_type, right_type); + self.complete_expr(syntax_id, result) + } + Continuation::UnaryInner { unary_expr } => { + let syntax_id = unary_expr.get_syntax_id(); + let inner_type = match result { + Ok(ty) => ty, + Err(err) => return self.complete_expr(syntax_id, Err(err)), + }; + let result = match unary_expr.get_op_token() { + Some(op_token) => { + infer_unary_expr_result(self.db, op_token.get_op(), inner_type) + } + None => Err(InferFailReason::None), + }; + self.complete_expr(syntax_id, result) + } + Continuation::TernaryTrue { ternary_expr } => { + let syntax_id = ternary_expr.get_syntax_id(); + let true_type = match result { + Ok(ty) => ty, + Err(err) => return self.complete_expr(syntax_id, Err(err)), + }; + let Some((_, false_expr)) = ternary_expr.get_true_false_exprs() else { + return self.complete_expr(syntax_id, Err(InferFailReason::None)); + }; + Step::Task( + Task::Expr(false_expr), + Some(Continuation::TernaryFalse { + ternary_expr, + true_type, + }), + ) + } + Continuation::TernaryFalse { + ternary_expr, + true_type, + } => { + let syntax_id = ternary_expr.get_syntax_id(); + let false_type = match result { + Ok(ty) => ty, + Err(err) => return self.complete_expr(syntax_id, Err(err)), + }; + let result = TypeOps::Union.apply(self.db, &true_type, &false_type); + self.complete_expr(syntax_id, Ok(result)) + } + } + } +} + +/// Consistent with the no_flow semantics of `try_infer_expr_for_index`: +/// after paren unwrapping, a closure is declined as an index prefix/key +fn is_declined_index_prefix(expr: &LuaExpr) -> bool { + let mut current = expr.clone(); + while let LuaExpr::ParenExpr(paren) = ¤t { + match paren.get_expr() { + Some(inner) => current = inner, + None => break, + } + } + matches!(current, LuaExpr::ClosureExpr(_)) +} + +#[cfg(test)] +mod tests { + use crate::VirtualWorkspace; + + // Deep index chain: the old implementation overflows the native stack at this depth; the explicit task stack no longer recurses + #[test] + fn test_deep_index_chain() { + let mut ws = VirtualWorkspace::new(); + ws.def("---@type { x: integer }\nlocal t"); + + let mut expr = "t".to_string(); + for _ in 0..1_500 { + expr.push_str(".x"); + } + let _ = ws.expr_ty(&expr); + } + + // Deep call chain: also handled by the explicit task stack + #[test] + fn test_deep_call_chain() { + let mut ws = VirtualWorkspace::new(); + ws.def("---@type fun(): any\nlocal f"); + + let mut expr = "f".to_string(); + for _ in 0..2_000 { + expr.push_str("()"); + } + let _ = ws.expr_ty(&expr); + } + + // Deep paren chain + #[test] + fn test_deep_paren_chain() { + let mut ws = VirtualWorkspace::new(); + let mut expr = "1".to_string(); + for _ in 0..150 { + expr.insert(0, '('); + expr.push(')'); + } + let ty = ws.expr_ty(&expr); + assert!(ty.is_integer()); + } + + // Deep binary chain (left-associative tree) + #[test] + fn test_deep_binary_chain() { + let mut ws = VirtualWorkspace::new(); + let mut expr = "1".to_string(); + for _ in 0..1_500 { + expr.push_str(" + 1"); + } + let ty = ws.expr_ty(&expr); + assert!(ty.is_integer()); + } +} diff --git a/crates/emmylua_code_analysis/src/semantic/infer/infer_binary/infer_binary_and.rs b/crates/emmylua_code_analysis/src/semantic/infer/infer_binary/infer_binary_and.rs index 4ae328e11..030b44d13 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/infer_binary/infer_binary_and.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/infer_binary/infer_binary_and.rs @@ -1,6 +1,9 @@ use emmylua_parser::LuaExpr; -use crate::{DbIndex, LuaType, TypeOps, semantic::infer::narrow::narrow_false_or_nil}; +use crate::{ + DbIndex, LuaType, TypeOps, + semantic::infer::{InferResult, narrow::narrow_false_or_nil}, +}; /// Special handling for `and` operator with specific patterns /// @@ -48,11 +51,7 @@ pub fn special_and_rule( /// - `x and y` where `x: string` → `y` (string is always truthy, returns right) /// - `x and y` where `x: boolean`, `y: string` → `false | string` /// - `x and y` where `x: string | nil`, `y: number` → `nil | number` -pub fn infer_binary_expr_and( - db: &DbIndex, - left: LuaType, - right: LuaType, -) -> crate::semantic::infer::InferResult { +pub fn infer_binary_expr_and(db: &DbIndex, left: LuaType, right: LuaType) -> InferResult { if left.is_always_falsy() { return Ok(left); } else if left.is_always_truthy() { diff --git a/crates/emmylua_code_analysis/src/semantic/infer/infer_binary/mod.rs b/crates/emmylua_code_analysis/src/semantic/infer/infer_binary/mod.rs index df3bf18b3..ef0d670e8 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/infer_binary/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/infer_binary/mod.rs @@ -7,22 +7,22 @@ use infer_binary_or::{infer_binary_expr_or, special_or_rule}; use smol_str::SmolStr; use crate::{ - LuaInferCache, TypeOps, check_type_compact, + TypeOps, check_type_compact, db_index::{DbIndex, LuaOperatorMetaMethod, LuaType}, get_real_type, }; -use super::{InferFailReason, InferResult, get_custom_type_operator, infer_expr}; +use super::{InferFailReason, InferResult, get_custom_type_operator}; -pub fn infer_binary_expr( +/// 左右操作数类型已知后的二元运算推断(纯函数, 无表达式推断) +pub(super) fn infer_binary_expr_result( db: &DbIndex, - cache: &mut LuaInferCache, expr: LuaBinaryExpr, + left_type: LuaType, + right_type: LuaType, ) -> InferResult { let op = expr.get_op_token().ok_or(InferFailReason::None)?.get_op(); let (left, right) = expr.get_exprs().ok_or(InferFailReason::None)?; - let left_type = infer_expr(db, cache, left.clone())?; - let right_type = infer_expr(db, cache, right.clone())?; let real_left_type = get_real_type(db, &left_type); let real_right_type = get_real_type(db, &right_type); let left_type_ref = real_left_type.unwrap_or(&left_type); diff --git a/crates/emmylua_code_analysis/src/semantic/infer/infer_call/infer_require.rs b/crates/emmylua_code_analysis/src/semantic/infer/infer_call/infer_require.rs index 668365d62..4c09fc029 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/infer_call/infer_require.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/infer_call/infer_require.rs @@ -5,7 +5,7 @@ use crate::{ semantic::infer::InferResult, }; -pub(super) fn infer_require_call( +pub(in crate::semantic) fn infer_require_call( db: &DbIndex, cache: &mut LuaInferCache, call_expr: LuaCallExpr, diff --git a/crates/emmylua_code_analysis/src/semantic/infer/infer_call/infer_setmetatable.rs b/crates/emmylua_code_analysis/src/semantic/infer/infer_call/infer_setmetatable.rs index f09ce60b6..d248190e8 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/infer_call/infer_setmetatable.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/infer_call/infer_setmetatable.rs @@ -6,7 +6,7 @@ use crate::{ semantic::{infer::InferResult, member::find_members_with_key}, }; -pub(super) fn infer_setmetatable_call( +pub(in crate::semantic) fn infer_setmetatable_call( db: &DbIndex, cache: &mut LuaInferCache, call_expr: LuaCallExpr, diff --git a/crates/emmylua_code_analysis/src/semantic/infer/infer_call/mod.rs b/crates/emmylua_code_analysis/src/semantic/infer/infer_call/mod.rs index 68b1c0a49..07a63e798 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/infer_call/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/infer_call/mod.rs @@ -4,25 +4,25 @@ use emmylua_parser::{LuaAstNode, LuaCallExpr, LuaExpr, LuaSyntaxKind}; use rowan::TextRange; use super::{ - super::{InferGuard, LuaInferCache, instantiate_type_generic, resolve_signature}, + super::{LuaInferCache, instantiate_type_generic, resolve_signature}, InferFailReason, InferResult, + engine::MAX_INFER_DEPTH, }; use crate::{ AsyncState, CacheEntry, DbIndex, InFiled, LuaFunctionType, LuaGenericType, LuaInstanceType, LuaIntersectionType, LuaOperatorMetaMethod, LuaOperatorOwner, LuaSignature, LuaSignatureId, - LuaType, LuaTypeDeclId, LuaTypeNode, LuaUnionType, TypeOps, TypeVisitTrait, VariadicType, + LuaType, LuaTypeDeclId, LuaTypeNode, LuaUnionType, TypeVisitTrait, VariadicType, }; use crate::{ InferGuardRef, semantic::{ generic::{TypeSubstitutor, instantiate_call_self_type}, - infer::narrow::get_type_at_call_expr_inline_cast, overload_resolve::{collect_callable_overload_groups, match_callable_by_arg_types}, }, }; use crate::{infer_call_generic, semantic::infer_expr}; -use infer_require::infer_require_call; -use infer_setmetatable::infer_setmetatable_call; +pub(super) use infer_require::infer_require_call; +pub(super) use infer_setmetatable::infer_setmetatable_call; mod infer_require; mod infer_setmetatable; @@ -37,6 +37,30 @@ pub fn infer_call_expr_func( call_expr_type: LuaType, infer_guard: &InferGuardRef, args_count: Option, +) -> InferCallFuncResult { + if cache.infer_depth >= MAX_INFER_DEPTH { + return Err(InferFailReason::DepthLimit); + } + cache.infer_depth += 1; + let result = infer_call_expr_func_inner( + db, + cache, + call_expr, + call_expr_type, + infer_guard, + args_count, + ); + cache.infer_depth -= 1; + result +} + +fn infer_call_expr_func_inner( + db: &DbIndex, + cache: &mut LuaInferCache, + call_expr: LuaCallExpr, + call_expr_type: LuaType, + infer_guard: &InferGuardRef, + args_count: Option, ) -> InferCallFuncResult { let syntax_id = call_expr.get_syntax_id(); let key = (syntax_id, args_count, call_expr_type.clone()); @@ -172,7 +196,11 @@ pub fn infer_call_expr_func( .insert(key, CacheEntry::Cache(func_ty.clone())); } } - Err(InferFailReason::None) | Err(InferFailReason::RecursiveInfer) if is_no_flow => { + Err(InferFailReason::None) + | Err(InferFailReason::RecursiveInfer) + | Err(InferFailReason::DepthLimit) + if is_no_flow => + { cache .call_no_flow_cache .insert(key, CacheEntry::Cache(None)); @@ -528,7 +556,7 @@ fn infer_union( first_func = Some(func); } } - Err(InferFailReason::RecursiveInfer) => { + Err(InferFailReason::RecursiveInfer) | Err(InferFailReason::DepthLimit) => { return Err(InferFailReason::RecursiveInfer); } Err(reason) if reason.is_need_resolve() => { @@ -588,7 +616,9 @@ fn infer_intersection( args_count, ) { Ok(func) => overloads.push(func), - Err(InferFailReason::RecursiveInfer) => return Err(InferFailReason::RecursiveInfer), + Err(InferFailReason::RecursiveInfer) | Err(InferFailReason::DepthLimit) => { + return Err(InferFailReason::RecursiveInfer); + } Err(reason) if reason.is_need_resolve() => { if need_resolve.is_none() { need_resolve = Some(reason); @@ -709,53 +739,7 @@ fn is_last_call_expr(call_expr: &LuaCallExpr) -> bool { false } -pub fn infer_call_expr( - db: &DbIndex, - cache: &mut LuaInferCache, - call_expr: LuaCallExpr, -) -> InferResult { - if call_expr.is_require() { - return infer_require_call(db, cache, call_expr); - } else if call_expr.is_setmetatable() { - return infer_setmetatable_call(db, cache, call_expr); - } - - check_can_infer(db, cache, &call_expr)?; - - let is_safe_call = call_expr.has_safe_navigation(); - - let prefix_expr = call_expr.get_prefix_expr().ok_or(InferFailReason::None)?; - let prefix_type = infer_expr(db, cache, prefix_expr)?; - let ret_type = infer_call_expr_func( - db, - cache, - call_expr.clone(), - prefix_type.clone(), - &InferGuard::new(), - None, - )? - .get_ret() - .clone(); - - let ret_type = if is_safe_call && prefix_type.is_nullable() { - TypeOps::Union.apply(db, &ret_type, &LuaType::Nil) - } else { - ret_type - }; - - if !cache.is_no_flow() - && let Some(tree) = db.get_flow_index().get_flow_tree(&cache.get_file_id()) - && let Some(flow_id) = tree.get_flow_id(call_expr.get_syntax_id()) - && let Some(flow_ret_type) = - get_type_at_call_expr_inline_cast(db, cache, tree, call_expr, flow_id, ret_type.clone()) - { - return Ok(flow_ret_type); - } - - Ok(ret_type) -} - -fn check_can_infer( +pub(super) fn check_can_infer( db: &DbIndex, cache: &LuaInferCache, call_expr: &LuaCallExpr, diff --git a/crates/emmylua_code_analysis/src/semantic/infer/infer_fail_reason.rs b/crates/emmylua_code_analysis/src/semantic/infer/infer_fail_reason.rs index 7c99d8826..4c9535549 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/infer_fail_reason.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/infer_fail_reason.rs @@ -6,6 +6,8 @@ use crate::{FileId, InFiled, LuaDeclId, LuaMemberId, LuaSignatureId, LuaTypeDecl pub enum InferFailReason { None, RecursiveInfer, + /// Inference nesting depth exceeded; degrade gracefully instead of overflowing the stack + DepthLimit, UnResolveExpr(InFiled), UnResolveSignatureReturn(LuaSignatureId), FieldNotFound, diff --git a/crates/emmylua_code_analysis/src/semantic/infer/infer_index/mod.rs b/crates/emmylua_code_analysis/src/semantic/infer/infer_index/mod.rs index 747a375b1..0d3c9b2f1 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/infer_index/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/infer_index/mod.rs @@ -1,7 +1,7 @@ mod infer_array; use emmylua_parser::{ - LuaExpr, LuaIndexExpr, LuaIndexKey, LuaIndexMemberExpr, LuaTernaryExpr, NumberResult, PathTrait, + LuaExpr, LuaIndexExpr, LuaIndexKey, LuaIndexMemberExpr, NumberResult, PathTrait, }; use hashbrown::HashSet; use internment::ArcIntern; @@ -34,7 +34,8 @@ use crate::{ }; use super::{ - InferFailReason, InferResult, infer_expr, infer_name::infer_global_type, try_infer_expr_no_flow, + InferFailReason, InferResult, engine::MAX_INFER_DEPTH, infer_expr, + infer_name::infer_global_type, try_infer_expr_no_flow, }; pub(crate) fn try_infer_expr_for_index( @@ -75,7 +76,6 @@ pub fn infer_index_expr( index_expr: LuaIndexExpr, pass_flow: bool, ) -> InferResult { - let is_safe = index_expr.is_safe_index(); let prefix_expr = index_expr.get_prefix_expr().ok_or(InferFailReason::None)?; let prefix_type = infer_expr_for_index(db, cache, prefix_expr)?; let index_member_expr = LuaIndexMemberExpr::IndexExpr(index_expr.clone()); @@ -88,6 +88,19 @@ pub fn infer_index_expr( &InferGuard::new(), )?; + infer_index_expr_with_member(db, cache, index_expr, prefix_type, member_type, pass_flow) +} + +/// Complete the index expression inference once the member type is known (flow narrowing + safe navigation) +pub(super) fn infer_index_expr_with_member( + db: &DbIndex, + cache: &mut LuaInferCache, + index_expr: LuaIndexExpr, + prefix_type: LuaType, + member_type: LuaType, + pass_flow: bool, +) -> InferResult { + let is_safe = index_expr.is_safe_index(); let mut result_type = if pass_flow { infer_member_type_pass_flow(db, cache, index_expr, member_type)? } else { @@ -101,19 +114,6 @@ pub fn infer_index_expr( Ok(result_type) } -pub fn infer_ternary_expr( - db: &DbIndex, - cache: &mut LuaInferCache, - ternary_expr: LuaTernaryExpr, -) -> InferResult { - let Some((true_expr, false_expr)) = ternary_expr.get_true_false_exprs() else { - return Err(InferFailReason::None); - }; - let true_type = infer_expr(db, cache, true_expr)?; - let false_type = infer_expr(db, cache, false_expr)?; - Ok(TypeOps::Union.apply(db, &true_type, &false_type)) -} - fn infer_member_type_pass_flow( db: &DbIndex, cache: &mut LuaInferCache, @@ -282,6 +282,22 @@ fn infer_member_by_lookup( prefix_type: &LuaType, lookup: &MemberLookupQuery, infer_guard: &InferGuardRef, +) -> InferResult { + if cache.infer_depth >= MAX_INFER_DEPTH { + return Err(InferFailReason::DepthLimit); + } + cache.infer_depth += 1; + let result = infer_member_by_lookup_inner(db, cache, prefix_type, lookup, infer_guard); + cache.infer_depth -= 1; + result +} + +fn infer_member_by_lookup_inner( + db: &DbIndex, + cache: &mut LuaInferCache, + prefix_type: &LuaType, + lookup: &MemberLookupQuery, + infer_guard: &InferGuardRef, ) -> InferResult { match &prefix_type { LuaType::Table | LuaType::Any | LuaType::Unknown => Ok(LuaType::Any), @@ -778,6 +794,23 @@ fn infer_member_by_operator_key_type( prefix_type: &LuaType, key_type: &LuaType, infer_guard: &InferGuardRef, +) -> InferResult { + if cache.infer_depth >= MAX_INFER_DEPTH { + return Err(InferFailReason::DepthLimit); + } + cache.infer_depth += 1; + let result = + infer_member_by_operator_key_type_inner(db, cache, prefix_type, key_type, infer_guard); + cache.infer_depth -= 1; + result +} + +fn infer_member_by_operator_key_type_inner( + db: &DbIndex, + cache: &mut LuaInferCache, + prefix_type: &LuaType, + key_type: &LuaType, + infer_guard: &InferGuardRef, ) -> InferResult { match &prefix_type { LuaType::TableConst(in_filed) => infer_member_by_index_table(db, in_filed, key_type), diff --git a/crates/emmylua_code_analysis/src/semantic/infer/infer_unary.rs b/crates/emmylua_code_analysis/src/semantic/infer/infer_unary.rs index 0f58d7f16..2bf334ae9 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/infer_unary.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/infer_unary.rs @@ -1,23 +1,15 @@ -use emmylua_parser::{LuaUnaryExpr, UnaryOperator}; +use emmylua_parser::UnaryOperator; -use crate::{ - LuaInferCache, - db_index::{DbIndex, LuaOperatorMetaMethod, LuaType}, -}; +use crate::db_index::{DbIndex, LuaOperatorMetaMethod, LuaType}; -use super::{InferFailReason, InferResult, get_custom_type_operator, infer_expr}; +use super::{InferResult, get_custom_type_operator}; -pub fn infer_unary_expr( +/// 操作数类型已知后的一元运算推断(纯函数, 无表达式推断) +pub(super) fn infer_unary_expr_result( db: &DbIndex, - cache: &mut LuaInferCache, - unary_expr: LuaUnaryExpr, + op: UnaryOperator, + inner_type: LuaType, ) -> InferResult { - let op = unary_expr - .get_op_token() - .ok_or(InferFailReason::None)? - .get_op(); - let inner_expr = unary_expr.get_expr().ok_or(InferFailReason::None)?; - let inner_type = infer_expr(db, cache, inner_expr)?; match op { UnaryOperator::OpNot => infer_unary_expr_not(inner_type), UnaryOperator::OpLen => Ok(LuaType::Integer), diff --git a/crates/emmylua_code_analysis/src/semantic/infer/mod.rs b/crates/emmylua_code_analysis/src/semantic/infer/mod.rs index 46a4ba32f..00517633d 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/mod.rs @@ -1,3 +1,4 @@ +mod engine; mod infer_binary; mod infer_call; mod infer_doc_type; @@ -15,8 +16,7 @@ use emmylua_parser::{ LuaAst, LuaAstNode, LuaCallExpr, LuaClosureExpr, LuaExpr, LuaLiteralExpr, LuaLiteralToken, LuaSyntaxId, LuaTableExpr, LuaVarExpr, NumberResult, }; -use infer_binary::infer_binary_expr; -use infer_call::infer_call_expr; +use engine::{InferEngine, MAX_INFER_DEPTH}; pub use infer_call::infer_call_expr_func; pub use infer_doc_type::{DocTypeInferContext, infer_doc_type}; pub use infer_fail_reason::InferFailReason; @@ -26,7 +26,6 @@ use infer_name::infer_name_expr; pub use infer_name::{find_self_decl_or_member_id, infer_param}; use infer_table::infer_table_expr; pub use infer_table::{infer_table_field_value_should_be, infer_table_should_be}; -use infer_unary::infer_unary_expr; pub use narrow::VarRefId; pub(super) use narrow::apply_assignment_target_casts; pub(in crate::semantic) use narrow::{ConditionFlowAction, InferConditionFlow}; @@ -37,7 +36,6 @@ use smol_str::SmolStr; use crate::{ InFiled, InferGuard, LuaMemberKey, VariadicType, db_index::{DbIndex, LuaOperator, LuaOperatorMetaMethod, LuaSignatureId, LuaType}, - semantic::infer::infer_index::infer_ternary_expr, }; use super::{CacheEntry, LuaInferCache, member::infer_raw_member_type}; @@ -45,7 +43,7 @@ use super::{CacheEntry, LuaInferCache, member::infer_raw_member_type}; pub type InferResult = Result; pub use infer_call::InferCallFuncResult; -fn prepare_expr_cache( +pub(super) fn prepare_expr_cache( db: &DbIndex, cache: &mut LuaInferCache, syntax_id: LuaSyntaxId, @@ -106,83 +104,13 @@ fn prepare_expr_cache( } pub fn infer_expr(db: &DbIndex, cache: &mut LuaInferCache, expr: LuaExpr) -> InferResult { - let no_flow = cache.is_no_flow(); - let syntax_id = expr.get_syntax_id(); - if let Some(result_type) = prepare_expr_cache(db, cache, syntax_id)? { - return Ok(result_type); + if cache.infer_depth >= MAX_INFER_DEPTH { + return Err(InferFailReason::DepthLimit); } - if no_flow - && matches!(expr, LuaExpr::TableExpr(_)) - && !cache.no_flow_table_exprs.contains(&syntax_id) - { - cache - .expr_no_flow_cache - .insert(syntax_id, CacheEntry::Cache(None)); - return Err(InferFailReason::None); - } - let result_type = match expr { - LuaExpr::CallExpr(call_expr) => infer_call_expr(db, cache, call_expr), - LuaExpr::TableExpr(table_expr) => infer_table_expr(db, cache, table_expr), - LuaExpr::LiteralExpr(literal_expr) => infer_literal_expr(db, cache, literal_expr), - LuaExpr::BinaryExpr(binary_expr) => infer_binary_expr(db, cache, binary_expr), - LuaExpr::UnaryExpr(unary_expr) => infer_unary_expr(db, cache, unary_expr), - LuaExpr::ClosureExpr(closure_expr) => infer_closure_expr(db, cache, closure_expr), - LuaExpr::ParenExpr(paren_expr) => infer_expr( - db, - cache, - paren_expr.get_expr().ok_or(InferFailReason::None)?, - ), - LuaExpr::NameExpr(name_expr) => infer_name_expr(db, cache, name_expr), - LuaExpr::IndexExpr(index_expr) => infer_index_expr(db, cache, index_expr, !no_flow), - LuaExpr::TernaryExpr(ternary_expr) => infer_ternary_expr(db, cache, ternary_expr), - }; - - match &result_type { - Ok(result_type) => { - if no_flow { - cache - .expr_no_flow_cache - .insert(syntax_id, CacheEntry::Cache(Some(result_type.clone()))); - } else { - cache - .expr_cache - .insert(syntax_id, CacheEntry::Cache(result_type.clone())); - } - } - Err(InferFailReason::None) | Err(InferFailReason::RecursiveInfer) => { - if no_flow { - cache - .expr_no_flow_cache - .insert(syntax_id, CacheEntry::Cache(None)); - } else { - cache - .expr_cache - .insert(syntax_id, CacheEntry::Cache(LuaType::Unknown)); - return Ok(LuaType::Unknown); - } - } - Err(InferFailReason::FieldNotFound) => { - if no_flow { - cache.expr_no_flow_cache.remove(&syntax_id); - } else if cache.get_config().analysis_phase.is_force() { - cache - .expr_cache - .insert(syntax_id, CacheEntry::Cache(LuaType::Nil)); - return Ok(LuaType::Nil); - } else { - cache.expr_cache.remove(&syntax_id); - } - } - _ => { - if no_flow { - cache.expr_no_flow_cache.remove(&syntax_id); - } else { - cache.expr_cache.remove(&syntax_id); - } - } - } - - result_type + cache.infer_depth += 1; + let result = InferEngine::new(db, cache).run(expr); + cache.infer_depth -= 1; + result } pub(crate) fn try_infer_expr_no_flow( @@ -192,7 +120,9 @@ pub(crate) fn try_infer_expr_no_flow( ) -> Result, InferFailReason> { match cache.with_no_flow(|cache| infer_expr(db, cache, expr)) { Ok(result_type) => Ok(Some(result_type)), - Err(InferFailReason::None) | Err(InferFailReason::RecursiveInfer) => Ok(None), + Err(InferFailReason::None) + | Err(InferFailReason::RecursiveInfer) + | Err(InferFailReason::DepthLimit) => Ok(None), Err(err) => Err(err), } } diff --git a/crates/emmylua_code_analysis/src/semantic/infer/narrow/get_type_at_flow.rs b/crates/emmylua_code_analysis/src/semantic/infer/narrow/get_type_at_flow.rs index 8333685e6..b4c863f1c 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/narrow/get_type_at_flow.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/narrow/get_type_at_flow.rs @@ -284,6 +284,7 @@ impl FlowReplayQuery { Err( InferFailReason::None | InferFailReason::RecursiveInfer + | InferFailReason::DepthLimit | InferFailReason::FieldNotFound, ) => None, Err(err) => return Err(err),