Skip to content

Commit b13c0c8

Browse files
committed
fix: stop flow inference from hanging on large self-assignments
Log a warning and return unknown when a single flow query exceeds the step budget. This keeps semantic model construction moving for pathological repeated assignments while preserving normal flow precision. Add a development/test switch that can turn the fallback off when a runaway flow query needs to be reproduced. Fixes #1114 Assisted-by: Codex
1 parent dca540c commit b13c0c8

6 files changed

Lines changed: 207 additions & 27 deletions

File tree

crates/emmylua_code_analysis/src/compilation/analyzer/infer_cache_manager.rs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,27 @@
11
use hashbrown::HashMap;
22

3-
use crate::{FileId, LuaAnalysisPhase, semantic::LuaInferCache};
3+
use crate::{CacheOptions, FileId, LuaAnalysisPhase, semantic::LuaInferCache};
44

55
#[derive(Debug, Default)]
66
pub struct InferCacheManager {
77
infer_map: HashMap<FileId, LuaInferCache>,
8+
cache_options: CacheOptions,
89
}
910

1011
impl InferCacheManager {
11-
pub fn new() -> Self {
12+
pub fn new(cache_options: CacheOptions) -> Self {
1213
InferCacheManager {
1314
infer_map: HashMap::new(),
15+
cache_options,
1416
}
1517
}
1618

1719
pub fn get_infer_cache(&mut self, file_id: FileId) -> &mut LuaInferCache {
18-
self.infer_map.entry(file_id).or_insert_with(|| {
19-
LuaInferCache::new(
20-
file_id,
21-
crate::CacheOptions {
22-
analysis_phase: LuaAnalysisPhase::Ordered,
23-
},
24-
)
25-
})
20+
let mut cache_options = self.cache_options;
21+
cache_options.analysis_phase = LuaAnalysisPhase::Ordered;
22+
self.infer_map
23+
.entry(file_id)
24+
.or_insert_with(|| LuaInferCache::new(file_id, cache_options))
2625
}
2726

2827
pub fn set_force(&mut self) {

crates/emmylua_code_analysis/src/compilation/analyzer/mod.rs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ mod lua;
77
mod unresolve;
88

99
use crate::{
10-
Emmyrc, FileId, InFiled, InferFailReason, LuaType, LuaTypeDeclId,
10+
CacheOptions, Emmyrc, FileId, InFiled, InferFailReason, LuaType, LuaTypeDeclId,
1111
db_index::{DbIndex, WorkspaceId},
1212
profile::Profile,
1313
};
@@ -27,12 +27,17 @@ where
2727
lua::func_body::analyze_func_body_missing_return_flags_with(body, infer_expr_type)
2828
}
2929

30-
pub fn analyze(db: &mut DbIndex, need_analyzed_files: Vec<InFiled<LuaChunk>>, config: Arc<Emmyrc>) {
30+
pub fn analyze(
31+
db: &mut DbIndex,
32+
need_analyzed_files: Vec<InFiled<LuaChunk>>,
33+
config: Arc<Emmyrc>,
34+
cache_options: CacheOptions,
35+
) {
3136
if need_analyzed_files.is_empty() {
3237
return;
3338
}
3439

35-
let contexts = module_analyze(db, need_analyzed_files, config);
40+
let contexts = module_analyze(db, need_analyzed_files, config, cache_options);
3641

3742
for (workspace_id, mut context) in contexts {
3843
context.workspace_id = Some(workspace_id);
@@ -58,6 +63,7 @@ fn module_analyze(
5863
db: &mut DbIndex,
5964
need_analyzed_files: Vec<InFiled<LuaChunk>>,
6065
config: Arc<Emmyrc>,
66+
cache_options: CacheOptions,
6167
) -> Vec<(WorkspaceId, AnalyzeContext)> {
6268
if need_analyzed_files.len() == 1 {
6369
let in_filed_tree = need_analyzed_files[0].clone();
@@ -75,11 +81,11 @@ fn module_analyze(
7581
.get_module_index_mut()
7682
.add_module_by_path(file_id, path_str);
7783
let workspace_id = workspace_id.unwrap_or(WorkspaceId::MAIN);
78-
let mut context = AnalyzeContext::new(config);
84+
let mut context = AnalyzeContext::new(config, cache_options);
7985
context.add_tree_chunk(in_filed_tree);
8086
return vec![(workspace_id, context)];
8187
} else if db.get_vfs().is_remote_file(&file_id) {
82-
let mut context = AnalyzeContext::new(config);
88+
let mut context = AnalyzeContext::new(config, cache_options);
8389
context.add_tree_chunk(in_filed_tree);
8490
return vec![(WorkspaceId::REMOTE, context)];
8591
};
@@ -118,14 +124,14 @@ fn module_analyze(
118124

119125
let mut contexts = Vec::new();
120126
if let Some(std_lib) = file_tree_map.remove(&WorkspaceId::STD) {
121-
let mut context = AnalyzeContext::new(config.clone());
127+
let mut context = AnalyzeContext::new(config.clone(), cache_options);
122128
context.tree_list = std_lib;
123129
contexts.push((WorkspaceId::STD, context));
124130
}
125131

126132
let mut main_vec = Vec::new();
127133
for (workspace_id, tree_list) in file_tree_map {
128-
let mut context = AnalyzeContext::new(config.clone());
134+
let mut context = AnalyzeContext::new(config.clone(), cache_options);
129135
context.tree_list = tree_list;
130136
if workspace_id.is_library() || workspace_id.is_remote() {
131137
contexts.push((workspace_id, context));
@@ -153,13 +159,13 @@ pub struct AnalyzeContext {
153159
}
154160

155161
impl AnalyzeContext {
156-
pub fn new(emmyrc: Arc<Emmyrc>) -> Self {
162+
pub fn new(emmyrc: Arc<Emmyrc>, cache_options: CacheOptions) -> Self {
157163
Self {
158164
tree_list: Vec::new(),
159165
config: emmyrc,
160166
metas: HashSet::new(),
161167
unresolves: Vec::new(),
162-
infer_manager: InferCacheManager::new(),
168+
infer_manager: InferCacheManager::new(cache_options),
163169
workspace_id: None,
164170
pending_type_generic_headers: Vec::new(),
165171
}

crates/emmylua_code_analysis/src/compilation/mod.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ mod test;
44
use std::sync::Arc;
55

66
use crate::{
7-
Emmyrc, FileId, InFiled, InferFailReason, LuaIndex, LuaInferCache, LuaType, db_index::DbIndex,
8-
semantic::SemanticModel,
7+
CacheOptions, Emmyrc, FileId, InFiled, InferFailReason, LuaIndex, LuaInferCache, LuaType,
8+
db_index::DbIndex, semantic::SemanticModel,
99
};
1010
use emmylua_parser::{LuaBlock, LuaExpr};
1111

@@ -23,21 +23,23 @@ where
2323
pub struct LuaCompilation {
2424
db: DbIndex,
2525
emmyrc: Arc<Emmyrc>,
26+
cache_options: CacheOptions,
2627
}
2728

2829
impl LuaCompilation {
2930
pub fn new(emmyrc: Arc<Emmyrc>) -> Self {
3031
let mut compilation = Self {
3132
db: DbIndex::new(),
3233
emmyrc: emmyrc.clone(),
34+
cache_options: CacheOptions::default(),
3335
};
3436

3537
compilation.db.update_config(emmyrc.clone());
3638
compilation
3739
}
3840

3941
pub fn get_semantic_model(&'_ self, file_id: FileId) -> Option<SemanticModel<'_>> {
40-
let cache = LuaInferCache::new(file_id, Default::default());
42+
let cache = LuaInferCache::new(file_id, self.cache_options);
4143
let tree = self.db.get_vfs().get_syntax_tree(&file_id)?;
4244
Some(SemanticModel::new(
4345
file_id,
@@ -64,7 +66,12 @@ impl LuaCompilation {
6466
});
6567
}
6668

67-
analyzer::analyze(&mut self.db, need_analyzed_files, self.emmyrc.clone());
69+
analyzer::analyze(
70+
&mut self.db,
71+
need_analyzed_files,
72+
self.emmyrc.clone(),
73+
self.cache_options,
74+
);
6875
}
6976

7077
pub fn remove_index(&mut self, file_ids: Vec<FileId>) {
@@ -83,6 +90,10 @@ impl LuaCompilation {
8390
&mut self.db
8491
}
8592

93+
pub fn get_cache_options_mut(&mut self) -> &mut CacheOptions {
94+
&mut self.cache_options
95+
}
96+
8697
pub fn update_config(&mut self, config: Arc<Emmyrc>) {
8798
self.emmyrc = config.clone();
8899
self.db.update_config(config);

crates/emmylua_code_analysis/src/compilation/test/flow.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod test {
88
const LARGE_LINEAR_ASSIGNMENT_STEPS: usize = 2048;
99
const MAXWELLHOME_ARRAY_VALUES: usize = 2048;
1010
const ISSUE_1100_HIGHLIGHT_GROUPS: usize = 2048;
11+
const ISSUE_1114_REPEATED_ASSIGNMENT_STEPS: usize = 512;
1112

1213
#[test]
1314
fn test_closure_return() {
@@ -469,6 +470,53 @@ mod test {
469470
assert_eq!(ws.humanize_type(after_assign), "integer");
470471
}
471472

473+
#[test]
474+
#[timeout(5000)]
475+
fn test_issue_1114_repeated_self_dependent_assignments_build_semantic_model() {
476+
let cases = [
477+
(
478+
"concat",
479+
r#"local value = """#,
480+
"value = value .. config.pic[idx][index]",
481+
),
482+
(
483+
"add",
484+
"local value = 0",
485+
"value = value + config.pic[idx][index]",
486+
),
487+
(
488+
"comparison",
489+
"local value = true",
490+
"value = value == config.pic[idx][index]",
491+
),
492+
];
493+
494+
for (name, init, assignment) in cases {
495+
let mut ws = VirtualWorkspace::new();
496+
let repeated_assignments =
497+
format!("{assignment}\n").repeat(ISSUE_1114_REPEATED_ASSIGNMENT_STEPS);
498+
let block = format!(
499+
r#"
500+
function f(idx, index)
501+
{init}
502+
{repeated_assignments}
503+
return value
504+
end
505+
"#
506+
);
507+
508+
let file_id = ws.def(&block);
509+
510+
assert!(
511+
ws.analysis
512+
.compilation
513+
.get_semantic_model(file_id)
514+
.is_some(),
515+
"expected semantic model for repeated self-dependent {name} assignment"
516+
);
517+
}
518+
}
519+
472520
#[test]
473521
#[timeout(5000)]
474522
fn test_issue_1094_self_call_fallback_stress() {

crates/emmylua_code_analysis/src/semantic/cache/cache_options.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
1-
#[derive(Debug)]
1+
#[derive(Debug, Clone, Copy)]
22
pub struct CacheOptions {
33
pub analysis_phase: LuaAnalysisPhase,
4+
// Development/test escape hatch: keep flow inference unbounded so budget
5+
// blowups can be reproduced instead of hidden behind `unknown`.
6+
pub disable_flow_inference_step_budget: bool,
47
}
58

69
impl Default for CacheOptions {
710
fn default() -> Self {
811
Self {
912
analysis_phase: LuaAnalysisPhase::Ordered,
13+
disable_flow_inference_step_budget: false,
1014
}
1115
}
1216
}
1317

14-
#[derive(Debug)]
18+
#[derive(Debug, Clone, Copy)]
1519
pub enum LuaAnalysisPhase {
1620
// Ordered phase
1721
Ordered,

0 commit comments

Comments
 (0)