Skip to content

Commit e71407d

Browse files
committed
fix(compilation): avoid value-field key rescans
analyze_table_field only needs eager member registration for resolved [expr] = value fields. Value fields and static keys are already registered during table-declaration analysis, so calling get_field_key() here just recomputed positional indices and rescanned large array tables. Skip that work, keep the dynamic-key path inline, and add regressions for the maxwellhome-like large-array case and for pairs() preserving integer keys on value fields.
1 parent 6ac29b6 commit e71407d

3 files changed

Lines changed: 154 additions & 71 deletions

File tree

crates/emmylua_code_analysis/src/compilation/analyzer/lua/stats.rs

Lines changed: 68 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -480,82 +480,79 @@ pub fn analyze_local_func_stat(
480480
Some(())
481481
}
482482

483-
fn register_expr_key_member(analyzer: &mut LuaAnalyzer, field: &LuaTableField) {
484-
// Register expression-key members early so table-decl inference (and pairs)
485-
// can see them even when the table itself has no explicit generic type.
486-
let Some(field_key) = field.get_field_key() else {
487-
return;
488-
};
489-
let LuaIndexKey::Expr(_) = &field_key else {
490-
return;
491-
};
492-
let member_id = LuaMemberId::new(field.get_syntax_id(), analyzer.file_id);
493-
if analyzer
494-
.db
495-
.get_member_index()
496-
.get_member(&member_id)
497-
.is_some()
498-
{
499-
return;
483+
/// Analyzes an assignment-style table field.
484+
///
485+
/// Table-declaration analysis already registers static keys and value fields, for
486+
/// example `{ name = value }`, `{ ["name"] = value }`, `{ [1] = value }`, and
487+
/// `{ value1, value2 }`.
488+
///
489+
/// This pass binds the field value type and eagerly materializes resolved
490+
/// bracket-key members such as `{ [key] = value }`, `{ [true] = value }`, or
491+
/// `{ [SomeEnum.A] = value }` so later consumers like table inference and
492+
/// `pairs` can see them before the unresolved table-field pass runs.
493+
pub fn analyze_table_field(analyzer: &mut LuaAnalyzer, field: LuaTableField) -> Option<()> {
494+
if !field.is_assign_field() {
495+
return Some(());
500496
}
501-
let cache = analyzer
502-
.context
503-
.infer_manager
504-
.get_infer_cache(analyzer.file_id);
505-
let Ok(member_key) = LuaMemberKey::from_index_key(analyzer.db, cache, &field_key) else {
506-
return;
507-
};
508-
if matches!(member_key, LuaMemberKey::ExprType(ref typ) if typ.is_unknown()) {
509-
return;
497+
498+
if let Some(field_key) = field.get_field_key() {
499+
if let LuaIndexKey::Expr(_) = &field_key {
500+
// Decl analysis leaves `[expr] = value` fields unresolved. If the key
501+
// already resolves here, materialize the member now.
502+
let db = &mut *analyzer.db;
503+
let member_id = LuaMemberId::new(field.get_syntax_id(), analyzer.file_id);
504+
if db.get_member_index().get_member(&member_id).is_none() {
505+
let cache = analyzer
506+
.context
507+
.infer_manager
508+
.get_infer_cache(analyzer.file_id);
509+
if let Ok(member_key) = LuaMemberKey::from_index_key(db, cache, &field_key) {
510+
if !matches!(member_key, LuaMemberKey::ExprType(ref typ) if typ.is_unknown()) {
511+
if let Some(table_expr) = field.get_parent::<LuaTableExpr>() {
512+
let owner_id = LuaMemberOwner::Element(InFiled::new(
513+
analyzer.file_id,
514+
table_expr.get_range(),
515+
));
516+
let decl_feature = if analyzer.context.metas.contains(&analyzer.file_id)
517+
{
518+
LuaMemberFeature::MetaDefine
519+
} else {
520+
LuaMemberFeature::FileDefine
521+
};
522+
let member = LuaMember::new(member_id, member_key, decl_feature, None);
523+
db.get_member_index_mut().add_member(owner_id, member);
524+
}
525+
}
526+
}
527+
}
528+
}
510529
}
511-
let Some(table_expr) = field.get_parent::<LuaTableExpr>() else {
512-
return;
513-
};
514-
let owner_id = LuaMemberOwner::Element(InFiled::new(analyzer.file_id, table_expr.get_range()));
515-
let decl_feature = if analyzer.context.metas.contains(&analyzer.file_id) {
516-
LuaMemberFeature::MetaDefine
517-
} else {
518-
LuaMemberFeature::FileDefine
519-
};
520-
let member = LuaMember::new(member_id, member_key, decl_feature, None);
521-
analyzer
522-
.db
523-
.get_member_index_mut()
524-
.add_member(owner_id, member);
525-
}
526530

527-
pub fn analyze_table_field(analyzer: &mut LuaAnalyzer, field: LuaTableField) -> Option<()> {
528-
register_expr_key_member(analyzer, &field);
529-
530-
if field.is_assign_field() {
531-
let value_expr = field.get_value_expr()?;
532-
let member_id = LuaMemberId::new(field.get_syntax_id(), analyzer.file_id);
533-
let value_type = match analyzer.infer_expr(&value_expr.clone()) {
534-
Ok(value_type) => match value_type {
535-
LuaType::Def(ref_id) => LuaType::Ref(ref_id),
536-
_ => value_type,
537-
},
538-
Err(InferFailReason::None) => LuaType::Unknown,
539-
Err(reason) => {
540-
let unresolve = UnResolveMember {
541-
file_id: analyzer.file_id,
542-
member_id,
543-
expr: Some(value_expr.clone()),
544-
prefix: None,
545-
ret_idx: 0,
546-
};
531+
let value_expr = field.get_value_expr()?;
532+
let member_id = LuaMemberId::new(field.get_syntax_id(), analyzer.file_id);
533+
let value_type = match analyzer.infer_expr(&value_expr.clone()) {
534+
Ok(value_type) => match value_type {
535+
LuaType::Def(ref_id) => LuaType::Ref(ref_id),
536+
_ => value_type,
537+
},
538+
Err(InferFailReason::None) => LuaType::Unknown,
539+
Err(reason) => {
540+
let unresolve = UnResolveMember {
541+
file_id: analyzer.file_id,
542+
member_id,
543+
expr: Some(value_expr.clone()),
544+
prefix: None,
545+
ret_idx: 0,
546+
};
547547

548-
analyzer.context.add_unresolve(unresolve.into(), reason);
549-
return None;
550-
}
551-
};
548+
analyzer.context.add_unresolve(unresolve.into(), reason);
549+
return None;
550+
}
551+
};
552+
553+
let cache = LuaTypeCache::InferType(value_type);
554+
bind_type(analyzer.db, member_id.into(), cache);
552555

553-
bind_type(
554-
analyzer.db,
555-
member_id.into(),
556-
LuaTypeCache::InferType(value_type),
557-
);
558-
}
559556
Some(())
560557
}
561558

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
#[cfg(test)]
22
mod test {
33
use crate::{DiagnosticCode, LuaType, VirtualWorkspace};
4+
use emmylua_parser::{LuaAstToken, LuaLocalName};
45

56
const STACKED_TYPE_GUARDS: usize = 180;
67
const LARGE_LINEAR_ASSIGNMENT_STEPS: usize = 2048;
8+
const MAXWELLHOME_ARRAY_VALUES: usize = 2048;
79

810
#[test]
911
fn test_closure_return() {
@@ -388,6 +390,47 @@ mod test {
388390
assert_eq!(ws.humanize_type(after_assign), "integer");
389391
}
390392

393+
#[test]
394+
fn test_issue_1028_maxwellhome_like_large_array_builds_semantic_model() {
395+
let mut ws = VirtualWorkspace::new();
396+
let mut block = String::from(
397+
r#"
398+
---@type integer
399+
local tile = ({
400+
layers = {
401+
{
402+
data = {
403+
"#,
404+
);
405+
406+
for i in 0..MAXWELLHOME_ARRAY_VALUES {
407+
block.push_str(&format!(" {},\n", i % 3));
408+
}
409+
410+
block.push_str(
411+
r#"
412+
},
413+
},
414+
},
415+
}).layers[1].data[1024]
416+
"#,
417+
);
418+
419+
let file_id = ws.def_file("maxwellhome.lua", &block);
420+
let semantic_model = ws
421+
.analysis
422+
.compilation
423+
.get_semantic_model(file_id)
424+
.expect("expected semantic model for maxwellhome-like large array stress case");
425+
let local_name = ws.get_node::<LuaLocalName>(file_id);
426+
let token = local_name.get_name_token().expect("name token must exist");
427+
let info = semantic_model
428+
.get_semantic_info(token.syntax().clone().into())
429+
.expect("semantic info must exist");
430+
431+
assert_eq!(ws.humanize_type(info.typ), "integer");
432+
}
433+
391434
#[test]
392435
fn test_pending_replay_order_uses_type_guard_before_self_return_cast_lookup() {
393436
let mut ws = VirtualWorkspace::new();

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,49 @@ mod test {
168168
assert_eq!(ws.expr_ty("key_out"), LuaType::String);
169169
}
170170

171+
#[test]
172+
fn test_pairs_value_field_integer_keys() {
173+
let mut ws = VirtualWorkspace::new_with_init_std_lib();
174+
175+
ws.def(
176+
r#"
177+
local t = { 10, 20, 30 }
178+
179+
for k, v in pairs(t) do
180+
key_out = k
181+
value_out = v
182+
end
183+
"#,
184+
);
185+
186+
let key_out = ws.expr_ty("key_out");
187+
let value_out = ws.expr_ty("value_out");
188+
let LuaType::Union(key_union) = key_out else {
189+
panic!("expected integer key union, got {:?}", key_out);
190+
};
191+
let LuaType::Union(value_union) = value_out else {
192+
panic!("expected value union, got {:?}", value_out);
193+
};
194+
195+
let expected_keys: HashSet<_> = vec![
196+
LuaType::IntegerConst(1),
197+
LuaType::IntegerConst(2),
198+
LuaType::IntegerConst(3),
199+
]
200+
.into_iter()
201+
.collect();
202+
let expected_values: HashSet<_> = vec![
203+
LuaType::DocIntegerConst(10),
204+
LuaType::DocIntegerConst(20),
205+
LuaType::DocIntegerConst(30),
206+
]
207+
.into_iter()
208+
.collect();
209+
210+
assert_eq!(key_union.into_set(), expected_keys);
211+
assert_eq!(value_union.into_set(), expected_values);
212+
}
213+
171214
#[test]
172215
fn test_issue_291() {
173216
let mut ws = VirtualWorkspace::new_with_init_std_lib();

0 commit comments

Comments
 (0)