mod control_flow;
mod declarations;
mod expressions;
mod flow;
mod loops;
mod return_type;
pub(crate) use flow::return_type_is_invalid;
pub(crate) use loops::infer_foreach_types;
pub(crate) use loops::resolve_iterator_item_types;
use loops::{vars_stabilized, widen_unstable};
pub(crate) use return_type::named_object_return_compatible;
pub(crate) use return_type::resolve_union_for_file;
pub(crate) use return_type::resolve_union_for_file_native;
pub(crate) use return_type::resolve_union_for_file_with_templates;
pub(crate) use return_type::return_arrays_compatible;
use std::sync::Arc;
use crate::parser::docblock::parse_type_string;
use php_ast::owned::{Expr, ExprKind, StmtKind};
use mir_issues::{Issue, IssueBuffer, IssueKind, Location};
use mir_types::{Atomic, Type};
use crate::body_analysis::AnalysisMode;
use crate::db::MirDatabase;
use crate::expr::ExpressionAnalyzer;
use crate::flow_state::FlowState;
use crate::php_version::PhpVersion;
use crate::symbol::ResolvedSymbol;
struct VarAnnotation {
name: Option<String>,
ty: mir_types::Type,
}
fn simple_assignment_lhs(stmt: &php_ast::owned::Stmt) -> Option<&str> {
let php_ast::owned::StmtKind::Expression(e) = &stmt.kind else {
return None;
};
let php_ast::owned::ExprKind::Assign(a) = &e.kind else {
return None;
};
if !matches!(&a.op, php_ast::ast::AssignOp::Assign) {
return None;
}
let php_ast::owned::ExprKind::Variable(lhs_name) = &a.target.kind else {
return None;
};
Some(lhs_name.trim_start_matches('$'))
}
fn apply_post_narrow(stmt: &php_ast::owned::Stmt, annotation: &VarAnnotation, ctx: &mut FlowState) {
let apply = |name: &str, ctx: &mut FlowState| match &annotation.name {
Some(var_name) => {
if name == var_name.as_str() {
ctx.set_var(var_name.as_str(), annotation.ty.clone());
}
}
None => ctx.set_var(name, annotation.ty.clone()),
};
match &stmt.kind {
php_ast::owned::StmtKind::Expression(e) => {
let php_ast::owned::ExprKind::Assign(a) = &e.kind else {
return;
};
if !matches!(&a.op, php_ast::ast::AssignOp::Assign) {
return;
}
let php_ast::owned::ExprKind::Variable(lhs_name) = &a.target.kind else {
return;
};
apply(lhs_name.trim_start_matches('$'), ctx);
}
php_ast::owned::StmtKind::StaticVar(vars) => {
if annotation.name.is_some() {
for sv in vars.iter() {
if let Some(name) = sv.name.as_deref() {
apply(name.trim_start_matches('$'), ctx);
}
}
} else if let [sv] = vars.as_ref() {
if let Some(name) = sv.name.as_deref() {
apply(name.trim_start_matches('$'), ctx);
}
}
}
_ => {}
}
}
fn closure_var_bind_scope(
db: &dyn MirDatabase,
file: &str,
stmt: &php_ast::owned::Stmt,
next: Option<&php_ast::owned::Stmt>,
) -> Option<Arc<str>> {
let StmtKind::Expression(e) = &stmt.kind else {
return None;
};
let ExprKind::Assign(assign) = &e.kind else {
return None;
};
if !matches!(&assign.op, php_ast::ast::AssignOp::Assign) {
return None;
}
let ExprKind::Variable(var_name) = &assign.target.kind else {
return None;
};
if !matches!(
&assign.value.kind,
ExprKind::Closure(_) | ExprKind::ArrowFunction(_)
) {
return None;
}
let next_expr: &Expr = match &next?.kind {
StmtKind::Expression(e) => e,
StmtKind::Return(Some(e)) => e,
_ => return None,
};
let call_expr = match &next_expr.kind {
ExprKind::Assign(a) => a.value.as_ref(),
_ => next_expr,
};
match &call_expr.kind {
ExprKind::StaticMethodCall(smc) => {
let ExprKind::Identifier(class_name) = &smc.class.kind else {
return None;
};
if !crate::db::resolve_name(db, file, class_name).eq_ignore_ascii_case("Closure") {
return None;
}
let ExprKind::Identifier(method_name) = &smc.method.kind else {
return None;
};
if !method_name.eq_ignore_ascii_case("bind") {
return None;
}
let first_arg = smc.args.first()?.value.as_ref()?;
if !matches!(&first_arg.kind, ExprKind::Variable(v) if v == var_name) {
return None;
}
let scope_expr = smc.args.get(2)?.value.as_ref()?;
crate::call::resolve_literal_class_scope(db, file, scope_expr)
}
ExprKind::MethodCall(mc) => {
if !matches!(&mc.object.kind, ExprKind::Variable(v) if v == var_name) {
return None;
}
let ExprKind::Identifier(method_name) = &mc.method.kind else {
return None;
};
if !method_name.eq_ignore_ascii_case("bindTo") {
return None;
}
let scope_expr = mc.args.get(1)?.value.as_ref()?;
crate::call::resolve_literal_class_scope(db, file, scope_expr)
}
_ => None,
}
}
pub struct StatementsAnalyzer<'a> {
pub db: &'a dyn MirDatabase,
pub file: Arc<str>,
pub source: &'a str,
pub source_map: &'a php_rs_parser::source_map::SourceMap,
pub issues: &'a mut IssueBuffer,
pub symbols: &'a mut Vec<ResolvedSymbol>,
pub php_version: PhpVersion,
pub mode: AnalysisMode,
pub collect_symbols: bool,
pub return_types: Vec<Type>,
pub yielded_types: Vec<(Type, Type)>,
break_ctx_stack: Vec<Vec<FlowState>>,
loop_kind_stack: Vec<bool>,
plugins: Option<std::sync::Arc<mir_plugin::PluginRegistry>>,
class_like_cache: Option<(Arc<str>, Option<crate::db::ClassLike>)>,
function_cache: Option<(
Arc<str>,
Option<Arc<mir_codebase::definitions::FunctionDef>>,
)>,
}
impl<'a> StatementsAnalyzer<'a> {
#[allow(clippy::too_many_arguments)]
pub fn new(
db: &'a dyn MirDatabase,
file: Arc<str>,
source: &'a str,
source_map: &'a php_rs_parser::source_map::SourceMap,
issues: &'a mut IssueBuffer,
symbols: &'a mut Vec<ResolvedSymbol>,
php_version: PhpVersion,
mode: AnalysisMode,
) -> Self {
Self {
db,
file,
source,
source_map,
issues,
symbols,
php_version,
mode,
collect_symbols: true,
return_types: Vec::new(),
yielded_types: Vec::new(),
break_ctx_stack: Vec::new(),
loop_kind_stack: Vec::new(),
plugins: mir_plugin::snapshot(),
class_like_cache: None,
function_cache: None,
}
}
fn cached_class_like(&mut self, fqcn: &str) -> Option<&crate::db::ClassLike> {
let hit = self
.class_like_cache
.as_ref()
.is_some_and(|(cached_fqcn, _)| cached_fqcn.as_ref() == fqcn);
if !hit {
let resolved =
crate::db::find_class_like(self.db, crate::db::Fqcn::from_str(self.db, fqcn));
self.class_like_cache = Some((Arc::from(fqcn), resolved));
}
self.class_like_cache
.as_ref()
.and_then(|(_, cl)| cl.as_ref())
}
fn cached_function(
&mut self,
fqn: &str,
) -> Option<Arc<mir_codebase::definitions::FunctionDef>> {
let hit = self
.function_cache
.as_ref()
.is_some_and(|(cached_fqn, _)| cached_fqn.as_ref() == fqn);
if !hit {
let resolved =
crate::db::find_function(self.db, crate::db::Fqcn::from_str(self.db, fqn));
self.function_cache = Some((Arc::from(fqn), resolved));
}
self.function_cache.as_ref().and_then(|(_, f)| f.clone())
}
pub fn analyze_stmts(&mut self, stmts: &[php_ast::owned::Stmt], ctx: &mut FlowState) {
for (i, stmt) in stmts.iter().enumerate() {
if ctx.diverges {
let (line, col_start) = self.offset_to_line_col(stmt.span.start);
let (line_end, col_end) = if stmt.span.start < stmt.span.end {
let (end_line, end_col) = self.offset_to_line_col(stmt.span.end);
(end_line, end_col)
} else {
(line, col_start + 1)
};
self.issues.add(
Issue::new(
IssueKind::UnreachableCode,
Location {
file: self.file.clone(),
line,
line_end,
col_start,
col_end: crate::diagnostics::clamp_col_end(
line, line_end, col_start, col_end,
),
},
)
.with_snippet(
crate::parser::span_text(self.source, stmt.span).unwrap_or_default(),
),
);
break;
}
match closure_var_bind_scope(self.db, &self.file, stmt, stmts.get(i + 1)) {
Some(scope) => {
let guard = crate::flow_state::ScopeOverrideGuard::apply(ctx, self.db, &scope);
self.analyze_stmt(stmt, ctx);
guard.restore(ctx);
}
None => self.analyze_stmt(stmt, ctx),
}
}
}
pub fn analyze_stmt(&mut self, stmt: &php_ast::owned::Stmt, ctx: &mut FlowState) {
let doc = crate::parser::find_preceding_docblock(self.source, stmt.span.start);
let suppressions = self.extract_suppressions_from(doc.as_deref());
let before = self.issues.issue_count();
let var_annotation = self.extract_var_annotation_from(
doc.as_deref(),
ctx.self_fqcn.as_deref(),
ctx.current_method_name.as_deref(),
ctx.current_function_fqn.as_deref(),
);
if let Some(ref ann) = var_annotation {
for atomic in &ann.ty.types {
if let Atomic::TNamedObject { fqcn, type_params } = atomic {
if crate::diagnostics::is_pseudo_type(fqcn.as_ref()) {
continue;
}
let (line, line_end, col_start, col_end) = self.span_to_location(stmt.span);
if !crate::db::class_exists(self.db, fqcn.as_ref()) {
self.issues.add(Issue::new(
IssueKind::UndefinedDocblockClass {
name: fqcn.to_string(),
},
Location {
file: self.file.clone(),
line,
line_end,
col_start,
col_end: crate::diagnostics::clamp_col_end(
line, line_end, col_start, col_end,
),
},
));
} else {
if self.mode == AnalysisMode::Full {
self.db.record_reference_location(crate::db::RefLoc {
symbol_key: Arc::from(format!("cls:{fqcn}")),
file: self.file.clone(),
line,
col_start,
col_end: crate::diagnostics::clamp_col_end(
line, line_end, col_start, col_end,
),
});
}
if !type_params.is_empty() {
let declared_count =
crate::db::class_template_params(self.db, fqcn.as_ref())
.map(|tps| tps.len())
.unwrap_or(0);
if declared_count != type_params.len() {
self.issues.add(Issue::new(
IssueKind::InvalidDocblock {
message: format!(
"{fqcn} expects {declared_count} template argument(s), got {}",
type_params.len()
),
},
Location {
file: self.file.clone(),
line,
line_end,
col_start,
col_end: crate::diagnostics::clamp_col_end(
line, line_end, col_start, col_end,
),
},
));
}
}
}
}
}
if let Some(ref name) = ann.name {
let current = ctx.get_var(name.as_str());
let current_mask = crate::body_analysis::type_family_mask(¤t);
let ann_mask = crate::body_analysis::type_family_mask(&ann.ty);
if current_mask != 0 && ann_mask != 0 && current_mask & ann_mask == 0 {
let (line, line_end, col_start, col_end) = self.span_to_location(stmt.span);
self.issues.add(Issue::new(
IssueKind::DocblockTypeContradiction {
expr: format!("@var {} ${name}", ann.ty),
declared: current.to_string(),
},
Location {
file: self.file.clone(),
line,
line_end,
col_start,
col_end: crate::diagnostics::clamp_col_end(
line, line_end, col_start, col_end,
),
},
));
}
ctx.set_var(name.as_str(), ann.ty.clone());
}
}
let mir_checks = self.extract_mir_checks_from(doc.as_deref());
let (line, line_end, col_start, col_end) = self.span_to_location(stmt.span);
for (expr_text, expected_str) in mir_checks {
let expected_raw = parse_type_string(&expected_str);
let expected = resolve_union_for_file(expected_raw, self.db, &self.file);
let actual_raw = self.expr_analyzer(ctx).eval_check_expr(&expr_text, ctx);
if !mir_check_matches(&expected, &actual_raw) {
self.issues.add(Issue::new(
IssueKind::TypeCheckMismatch {
var: expr_text,
expected: expected.to_string(),
actual: widen_for_check(actual_raw).to_string(),
},
Location {
file: self.file.clone(),
line,
line_end,
col_start,
col_end: crate::diagnostics::clamp_col_end(
line, line_end, col_start, col_end,
),
},
));
}
}
if let Some(doc_str) = doc.as_deref() {
let trace_vars = crate::parser::DocblockParser::parse(doc_str).trace_vars;
for var_name in trace_vars {
let ty = ctx.get_var(&var_name);
self.issues.add(Issue::new(
IssueKind::Trace {
variable: var_name,
type_info: ty.to_string(),
},
Location {
file: self.file.clone(),
line,
line_end,
col_start,
col_end: crate::diagnostics::clamp_col_end(
line, line_end, col_start, col_end,
),
},
));
}
}
match &stmt.kind {
StmtKind::Expression(expr) => {
if matches!(
expr.kind,
php_ast::owned::ExprKind::Closure(_)
| php_ast::owned::ExprKind::ArrowFunction(_)
) {
if let Some(raw_doc) = doc.as_deref() {
let parsed = crate::parser::DocblockParser::parse(raw_doc);
if !parsed.templates.is_empty() {
let lc = self.source_map.offset_to_line_col(stmt.span.start);
let line = lc.line + 1;
self.issues.add(Issue::new(
IssueKind::InvalidDocblock {
message: "@template annotations are not supported on closures or arrow functions".to_string(),
},
Location {
file: self.file.clone(),
line,
line_end: line,
col_start: 0,
col_end: 0,
},
));
}
}
}
self.analyze_expression_stmt(expr, ctx);
}
StmtKind::Echo(exprs) => {
self.analyze_echo_stmt(exprs, stmt.span, ctx);
}
StmtKind::Return(opt_expr) => {
self.analyze_return_stmt(opt_expr, stmt, ctx);
}
StmtKind::Throw(expr) => {
self.analyze_throw_stmt(expr, stmt.span, ctx);
}
StmtKind::If(if_stmt) => {
self.analyze_if_stmt(if_stmt, ctx);
}
StmtKind::While(w) => {
self.analyze_while_stmt(w, ctx);
}
StmtKind::DoWhile(dw) => {
self.analyze_dowhile_stmt(dw, ctx);
}
StmtKind::For(f) => {
self.analyze_for_stmt(f, ctx);
}
StmtKind::Foreach(fe) => {
self.analyze_foreach_stmt(fe, stmt, ctx);
}
StmtKind::Switch(sw) => {
self.analyze_switch_stmt(sw, ctx);
}
StmtKind::TryCatch(tc) => {
self.analyze_trycatch_stmt(tc, ctx);
}
StmtKind::Block(block) => {
self.analyze_stmts(&block.stmts, ctx);
}
StmtKind::Break(level) => {
self.analyze_break_stmt(ctx, break_continue_level(level));
}
StmtKind::Continue(level) => {
self.analyze_continue_stmt(ctx, break_continue_level(level));
}
StmtKind::Unset(vars) => {
self.analyze_unset_stmt(vars, ctx);
}
StmtKind::StaticVar(vars) => {
self.analyze_static_var_stmt(vars, ctx);
}
StmtKind::Global(vars) => {
self.analyze_global_stmt(vars, ctx);
}
StmtKind::Declare(d) => {
self.analyze_declare_stmt(d, ctx);
}
StmtKind::Function(decl) => {
self.analyze_function_decl_stmt(decl, ctx);
}
StmtKind::Class(decl) => {
self.analyze_class_decl_stmt(decl, ctx);
}
StmtKind::Interface(_) | StmtKind::Trait(_) | StmtKind::Enum(_) => {
}
StmtKind::Namespace(_) | StmtKind::Use(_) | StmtKind::Const(_) => {}
StmtKind::InlineHtml(_)
| StmtKind::Nop
| StmtKind::Goto(_)
| StmtKind::Label(_)
| StmtKind::HaltCompiler(_) => {}
StmtKind::Error => {}
}
if let Some(ref ann) = var_annotation {
if self.mode == AnalysisMode::Full {
if let Some(lhs) = simple_assignment_lhs(stmt) {
let applies = ann.name.as_deref().is_none_or(|n| n == lhs);
if applies {
let ann_ty = crate::expr::helpers::resolve_named_objects_in_union(
ann.ty.clone(),
self.db,
self.file.as_ref(),
);
let inferred = ctx.get_var(lhs);
if !inferred.is_mixed() && inferred.to_string() == ann_ty.to_string() {
self.issues.add(Issue::new(
IssueKind::UnnecessaryVarAnnotation {
var: lhs.to_string(),
},
Location {
file: self.file.clone(),
line,
line_end,
col_start,
col_end: crate::diagnostics::clamp_col_end(
line, line_end, col_start, col_end,
),
},
));
}
}
}
}
apply_post_narrow(stmt, ann, ctx);
}
if let Some(plugins) = self.plugins.clone() {
if plugins.hooks().after_statement_analysis {
let file = self.file.clone();
let mut event = mir_plugin::AfterStatementAnalysisEvent {
stmt,
file: file.as_ref(),
issues: Vec::new(),
};
plugins.after_statement_analysis(&mut event);
let issues = event.issues;
self.emit_plugin_issues(issues, stmt.span);
}
}
if !suppressions.is_empty() {
self.issues.suppress_range(before, &suppressions);
}
}
fn emit_plugin_issues(
&mut self,
issues: Vec<mir_plugin::PluginIssue>,
default_span: php_ast::Span,
) {
for pi in issues {
let span = pi.span.unwrap_or(default_span);
let (line, line_end, col_start, col_end) = self.span_to_location(span);
let mut issue = Issue::new(
IssueKind::PluginIssue {
name: pi.name,
message: pi.message,
},
Location {
file: self.file.clone(),
line,
line_end,
col_start,
col_end: crate::diagnostics::clamp_col_end(line, line_end, col_start, col_end),
},
);
issue.severity = pi.severity;
if let Some(text) = crate::parser::span_text(self.source, span) {
let trimmed = text.trim();
if !trimmed.is_empty() {
issue.snippet = Some(trimmed.to_string());
}
}
self.issues.add(issue);
}
}
pub(crate) fn expr_analyzer<'b>(&'b mut self, ctx: &FlowState) -> ExpressionAnalyzer<'b>
where
'a: 'b,
{
let mut ea = ExpressionAnalyzer::new(
self.db,
self.file.clone(),
self.source,
self.source_map,
self.issues,
self.symbols,
self.php_version,
self.mode,
&mut self.yielded_types,
);
ea.strict_types = ctx.strict_types;
ea.collect_symbols = self.collect_symbols;
ea
}
fn record_symbol_for_var(&mut self, span: php_ast::Span, var_name: &str, ty: Type) {
use crate::symbol::ReferenceKind;
if !self.collect_symbols {
return;
}
self.symbols.push(ResolvedSymbol {
file: self.file.clone(),
span,
expr_span: None,
kind: ReferenceKind::Variable(Arc::from(var_name)),
resolved_type: ty,
});
}
fn offset_to_line_col(&self, offset: u32) -> (u32, u16) {
crate::diagnostics::offset_to_line_col(self.source, offset, self.source_map)
}
fn span_to_location(&self, span: php_ast::Span) -> (u32, u32, u16, u16) {
let (line, col_start) = self.offset_to_line_col(span.start);
let (line_end, col_end) = if span.start < span.end {
self.offset_to_line_col(span.end)
} else {
(line, col_start)
};
(line, line_end, col_start, col_end)
}
fn check_name_undefined_class(&mut self, name: &php_ast::owned::Name) {
let raw = crate::parser::name_to_string_owned(name);
let resolved = crate::db::resolve_name(self.db, &self.file, &raw);
if matches!(resolved.as_str(), "self" | "static" | "parent") {
return;
}
if crate::db::class_exists(self.db, &resolved) {
return;
}
let span = name.span;
let (line, col_start) = self.offset_to_line_col(span.start);
let (line_end, col_end) = self.offset_to_line_col(span.end);
self.issues.add(Issue::new(
IssueKind::UndefinedClass { name: resolved },
Location {
file: self.file.clone(),
line,
line_end,
col_start,
col_end: crate::diagnostics::clamp_col_end(line, line_end, col_start, col_end),
},
));
}
pub(crate) fn record_class_like_ref(&mut self, resolved: &str, span: php_ast::Span) {
if self.mode != AnalysisMode::Full || !crate::db::class_exists(self.db, resolved) {
return;
}
let (line, col_start) = self.offset_to_line_col(span.start);
let (line_end, col_end) = self.offset_to_line_col(span.end);
self.db.record_reference_location(crate::db::RefLoc {
symbol_key: Arc::from(format!("cls:{resolved}")),
file: self.file.clone(),
line,
col_start,
col_end: crate::diagnostics::clamp_col_end(line, line_end, col_start, col_end),
});
}
pub(crate) fn check_name_undefined_trait(&mut self, name: &php_ast::owned::Name) {
let raw = crate::parser::name_to_string_owned(name);
let resolved = crate::db::resolve_name(self.db, &self.file, &raw);
if crate::db::class_exists(self.db, &resolved) {
return;
}
let short = resolved
.rsplit('\\')
.next()
.unwrap_or(resolved.as_str())
.to_string();
let span = name.span;
let (line, col_start) = self.offset_to_line_col(span.start);
let (line_end, col_end) = self.offset_to_line_col(span.end);
self.issues.add(Issue::new(
IssueKind::UndefinedTrait { name: short },
Location {
file: self.file.clone(),
line,
line_end,
col_start,
col_end: crate::diagnostics::clamp_col_end(line, line_end, col_start, col_end),
},
));
}
fn extract_suppressions_from(&self, doc: Option<&str>) -> Vec<String> {
let Some(doc) = doc else {
return vec![];
};
let mut suppressions = Vec::new();
for line in doc.lines() {
let line = line.trim().trim_start_matches('*').trim();
let rest = if let Some(r) = line.strip_prefix("@psalm-suppress ") {
r
} else if let Some(r) = line.strip_prefix("@suppress ") {
r
} else {
continue;
};
for name in rest.split_whitespace() {
suppressions.push(name.to_string());
}
}
suppressions
}
fn extract_mir_checks_from(&self, doc: Option<&str>) -> Vec<(String, String)> {
let Some(doc) = doc else {
return vec![];
};
crate::parser::DocblockParser::parse(doc).mir_checks
}
fn extract_var_annotation_from(
&mut self,
doc: Option<&str>,
self_fqcn: Option<&str>,
current_method_name: Option<&str>,
current_function_fqn: Option<&str>,
) -> Option<VarAnnotation> {
let parsed = crate::parser::DocblockParser::parse(doc?);
let mut ty = parsed.var_type?;
let mut template_names: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
let mut template_params: Vec<mir_codebase::definitions::TemplateParam> = Vec::new();
let defining_entity = self_fqcn.or(current_function_fqn).unwrap_or_default();
if let Some(fqcn) = self_fqcn {
if let Some(class_like) = self.cached_class_like(fqcn) {
let aliases = class_like.type_aliases();
if !aliases.is_empty() {
ty = crate::collector::expand_aliases_only(ty, aliases);
}
template_names.extend(
class_like
.template_params()
.iter()
.map(|tp| tp.name.to_string()),
);
template_params.extend(class_like.template_params().iter().cloned());
if let Some(method_name) = current_method_name {
let method_name = crate::util::php_ident_lowercase(method_name);
if let Some(method) = class_like.own_methods().get(method_name.as_str()) {
template_names
.extend(method.template_params.iter().map(|tp| tp.name.to_string()));
template_params.extend(method.template_params.iter().cloned());
}
}
}
} else if let Some(fqn) = current_function_fqn {
if let Some(function) = self.cached_function(fqn) {
if !function.type_aliases.is_empty() {
ty = crate::collector::expand_aliases_only(ty, &function.type_aliases);
}
template_names.extend(
function
.template_params
.iter()
.map(|tp| tp.name.to_string()),
);
template_params.extend(function.template_params.iter().cloned());
}
}
Some(VarAnnotation {
name: parsed.var_name,
ty: return_type::resolve_union_for_file_with_templates(
ty,
self.db,
&self.file,
&template_names,
&template_params,
defining_entity,
),
})
}
fn analyze_loop_widened<F>(
&mut self,
pre: &FlowState,
entry: FlowState,
mut body: F,
loop_guaranteed: bool,
is_infinite: bool,
exit_condition: Option<&Expr>,
) -> FlowState
where
F: FnMut(&mut Self, &mut FlowState),
{
const MAX_ITERS: usize = 3;
self.break_ctx_stack.push(Vec::new());
self.loop_kind_stack.push(true);
let mut current = entry;
current.inside_loop = true;
for iter_idx in 0..MAX_ITERS {
let prev_vars = current.vars.clone();
let issues_mark = self.issues.issue_count();
let mut iter = current.clone();
iter.inside_loop = true;
body(self, &mut iter);
let mut next = FlowState::merge_branches(pre, iter.clone(), None);
for name in iter.read_vars.iter() {
if let Some(pre_locs) = pre.last_write_locs.get(name) {
if let Some(locs) = next.last_write_locs.get_mut(name) {
locs.retain(|l| !pre_locs.contains(l));
if locs.is_empty() {
next.last_write_locs.remove(name);
}
}
}
}
if vars_stabilized(&prev_vars, &next.vars) {
current = next;
break;
}
if iter_idx + 1 < MAX_ITERS {
self.issues.truncate_to(issues_mark);
}
current = next;
}
widen_unstable(
&pre.vars,
std::sync::Arc::make_mut(&mut current.vars),
loop_guaranteed,
);
if is_infinite {
current.diverges = true;
} else if let Some(cond) = exit_condition {
crate::narrowing::narrow_from_condition(cond, &mut current, false, self.db, &self.file);
}
let break_ctxs = self.break_ctx_stack.pop().unwrap_or_default();
self.loop_kind_stack.pop();
for bctx in break_ctxs {
current = FlowState::merge_branches(pre, current, Some(bctx));
}
current.inside_loop = pre.inside_loop;
current
}
}
fn break_continue_level(level_expr: &Option<Box<Expr>>) -> usize {
match level_expr.as_deref().map(|e| &e.kind) {
Some(ExprKind::Int(n)) if *n >= 1 => *n as usize,
_ => 1,
}
}
pub(crate) fn mir_check_matches(expected: &Type, actual: &Type) -> bool {
let expected_str = expected.to_string();
expected_str == actual.to_string()
|| expected_str == widen_for_check(actual.clone()).to_string()
}
pub(crate) fn widen_for_check(u: Type) -> Type {
let mut out = Type::empty();
for atomic in u.types {
let widened = match atomic {
Atomic::TLiteralInt(_) | Atomic::TIntRange { .. } => Atomic::TInt,
Atomic::TLiteralString(_) => Atomic::TString,
Atomic::TLiteralFloat(_, _) => Atomic::TFloat,
other => other,
};
out.add_type(widened);
}
out
}