diff --git a/crates/emmylua_code_analysis/resources/schema.json b/crates/emmylua_code_analysis/resources/schema.json index 2b39d3c8d..e21869acd 100644 --- a/crates/emmylua_code_analysis/resources/schema.json +++ b/crates/emmylua_code_analysis/resources/schema.json @@ -182,6 +182,11 @@ "type": "string", "const": "type-not-found" }, + { + "description": "Type evaluation exceeded its resource budget", + "type": "string", + "const": "type-complexity" + }, { "description": "Missing return statement", "type": "string", diff --git a/crates/emmylua_code_analysis/src/check/checker/attribute_check.rs b/crates/emmylua_code_analysis/src/check/checker/attribute_check.rs index e8eb5a81b..e3c0ac91c 100644 --- a/crates/emmylua_code_analysis/src/check/checker/attribute_check.rs +++ b/crates/emmylua_code_analysis/src/check/checker/attribute_check.rs @@ -274,29 +274,19 @@ fn add_type_check_diagnostic( arg_type: &LuaType, param_type: &LuaType, ) { - match check_type_detail(semantic_model, arg_type, param_type) { - Ok(()) => {} - Err(reason) => { - let reason_message = match reason { - TypeCheckFailReason::TypeNotMatchWithReason(reason) => reason, - TypeCheckFailReason::TypeNotMatch => String::new(), - TypeCheckFailReason::TypeRecursion => "type recursion".to_string(), - }; - let reason = if reason_message.is_empty() { - String::new() - } else { - format!(" {}", reason_message) - }; - context.add_diagnostic( - DiagnosticCode::AttributeParamTypeMismatch, - range, - t!( - "expected `%{expected}` but found `%{found}`.%{reason}", - expected = humanize_type(semantic_model, param_type), - found = humanize_type(semantic_model, arg_type), - reason = reason - ), - ); - } - } + let reason = match check_type_detail(semantic_model, arg_type, param_type) { + Err(TypeCheckFailReason::TypeNotMatchWithReason(reason)) => format!(" {reason}"), + Err(TypeCheckFailReason::TypeNotMatch) => String::new(), + _ => return, + }; + context.add_diagnostic( + DiagnosticCode::AttributeParamTypeMismatch, + range, + t!( + "expected `%{expected}` but found `%{found}`.%{reason}", + expected = humanize_type(semantic_model, param_type), + found = humanize_type(semantic_model, arg_type), + reason = reason + ), + ); } diff --git a/crates/emmylua_code_analysis/src/check/checker/check_field.rs b/crates/emmylua_code_analysis/src/check/checker/check_field.rs index 61e00ae6b..35cf26271 100644 --- a/crates/emmylua_code_analysis/src/check/checker/check_field.rs +++ b/crates/emmylua_code_analysis/src/check/checker/check_field.rs @@ -10,6 +10,7 @@ use emmylua_parser::{ use crate::DiagnosticCode; use crate::semantic_db::def::{SemanticId, TypeDef, TypeDefKind}; use crate::semantic_model::SemanticModel; +use crate::semantic_model::type_eval::expand_alias_branches; use crate::{LuaMemberKey, LuaType, LuaTypeDeclId, LuaUnionType}; use super::assign_type_mismatch::signature_matches_key; @@ -485,7 +486,7 @@ fn check_against_type( // MissingFields: required (non-nullable) members are missing. let mut missing: Vec<_> = members .iter() - .filter(|(key, ty)| !field_keys.contains(*key) && !is_optional(ty)) + .filter(|(key, ty)| !field_keys.contains(*key) && !is_optional(semantic_model, ty)) .map(|(key, _)| key.to_path()) .collect(); missing.sort_unstable(); @@ -577,8 +578,9 @@ fn find_type_def(semantic_model: &SemanticModel<'_>, type_def_id: &SemanticId) - } /// Whether the field is optional (nullable / unknown). -fn is_optional(ty: &LuaType) -> bool { - match ty { +fn is_optional(semantic_model: &SemanticModel<'_>, ty: &LuaType) -> bool { + let ty = expand_alias_branches(semantic_model, ty).unwrap_or_else(|| ty.clone()); + match &ty { LuaType::Any | LuaType::Unknown | LuaType::Nil => true, LuaType::Union(union) => union.into_vec().iter().any(|t| matches!(t, LuaType::Nil)), _ => false, diff --git a/crates/emmylua_code_analysis/src/check/checker/duplicate_field.rs b/crates/emmylua_code_analysis/src/check/checker/duplicate_field.rs index c0441df24..11a6b8aa9 100644 --- a/crates/emmylua_code_analysis/src/check/checker/duplicate_field.rs +++ b/crates/emmylua_code_analysis/src/check/checker/duplicate_field.rs @@ -12,6 +12,7 @@ use crate::FileId; use crate::LuaType; use crate::semantic_db::def::{Member, SemanticId}; use crate::semantic_model::SemanticModel; +use crate::semantic_model::type_eval::expand_alias_head; use super::{CheckContext, Checker}; @@ -163,7 +164,7 @@ fn required_module_file( semantic_model.module_file_of(&module_name) } -/// Whether a runtime member is a function/closure definition. +/// Whether a member resolves to a function or has a closure value. fn is_function_like(semantic_model: &SemanticModel<'_>, member: &Member) -> bool { if let Some(value_syntax) = member.value_syntax && let Some(tree) = semantic_model.syntax_tree() @@ -172,10 +173,13 @@ fn is_function_like(semantic_model: &SemanticModel<'_>, member: &Member) -> bool { return true; } - semantic_model.type_of_member(&member.id).is_some_and(|ty| { - matches!( - ty, - LuaType::DocFunction(_) | LuaType::Signature(_) | LuaType::Function - ) - }) + semantic_model + .type_of_member(&member.id) + .and_then(|ty| expand_alias_head(semantic_model, &ty)) + .is_some_and(|ty| { + matches!( + ty, + LuaType::DocFunction(_) | LuaType::Signature(_) | LuaType::Function + ) + }) } diff --git a/crates/emmylua_code_analysis/src/check/checker/mod.rs b/crates/emmylua_code_analysis/src/check/checker/mod.rs index 3beb6584e..a149d639e 100644 --- a/crates/emmylua_code_analysis/src/check/checker/mod.rs +++ b/crates/emmylua_code_analysis/src/check/checker/mod.rs @@ -259,6 +259,25 @@ pub fn check_file(semantic_model: &SemanticModel<'_>, config: Arc) run_checked::(&mut context, semantic_model); run_checked::(&mut context, semantic_model); run_checked::(&mut context, semantic_model); + let mut ranges: Vec<_> = semantic_model + .type_evaluation_errors() + .into_iter() + .map(|origin| { + origin + .and_then(|id| semantic_model.type_def_of(&id)) + .filter(|def| def.file_id == semantic_model.file_id()) + .map_or(TextRange::empty(0.into()), |def| def.name_range) + }) + .collect(); + ranges.sort_unstable_by_key(|range| (range.start(), range.end())); + ranges.dedup(); + for range in ranges { + context.add_diagnostic( + DiagnosticCode::TypeComplexity, + range, + t!("Type evaluation is too complex or possibly infinite."), + ); + } context.into_diagnostics() } diff --git a/crates/emmylua_code_analysis/src/check/diagnostic_code.rs b/crates/emmylua_code_analysis/src/check/diagnostic_code.rs index f100e5cfe..8f6f463a1 100644 --- a/crates/emmylua_code_analysis/src/check/diagnostic_code.rs +++ b/crates/emmylua_code_analysis/src/check/diagnostic_code.rs @@ -15,6 +15,8 @@ pub enum DiagnosticCode { DocSyntaxError, /// Type not found TypeNotFound, + /// Type evaluation exceeded its resource budget + TypeComplexity, /// Missing return statement MissingReturn, /// Param Type not match diff --git a/crates/emmylua_code_analysis/src/check/test/assign_type_mismatch_test.rs b/crates/emmylua_code_analysis/src/check/test/assign_type_mismatch_test.rs index d1c090465..26795936d 100644 --- a/crates/emmylua_code_analysis/src/check/test/assign_type_mismatch_test.rs +++ b/crates/emmylua_code_analysis/src/check/test/assign_type_mismatch_test.rs @@ -1906,6 +1906,9 @@ return t ---@class B ---@class C ---@class D + ---@alias Pair A | B + ---@alias TooWide A | B | C | D + ---@alias Identity T ---@param a A | B | C ---@return boolean @@ -1914,21 +1917,23 @@ return t end "#, ); - assert!(!ws.has_no_diagnostic( - DiagnosticCode::AssignTypeMismatch, - r#" - ---@type fun(a: A | B | C | D): boolean - local tmp = condition - "#, - )); - - assert!(ws.has_no_diagnostic( - DiagnosticCode::AssignTypeMismatch, - r#" - ---@type fun(a: A | B): boolean - local tmp = condition - "#, - )); + for (parameter, accepted) in [ + ("A | B | C | D", false), + ("TooWide", false), + ("Identity", false), + ("A | B", true), + ("Pair", true), + ("Identity", true), + ] { + assert_eq!( + ws.has_no_diagnostic( + DiagnosticCode::AssignTypeMismatch, + &format!("---@type fun(a: {parameter}): boolean\nlocal tmp = condition"), + ), + accepted, + "{parameter}" + ); + } } #[test] diff --git a/crates/emmylua_code_analysis/src/check/test/duplicate_field_test.rs b/crates/emmylua_code_analysis/src/check/test/duplicate_field_test.rs index 7fcd58a2a..65ca39d91 100644 --- a/crates/emmylua_code_analysis/src/check/test/duplicate_field_test.rs +++ b/crates/emmylua_code_analysis/src/check/test/duplicate_field_test.rs @@ -66,18 +66,44 @@ mod test { "# )); - assert!(ws.has_no_diagnostic_in_namespace( - DiagnosticCode::DuplicateDocField, - r#" - ---@class Test - ---@field a fun() - ---@field a fun() - local Test = {} - - function Test.a() - end - "# - )); + for (first, second, overloads) in [ + ( + "fun(value: string): string", + "fun(value: integer): integer", + true, + ), + ("Callback", "Callback", true), + ( + "Identity>", + "Identity>", + true, + ), + ("Choose", "Choose<'fixed'>", true), + ("Identity", "Identity", false), + ("Callback", "Identity", false), + ] { + assert_eq!( + ws.has_no_diagnostic_in_namespace( + DiagnosticCode::DuplicateDocField, + &format!( + r#" + ---@alias Callback fun(value: T): T + ---@alias Identity T + ---@alias Choose T extends string and Callback or integer + ---@class Test + ---@field a {first} + ---@field a {second} + local Test = {{}} + + function Test.a() + end + "# + ) + ), + overloads, + "field types: {first}, {second}" + ); + } assert!(!ws.has_no_diagnostic_in_namespace( DiagnosticCode::DuplicateSetField, diff --git a/crates/emmylua_code_analysis/src/check/test/missing_fields_test.rs b/crates/emmylua_code_analysis/src/check/test/missing_fields_test.rs index 9c471db31..be51491bc 100644 --- a/crates/emmylua_code_analysis/src/check/test/missing_fields_test.rs +++ b/crates/emmylua_code_analysis/src/check/test/missing_fields_test.rs @@ -340,22 +340,46 @@ foo({}) } #[test] - fn test_multiline_union_nil_field_is_optional() { - let mut ws = VirtualWorkspace::new(); - assert!(ws.has_no_diagnostic( - DiagnosticCode::MissingFields, - r#" + fn test_alias_field_optionality() { + for (field_type, optional) in [ + ("PersonAge", true), + ("Maybe", true), + ("Wrapped", true), + ("Identity", true), + ("Optional", true), + ("Identity", false), + ("Optional", false), + ("Identity", false), + ] { + let mut ws = VirtualWorkspace::new(); + assert_eq!( + ws.has_no_diagnostic( + DiagnosticCode::MissingFields, + &format!( + r#" ---@alias PersonAge --- | integer --- | nil + ---@alias Maybe T | nil + ---@alias Wrapped Maybe | T + ---@alias Identity T + ---@alias Optional T extends string and nil or T + + ---@class Node + ---@field next Node ---@class Person ---@field name string - ---@field age PersonAge + ---@field age {field_type} ---@type Person - local person = { name = "123" } + local person = {{ name = "123" }} "# - )); + ) + ), + optional, + "field type: {field_type}" + ); + } } } diff --git a/crates/emmylua_code_analysis/src/check/test/mod.rs b/crates/emmylua_code_analysis/src/check/test/mod.rs index 94f378922..3020a1014 100644 --- a/crates/emmylua_code_analysis/src/check/test/mod.rs +++ b/crates/emmylua_code_analysis/src/check/test/mod.rs @@ -34,6 +34,7 @@ mod param_type_check_test; mod readonly_test; mod redefined_local_test; mod redundant_parameter_test; +mod reentry_test; mod require_module_visibility_test; mod return_type_mismatch_test; mod syntax_error_test; diff --git a/crates/emmylua_code_analysis/src/check/test/param_type_check_test.rs b/crates/emmylua_code_analysis/src/check/test/param_type_check_test.rs index 354a00a5a..a8132e312 100644 --- a/crates/emmylua_code_analysis/src/check/test/param_type_check_test.rs +++ b/crates/emmylua_code_analysis/src/check/test/param_type_check_test.rs @@ -1680,23 +1680,39 @@ mod test { #[test] fn test_index_access_with_keyof_alias() { let mut ws = VirtualWorkspace::new(); - assert!(ws.has_no_diagnostic( - DiagnosticCode::ParamTypeMismatch, + ws.def( r#" ---@class A ---@field one 1 - + ---@class B + ---@field one string + ---@alias Key 'one' ---@alias KeyofA keyof A - ---@type A[KeyofA] - local tmp - - ---@param v 1 - local function test(v) - end - - test(tmp) - "#, - )); + ---@alias KeyofB keyof B + "#, + ); + for indexed in [ + "A[KeyofA]", + "A[Key]", + "A[KeyofB]", + "RawGet", + "RawGet", + "[1, string][0]", + "RawGet<[1, string], 0>", + ] { + for (parameter, accepted) in [("1", true), ("string", false)] { + assert_eq!( + ws.has_no_diagnostic( + DiagnosticCode::ParamTypeMismatch, + &format!( + "---@type {indexed}\nlocal value\n---@param v {parameter}\nlocal function accept(v) end\naccept(value)" + ), + ), + accepted, + "{indexed} -> {parameter}" + ); + } + } } #[test] diff --git a/crates/emmylua_code_analysis/src/check/test/reentry_test.rs b/crates/emmylua_code_analysis/src/check/test/reentry_test.rs new file mode 100644 index 000000000..574ff6d15 --- /dev/null +++ b/crates/emmylua_code_analysis/src/check/test/reentry_test.rs @@ -0,0 +1,373 @@ +use super::check_source; +use crate::DiagnosticCode; + +#[test] +fn recursive_type_queries_keep_unrelated_assignment_error() { + for definitions in [ + r#" + ---@alias Lookup RawGet + ---@class Owner + ---@field next Lookup> + ---@alias Input Lookup> + "#, + r#" + ---@alias Input Filter> + ---@alias Filter T extends string and T or T[] + "#, + r#" + ---@class Owner + ---@field next Input + ---@alias Input RawGet, 'next'> + "#, + r#" + ---@class Owner + ---@field next RawGet, 'next'> + ---@alias Input RawGet, 'next'> + "#, + ] { + let diagnostics = check_source(&format!( + r#" + {definitions} + ---@param value table> + local function accept(value) return value end + accept({{name = {{}}}}) + ---@type string + local unrelated = 42 + return unrelated + "#, + )); + assert!( + diagnostics + .iter() + .any(|d| d.code == DiagnosticCode::TypeComplexity), + "{definitions}: {diagnostics:?}" + ); + assert!( + diagnostics.iter().any(|d| { + d.code == DiagnosticCode::AssignTypeMismatch + && d.message.contains("string") + && d.message.contains("42") + }), + "{definitions}: {diagnostics:?}" + ); + } +} + +#[test] +fn long_binary_expressions_keep_assignment_checks() { + let expression = std::iter::repeat_n("'a'", 512) + .collect::>() + .join(" .. "); + for (target, expected) in [ + ("string", vec![]), + ("integer", vec![DiagnosticCode::AssignTypeMismatch]), + ] { + let diagnostics = check_source(&format!( + "---@type {target}\nlocal value = {expression}\nreturn value" + )); + assert_eq!( + diagnostics.iter().map(|d| d.code).collect::>(), + expected, + "{diagnostics:?}" + ); + } +} + +#[test] +fn long_member_expressions_keep_assignment_checks() { + let expression = format!("value{}", ".child".repeat(256)); + for (target, expected) in [ + ("Node", vec![]), + ("integer", vec![DiagnosticCode::AssignTypeMismatch]), + ] { + let diagnostics = check_source(&format!( + "---@class Node\n---@field child Node\n---@param value Node\nlocal function read(value)\n---@type {target}\nlocal result = {expression}\nreturn result\nend\nreturn read" + )); + assert_eq!( + diagnostics.iter().map(|d| d.code).collect::>(), + expected, + "{diagnostics:?}" + ); + } +} + +#[test] +fn recursive_calls_keep_unrelated_assignment_error() { + let diagnostics = check_source( + r#" + ---@param ... boolean + local function recurse(...) + if ... then return 1, 'done' end + return 1, recurse(true) + end + local result = recurse(false) + ---@type string + local wrong = 42 + return result, wrong + "#, + ); + assert_eq!( + diagnostics.iter().map(|d| d.code).collect::>(), + [DiagnosticCode::AssignTypeMismatch], + "{diagnostics:?}" + ); +} + +#[test] +fn cyclic_fields_keep_independent_member_checks() { + for record in [ + "---@alias Record {field: T, cycle: Cycle}", + "---@alias Record {field: T, next?: Record}", + "---@alias Record First>", + "---@alias Record Second, T>", + "---@alias Record Forward>", + "---@alias Record ForwardSecond, T>", + "---@alias Record Forward>, Cycle>", + ] { + for (target, expected) in [ + ("string", vec![]), + ("integer", vec![DiagnosticCode::ReturnTypeMismatch]), + ] { + let diagnostics = check_source(&format!( + r#" + ---@alias Cycle Filter> + ---@alias Filter T extends string and T or T[] + ---@alias First {{field: A}} + ---@alias Second {{field: B}} + ---@alias Forward First + ---@alias ForwardSecond Second + ---@alias Identity T + {record} + ---@param value Record + ---@return {target} + local function read(value) + return value.field + end + return read + "#, + )); + assert_eq!( + diagnostics.iter().map(|d| d.code).collect::>(), + expected, + "{diagnostics:?}" + ); + } + } +} + +#[test] +fn long_alias_chains_keep_return_checks() { + let aliases = (0..512) + .map(|i| format!("---@alias Alias{i} Alias{}\n", i + 1)) + .collect::(); + for (source, target, expected) in [ + ("Alias0", "string", vec![]), + ( + "Alias0", + "integer", + vec![DiagnosticCode::ReturnTypeMismatch], + ), + ("string", "Alias0", vec![]), + ( + "integer", + "Alias0", + vec![DiagnosticCode::ReturnTypeMismatch], + ), + ] { + let diagnostics = check_source(&format!( + "{aliases}---@alias Alias512 string\n---@param value {source}\n---@return {target}\nlocal function read(value)\nreturn value\nend\nreturn read" + )); + assert_eq!( + diagnostics.iter().map(|d| d.code).collect::>(), + expected, + "{source} -> {target}: {diagnostics:?}" + ); + } +} + +#[test] +fn conditional_inference_resolves_lazy_alias_fields() { + for (value, expected) in [ + ("42", vec![]), + ("true", vec![DiagnosticCode::AssignTypeMismatch]), + ] { + let diagnostics = check_source(&format!( + r#" + ---@alias Items T[] + ---@alias Wrapped {{items: Items}} + ---@alias Element T extends {{items: (infer U)[]}} and integer or boolean + ---@type Element> + local result = {value} + return result + "#, + )); + assert_eq!( + diagnostics.iter().map(|d| d.code).collect::>(), + expected, + "{diagnostics:?}" + ); + } +} + +#[test] +fn lazy_callable_fields_keep_argument_checks() { + for (field, nullable) in [ + ("Choose", false), + ("Identity>", false), + ("Maybe | nil", true), + ("Wrapped", true), + ] { + for (argument, expected) in [ + ("42", vec![]), + ("true", vec![DiagnosticCode::ParamTypeMismatch]), + ("", vec![DiagnosticCode::MissingParameter]), + ] { + let diagnostics = check_source(&format!( + r#" + ---@alias Choose T extends string and fun(value: integer): string or boolean + ---@alias Identity T + ---@alias Maybe Choose | nil + ---@alias Wrapped Maybe | nil + ---@class Box + ---@field callback {field} + ---@param box Box + local function call(box) + return box.callback({argument}) + end + return call + "#, + )); + let mut expected = expected; + if nullable { + expected.push(DiagnosticCode::NeedCheckNil); + } + assert_eq!( + diagnostics.iter().map(|d| d.code).collect::>(), + expected, + "{field}, {argument}: {diagnostics:?}" + ); + } + } +} + +#[test] +fn lazy_union_fields_keep_nil_checks() { + for field in ["Wrapped", "Nested"] { + let diagnostics = check_source(&format!( + r#" + ---@alias Nullable T | nil + ---@alias Wrapped Nullable | T + ---@alias Nested Wrapped | T + ---@class Value + ---@field name string + ---@class Box + ---@field value {field} + ---@param box Box + ---@return string + local function read(box) + return box.value.name + end + return read + "#, + )); + assert_eq!( + diagnostics.iter().map(|d| d.code).collect::>(), + [DiagnosticCode::NeedCheckNil], + "{field}: {diagnostics:?}" + ); + } +} + +#[test] +fn lazy_callable_fields_keep_function_generic_scope() { + for field in ["fun(value: U): U", "Identity(value: U): U>"] { + for (target, expected) in [ + ("string", vec![]), + ("integer", vec![DiagnosticCode::AssignTypeMismatch]), + ] { + let diagnostics = check_source(&format!( + r#" + ---@alias Identity T + ---@class Box + ---@field callback {field} + ---@param box Box + local function call(box) + local result = box.callback('hello') + ---@type {target} + local checked = result + return checked + end + return call + "#, + )); + assert_eq!( + diagnostics.iter().map(|d| d.code).collect::>(), + expected, + "{field}, {target}: {diagnostics:?}" + ); + } + } +} + +#[test] +fn lazy_optional_fields_keep_nil_in_assignments() { + for (target, expected) in [ + ("integer?", vec![]), + ("integer", vec![DiagnosticCode::AssignTypeMismatch]), + ] { + let diagnostics = check_source(&format!( + r#" + ---@alias Identity T + ---@class Box + ---@field value? Identity + ---@param box Box + local function read(box) + ---@type {target} + local result = box.value + return result + end + return read + "#, + )); + assert_eq!( + diagnostics.iter().map(|d| d.code).collect::>(), + expected, + "{target}: {diagnostics:?}" + ); + } +} + +#[test] +fn lazy_alias_fields_keep_omission_checks_in_intersections() { + for (field, expected) in [ + ("Maybe", vec![]), + ("Maybe | boolean", vec![]), + ( + "Identity", + vec![DiagnosticCode::AssignTypeMismatch], + ), + ] { + let diagnostics = check_source(&format!( + r#" + ---@alias Maybe T | nil + ---@alias Identity T + ---@class Empty + ---@class Extra + ---@class Part + ---@field value {field} + ---@param value Empty + local function copy(value) + ---@type Part & Extra + local result = value + return result + end + return copy + "#, + )); + assert_eq!( + diagnostics.iter().map(|d| d.code).collect::>(), + expected, + "{field}: {diagnostics:?}" + ); + } +} diff --git a/crates/emmylua_code_analysis/src/semantic_db/def/lua_type/types/complex.rs b/crates/emmylua_code_analysis/src/semantic_db/def/lua_type/types/complex.rs index c76206664..2308db9c1 100644 --- a/crates/emmylua_code_analysis/src/semantic_db/def/lua_type/types/complex.rs +++ b/crates/emmylua_code_analysis/src/semantic_db/def/lua_type/types/complex.rs @@ -1118,10 +1118,11 @@ impl Hash for LuaMultiLineUnion { } } -#[derive(Debug, Clone, Hash, PartialEq, Eq)] +#[derive(Debug, Clone)] pub struct LuaArrayType { base: LuaType, len: LuaArrayLen, + hash: OnceCell, } #[derive(Debug, Clone, Hash, PartialEq, Eq)] @@ -1133,16 +1134,17 @@ pub enum LuaArrayLen { impl LuaArrayType { pub fn new(base: LuaType, len: LuaArrayLen) -> Self { - Self { base, len } - } - - pub fn from_base_type(base: LuaType) -> Self { Self { base, - len: LuaArrayLen::None, + len, + hash: OnceCell::new(), } } + pub fn from_base_type(base: LuaType) -> Self { + Self::new(base, LuaArrayLen::None) + } + pub fn get_base(&self) -> &LuaType { &self.base } @@ -1156,6 +1158,30 @@ impl LuaArrayType { } } +impl PartialEq for LuaArrayType { + fn eq(&self, other: &Self) -> bool { + if self + .hash + .get() + .zip(other.hash.get()) + .is_some_and(|(left, right)| left != right) + { + return false; + } + self.base == other.base && self.len == other.len + } +} + +impl Eq for LuaArrayType {} + +impl Hash for LuaArrayType { + fn hash(&self, state: &mut H) { + self.hash + .get_or_init(|| cached_type_hash((&self.base, &self.len))) + .hash(state); + } +} + #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct LuaConditionalType { checked_type: LuaType, @@ -1246,9 +1272,21 @@ impl LuaMappedType { #[cfg(test)] mod tests { - use std::ptr; + use std::{hash::BuildHasher, ptr}; + + use rustc_hash::FxBuildHasher; - use crate::{LuaMultiLineUnion, LuaObjectType, LuaType, LuaUnionType}; + use crate::{LuaArrayType, LuaMultiLineUnion, LuaObjectType, LuaType, LuaUnionType}; + + #[test] + fn array_types_keep_structural_identity_after_hashing() { + let array = LuaArrayType::from_base_type(LuaType::String); + let equal = LuaArrayType::from_base_type(LuaType::String); + + let hash = FxBuildHasher.hash_one(&array); + assert_eq!(array, equal); + assert_eq!(FxBuildHasher.hash_one(&equal), hash); + } #[test] fn test_union_iterator() { diff --git a/crates/emmylua_code_analysis/src/semantic_model/access.rs b/crates/emmylua_code_analysis/src/semantic_model/access.rs index 5e719e848..a9f87df27 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/access.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/access.rs @@ -8,9 +8,12 @@ impl<'db> SemanticModel<'db> { pub fn lua_version(&self) -> Option { self.db().lua_version() } - /// A model for any file on the same analysis database. + /// A file view that shares this model's query results and recursion state. pub fn model_for(&self, file_id: FileId) -> Self { - SemanticModel::new(self.db(), file_id) + Self { + view: self.analysis().file(file_id), + cache: self.cache.clone(), + } } pub fn file_id(&self) -> FileId { self.view.file_id() @@ -530,14 +533,15 @@ impl<'db> SemanticModel<'db> { /// same-file member -> same-file class `@field` -> cross-file runtime member (resolve owner -> merge members). /// Cached `callable_functions` result for a callee type. pub(crate) fn callable_functions_cached(&self, ty: &LuaType) -> Vec { - if let Some(cached) = self.cache.borrow().callable_functions.get(ty) { + let key = (self.file_id(), ty.clone()); + if let Some(cached) = self.cache.borrow().callable_functions.get(&key) { return cached.clone(); } + let revision = self.cache.borrow().revision(); let value = callable_functions(self, ty); - self.cache - .borrow_mut() - .callable_functions - .insert(ty.clone(), value.clone()); + self.cache.publish_if_complete(revision, |cache| { + cache.callable_functions.insert(key, value.clone()); + }); value } /// Unified callable candidates for a name callee. @@ -629,11 +633,13 @@ impl<'db> SemanticModel<'db> { { return cached.clone(); } + let revision = self.cache.borrow().revision(); let value = callable_candidates_uncached(self, callee); - self.cache - .borrow_mut() - .callable_candidates - .insert((file_id, syntax), value.clone()); + self.cache.publish_if_complete(revision, |cache| { + cache + .callable_candidates + .insert((file_id, syntax), value.clone()); + }); value } pub(crate) fn call_site_analysis(&self, call_expr: &LuaCallExpr) -> CallSiteAnalysis { @@ -642,11 +648,11 @@ impl<'db> SemanticModel<'db> { if let Some(cached) = self.cache.borrow().call_site.get(&(file_id, syntax)) { return cached.clone(); } + let revision = self.cache.borrow().revision(); let analysis = self.call_site_analysis_uncached(call_expr); - self.cache - .borrow_mut() - .call_site - .insert((file_id, syntax), analysis.clone()); + self.cache.publish_if_complete(revision, |cache| { + cache.call_site.insert((file_id, syntax), analysis.clone()); + }); analysis } pub(crate) fn call_site_analysis_uncached(&self, call_expr: &LuaCallExpr) -> CallSiteAnalysis { @@ -716,6 +722,7 @@ impl<'db> SemanticModel<'db> { { return cached.clone(); } + let revision = self.cache.borrow().revision(); let analysis = self.call_site_analysis(call_expr); let signatures = resolved_call_signatures( self, @@ -725,10 +732,11 @@ impl<'db> SemanticModel<'db> { analysis.colon_call, &analysis.receiver_ty, ); - self.cache - .borrow_mut() - .call_site_signatures - .insert((file_id, syntax), signatures.clone()); + self.cache.publish_if_complete(revision, |cache| { + cache + .call_site_signatures + .insert((file_id, syntax), signatures.clone()); + }); signatures } pub fn resolve_member(&self, index_expr: &LuaIndexExpr) -> Option { @@ -737,11 +745,13 @@ impl<'db> SemanticModel<'db> { if let Some(cached) = self.cache.borrow().resolve_member.get(&(file_id, syntax)) { return cached.clone(); } + let revision = self.cache.borrow().revision(); let result = self.resolve_member_impl(index_expr); - self.cache - .borrow_mut() - .resolve_member - .insert((file_id, syntax), result.clone()); + self.cache.publish_if_complete(revision, |cache| { + cache + .resolve_member + .insert((file_id, syntax), result.clone()); + }); result } pub(crate) fn resolve_member_impl(&self, index_expr: &LuaIndexExpr) -> Option { diff --git a/crates/emmylua_code_analysis/src/semantic_model/cache.rs b/crates/emmylua_code_analysis/src/semantic_model/cache.rs index 84677430c..a143d39cd 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/cache.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/cache.rs @@ -1,9 +1,10 @@ -//! Per-`SemanticModel` local cache. +//! Request-local cache shared across `SemanticModel` file views. //! -//! A model owns one cache for its lifetime. Recursion state is represented by -//! `CacheEntry::InProgress` so re-entry checks stay O(1). +//! A root model and its `model_for` views share one cache. Recursion state is +//! represented by `CacheEntry::InProgress` so re-entry checks stay O(1). -use hashbrown::HashMap; +use hashbrown::{HashMap, HashSet}; +use std::cell::Cell; use emmylua_parser::{LuaExpr, LuaSyntaxId}; use rowan::TextSize; @@ -13,19 +14,154 @@ use crate::member_key::LuaMemberKey; use crate::semantic_db::MemberList; use crate::semantic_db::def::SemanticId; use crate::semantic_db::flow::FlowId; -use crate::{FileId, LuaFunctionType, LuaType, LuaTypeDeclId}; +use crate::{FileId, LuaFunctionType, LuaType, LuaTypeDeclId, LuaTypeNode}; use super::infer::unify::TplBindings; use super::member::MemberInfo; use super::{CallSiteAnalysis, ResolvedMember}; +/// Keep provisional memo entries while dependent queries still need them. +/// Discard them at the outer query boundary so the next query can retry. +#[must_use = "keep the scope alive until the query finishes"] +pub(super) struct QueryCacheScope<'a> { + cache: &'a super::LazyCache, + revision: u64, +} + +impl<'a> QueryCacheScope<'a> { + pub(super) fn new(cache: &'a super::LazyCache) -> Self { + let mut state = cache.borrow_mut(); + if state.query_depth == 0 { + state.evaluation_work = 0; + state.evaluation_exhausted = false; + state.evaluation_origin = None; + } + state.query_depth += 1; + Self { + cache, + revision: state.revision(), + } + } +} + +/// Share the resource budget across alias evaluation and type comparisons. +/// Queue-driven recursion uses the work budget. Nested semantic queries also +/// have a stack guard. Exhaustion is reported separately from type mismatch. +pub(super) struct TypeEvaluationScope<'a> { + query: QueryCacheScope<'a>, +} + +impl<'a> TypeEvaluationScope<'a> { + pub(super) fn new(cache: &'a super::LazyCache) -> Option { + let query = QueryCacheScope::new(cache); + let mut state = cache.borrow_mut(); + const MAX_EVALUATION_DEPTH: usize = 64; + if state.evaluation_depth >= MAX_EVALUATION_DEPTH || state.evaluation_exhausted { + state.exhaust_evaluation(); + return None; + } + state.evaluation_depth += 1; + Some(Self { query }) + } + + /// Charge the types inspected or rebuilt by one evaluator operation. + pub(super) fn visit(&self, ty: &LuaType) -> bool { + let mut cache = self.query.cache.borrow_mut(); + if cache.evaluation_origin.is_none() { + cache.evaluation_origin = match ty { + LuaType::Ref(id) | LuaType::Def(id) => Some(id.clone()), + LuaType::Generic(generic) => Some(generic.get_base_type_id()), + _ => None, + }; + } + let mut pending = vec![ty]; + const MAX_TYPE_VISITS: usize = 1_000_000; + while let Some(ty) = pending.pop() { + if cache.evaluation_exhausted || cache.evaluation_work >= MAX_TYPE_VISITS { + cache.exhaust_evaluation(); + return false; + } + cache.evaluation_work += 1; + ty.push_direct_children(&mut pending); + } + true + } +} + +impl Drop for TypeEvaluationScope<'_> { + fn drop(&mut self) { + self.query.cache.borrow_mut().evaluation_depth -= 1; + } +} + +impl Drop for QueryCacheScope<'_> { + fn drop(&mut self) { + let mut cache = self.cache.borrow_mut(); + cache.query_depth -= 1; + if cache.query_depth == 0 && !cache.is_complete(self.revision) { + cache.discard_provisional(); + } + } +} + #[derive(Debug, Clone)] pub(crate) enum CacheEntry { InProgress, Ready(T), + /// An unfinished result reused only within the current outer query. + Provisional(T), } -/// Memo key for one declaration-trace state: `(declaration, flow node, options, mode)`. +impl CacheEntry { + fn is_ready(&self) -> bool { + matches!(self, Self::Ready(_)) + } + + pub(crate) fn finish(value: T, complete: bool) -> Self { + if complete { + Self::Ready(value) + } else { + Self::Provisional(value) + } + } + + /// An unfinished comparison must not become proof of either outcome. + pub(crate) fn get_complete<'a>(&'a self, cache: &SemanticLocalCache) -> Option<&'a T> { + if let Self::Ready(value) = self { + Some(value) + } else { + cache.mark_incomplete(); + None + } + } + + pub(crate) fn get<'a>(&'a self, cache: &SemanticLocalCache) -> Option<&'a T> { + match self { + Self::Ready(value) => Some(value), + Self::InProgress => { + cache.mark_incomplete(); + None + } + Self::Provisional(value) => { + cache.mark_incomplete(); + Some(value) + } + } + } + + /// Base expression/declaration/member inference uses empty recursive reads + /// as seeds (for example, `value = value or {}`). Preserve that rule while + /// propagating provisional results from dependent semantic queries. + pub(crate) fn get_inference_seed<'a>(&'a self, cache: &SemanticLocalCache) -> Option<&'a T> { + if matches!(self, Self::InProgress) { + None + } else { + self.get(cache) + } + } +} + +/// Memo key for one declaration-trace state: `(file, declaration, flow node, options, mode)`. /// /// The incoming path is deliberately absent. It is safe to omit only for flow /// graphs without loops, where every route to a flow node crosses the same @@ -33,6 +169,7 @@ pub(crate) enum CacheEntry { /// memo only in that case and recomputes otherwise. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub(crate) struct TraceStateKey { + pub(crate) file_id: FileId, pub(crate) decl: SemanticId, pub(crate) flow_id: u32, pub(crate) guards: bool, @@ -49,11 +186,23 @@ pub(crate) struct TypeDefCache { #[derive(Default)] pub(crate) struct SemanticLocalCache { + /// Changes whenever a query reads an unfinished dependency. Most queries + /// use this to decide whether a result can be cached. Type comparisons + /// track completeness in their result, including discarded alternatives. + incomplete_revision: Cell, + query_depth: usize, + evaluation_depth: usize, + evaluation_work: usize, + pub(crate) evaluation_exhausted: bool, + evaluation_origin: Option, + pub(crate) evaluation_errors: HashSet>, + /// Closure returns depend on the VM environment: track active queries only. + pub(crate) closure_return_in_progress: HashSet<(FileId, LuaSyntaxId)>, pub(crate) expr_type: HashMap<(FileId, LuaSyntaxId), CacheEntry>, pub(crate) decl_type: HashMap<(FileId, SemanticId), CacheEntry>>, pub(crate) member_type: HashMap<(FileId, SemanticId), CacheEntry>>, pub(crate) resolve_member: HashMap<(FileId, LuaSyntaxId), Option>, - pub(crate) expr_type_at: HashMap<(FileId, LuaSyntaxId, TextSize), LuaType>, + pub(crate) expr_type_at: HashMap<(FileId, LuaSyntaxId, TextSize), CacheEntry>, pub(crate) resolve_name: HashMap<(FileId, TextSize), Option>, pub(crate) resolve_owner_set: HashMap>, pub(crate) member_type_at: HashMap<(FileId, SemanticId, TextSize), LuaType>, @@ -70,13 +219,48 @@ pub(crate) struct SemanticLocalCache { pub(crate) call_site: HashMap<(FileId, LuaSyntaxId), CallSiteAnalysis>, pub(crate) call_site_signatures: HashMap<(FileId, LuaSyntaxId), Vec<(LuaFunctionType, TplBindings)>>, - pub(crate) member_infos: HashMap>, + pub(crate) member_infos: HashMap<(FileId, LuaType), Vec>, pub(crate) members_of_owner: HashMap, pub(crate) members_of_owner_named: HashMap<(SemanticId, SmolStr), MemberList>, - pub(crate) member_info: HashMap<(LuaType, LuaMemberKey), Option>, - pub(crate) type_check: HashMap<(LuaType, LuaType), bool>, - pub(crate) callable_functions: HashMap>, - pub(crate) alias_targets: HashMap>, - + pub(crate) member_info: HashMap<(FileId, LuaType, LuaMemberKey), Option>, + pub(crate) type_check: HashMap<(FileId, LuaType, LuaType, bool), CacheEntry>, + pub(crate) callable_functions: HashMap<(FileId, LuaType), Vec>, + /// Active alias instances, shared with queries called during expansion. + pub(crate) expanding_aliases: HashSet, + pub(crate) alias_targets: HashMap>>, + pub(crate) alias_heads: HashMap<(FileId, LuaType), CacheEntry>>, pub(crate) type_defs: HashMap, } + +impl SemanticLocalCache { + fn exhaust_evaluation(&mut self) { + self.evaluation_exhausted = true; + self.evaluation_errors + .insert(self.evaluation_origin.clone()); + self.mark_incomplete(); + } + + fn discard_provisional(&mut self) { + self.expr_type.retain(|_, entry| entry.is_ready()); + self.decl_type.retain(|_, entry| entry.is_ready()); + self.member_type.retain(|_, entry| entry.is_ready()); + self.expr_type_at.retain(|_, entry| entry.is_ready()); + self.trace_state.retain(|_, entry| entry.is_ready()); + self.type_check.retain(|_, entry| entry.is_ready()); + self.alias_targets.retain(|_, entry| entry.is_ready()); + self.alias_heads.retain(|_, entry| entry.is_ready()); + } + + pub(crate) fn revision(&self) -> u64 { + self.incomplete_revision.get() + } + + pub(crate) fn mark_incomplete(&self) { + self.incomplete_revision + .set(self.incomplete_revision.get().wrapping_add(1)); + } + + pub(crate) fn is_complete(&self, revision: u64) -> bool { + self.revision() == revision + } +} diff --git a/crates/emmylua_code_analysis/src/semantic_model/doc_types.rs b/crates/emmylua_code_analysis/src/semantic_model/doc_types.rs index b9b6a0ae3..60f4ca043 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/doc_types.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/doc_types.rs @@ -4,14 +4,24 @@ use crate::{LuaArrayType, LuaConditionalType, LuaGenericType, LuaMappedType}; impl<'db> SemanticModel<'db> { /// Alias target type (after projection; generic parameter references keep `TplRef`, and instantiation is substituted by the caller). pub fn alias_target(&self, def: &TypeDef) -> Option { - if let Some(cached) = self.cache.borrow().alias_targets.get(&def.id) { - return cached.clone(); + { + let cache = self.cache.borrow(); + if let Some(entry) = cache.alias_targets.get(&def.id) { + return entry.get(&cache).cloned().flatten(); + } } - let result = self.alias_target_uncached(def); + let _query = cache::QueryCacheScope::new(&self.cache); + // Projection can re-enter semantic queries. Publish the guard before + // starting it, and release the cache borrow before calling the query. self.cache .borrow_mut() .alias_targets - .insert(def.id.clone(), result.clone()); + .insert(def.id.clone(), cache::CacheEntry::InProgress); + let revision = self.cache.borrow().revision(); + let result = self.alias_target_uncached(def); + let mut cache = self.cache.borrow_mut(); + let entry = cache::CacheEntry::finish(result.clone(), cache.is_complete(revision)); + cache.alias_targets.insert(def.id.clone(), entry); result } pub(crate) fn alias_target_uncached(&self, def: &TypeDef) -> Option { diff --git a/crates/emmylua_code_analysis/src/semantic_model/flow.rs b/crates/emmylua_code_analysis/src/semantic_model/flow.rs index b776012f2..c8f651c83 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/flow.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/flow.rs @@ -425,22 +425,13 @@ pub fn type_of_member_before_at( .unwrap_or_else(fallback) } -/// Flow-sensitive type of an expression at `offset`: NameExpr → decl flow type; IndexExpr → member flow type; -/// other expressions fall back to ordinary inference. -pub fn type_of_expr_at( +/// Evaluate one flow expression after its operands and prefix are cached. +pub(crate) fn type_of_expr_at_node( model: &SemanticModel, - expr_syntax: emmylua_parser::LuaSyntaxId, + expr: LuaExpr, offset: TextSize, ) -> LuaType { - let Some(tree) = model.syntax_tree() else { - return model.type_of_expr(expr_syntax); - }; - let Some(node) = expr_syntax.to_node_from_root(&tree.get_red_root()) else { - return model.type_of_expr(expr_syntax); - }; - let Some(expr) = LuaExpr::cast(node) else { - return model.type_of_expr(expr_syntax); - }; + let expr_syntax = expr.get_syntax_id(); match &expr { LuaExpr::NameExpr(name_expr) => { if let Some(decl) = model.resolve_name(name_expr.get_position()) { @@ -497,7 +488,7 @@ pub fn type_of_expr_at( && let Some(prefix) = index_expr.get_prefix_expr() && let LuaExpr::NameExpr(_) = &prefix { - let prefix_ty = type_of_expr_at(model, prefix.get_syntax_id(), offset); + let prefix_ty = model.type_of_expr_at(prefix.get_syntax_id(), offset); if matches!( &prefix_ty, LuaType::Ref(_) | LuaType::Def(_) | LuaType::Generic(_) @@ -520,7 +511,7 @@ pub fn type_of_expr_at( && let Some(member_def) = member_facts.member_by_id(&member_id) && model.is_initializer_table_field(&member_id, member_def) { - let prefix_ty = type_of_expr_at(model, prefix.get_syntax_id(), offset); + let prefix_ty = model.type_of_expr_at(prefix.get_syntax_id(), offset); if let Some(value_syntax) = member_def.value_syntax { let expr_ty = model.type_of_expr(value_syntax); match expr_ty { @@ -561,7 +552,7 @@ pub fn type_of_expr_at( if let Some(key) = member_key_from_index_expr(model, index_expr) && let Some(prefix) = index_expr.get_prefix_expr() { - let prefix_ty = type_of_expr_at(model, prefix.get_syntax_id(), offset); + let prefix_ty = model.type_of_expr_at(prefix.get_syntax_id(), offset); if let Some(prefix_member_ty) = model.member_type(&prefix_ty, &key) && !matches!(prefix_member_ty, LuaType::Unknown | LuaType::Any) && (member_type_more_specific(model, &prefix_member_ty, &member_ty) @@ -607,14 +598,14 @@ pub fn type_of_expr_at( BinaryOperator::OpAnd | BinaryOperator::OpOr ) && let Some(tuple_ty) = array_table_literal_tuple_type(model, &right_expr) { - let left_ty = type_of_expr_at(model, left_expr.get_syntax_id(), offset); + let left_ty = model.type_of_expr_at(left_expr.get_syntax_id(), offset); let result = logical_binary_type(model, op_token.get_op(), left_ty, tuple_ty); return apply_expr_casts(model, expr_syntax, result, All); } // General binary expressions: subexpressions first use flow-sensitive types (member/variable branch narrowing), // then merge with the same VM operator rules, ensuring `right.value + 1` uses the narrow type of `right.value` in the branch. - let left_ty = type_of_expr_at(model, left_expr.get_syntax_id(), offset); - let right_ty = type_of_expr_at(model, right_expr.get_syntax_id(), offset); + let left_ty = model.type_of_expr_at(left_expr.get_syntax_id(), offset); + let right_ty = model.type_of_expr_at(right_expr.get_syntax_id(), offset); // If either operand in an arithmetic expression fails to infer (unresolved global/field), treat it as an "inference error": // don't return the VM's fallback number; let the assignment layer keep the variable's previously known type. if matches!( @@ -1509,6 +1500,7 @@ fn trace_decl( return trace_decl_uncached(model, decl, tree, flow_id, options, mode, visited, path); } let key = cache::TraceStateKey { + file_id: model.file_id(), decl: decl.clone(), flow_id: flow_id.0, guards: options.guards, @@ -1516,15 +1508,23 @@ fn trace_decl( assignments: options.assignments, merge_branch: matches!(mode, TraceMode::MergeBranch), }; - match model.trace_state_cache_get(&key) { - Some(cache::CacheEntry::Ready(cached)) => return cached, - // Not expected on a loop-free graph; treat defensively as a miss so the - // result is always computed rather than silently dropped. - Some(cache::CacheEntry::InProgress) | None => {} + { + let cache = model.cache.borrow(); + if let Some(entry) = cache.trace_state.get(&key) { + return entry.get(&cache).cloned().flatten(); + } } - model.trace_state_cache_mark_in_progress(key.clone()); + let _query = cache::QueryCacheScope::new(&model.cache); + model + .cache + .borrow_mut() + .trace_state + .insert(key.clone(), cache::CacheEntry::InProgress); + let revision = model.cache.borrow().revision(); let result = trace_decl_uncached(model, decl, tree, flow_id, options, mode, visited, path); - model.trace_state_cache_store(key, result.clone()); + let mut cache = model.cache.borrow_mut(); + let entry = cache::CacheEntry::finish(result.clone(), cache.is_complete(revision)); + cache.trace_state.insert(key, entry); result } @@ -3626,12 +3626,11 @@ fn return_cast_for_call( if let Some(cached) = model.cache.borrow().return_cast.get(&key) { return cached.clone(); } + let revision = model.cache.borrow().revision(); let result = return_cast_for_call_uncached(model, call); - model - .cache - .borrow_mut() - .return_cast - .insert(key, result.clone()); + model.cache.publish_if_complete(revision, |cache| { + cache.return_cast.insert(key, result.clone()); + }); result } diff --git a/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/semantic_generic_test.rs b/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/semantic_generic_test.rs index 7598e02a2..c7c83a73f 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/semantic_generic_test.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/semantic_generic_test.rs @@ -452,20 +452,25 @@ result = { #[test] fn test_nested_function_generic_shadows_outer_function_generic() { - let mut ws = VirtualWorkspace::new(); - ws.def( - r#" + for (return_type, callable) in [ + ("fun(value: T): T", "returned"), + ("{callback: fun(value: T): T}", "returned.callback"), + ] { + let mut ws = VirtualWorkspace::new(); + ws.def(&format!( + r#" ---@generic T ---@param value T - ---@return fun(value: T): T + ---@return {return_type} function make(value) end - local fn = make("outer") - result = fn(1) - "#, - ); + local returned = make("outer") + result = {callable}(1) + "#, + )); - assert_eq!(ws.expr_ty("result"), ws.ty("integer")); + assert_eq!(ws.expr_ty("result"), ws.ty("integer"), "{return_type}"); + } } } diff --git a/crates/emmylua_code_analysis/src/semantic_model/infer/test.rs b/crates/emmylua_code_analysis/src/semantic_model/infer/test.rs index 07910c2b3..f53c601f1 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/infer/test.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/infer/test.rs @@ -1411,6 +1411,36 @@ fn test_require_literal_vm() { ); } +#[test] +fn test_cross_file_member_cycles_keep_independent_fields() { + for (value, expected) in [ + ("42", LuaType::IntegerConst(42)), + ("a.value", LuaType::Unknown), + ] { + let mut ws = crate::VirtualWorkspace::new(); + ws.def_files(vec![ + ( + "a.lua", + "local b = require('b')\nreturn { value = b.value, known = 'a' }", + ), + ( + "b.lua", + &format!("local a = require('a')\nreturn {{ value = {value}, known = 42 }}"), + ), + ]); + let file_id = ws.def("local value = require('a').value\nlocal known = require('b').known"); + let model = ws.analysis.semantic_model(file_id); + assert_eq!( + model.type_of_decl(&decl_of(&model, "value")), + Some(expected) + ); + assert_eq!( + model.type_of_decl(&decl_of(&model, "known")), + Some(LuaType::IntegerConst(42)) + ); + } +} + // ────────────────────────────────────────────── // is_reference_to / is_visible // ────────────────────────────────────────────── diff --git a/crates/emmylua_code_analysis/src/semantic_model/infer/vm.rs b/crates/emmylua_code_analysis/src/semantic_model/infer/vm.rs index 1ce33991b..559a04e21 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/infer/vm.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/infer/vm.rs @@ -26,8 +26,8 @@ use crate::semantic_db::types::PrimitiveType; use crate::semantic_model::flow::{ExprCastMode, type_of_expr_with_cast}; use crate::semantic_model::member::{MemberInfo, table_const_class_type, table_metatable_type}; use crate::semantic_model::type_eval::{ - class_generic_map, eval_conditionals, expand_alias_generic, sanitize_unresolved_generics, - sanitize_unresolved_generics_with_model, substitute_named_refs, + class_generic_map, eval_conditionals, expand_alias_generic, expand_alias_head, + sanitize_unresolved_generics, sanitize_unresolved_generics_with_model, substitute_named_refs, }; use crate::{ AsyncState, FileId, GenericTpl, GenericTplId, InFiled, LuaAliasCallKind, LuaArrayType, @@ -1030,7 +1030,16 @@ impl<'a> InferVm<'a> { None } - fn index_member(&mut self, mut owner: Value, key: &LuaMemberKey) -> Value { + fn index_member(&mut self, owner: Value, key: &LuaMemberKey) -> Value { + let mut value = self.index_member_impl(owner, key); + // Member projection only substitutes arguments. Evaluate the result + // here, where the expression consumes it, rather than during lookup. + value.ty = expand_alias_generic(self.model, &value.ty); + value.ty = eval_conditionals(self.model, &value.ty); + value + } + + fn index_member_impl(&mut self, mut owner: Value, key: &LuaMemberKey) -> Value { let receiver_ty = Some(owner.ty.clone()); // On `any` (including alias cycles that converge to any), any field is still any. if owner.ty.is_any() { @@ -3099,7 +3108,8 @@ impl<'a> InferVm<'a> { visited.push(id.clone()); let mut out = Vec::new(); if let Some(def) = self.model.type_def_of(id) - && let Some(target) = self.model.alias_target(&def) + && def.kind == TypeDefKind::Alias + && let Some(target) = expand_alias_head(self.model, ty) { out = self.expand_callable_types(&target, visited); } @@ -3151,16 +3161,9 @@ impl<'a> InferVm<'a> { let mut out = Vec::new(); if let Some(def) = self.model.type_def_of(&base_id) && def.kind == TypeDefKind::Alias - && let Some(target) = self.model.alias_target(&def) + && let Some(target) = expand_alias_head(self.model, ty) { - let bindings: unify::TplBindings = generic - .get_params() - .iter() - .enumerate() - .map(|(index, ty)| (GenericTplId::Type(index as u32), ty.clone())) - .collect(); - let instantiated = unify::substitute(&target, &bindings); - out = self.expand_callable_types(&instantiated, visited); + out = self.expand_callable_types(&target, visited); } if out.is_empty() && let Some(def) = self.model.type_def_of(&base_id) @@ -3201,6 +3204,11 @@ impl<'a> InferVm<'a> { visited.pop(); out } + LuaType::Call(_) | LuaType::Conditional(_) | LuaType::Mapped(_) => { + expand_alias_head(self.model, ty) + .map(|expanded| self.expand_callable_types(&expanded, visited)) + .unwrap_or_default() + } _ => Vec::new(), } } diff --git a/crates/emmylua_code_analysis/src/semantic_model/member.rs b/crates/emmylua_code_analysis/src/semantic_model/member.rs index 3f6e0db23..057e55a88 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/member.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/member.rs @@ -5,9 +5,7 @@ use hashbrown::HashMap; use crate::semantic_db::def::{DeclKind, MemberRef, SemanticId, TypeDef, TypeDefKind}; use crate::semantic_model::infer::unify::{self, TplBindings}; -use crate::semantic_model::type_eval::{ - eval_conditionals, expand_alias_generic, substitute_named_refs, -}; +use crate::semantic_model::type_eval::{map_type, substitute_named_refs, with_expanded_alias}; use crate::{FileId, GenericTplId, InFiled, LuaMemberKey, LuaType}; use crate::{Member, semantic_db::facts::FileFacts}; use smol_str::SmolStr; @@ -173,32 +171,6 @@ fn inherited_base_types( .collect() } -/// Follows alias references until reaching a non-alias named type/object/array/string. -/// A local visited set prevents recursive aliases (`A -> B -> A`) from looping forever. -fn direct_member_info_alias( - model: &SemanticModel, - prefix_type: &LuaType, - key: &LuaMemberKey, -) -> Option { - let mut current = prefix_type.clone(); - let mut visited = Vec::new(); - loop { - let id = match ¤t { - LuaType::Ref(id) | LuaType::Def(id) => id.clone(), - _ => return direct_member_info(model, ¤t, key), - }; - if visited.contains(&id) { - return None; - } - let def = model.type_def_of(&id)?; - if def.kind != TypeDefKind::Alias { - return direct_member_info(model, ¤t, key); - } - visited.push(id.clone()); - current = model.alias_target(&def)?; - } -} - fn direct_member_info( model: &SemanticModel, prefix_type: &LuaType, @@ -208,7 +180,10 @@ fn direct_member_info( LuaType::Ref(id) | LuaType::Def(id) => { let def = model.type_def_of(id)?; if def.kind == TypeDefKind::Alias { - return direct_member_info_alias(model, prefix_type, key); + return with_expanded_alias(model, prefix_type, |expanded| { + direct_member_info(model, expanded, key) + }) + .flatten(); } let mut visited = Vec::new(); // Bare generic type (`Box`) uses its declared defaults/constraints for member lookup. @@ -248,8 +223,10 @@ fn direct_member_info( LuaType::Generic(generic) => { let def = model.type_def_of(&generic.get_base_type_id())?; if def.kind == TypeDefKind::Alias { - let expanded = expand_alias_generic(model, prefix_type); - return direct_member_info(model, &expanded, key); + return with_expanded_alias(model, prefix_type, |expanded| { + direct_member_info(model, expanded, key) + }) + .flatten(); } let bindings: TplBindings = generic .get_params() @@ -586,25 +563,19 @@ fn collect_members( // Named type: @field + inheritance + runtime value members. LuaType::Ref(decl_id) | LuaType::Def(decl_id) => { if let Some(def) = model.type_def_of(decl_id) { - // Alias: collect members of the alias target instead of looking for `@field` on the alias - // definition itself. The visited set also guards recursive aliases. + // Keep the alias active through traversal of recursive structures. if def.kind == TypeDefKind::Alias { - if visited.contains(&def.id) { - return; - } - visited.push(def.id.clone()); - if let Some(target) = model.alias_target(&def) { + with_expanded_alias(model, prefix_type, |target| { collect_members( model, - &target, + target, bindings, name_bindings, visited, out, filter, ); - } - visited.pop(); + }); return; } // Bare generic types (`Box`) fill in default generic arguments on member access, @@ -666,8 +637,9 @@ fn collect_members( if let Some(def) = model.type_def_of(&generic.get_base_type_id()) && def.kind == TypeDefKind::Alias { - let expanded = expand_alias_generic(model, prefix_type); - collect_members(model, &expanded, None, None, visited, out, filter); + with_expanded_alias(model, prefix_type, |expanded| { + collect_members(model, expanded, None, None, visited, out, filter); + }); return; } let def = model.type_def_of(&generic.get_base_type_id()); @@ -1065,6 +1037,7 @@ fn member_info_of( } // Nullable fields merge nil; function fields stay pure-function shaped to avoid parameter/self inference // being disturbed by a `fun | nil` union (flow queries still add nil through flow_member_value_type). + // Generic aliases stay lazy and can resolve to primitives, so they retain nil. if member.is_nullable && !typ.is_nullable() && !matches!( @@ -1073,32 +1046,27 @@ fn member_info_of( | LuaType::DocFunction(_) | LuaType::Ref(_) | LuaType::Def(_) - | LuaType::Generic(_) | LuaType::Array(_) | LuaType::Object(_) ) { typ = LuaType::from_vec(vec![typ, LuaType::Nil]); } - // A method function's own generics (`---@generic T`) need a separate `Func` ID on member access, - // to prevent `Test` class generics from substituting the method-level T with integer. - if let LuaType::DocFunction(fun) = &typ - && !fun.get_generic_params().is_empty() - { - let fun = reassign_function_generics_to_func_ids(fun.as_ref().clone()); - typ = LuaType::DocFunction(Rc::new(fun)); - } + // Function generics need separate IDs before class substitution, including + // functions inside lazy alias arguments. + typ = map_type(&typ, &mut |ty| match ty { + LuaType::DocFunction(fun) if !fun.get_generic_params().is_empty() => { + let fun = reassign_function_generics_to_func_ids(fun.as_ref().clone()); + Some(LuaType::DocFunction(Rc::new(fun))) + } + _ => None, + }); if let Some(bindings) = bindings { typ = unify::substitute(&typ, bindings); } if let Some(name_bindings) = name_bindings { typ = substitute_named_refs(&typ, name_bindings); } - // After substituting class/generic arguments, member types still need alias expansion and conditional evaluation: - // `Mock.ctx.calls`'s field is `MockContextCalls>[]`; - // without expansion it stays `Alias<...>[]`, so rendering/diagnostics/later access cannot see `any[]`. - typ = expand_alias_generic(model, &typ); - typ = eval_conditionals(model, &typ); Some(MemberInfo { key: member_key_to_lua(&member.key), diff --git a/crates/emmylua_code_analysis/src/semantic_model/mod.rs b/crates/emmylua_code_analysis/src/semantic_model/mod.rs index e7e1ac57c..090e7542d 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/mod.rs @@ -15,8 +15,8 @@ mod types; #[cfg(test)] mod legacy_visibility_tests; -use hashbrown::HashSet; use std::cell::{Ref, RefCell, RefMut}; +use std::rc::Rc; use emmylua_parser::{LuaSyntaxId, VisibilityKind}; use smol_str::SmolStr; @@ -25,7 +25,7 @@ use crate::semantic_db::def::SemanticId; use crate::semantic_db::{AnalysisView, FileView, SemanticDatabase}; use crate::{FileId, LuaFunctionType, LuaType}; -/// Lazily allocated per-model local query cache. +/// Lazily allocated query cache shared by a model and its file views. /// /// A typical `SemanticModel` query only touches a few cache maps (or none at /// all). Keeping `SemanticLocalCache` inline makes every model ~1KB; this @@ -55,53 +55,26 @@ impl LazyCache { inner.as_deref_mut().expect("lazy cache initialized") }) } -} - -/// Lazily allocated in-progress set (same motivation as [`LazyCache`]). -struct LazySet { - inner: RefCell>>>, -} -impl Default for LazySet { - fn default() -> Self { - Self { - inner: RefCell::new(None), + /// Publish an already computed result only if its dependencies completed. + fn publish_if_complete( + &self, + revision: u64, + publish: impl FnOnce(&mut cache::SemanticLocalCache), + ) { + let mut cache = self.borrow_mut(); + if cache.is_complete(revision) { + publish(&mut cache); } } } -impl LazySet { - fn ensure(&self) { - if self.inner.borrow().is_none() { - *self.inner.borrow_mut() = Some(Box::new(HashSet::new())); - } - } - - fn borrow(&self) -> Ref<'_, HashSet> { - self.ensure(); - Ref::map(self.inner.borrow(), |inner| { - inner.as_deref().expect("lazy set initialized") - }) - } - - fn borrow_mut(&self) -> RefMut<'_, HashSet> { - self.ensure(); - RefMut::map(self.inner.borrow_mut(), |inner| { - inner.as_deref_mut().expect("lazy set initialized") - }) - } -} - /// Semantic model: a per-file access handle, only through the semantic analysis layer. pub struct SemanticModel<'db> { view: FileView<'db>, - /// Per-model local query cache. Recursion-in-progress state is stored in - /// cache entries, so no separate O(n) guard stacks are needed. - cache: LazyCache, - /// Closure-return inference depends on the VM closure environment, so its - /// result cannot be globally memoized; only O(1) in-progress tracking is kept. - closure_return_in_progress: LazySet, + /// Query results and recursion state shared across `model_for` file views. + cache: Rc, } /// Member-reference resolution result: index expression -> actual member declaration. @@ -159,8 +132,7 @@ impl<'db> SemanticModel<'db> { pub fn new(db: &'db SemanticDatabase, file_id: FileId) -> Self { Self { view: AnalysisView::new(db).file(file_id), - cache: LazyCache::default(), - closure_return_in_progress: LazySet::default(), + cache: Rc::new(LazyCache::default()), } } @@ -171,49 +143,25 @@ impl<'db> SemanticModel<'db> { self.view.analysis() } - /// Look up a memoized declaration-trace state (see [`cache::TraceStateKey`]). - pub(crate) fn trace_state_cache_get( - &self, - key: &cache::TraceStateKey, - ) -> Option>> { - self.cache.borrow().trace_state.get(key).cloned() - } - - /// Mark a declaration-trace state as being computed. - pub(crate) fn trace_state_cache_mark_in_progress(&self, key: cache::TraceStateKey) { - self.cache - .borrow_mut() - .trace_state - .insert(key, cache::CacheEntry::InProgress); - } - - /// Store a finished declaration-trace state. - pub(crate) fn trace_state_cache_store( - &self, - key: cache::TraceStateKey, - result: Option, - ) { - self.cache - .borrow_mut() - .trace_state - .insert(key, cache::CacheEntry::Ready(result)); - } pub(crate) fn is_closure_return_in_progress(&self, closure_syntax: LuaSyntaxId) -> bool { - self.closure_return_in_progress + self.cache .borrow() - .contains(&closure_syntax) + .closure_return_in_progress + .contains(&(self.file_id(), closure_syntax)) } pub(crate) fn begin_closure_return_infer(&self, closure_syntax: LuaSyntaxId) { - self.closure_return_in_progress + self.cache .borrow_mut() - .insert(closure_syntax); + .closure_return_in_progress + .insert((self.file_id(), closure_syntax)); } pub(crate) fn end_closure_return_infer(&self, closure_syntax: LuaSyntaxId) { - self.closure_return_in_progress + self.cache .borrow_mut() - .remove(&closure_syntax); + .closure_return_in_progress + .remove(&(self.file_id(), closure_syntax)); } } diff --git a/crates/emmylua_code_analysis/src/semantic_model/render/mod.rs b/crates/emmylua_code_analysis/src/semantic_model/render/mod.rs index 170a1846f..12396af02 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/render/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/render/mod.rs @@ -7,20 +7,18 @@ use std::collections::HashSet; use std::fmt::{self, Write}; -use std::rc::Rc; use hashbrown::HashMap; use crate::{ - AsyncState, GenericTplId, LuaFunctionType, LuaMemberKey, LuaType, LuaTypeDeclId, LuaUnionType, - VariadicType, -}; -use crate::{ - GenericParam, LuaAliasCallType, LuaArrayType, LuaConditionalType, LuaMappedType, LuaObjectType, - LuaTupleType, TypeDef, TypeDefKind, + AsyncState, GenericTplId, LuaFunctionType, LuaMappedType, LuaMemberKey, LuaType, LuaTypeDeclId, + LuaUnionType, TypeDef, TypeDefKind, VariadicType, }; -use super::type_eval::{eval_conditionals, expand_alias_generic, expand_index_call, expand_mapped}; +use super::type_eval::{ + eval_conditionals, expand_alias_generic, expand_index_call, expand_mapped, + substitute_named_refs, +}; use super::{SemanticModel, infer}; #[cfg(test)] @@ -464,7 +462,7 @@ impl<'a> TypeHumanizer<'a> { name_bindings.insert(param.name.to_string(), ty.clone()); } } - let target = self.substitute_named_refs(&target, &name_bindings); + let target = substitute_named_refs(&target, &name_bindings); let target = infer::unify::substitute(&target, &bindings); let target = expand_alias_generic(self.model, &target); let target = eval_conditionals(self.model, &target); @@ -667,109 +665,6 @@ impl<'a> TypeHumanizer<'a> { w.write_str(" }") } - fn substitute_named_refs(&self, ty: &LuaType, map: &HashMap) -> LuaType { - use LuaType::*; - match ty { - Ref(id) | Def(id) => map - .get(id.get_name()) - .cloned() - .unwrap_or_else(|| ty.clone()), - Array(array) => Array(Rc::new(LuaArrayType::from_base_type( - self.substitute_named_refs(array.get_base(), map), - ))), - Tuple(tuple) => Tuple(Rc::new(LuaTupleType::new( - tuple - .get_types() - .iter() - .map(|t| self.substitute_named_refs(t, map)) - .collect(), - tuple.status, - ))), - Union(union) => Union(Rc::new(LuaUnionType::from_vec( - union - .iter() - .map(|t| self.substitute_named_refs(t, map)) - .collect(), - ))), - Object(object) => Object(Rc::new(LuaObjectType::new_with_fields( - object - .get_fields() - .iter() - .map(|(k, v)| (k.clone(), self.substitute_named_refs(v, map))) - .collect(), - object - .get_index_access() - .iter() - .map(|(k, v)| { - ( - self.substitute_named_refs(k, map), - self.substitute_named_refs(v, map), - ) - }) - .collect(), - ))), - Variadic(variadic) => Variadic(Rc::new(match variadic.as_ref() { - VariadicType::Base(base) => { - VariadicType::Base(self.substitute_named_refs(base, map)) - } - VariadicType::Repeat(base) => { - VariadicType::Repeat(self.substitute_named_refs(base, map)) - } - VariadicType::Multi(types) => VariadicType::Multi( - types - .iter() - .map(|t| self.substitute_named_refs(t, map)) - .collect(), - ), - })), - Call(call) => Call(Rc::new(LuaAliasCallType::new( - call.get_call_kind(), - call.get_operands() - .iter() - .map(|t| self.substitute_named_refs(t, map)) - .collect(), - ))), - Mapped(mapped) => { - let constraint = mapped - .param - .1 - .constraint - .as_ref() - .map(|t| self.substitute_named_refs(t, map)); - let default = mapped - .param - .1 - .default - .as_ref() - .map(|t| self.substitute_named_refs(t, map)); - Mapped(Rc::new(LuaMappedType::new( - ( - mapped.param.0, - GenericParam::new( - mapped.param.1.name.clone(), - constraint, - default, - mapped.param.1.is_const, - mapped.param.1.attributes.clone(), - ), - ), - self.substitute_named_refs(&mapped.value, map), - mapped.is_readonly, - mapped.is_optional, - ))) - } - Conditional(conditional) => Conditional(Rc::new(LuaConditionalType::new( - self.substitute_named_refs(conditional.get_checked_type(), map), - self.substitute_named_refs(conditional.get_extends_type(), map), - self.substitute_named_refs(conditional.get_true_type(), map), - self.substitute_named_refs(conditional.get_false_type(), map), - conditional.get_infer_params().to_vec(), - conditional.has_new, - ))), - _ => ty.clone(), - } - } - /// Mapped types: prefer semantic expansion into an object; keep the mapped structure when expansion fails. fn write_mapped_type(&mut self, mapped: &LuaMappedType, w: &mut W) -> fmt::Result { if let Some(expanded) = expand_mapped(self.model, mapped) { diff --git a/crates/emmylua_code_analysis/src/semantic_model/render/tests.rs b/crates/emmylua_code_analysis/src/semantic_model/render/tests.rs index d6cb620fc..6599c11c2 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/render/tests.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/render/tests.rs @@ -76,15 +76,30 @@ fn rendering_still_fills_defaults_and_missing_parameters() { } #[gtest] -fn rendering_excess_arguments_keeps_alias_expansion() { +fn rendering_instantiates_generic_aliases() { let mut ws = VirtualWorkspace::new(); - ws.def("---@alias List T[]\n---@alias Value T['value']"); + ws.def( + r#" + ---@class Box + ---@alias List T[] + ---@alias Value T['value'] + ---@alias Nested Box + ---@alias Callback fun(value: T): T + ---@alias Values table + "#, + ); for (annotation, detailed) in [ ("List", "List = string[]"), ( "Value<{value:integer},string>", "Value<{ value: integer },string> = integer", ), + ("Nested", "Nested = Box"), + ( + "Callback", + "Callback = fun(value: string) -> string", + ), + ("Values", "Values = table"), ] { let ty = ws.ty(annotation); assert_that!( diff --git a/crates/emmylua_code_analysis/src/semantic_model/signature.rs b/crates/emmylua_code_analysis/src/semantic_model/signature.rs index 4f80a8235..26ef66977 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/signature.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/signature.rs @@ -144,20 +144,25 @@ impl<'db> SemanticModel<'db> { } pub(crate) fn type_of_expr_impl(&self, expr_syntax: LuaSyntaxId) -> LuaType { let key = (self.view.file_id(), expr_syntax); - match self.cache.borrow().expr_type.get(&key) { - Some(cache::CacheEntry::Ready(cached)) => return cached.clone(), - Some(cache::CacheEntry::InProgress) => return LuaType::Unknown, - None => {} + { + let cache = self.cache.borrow(); + if let Some(entry) = cache.expr_type.get(&key) { + return entry + .get_inference_seed(&cache) + .cloned() + .unwrap_or(LuaType::Unknown); + } } + let _query = cache::QueryCacheScope::new(&self.cache); self.cache .borrow_mut() .expr_type .insert(key, cache::CacheEntry::InProgress); + let revision = self.cache.borrow().revision(); let ty = infer::infer_expr(self, expr_syntax); - self.cache - .borrow_mut() - .expr_type - .insert(key, cache::CacheEntry::Ready(ty.clone())); + let mut cache = self.cache.borrow_mut(); + let entry = cache::CacheEntry::finish(ty.clone(), cache.is_complete(revision)); + cache.expr_type.insert(key, entry); ty } /// Hover/display layer only: expands generic alias instances (`Pick<...>` -> `T[K]` structure). @@ -170,26 +175,54 @@ impl<'db> SemanticModel<'db> { } /// Type compatibility check (boolean version, mirrors the old `SemanticModel::type_check`). pub fn type_check(&self, source: &LuaType, target: &LuaType) -> bool { - if let Some(cached) = self - .cache - .borrow() - .type_check - .get(&(source.clone(), target.clone())) + self.type_check_cached(source, target, false) + .unwrap_or(false) + } + /// Precise subtype proof, including object fields and generic arguments. + pub fn type_check_subtype(&self, source: &LuaType, target: &LuaType) -> bool { + self.type_check_cached(source, target, true) + .unwrap_or(false) + } + pub(super) fn type_check_cached( + &self, + source: &LuaType, + target: &LuaType, + subtype: bool, + ) -> Option { + let key = (self.file_id(), source.clone(), target.clone(), subtype); { - return *cached; + let cache = self.cache.borrow(); + if let Some(entry) = cache.type_check.get(&key) { + return entry.get_complete(&cache).copied(); + } } - let result = type_check::is_compatible_uncached(self, source, target); + let _query = cache::QueryCacheScope::new(&self.cache); self.cache .borrow_mut() .type_check - .insert((source.clone(), target.clone()), result); + .insert(key.clone(), cache::CacheEntry::InProgress); + let result = if subtype { + type_check::check_type_subtype(self, source, target) + } else { + type_check::check_type(self, source, target) + }; + let result = match result { + Ok(()) => Some(true), + Err(err) if err.is_type_not_match() => Some(false), + Err(_) => None, + }; + let mut cache = self.cache.borrow_mut(); + if result.is_none() { + cache.mark_incomplete(); + } + // Comparison outcomes carry their own completeness. An unfinished + // alternative does not invalidate a proof from another alternative. + cache.type_check.insert( + key, + cache::CacheEntry::finish(result.unwrap_or(false), result.is_some()), + ); result } - /// Strict subtype check: union targets across all components, object field-level checks, generic alias expansion. - /// Only for tests/callers needing precise subtype relations; does not affect the old `type_check` loose compatibility semantics. - pub fn type_check_subtype(&self, source: &LuaType, target: &LuaType) -> bool { - type_check::check_type_subtype(self, source, target).is_ok() - } /// Multi-value expression list types (mirrors old `infer_expr_list_types`; trailing multi-returns expand according to var_count). pub fn infer_expr_list_types( &self, diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/assign.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/assign.rs index cc2beae42..4161a98ef 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/assign.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/assign.rs @@ -7,27 +7,23 @@ use crate::{LuaMemberKey, LuaType, SemanticId, TypeDef}; use crate::semantic_model::SemanticModel; use super::context::{TypeCheckContext, TypeCheckMode}; -use super::guard::TypeCheckGuard; -use super::{TypeCheckResult, check_general_type_compact}; +use super::{RequiredChecks, TypeCheckFailReason, check_general_type_compact}; pub(crate) fn check_special( context: &mut TypeCheckContext, source: &LuaType, target: &LuaType, - guard: TypeCheckGuard, -) -> Option { - if let Some(result) = check_object_index_against_class(context, source, target, guard) { - return Some(result); +) -> Result, TypeCheckFailReason> { + if check_object_index_against_class(context, source, target)?.is_some() { + return Ok(Some(())); } - if let Some(result) = check_named_intersection(context, source, target, guard) { - return Some(result); + if check_named_intersection(context, source, target)?.is_some() { + return Ok(Some(())); } - if context.mode == TypeCheckMode::Assignment - && let Some(result) = check_function_assign(context, source, target, guard) - { - return Some(result); + if context.mode == TypeCheckMode::Assignment { + return check_function_assign(context, source, target); } - None + Ok(None) } /// Check integer index signatures item-by-item for `Object -> Ref/Def`. @@ -35,26 +31,23 @@ fn check_object_index_against_class( context: &mut TypeCheckContext, source: &LuaType, target: &LuaType, - guard: TypeCheckGuard, -) -> Option { +) -> Result, TypeCheckFailReason> { let (LuaType::Object(source_object), LuaType::Ref(_) | LuaType::Def(_)) = (source, target) else { - return None; + return Ok(None); + }; + let Some(def) = named_def(context.model, target) else { + return Ok(None); }; - let def = named_def(context.model, target)?; let mut index_signatures: Vec<(String, LuaType)> = Vec::new(); - collect_members_with_index_signatures( - context.model, - &def, - &mut Vec::new(), - &mut Vec::new(), - &mut Vec::new(), - &mut index_signatures, - ); + context.require_complete(|| { + collect_index_signatures(context.model, &def, &mut Vec::new(), &mut index_signatures); + })?; if index_signatures.is_empty() { - return None; + return Ok(None); } let mut matched = false; + let mut checks = RequiredChecks::default(); for (source_key, source_value) in source_object.get_index_access() { let Some((_, expected)) = index_signatures.iter().find(|(signature, _)| { matches!( @@ -69,17 +62,20 @@ fn check_object_index_against_class( continue; }; matched = true; - if !matches!(expected, LuaType::Any | LuaType::Unknown) && { - let next_guard = match guard.next_level() { - Ok(g) => g, - Err(err) => return Some(Err(err)), - }; - check_general_type_compact(context, source_value, expected, next_guard).is_err() - } { - return Some(Err(context.mismatch(source, target))); + if !matches!(expected, LuaType::Any | LuaType::Unknown) { + checks + .require(check_general_type_compact(context, source_value, expected)) + .map_err(|err| { + if err.is_type_not_match() { + context.mismatch(source, target) + } else { + err + } + })?; } } - if matched { Some(Ok(())) } else { None } + checks.finish()?; + Ok(matched.then_some(())) } /// Check member completeness for `Ref/Def -> Intersection`. @@ -87,47 +83,53 @@ fn check_named_intersection( context: &mut TypeCheckContext, source: &LuaType, target: &LuaType, - guard: TypeCheckGuard, -) -> Option { +) -> Result, TypeCheckFailReason> { let LuaType::Intersection(intersection) = target else { - return None; + return Ok(None); + }; + let Some(source_def) = named_def(context.model, source) else { + return Ok(None); }; - let source_def = named_def(context.model, source)?; let mut source_members: Vec<(LuaMemberKey, LuaType)> = Vec::new(); - collect_named_members( - context.model, - &source_def, - &mut Vec::new(), - &mut source_members, - ); + context.require_complete(|| { + collect_named_members( + context.model, + &source_def, + &mut Vec::new(), + &mut source_members, + ); + })?; let mut members: Vec<(LuaMemberKey, LuaType)> = Vec::new(); + let mut checks = RequiredChecks::default(); for component in intersection.get_types() { let Some(def) = named_def(context.model, component) else { - return Some(Err(context.mismatch(source, target))); + return Err(context.mismatch(source, target)); }; members.clear(); - collect_named_members(context.model, &def, &mut Vec::new(), &mut members); + if let Err(err) = context.require_complete(|| { + collect_named_members(context.model, &def, &mut Vec::new(), &mut members); + }) { + checks.require(Err(err))?; + continue; + } for (key, expected) in &members { - let Some((_, actual)) = source_members.iter().find(|(existing, _)| existing == key) - else { - if expected.is_nullable() { - continue; - } - return Some(Err(context.mismatch(source, target))); - }; - let res = { - let next_guard = match guard.next_level() { - Ok(g) => g, - Err(err) => return Some(Err(err)), - }; - check_general_type_compact(context, actual, expected, next_guard).is_err() - }; - if res { - return Some(Err(context.mismatch(source, target))); - } + let actual = source_members + .iter() + .find(|(existing, _)| existing == key) + .map_or(&LuaType::Nil, |(_, actual)| actual); + checks + .require(check_general_type_compact(context, actual, expected)) + .map_err(|err| { + if err.is_type_not_match() { + context.mismatch(source, target) + } else { + err + } + })?; } } - Some(Ok(())) + checks.finish()?; + Ok(Some(())) } /// Function assignment: parameter contravariance and return covariance. @@ -135,65 +137,38 @@ fn check_function_assign( context: &mut TypeCheckContext, source: &LuaType, target: &LuaType, - guard: TypeCheckGuard, -) -> Option { +) -> Result, TypeCheckFailReason> { let (LuaType::DocFunction(source_fun), LuaType::DocFunction(target_fun)) = (source, target) else { - return None; + return Ok(None); }; let source_params = source_fun.get_params(); let target_params = target_fun.get_params(); if source_params.len() != target_params.len() { - return Some(Err(context.mismatch(source, target))); + return Err(context.mismatch(source, target)); } + let mut checks = RequiredChecks::default(); for (source_param, target_param) in source_params.iter().zip(target_params.iter()) { if let (Some(source_ty), Some(target_ty)) = (&source_param.1, &target_param.1) { // Parameter position is contravariant: the target parameter type must be acceptable by the source parameter type (including union component rules). - let next_guard = match guard.next_level() { - Ok(g) => g, - Err(err) => return Some(Err(err)), - }; - if param_accepts_assign(context, target_ty, source_ty, next_guard).is_err() { - return Some(Err(context.mismatch(source, target))); - } + checks + .require(check_general_type_compact(context, target_ty, source_ty)) + .map_err(|err| { + if err.is_type_not_match() { + context.mismatch(source, target) + } else { + err + } + })?; } } - let next_guard = match guard.next_level() { - Ok(g) => g, - Err(err) => return Some(Err(err)), - }; - let ret_result = check_general_type_compact( + checks.require(check_general_type_compact( context, source_fun.get_ret(), target_fun.get_ret(), - next_guard, - ); - Some(ret_result) -} - -fn param_accepts_assign( - context: &mut TypeCheckContext, - target: &LuaType, - source: &LuaType, - guard: TypeCheckGuard, -) -> TypeCheckResult { - if let LuaType::Union(target_union) = target { - for component in target_union.iter() { - let next_guard = guard.next_level()?; - param_accepts_assign(context, component, source, next_guard)?; - } - return Ok(()); - } - if let LuaType::Union(source_union) = source { - for component in source_union.iter() { - let next_guard = guard.next_level()?; - if param_accepts_assign(context, target, component, next_guard).is_ok() { - return Ok(()); - } - } - return Err(context.mismatch(source, target)); - } - check_general_type_compact(context, target, source, guard) + ))?; + checks.finish()?; + Ok(Some(())) } fn named_def(model: &SemanticModel<'_>, ty: &LuaType) -> Option { @@ -237,12 +212,10 @@ fn collect_named_members( } } -fn collect_members_with_index_signatures( +fn collect_index_signatures( model: &SemanticModel<'_>, def: &TypeDef, visited: &mut Vec, - out: &mut Vec<(String, LuaType)>, - required_out: &mut Vec<(String, LuaType)>, index_signatures: &mut Vec<(String, LuaType)>, ) { if visited.contains(&def.id) { @@ -256,34 +229,21 @@ fn collect_members_with_index_signatures( let Some(member) = facts.member_by_id(&member_ref.id) else { continue; }; + if !member.is_index_signature { + continue; + } let name = member_ref.name.to_string(); - let ty = model - .type_of_member(&member_ref.id) - .unwrap_or(LuaType::Unknown); - if member.is_index_signature { - if !index_signatures - .iter() - .any(|(existing, _)| existing == &name) - { - index_signatures.push((name, ty)); - } - } else { - if !out.iter().any(|(existing, _)| existing == &name) { - out.push((name.clone(), ty.clone())); - } - if !member.is_nullable && !required_out.iter().any(|(existing, _)| existing == &name) { - required_out.push((name, ty)); - } + if !index_signatures + .iter() + .any(|(existing, _)| existing == &name) + { + let ty = model + .type_of_member(&member_ref.id) + .unwrap_or(LuaType::Unknown); + index_signatures.push((name, ty)); } } for super_def in model.base_type_defs(&def.type_decl_id()) { - collect_members_with_index_signatures( - model, - &super_def, - visited, - out, - required_out, - index_signatures, - ); + collect_index_signatures(model, &super_def, visited, index_signatures); } } diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/comparison.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/comparison.rs new file mode 100644 index 000000000..8a28e23a6 --- /dev/null +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/comparison.rs @@ -0,0 +1,231 @@ +//! Evaluate recursive comparisons with a work queue. +//! +//! A structural cycle can provisionally match. Retry its dependents whenever +//! a result changes, and settle the dependencies that support the final result. + +use hashbrown::hash_map::EntryRef; +use hashbrown::{HashMap, HashSet}; +use std::collections::VecDeque; + +use super::{TypeCheckContext, TypeCheckFailReason, TypeCheckMode, TypeCheckResult}; +use crate::LuaType; + +#[derive(Clone, PartialEq, Eq, Hash)] +struct Comparison { + source: LuaType, + target: LuaType, + mode: TypeCheckMode, + strict_union: bool, + strict_object: bool, + strict_generic: bool, +} + +struct Node { + key: Comparison, + result: TypeCheckResult, + parents: HashSet, + dependencies: Vec, + queued: bool, +} + +#[derive(Default)] +pub(super) struct Comparisons { + index: HashMap, + nodes: Vec, + pending: VecDeque, + current: Option, + reads: Vec<(usize, bool)>, +} + +impl Comparisons { + fn intern(&mut self, key: &Comparison) -> usize { + let id = self.nodes.len(); + match self.index.entry_ref(key) { + EntryRef::Occupied(entry) => return *entry.get(), + EntryRef::Vacant(entry) => { + entry.insert(id); + } + } + self.nodes.push(Node { + key: key.clone(), + result: Err(TypeCheckFailReason::Unresolved), + parents: HashSet::new(), + dependencies: Vec::new(), + queued: true, + }); + self.pending.push_back(id); + id + } + + fn read(&mut self, key: &Comparison) -> TypeCheckResult { + let previous_len = self.nodes.len(); + let id = self.intern(key); + let assume = id < previous_len + && self.nodes[id].result == Err(TypeCheckFailReason::Unresolved) + && self.guarded_cycle(id); + let result = if assume { + Ok(()) + } else { + self.nodes[id].result.clone() + }; + if let Some(parent) = self.current { + self.nodes[id].parents.insert(parent); + if !result.as_ref().is_err_and(|err| err.is_unresolved()) { + self.reads.push((id, result.is_ok())); + } + } + result + } + + /// Only a cycle through data constructors can justify a recursive match. + /// Alias forwarding and union alternatives alone provide no evidence. + fn guarded_cycle(&self, key: usize) -> bool { + fn is_structure(ty: &LuaType) -> bool { + matches!( + ty, + LuaType::Object(_) + | LuaType::Array(_) + | LuaType::Tuple(_) + | LuaType::TableGeneric(_) + | LuaType::DocFunction(_) + ) + } + let mut pending: Vec<_> = self.current.iter().map(|key| (*key, false)).collect(); + let mut seen = HashSet::new(); + let mut guarded_cycle = false; + while let Some((previous, guarded)) = pending.pop() { + let comparison = &self.nodes[previous].key; + let guarded = + guarded || (is_structure(&comparison.source) && is_structure(&comparison.target)); + if !seen.insert((previous, guarded)) { + continue; + } + if previous == key { + if !guarded { + return false; + } + guarded_cycle = true; + } else { + pending.extend( + self.nodes[previous] + .parents + .iter() + .map(|parent| (*parent, guarded)), + ); + } + } + guarded_cycle + } + + fn finish(&mut self, id: usize, result: TypeCheckResult) { + // Only definite reads with the same outcome support a proof. + let dependencies = self + .reads + .drain(..) + .filter_map(|(child, matched)| (result.is_ok() == matched).then_some(child)) + .collect(); + let node = &mut self.nodes[id]; + node.dependencies = dependencies; + if node.result == result { + return; + } + node.result = result; + let parents: Vec<_> = node.parents.iter().copied().collect(); + for parent in parents { + let node = &mut self.nodes[parent]; + if !node.queued { + node.queued = true; + self.pending.push_back(parent); + } + } + } + + fn is_complete(&self, root: usize) -> bool { + // Avoid allocating a traversal when the root alone decides completeness. + let node = &self.nodes[root]; + if node.queued || node.result.as_ref().is_err_and(|err| err.is_unresolved()) { + return false; + } + if node.dependencies.is_empty() { + return true; + } + let mut pending = vec![root]; + let mut seen = HashSet::new(); + while let Some(id) = pending.pop() { + if !seen.insert(id) { + continue; + } + let node = &self.nodes[id]; + if node.queued || node.result.as_ref().is_err_and(|err| err.is_unresolved()) { + return false; + } + pending.extend(&node.dependencies); + } + true + } +} + +pub(super) fn check( + context: &mut TypeCheckContext, + source: &LuaType, + target: &LuaType, +) -> TypeCheckResult { + let key = Comparison { + source: source.clone(), + target: target.clone(), + mode: context.mode, + strict_union: context.strict_union, + strict_object: context.strict_object, + strict_generic: context.strict_generic, + }; + let comparisons = context.comparisons.clone(); + if comparisons.borrow().current.is_some() { + return comparisons.borrow_mut().read(&key); + } + let Some(evaluation) = + crate::semantic_model::cache::TypeEvaluationScope::new(&context.model.cache) + else { + return Err(TypeCheckFailReason::TooComplex); + }; + let root = comparisons.borrow_mut().intern(&key); + loop { + let next = { + let mut graph = comparisons.borrow_mut(); + if graph.is_complete(root) { + return graph.nodes[root].result.clone(); + } + graph.pending.pop_front() + }; + let Some(next) = next else { + return Err(if context.model.cache.borrow().evaluation_exhausted { + TypeCheckFailReason::TooComplex + } else { + TypeCheckFailReason::Unresolved + }); + }; + comparisons.borrow_mut().nodes[next].queued = false; + let key = comparisons.borrow().nodes[next].key.clone(); + if !evaluation.visit(&key.source) || !evaluation.visit(&key.target) { + return Err(TypeCheckFailReason::TooComplex); + } + let result = evaluate(context, next, &key); + comparisons.borrow_mut().finish(next, result); + } +} + +fn evaluate(context: &TypeCheckContext, id: usize, key: &Comparison) -> TypeCheckResult { + { + let mut graph = context.comparisons.borrow_mut(); + graph.current = Some(id); + } + let mut current = TypeCheckContext { + mode: key.mode, + strict_union: key.strict_union, + strict_object: key.strict_object, + strict_generic: key.strict_generic, + ..context.clone() + }; + let result = super::check_comparison(&mut current, &key.source, &key.target); + context.comparisons.borrow_mut().current = None; + result +} diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/complex_type.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/complex_type.rs index f25927e0b..a2fe5ab35 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/complex_type.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/complex_type.rs @@ -3,52 +3,46 @@ use crate::{LuaMemberKey, LuaType}; use super::context::TypeCheckContext; -use super::guard::TypeCheckGuard; -use super::{TypeCheckResult, check_general_type_compact}; +use super::{ + RequiredChecks, TypeCheckResult, check_all, check_alternatives, check_general_type_compact, +}; pub fn check_complex_type_compact( context: &mut TypeCheckContext, source: &LuaType, compact_type: &LuaType, - check_guard: TypeCheckGuard, ) -> TypeCheckResult { match source { LuaType::Array(source_array) => { let base = source_array.get_base().clone(); match compact_type { - LuaType::Array(compact_array) => check_general_type_compact( - context, - &base, - compact_array.get_base(), - check_guard.next_level()?, - ), + LuaType::Array(compact_array) => { + check_general_type_compact(context, &base, compact_array.get_base()) + } LuaType::Object(object) if context.mode.uses_assignment_rules() => { let array_base = source_array.get_base(); - for (key_ty, expected) in object.get_index_access() { - if matches!( - key_ty, - LuaType::Integer | LuaType::Number | LuaType::IntegerConst(_) - ) && (matches!(expected, LuaType::Any | LuaType::Unknown) - || check_general_type_compact( - context, - array_base, - expected, - check_guard.next_level()?, - ) - .is_ok()) - { - return Ok(()); - } - } - Err(context.mismatch(source, compact_type)) - } - LuaType::Tuple(tuple) => { - let next_guard = check_guard.next_level()?; - for element in tuple.get_types() { - check_general_type_compact(context, &base, element, next_guard)?; - } - Ok(()) + check_alternatives( + context.mismatch(source, compact_type), + object + .get_index_access() + .iter() + .filter(|(key_ty, _)| { + matches!( + key_ty, + LuaType::Integer | LuaType::Number | LuaType::IntegerConst(_) + ) + }) + .map(|(_, expected)| { + check_general_type_compact(context, array_base, expected) + }), + ) } + LuaType::Tuple(tuple) => check_all( + tuple + .get_types() + .iter() + .map(|element| check_general_type_compact(context, &base, element)), + ), LuaType::Table | LuaType::TableGeneric(_) | LuaType::Ref(_) @@ -64,11 +58,12 @@ pub fn check_complex_type_compact( if source_types.len() != compact_types.len() { return Err(context.mismatch(source, compact_type)); } - let next_guard = check_guard.next_level()?; - for (s, t) in source_types.iter().zip(compact_types.iter()) { - check_general_type_compact(context, s, t, next_guard)?; - } - Ok(()) + check_all( + source_types + .iter() + .zip(compact_types) + .map(|(s, t)| check_general_type_compact(context, s, t)), + ) } LuaType::Table | LuaType::Array(_) @@ -85,11 +80,12 @@ pub fn check_complex_type_compact( if source_types.len() != compact_types.len() { return Err(context.mismatch(source, compact_type)); } - let next_guard = check_guard.next_level()?; - for (s, t) in source_types.iter().zip(compact_types.iter()) { - check_general_type_compact(context, s, t, next_guard)?; - } - Ok(()) + check_all( + source_types + .iter() + .zip(compact_types) + .map(|(s, t)| check_general_type_compact(context, s, t)), + ) } LuaType::Table | LuaType::Array(_) @@ -100,39 +96,39 @@ pub fn check_complex_type_compact( }, LuaType::Object(source_object) if context.strict_object => match compact_type { LuaType::Object(compact_object) => { - let next_guard = check_guard.next_level()?; + let mut checks = RequiredChecks::default(); for (key, expected) in source_object.get_fields() { if let Some(actual) = compact_object.get_field(key) { - check_general_type_compact(context, expected, actual, next_guard)?; + checks.require(check_general_type_compact(context, expected, actual))?; } } - Ok(()) + checks.finish() } LuaType::Tuple(tuple) => { - let next_guard = check_guard.next_level()?; + let mut checks = RequiredChecks::default(); for (index, element_ty) in tuple.get_types().iter().enumerate() { let key = LuaMemberKey::Integer(index as i64 + 1); let Some(expected) = source_object.get_fields().get(&key) else { return Err(context.mismatch(source, compact_type)); }; - check_general_type_compact(context, expected, element_ty, next_guard)?; + checks.require(check_general_type_compact(context, expected, element_ty))?; } - Ok(()) + checks.finish() } LuaType::TableConst(_) => { - let table_infos = context.model.member_infos(compact_type); - let next_guard = check_guard.next_level()?; + let table_infos = + context.require_complete(|| context.model.member_infos(compact_type))?; + let mut checks = RequiredChecks::default(); for (key, expected) in source_object.get_fields() { - let Some(actual) = table_infos + if let Some(actual) = table_infos .iter() .find(|info| &info.key == key) - .map(|info| info.typ.clone()) - else { - continue; - }; - check_general_type_compact(context, expected, &actual, next_guard)?; + .map(|info| &info.typ) + { + checks.require(check_general_type_compact(context, expected, actual))?; + } } - Ok(()) + checks.finish() } LuaType::Table | LuaType::Ref(_) | LuaType::Def(_) | LuaType::Generic(_) => Ok(()), _ => Err(context.mismatch(source, compact_type)), @@ -164,7 +160,7 @@ pub fn check_complex_type_compact( } LuaType::MultiLineUnion(source_multi) => { let union = source_multi.to_union(); - check_general_type_compact(context, &union, compact_type, check_guard.next_level()?) + check_general_type_compact(context, &union, compact_type) } _ => Err(context.mismatch(source, compact_type)), } diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/context.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/context.rs index 8fe9abb1e..197f86545 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/context.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/context.rs @@ -14,7 +14,7 @@ use crate::semantic_model::render::humanize_type; /// Rules for comparing a value's type with the expected type. /// Call argument counts are checked separately. -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum TypeCheckMode { /// General compatibility checks, such as checking function arguments. /// If `Dog` extends `Animal`, a `Dog` can supply an `Animal` parameter. @@ -48,6 +48,7 @@ pub struct TypeCheckContext<'db> { pub strict_object: bool, /// Strict generic inheritance mode: retain parent class generic arguments for precise inheritance checks. pub strict_generic: bool, + pub(super) comparisons: std::rc::Rc>, } impl<'db> TypeCheckContext<'db> { @@ -59,6 +60,24 @@ impl<'db> TypeCheckContext<'db> { strict_union: false, strict_object: false, strict_generic: false, + comparisons: Default::default(), + } + } + + /// Do not use an unfinished semantic dependency as comparison evidence. + pub(super) fn require_complete( + &self, + query: impl FnOnce() -> T, + ) -> Result { + let revision = self.model.cache.borrow().revision(); + let result = query(); + let cache = self.model.cache.borrow(); + if cache.evaluation_exhausted { + Err(TypeCheckFailReason::TooComplex) + } else if cache.is_complete(revision) { + Ok(result) + } else { + Err(TypeCheckFailReason::Unresolved) } } @@ -87,20 +106,6 @@ impl<'db> TypeCheckContext<'db> { self.model.base_types(id) } - pub fn is_alias(&self, id: &LuaTypeDeclId) -> bool { - self.type_def_of(id) - .is_some_and(|def| def.kind == TypeDefKind::Alias) - } - - /// Alias expansion: the target type (after projection, generic references keep `TplRef`). - pub fn alias_target_of(&self, id: &LuaTypeDeclId) -> Option { - let def = self.type_def_of(id)?; - if def.kind != TypeDefKind::Alias { - return None; - } - self.model.alias_target(&def) - } - pub fn is_enum(&self, id: &LuaTypeDeclId) -> bool { self.type_def_of(id) .is_some_and(|def| def.kind == TypeDefKind::Enum) diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/fail_reason.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/fail_reason.rs index b4a5a2cc4..64946ba6b 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/fail_reason.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/fail_reason.rs @@ -1,13 +1,19 @@ //! Type-check failure reasons (detail layer beyond the boolean interface). -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum TypeCheckFailReason { TypeNotMatch, - TypeRecursion, + /// A semantic dependency is unfinished, so neither outcome is proven. + Unresolved, + /// Evaluation exceeded the shared resource budget. + TooComplex, TypeNotMatchWithReason(String), } impl TypeCheckFailReason { + pub(super) fn is_unresolved(&self) -> bool { + matches!(self, Self::Unresolved | Self::TooComplex) + } pub fn is_type_not_match(&self) -> bool { matches!( self, diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/func_type.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/func_type.rs index 8284f8f27..2e22c3630 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/func_type.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/func_type.rs @@ -7,25 +7,20 @@ use crate::LuaSignatureId; use crate::LuaType; use super::context::TypeCheckContext; -use super::guard::TypeCheckGuard; -use super::{TypeCheckResult, check_general_type_compact}; +use super::{RequiredChecks, TypeCheckResult, check_all, check_general_type_compact}; pub fn check_doc_func_type_compact( context: &mut TypeCheckContext, source_func: &LuaFunctionType, compact_type: &LuaType, - check_guard: TypeCheckGuard, ) -> TypeCheckResult { match compact_type { LuaType::DocFunction(compact_func) => { - check_doc_func_type_compact_for_params(context, source_func, compact_func, check_guard) + check_doc_func_type_compact_for_params(context, source_func, compact_func) + } + LuaType::Signature(signature_id) => { + check_doc_func_type_compact_for_signature(context, source_func, signature_id) } - LuaType::Signature(signature_id) => check_doc_func_type_compact_for_signature( - context, - source_func, - signature_id, - check_guard, - ), LuaType::Ref(id) | LuaType::Def(id) => { // Only callable classes declaring `---@overload fun` accept function values; ordinary classes // should not accept arbitrary functions as compatible (otherwise flow branches like `fun() | B` would lose errors). @@ -41,17 +36,13 @@ pub fn check_doc_func_type_compact( )) } } - LuaType::Union(union) => { - for union_type in union.iter() { - check_doc_func_type_compact( - context, - source_func, - union_type, - check_guard.next_level()?, - )?; - } - Ok(()) - } + LuaType::Union(union) => check_all(union.iter().map(|union_type| { + check_general_type_compact( + context, + &LuaType::DocFunction(Rc::new(source_func.clone())), + union_type, + ) + })), LuaType::Function => Ok(()), _ => Err(context.mismatch( &LuaType::DocFunction(Rc::new(source_func.clone())), @@ -64,7 +55,6 @@ fn check_doc_func_type_compact_for_params( context: &mut TypeCheckContext, source_func: &LuaFunctionType, compact_func: &LuaFunctionType, - check_guard: TypeCheckGuard, ) -> TypeCheckResult { let source_params = source_func.get_params(); let mut compact_params: Vec<(String, Option)> = compact_func.get_params().to_vec(); @@ -77,6 +67,7 @@ fn check_doc_func_type_compact_for_params( let compact_is_variadic = compact_func.is_variadic(); let source_len = source_params.len(); let compact_len = compact_params.len(); + let mut checks = RequiredChecks::default(); for i in 0..compact_len { let Some(source_param) = source_params.get(i) else { break; @@ -85,12 +76,11 @@ fn check_doc_func_type_compact_for_params( let source_param_type = &source_param.1; if source_is_variadic && i + 1 == source_len { - check_doc_func_type_compact_for_varargs( + checks.require(check_doc_func_type_compact_for_varargs( context, source_param_type, &compact_params[i..], - check_guard.next_level()?, - )?; + ))?; } if compact_is_variadic && i + 1 == compact_len { @@ -100,83 +90,71 @@ fn check_doc_func_type_compact_for_params( let compact_param_type = &compact_param.1; if let (Some(source_type), Some(compact_type)) = (source_param_type, compact_param_type) { // Parameters: when injecting values, the compact parameter type must be acceptable to the source. - match check_general_type_compact( - context, - compact_type, - source_type, - check_guard.next_level()?, - ) { - Ok(()) => {} - Err(e) if e.is_type_not_match() => { - if i == 0 && source_type.is_self_infer() && compact_param.0 == "self" { - continue; - } - return Err(e); - } - Err(e) => return Err(e), + let result = check_general_type_compact(context, compact_type, source_type); + if result.as_ref().is_err_and(|err| err.is_type_not_match()) + && i == 0 + && source_type.is_self_infer() + && compact_param.0 == "self" + { + continue; } + checks.require(result)?; } } // Return covariance (not checked in the old version; added here): the source return must be assignable to the compact return. - check_general_type_compact( + checks.require(check_general_type_compact( context, source_func.get_ret(), compact_func.get_ret(), - check_guard.next_level()?, - ) + ))?; + checks.finish() } fn check_doc_func_type_compact_for_varargs( context: &mut TypeCheckContext, varargs: &Option, compact_params: &[(String, Option)], - check_guard: TypeCheckGuard, ) -> TypeCheckResult { - if let Some(varargs_type) = varargs { - for compact_param in compact_params { - if let Some(compact_param_type) = &compact_param.1 { - check_general_type_compact( - context, - compact_param_type, - varargs_type, - check_guard.next_level()?, - )?; - } - } - } - Ok(()) + let Some(varargs_type) = varargs else { + return Ok(()); + }; + check_all( + compact_params + .iter() + .filter_map(|(_, ty)| ty.as_ref()) + .map(|compact_param_type| { + check_general_type_compact(context, compact_param_type, varargs_type) + }), + ) } fn check_doc_func_type_compact_for_signature( context: &mut TypeCheckContext, source_func: &LuaFunctionType, signature_id: &LuaSignatureId, - check_guard: TypeCheckGuard, ) -> TypeCheckResult { // M0: convert a semantic Signature (params/doc) into a DocFunction structure for comparison. - let Some(signature) = context.model.function_type_by_signature_id(signature_id) else { + let Some(signature) = + context.require_complete(|| context.model.function_type_by_signature_id(signature_id))? + else { return Err(context.mismatch( &LuaType::DocFunction(Rc::new(source_func.clone())), &LuaType::Signature(*signature_id), )); }; - check_doc_func_type_compact_for_params( - context, - source_func, - &signature, - check_guard.next_level()?, - ) + check_doc_func_type_compact_for_params(context, source_func, &signature) } pub fn check_sig_type_compact( context: &mut TypeCheckContext, sig_id: &LuaSignatureId, compact_type: &LuaType, - check_guard: TypeCheckGuard, ) -> TypeCheckResult { - let Some(signature) = context.model.function_type_by_signature_id(sig_id) else { + let Some(signature) = + context.require_complete(|| context.model.function_type_by_signature_id(sig_id))? + else { return Err(context.mismatch(&LuaType::Signature(*sig_id), compact_type)); }; - check_doc_func_type_compact(context, &signature, compact_type, check_guard.next_level()?) + check_doc_func_type_compact(context, &signature, compact_type) } diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/generic_type.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/generic_type.rs index 793d28dd8..4265d8031 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/generic_type.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/generic_type.rs @@ -5,15 +5,15 @@ use std::rc::Rc; use crate::{LuaGenericType, LuaType, LuaTypeDeclId}; use super::context::TypeCheckContext; -use super::guard::TypeCheckGuard; use super::sub_type::is_sub_type_of; -use super::{TypeCheckResult, check_general_type_compact}; +use super::{ + TypeCheckFailReason, TypeCheckResult, check_all, check_alternatives, check_general_type_compact, +}; pub fn check_generic_type_compact( context: &mut TypeCheckContext, source_generic: &LuaGenericType, compact_type: &LuaType, - check_guard: TypeCheckGuard, ) -> TypeCheckResult { let is_tpl = source_generic.contain_tpl(); @@ -22,60 +22,57 @@ pub fn check_generic_type_compact( if is_tpl { return Ok(()); } - let first_result = check_generic_type_compact_generic( - context, - source_generic, - compact_generic, - check_guard.next_level()?, - ); - if first_result.is_ok() { + let first_result = + check_generic_type_compact_generic(context, source_generic, compact_generic); + let Err(failure) = first_result else { return Ok(()); - } + }; // Generic inheritance: the source generic class inherits from the target generic class (keeping parent generic arguments). - if is_generic_sub_of_generic(context, source_generic, compact_generic) { - return Ok(()); - } + let inherited = context + .require_complete(|| { + is_generic_sub_of_generic(context, source_generic, compact_generic) + }) + .and_then(|matches| { + if matches { + Ok(()) + } else { + Err(TypeCheckFailReason::TypeNotMatch) + } + }); // Inheritance chain. - for super_type in context.super_types_of(&compact_generic.get_base_type_id()) { - if check_general_type_compact( - context, - &LuaType::Generic(Rc::new(source_generic.clone())), - &super_type, - check_guard.next_level()?, - ) - .is_ok() - { - return Ok(()); - } - } - first_result + check_alternatives( + failure, + std::iter::once(inherited).chain( + context + .super_types_of(&compact_generic.get_base_type_id()) + .iter() + .map(|super_type| { + check_general_type_compact( + context, + &LuaType::Generic(Rc::new(source_generic.clone())), + super_type, + ) + }), + ), + ) } LuaType::Ref(ref_id) | LuaType::Def(ref_id) => { if is_tpl { return Ok(()); } - check_generic_type_compact_ref_type( - context, - source_generic, - ref_id, - check_guard.next_level()?, - ) + check_generic_type_compact_ref_type(context, source_generic, ref_id) } LuaType::Table | LuaType::TableConst(_) | LuaType::Object(_) | LuaType::Tuple(_) => { // Table structures: M0 checks by base type name (`table`). Ok(()) } - LuaType::Union(union) => { - for sub in union.iter() { - check_generic_type_compact( - context, - source_generic, - sub, - check_guard.next_level()?, - )?; - } - Ok(()) - } + LuaType::Union(union) => check_all(union.iter().map(|sub| { + check_general_type_compact( + context, + &LuaType::Generic(Rc::new(source_generic.clone())), + sub, + ) + })), _ => Err(context.mismatch( &LuaType::Generic(Rc::new(source_generic.clone())), compact_type, @@ -122,7 +119,6 @@ fn check_generic_type_compact_generic( context: &mut TypeCheckContext, source_generic: &LuaGenericType, compact_generic: &LuaGenericType, - check_guard: TypeCheckGuard, ) -> TypeCheckResult { let source_base_id = source_generic.get_base_type_id(); let compact_base_id = compact_generic.get_base_type_id(); @@ -140,22 +136,21 @@ fn check_generic_type_compact_generic( &LuaType::Generic(Rc::new(compact_generic.clone())), )); } - let next_guard = check_guard.next_level()?; - for (source_param, compact_param) in source_params.iter().zip(compact_params.iter()) { - check_general_type_compact(context, source_param, compact_param, next_guard)?; - } - Ok(()) + check_all( + source_params + .iter() + .zip(compact_params) + .map(|(source_param, compact_param)| { + check_general_type_compact(context, source_param, compact_param) + }), + ) } fn check_generic_type_compact_ref_type( context: &mut TypeCheckContext, source_generic: &LuaGenericType, ref_id: &LuaTypeDeclId, - check_guard: TypeCheckGuard, ) -> TypeCheckResult { - if context.is_alias(ref_id) { - // semantic has no alias origin, so this is a nominal failure (unless the generic parameter is `any`). - } let base_id = source_generic.get_base_type_id(); if &base_id == ref_id || is_sub_type_of(context, ref_id, &base_id) @@ -169,7 +164,6 @@ fn check_generic_type_compact_ref_type( context, &source_generic.get_base_type(), &LuaType::Ref(ref_id.clone()), - check_guard.next_level()?, ); } Err(context.mismatch( diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/guard.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/guard.rs deleted file mode 100644 index 1d496fbfd..000000000 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/guard.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! Recursion depth guard (prevents infinite types such as `---@alias A = A`). - -use super::fail_reason::TypeCheckFailReason; - -const MAX_TYPE_CHECK_LEVEL: i32 = 100; -pub type TypeCheckLevelResult = Result; - -#[derive(Debug, Clone, Copy)] -pub struct TypeCheckGuard { - stack_level: i32, -} - -impl TypeCheckGuard { - pub fn new() -> Self { - Self { stack_level: 0 } - } - - pub fn next_level(&self) -> TypeCheckLevelResult { - let next_level = self.stack_level + 1; - if next_level > MAX_TYPE_CHECK_LEVEL { - return Err(TypeCheckFailReason::TypeRecursion); - } - Ok(Self { - stack_level: next_level, - }) - } -} diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/mod.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/mod.rs index 19142d496..c8f913f02 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/mod.rs @@ -4,16 +4,16 @@ //! but resolving the type environment only through `semantic_model::SemanticModel` (semantic): //! - **Boolean first**: `is_compatible(source, target) -> bool` (zero reason overhead); //! - **Detail interface**: `check_type(...) -> TypeCheckResult` (includes failure reason strings); -//! - Recursion + depth guard (`TypeCheckGuard`) protects against infinite types; +//! - Comparisons use a dependency graph and an explicit work queue; //! - Parts still missing from semantic (alias origin, enum field unions, call operators) fall back to nominal checks. mod assign; +mod comparison; mod complex_type; mod context; mod fail_reason; mod func_type; mod generic_type; -mod guard; #[cfg(test)] mod legacy_tests; mod ref_type; @@ -28,11 +28,45 @@ pub use context::{TypeCheckContext, TypeCheckMode}; pub use fail_reason::TypeCheckFailReason; use crate::semantic_model::SemanticModel; -use crate::semantic_model::type_eval::expand_alias_generic; +use crate::semantic_model::type_eval::expand_alias_head; use crate::{InFiled, LuaType, LuaTypeDeclId, LuaUnionType, SemanticId, TypeDef, TypeDefKind}; pub type TypeCheckResult = Result<(), TypeCheckFailReason>; +/// A definite mismatch rejects a required check even if another is unresolved. +#[derive(Default)] +pub(super) struct RequiredChecks { + unresolved: Option, +} + +impl RequiredChecks { + pub(super) fn require(&mut self, result: TypeCheckResult) -> TypeCheckResult { + match result { + Err(err) if err.is_unresolved() => { + self.unresolved = Some(err); + Ok(()) + } + result => result, + } + } + + pub(super) fn finish(self) -> TypeCheckResult { + if let Some(err) = self.unresolved { + Err(err) + } else { + Ok(()) + } + } +} + +pub(super) fn check_all(results: impl IntoIterator) -> TypeCheckResult { + let mut checks = RequiredChecks::default(); + for result in results { + checks.require(result)?; + } + checks.finish() +} + /// Boolean compatibility: whether `source` is assignable to `target`. pub fn is_compatible(model: &SemanticModel, source: &LuaType, target: &LuaType) -> bool { // Route through SemanticModel's local cache so repeated compatibility checks @@ -40,13 +74,30 @@ pub fn is_compatible(model: &SemanticModel, source: &LuaType, target: &LuaType) model.type_check(source, target) } -pub(crate) fn is_compatible_uncached( +pub(crate) fn check_type( model: &SemanticModel, source: &LuaType, target: &LuaType, -) -> bool { +) -> TypeCheckResult { let mut context = TypeCheckContext::new(model, false); - check_general_type_compact(&mut context, source, target, guard::TypeCheckGuard::new()).is_ok() + check_general_type_compact(&mut context, source, target) +} + +/// A complete alternative proves a match even if another is unresolved. +/// Without a match, an unresolved alternative prevents a definite rejection. +fn check_alternatives( + mut failure: TypeCheckFailReason, + checks: impl IntoIterator, +) -> TypeCheckResult { + for result in checks { + match result { + Ok(()) => return Ok(()), + Err(err) if err.is_type_not_match() => {} + Err(err) if err.is_unresolved() => failure = err, + Err(err) => return Err(err), + } + } + Err(failure) } /// Detailed compatibility: returns a reason on failure (detail mode includes human-readable strings). @@ -56,7 +107,7 @@ pub fn check_type_detail( target: &LuaType, ) -> TypeCheckResult { let mut context = TypeCheckContext::new(model, true); - check_general_type_compact(&mut context, source, target, guard::TypeCheckGuard::new()) + check_general_type_compact(&mut context, source, target) } /// Assignment-compatibility boolean interface: uses assignment semantics. @@ -74,7 +125,7 @@ pub fn check_type_subtype( context.strict_union = true; context.strict_object = true; context.strict_generic = true; - check_general_type_compact(&mut context, source, target, guard::TypeCheckGuard::new()) + check_general_type_compact(&mut context, source, target) } /// Assignment-compatibility detail interface: includes a detailed reason on failure. @@ -85,14 +136,21 @@ pub fn check_assign_type_detail( ) -> TypeCheckResult { let mut context = TypeCheckContext::new(model, true); context.mode = TypeCheckMode::Assignment; - check_general_type_compact(&mut context, source, target, guard::TypeCheckGuard::new()) + check_general_type_compact(&mut context, source, target) } fn check_general_type_compact( context: &mut TypeCheckContext, source: &LuaType, compact_type: &LuaType, - check_guard: guard::TypeCheckGuard, +) -> TypeCheckResult { + comparison::check(context, source, compact_type) +} + +fn check_comparison( + context: &mut TypeCheckContext, + source: &LuaType, + compact_type: &LuaType, ) -> TypeCheckResult { if source == compact_type { return Ok(()); @@ -121,57 +179,35 @@ fn check_general_type_compact( return Err(TypeCheckFailReason::TypeNotMatch); } - // Alias expansion: when `compact` is an alias reference, replace it with its target type and re-check (recursive aliases are cycle-protected by the guard depth). - if let LuaType::Ref(id) | LuaType::Def(id) = compact_type - && context.is_alias(id) - && let Some(target) = context.alias_target_of(id) - { - return check_general_type_compact(context, source, &target, check_guard.next_level()?); - } - - // Generic alias as source: expand plain aliases (skip mapped/conditional/call; these are handled by type_eval/param checks). - if let LuaType::Generic(generic) = source - && context.is_alias(&generic.get_base_type_id()) - && let Some(target) = context.alias_target_of(&generic.get_base_type_id()) - && !matches!( - target, - LuaType::Mapped(_) | LuaType::Call(_) | LuaType::Conditional(_) - ) - { - let expanded = expand_alias_generic(context.model, source); - if expanded != *source { - return check_general_type_compact( - context, - &expanded, - compact_type, - check_guard.next_level()?, - ); + // Resolve outer computations before comparing their data constructors. + for (is_source, ty) in [(false, compact_type), (true, source)] { + let expanded = context + .require_complete(|| expand_alias_head(context.model, ty))? + .ok_or(TypeCheckFailReason::Unresolved)?; + if expanded == *ty { + continue; } + return if is_source { + check_general_type_compact(context, &expanded, compact_type) + } else { + check_general_type_compact(context, source, &expanded) + }; } + check_general_type_compact_inner(context, source, compact_type) +} +fn check_general_type_compact_inner( + context: &mut TypeCheckContext, + source: &LuaType, + compact_type: &LuaType, +) -> TypeCheckResult { if fast_eq_check(source, compact_type) { return Ok(()); } - // Resolve boolean aliases before assignment mode selects a target union member. - if matches!(source, LuaType::Ref(_) | LuaType::Def(_)) { - let mut resolved = source.clone(); - let mut alias_guard = check_guard; - while let LuaType::Ref(id) | LuaType::Def(id) = &resolved - && let Some(target) = context.alias_target_of(id) - && let Ok(next_guard) = alias_guard.next_level() - { - resolved = target; - alias_guard = next_guard; - } - if resolved.is_boolean() { - return check_general_type_compact(context, &resolved, compact_type, alias_guard); - } - } - // Check the whole boolean domain before assignment mode selects a union member. if matches!(source, LuaType::Boolean) { - return simple_type::check_simple_type_compact(context, source, compact_type, check_guard); + return simple_type::check_simple_type_compact(context, source, compact_type); } // Assignment mode: integer is assignable to number; number is not assignable to integer. @@ -219,35 +255,32 @@ fn check_general_type_compact( if let LuaType::Union(union) = compact_type && !matches!(source, LuaType::Union(_)) { - for component in union.iter() { - if check_general_type_compact(context, source, component, check_guard.next_level()?) - .is_ok() - { - return Ok(()); - } - } - return Err(context.mismatch(source, compact_type)); + return check_alternatives( + context.mismatch(source, compact_type), + union + .iter() + .map(|component| check_general_type_compact(context, source, component)), + ); } } // Assignment-specific checks: semantics for object/intersection/function assignment not yet in the general path. if context.mode.uses_assignment_rules() - && let Some(result) = assign::check_special(context, source, compact_type, check_guard) + && assign::check_special(context, source, compact_type)?.is_some() { - return result; + return Ok(()); } // `compact` is an intersection: source satisfies all components. if let LuaType::Intersection(compact_intersection) = compact_type { if !matches!(source, LuaType::Intersection(_)) { - for component in compact_intersection.get_types() { - if check_general_type_compact(context, source, component, check_guard.next_level()?) - .is_ok() - { - return Ok(()); - } - } - return Err(context.mismatch(source, compact_type)); + return check_alternatives( + context.mismatch(source, compact_type), + compact_intersection + .get_types() + .iter() + .map(|component| check_general_type_compact(context, source, component)), + ); } } @@ -255,14 +288,9 @@ fn check_general_type_compact( LuaType::Unknown | LuaType::Any => Ok(()), LuaType::TplRef(tpl) => { if let Some(source_constraint) = tpl.get_constraint() { - return check_general_type_compact( - context, - source_constraint, - compact_type, - check_guard.next_level()?, - ); + return check_general_type_compact(context, source_constraint, compact_type); } - simple_type::check_simple_type_compact(context, source, compact_type, check_guard) + simple_type::check_simple_type_compact(context, source, compact_type) } LuaType::Nil | LuaType::Table @@ -287,21 +315,21 @@ fn check_general_type_compact( | LuaType::Namespace(_) | LuaType::Variadic(_) | LuaType::Language(_) => { - simple_type::check_simple_type_compact(context, source, compact_type, check_guard) + simple_type::check_simple_type_compact(context, source, compact_type) } LuaType::Ref(type_decl_id) => { - ref_type::check_ref_type_compact(context, type_decl_id, compact_type, check_guard) + ref_type::check_ref_type_compact(context, type_decl_id, compact_type) } LuaType::Def(type_decl_id) => { - ref_type::check_ref_type_compact(context, type_decl_id, compact_type, check_guard) + ref_type::check_ref_type_compact(context, type_decl_id, compact_type) } LuaType::DocFunction(doc_func) => { - func_type::check_doc_func_type_compact(context, doc_func, compact_type, check_guard) + func_type::check_doc_func_type_compact(context, doc_func, compact_type) } LuaType::Signature(sig_id) => { - func_type::check_sig_type_compact(context, sig_id, compact_type, check_guard) + func_type::check_sig_type_compact(context, sig_id, compact_type) } LuaType::Array(_) @@ -312,19 +340,16 @@ fn check_general_type_compact( | LuaType::TableGeneric(_) | LuaType::Call(_) | LuaType::MultiLineUnion(_) => { - check_union_intersection_source(context, source, compact_type, check_guard) + check_union_intersection_source(context, source, compact_type) } LuaType::Generic(generic) => { - generic_type::check_generic_type_compact(context, generic, compact_type, check_guard) + generic_type::check_generic_type_compact(context, generic, compact_type) } - LuaType::Instance(instantiate) => check_general_type_compact( - context, - instantiate.get_base(), - compact_type, - check_guard.next_level()?, - ), + LuaType::Instance(instantiate) => { + check_general_type_compact(context, instantiate.get_base(), compact_type) + } LuaType::TypeGuard(_) => { if compact_type.is_boolean() { return Ok(()); @@ -344,64 +369,44 @@ fn check_union_intersection_source( context: &mut TypeCheckContext, source: &LuaType, compact_type: &LuaType, - check_guard: guard::TypeCheckGuard, ) -> TypeCheckResult { match source { LuaType::Union(union) => { // Assignment mode: every member of the source union must be assignable to the target. if context.mode.uses_assignment_rules() { - for member in union.iter() { - check_general_type_compact( - context, - member, - compact_type, - check_guard.next_level()?, - )?; - } - return Ok(()); + return check_all( + union + .iter() + .map(|member| check_general_type_compact(context, member, compact_type)), + ); } // Strict subtype mode: when the target is also a union, every target component must have a matching member in the source union. if context.strict_union && let LuaType::Union(target_union) = compact_type { - let next_guard = check_guard.next_level()?; - for target_component in target_union.iter() { - check_general_type_compact(context, source, target_component, next_guard)?; - } - return Ok(()); + return check_all(target_union.iter().map(|target_component| { + check_general_type_compact(context, source, target_component) + })); } // Source union: pass if any member is assignable (old semantics). - for member in union.iter() { - match check_general_type_compact( - context, - member, - compact_type, - check_guard.next_level()?, - ) { - Ok(()) => return Ok(()), - Err(err) if err.is_type_not_match() => {} - Err(err) => return Err(err), - } - } - Err(context.mismatch(source, compact_type)) + check_alternatives( + context.mismatch(source, compact_type), + union + .iter() + .map(|member| check_general_type_compact(context, member, compact_type)), + ) } LuaType::Intersection(intersection) => { // Source intersection: any member matching is enough. - for member in intersection.get_types() { - if check_general_type_compact( - context, - member, - compact_type, - check_guard.next_level()?, - ) - .is_ok() - { - return Ok(()); - } - } - Err(context.mismatch(source, compact_type)) + check_alternatives( + context.mismatch(source, compact_type), + intersection + .get_types() + .iter() + .map(|member| check_general_type_compact(context, member, compact_type)), + ) } - _ => complex_type::check_complex_type_compact(context, source, compact_type, check_guard), + _ => complex_type::check_complex_type_compact(context, source, compact_type), } } diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/ref_type.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/ref_type.rs index e7dc08875..fbb6b5a5a 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/ref_type.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/ref_type.rs @@ -2,12 +2,11 @@ //! //! Parts still missing from semantic fall back: alias origin, enum field unions, call operator. -use crate::{LuaGenericType, LuaType, LuaTypeDeclId}; +use crate::{LuaGenericType, LuaType, LuaTypeDeclId, TypeDefKind}; use super::context::TypeCheckContext; -use super::guard::TypeCheckGuard; use super::sub_type::{get_base_type_id, is_sub_type_of}; -use super::{TypeCheckResult, check_general_type_compact}; +use super::{TypeCheckResult, check_all, check_general_type_compact}; /// Generic inheritance matching: whether sub's inheritance chain contains a generic parent matching the target generic instance arguments. fn is_sub_type_of_generic( @@ -52,55 +51,23 @@ pub fn check_ref_type_compact( context: &mut TypeCheckContext, source_id: &LuaTypeDeclId, compact_type: &LuaType, - check_guard: TypeCheckGuard, ) -> TypeCheckResult { // The type definition must exist. let Some(type_def) = context.type_def_of(source_id) else { return Err(context.mismatch(&LuaType::Ref(source_id.clone()), compact_type)); }; - let _ = type_def; - if context.is_alias(source_id) { - // Source is an alias: expand to its target type and re-check as the new source (recursive aliases are cycle-protected by the guard). - if let Some(target) = context.alias_target_of(source_id) { - return check_general_type_compact( - context, - &target, - compact_type, - check_guard.next_level()?, - ); - } - // The alias's expected type must accept every branch of the actual union. - if let LuaType::Union(compact_union) = compact_type { - for compact_sub_type in compact_union.iter() { - check_ref_type_compact( - context, - source_id, - compact_sub_type, - check_guard.next_level()?, - )?; - } - return Ok(()); - } - // Without an alias origin type, fall back to nominal: same id passes. - if matches!(compact_type, LuaType::Def(id) | LuaType::Ref(id) if id == source_id) { - return Ok(()); - } - return Err(context.mismatch(&LuaType::Ref(source_id.clone()), compact_type)); - } - - if context.is_enum(source_id) { - return check_ref_enum(context, source_id, compact_type, check_guard); + if type_def.kind == TypeDefKind::Enum { + return check_ref_enum(context, source_id, compact_type); } - check_ref_class(context, source_id, compact_type, check_guard) + check_ref_class(context, source_id, compact_type) } fn check_ref_enum( context: &mut TypeCheckContext, source_id: &LuaTypeDeclId, compact_type: &LuaType, - _check_guard: TypeCheckGuard, ) -> TypeCheckResult { // Directly match the same type. if matches!(compact_type, LuaType::Def(id) | LuaType::Ref(id) if id == source_id) { @@ -130,7 +97,6 @@ fn check_ref_class( context: &mut TypeCheckContext, source_id: &LuaTypeDeclId, compact_type: &LuaType, - check_guard: TypeCheckGuard, ) -> TypeCheckResult { match compact_type { LuaType::Def(id) | LuaType::Ref(id) => { @@ -153,19 +119,15 @@ fn check_ref_class( } LuaType::Table => Ok(()), LuaType::Union(union_type) => { - for typ in union_type.iter() { - check_general_type_compact( - context, - &LuaType::Ref(source_id.clone()), - typ, - check_guard.next_level()?, - )?; - } - Ok(()) + check_all(union_type.iter().map(|typ| { + check_general_type_compact(context, &LuaType::Ref(source_id.clone()), typ) + })) } LuaType::Generic(generic) => { let base_type_id = generic.get_base_type_id(); - if (context.strict_generic && is_sub_type_of_generic(context, source_id, generic)) + if (context.strict_generic + && context + .require_complete(|| is_sub_type_of_generic(context, source_id, generic))?) || (!context.strict_generic && (source_id == &base_type_id || is_sub_type_of(context, &base_type_id, source_id) diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/simple_type.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/simple_type.rs index 2f8e4452d..577016f23 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/simple_type.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/simple_type.rs @@ -6,16 +6,14 @@ use std::rc::Rc; use crate::{LuaMemberKey, LuaType, LuaTypeDeclId, VariadicType}; -use super::TypeCheckResult; use super::context::{TypeCheckContext, TypeCheckMode}; -use super::guard::TypeCheckGuard; use super::sub_type::{base_type_name, get_base_type_id, is_sub_type_of}; +use super::{TypeCheckResult, check_all, check_alternatives}; pub fn check_simple_type_compact( context: &mut TypeCheckContext, source: &LuaType, compact_type: &LuaType, - check_guard: TypeCheckGuard, ) -> TypeCheckResult { match source { LuaType::Unknown | LuaType::Any => return Ok(()), @@ -34,21 +32,23 @@ pub fn check_simple_type_compact( }, ..context.clone() }; - let next_guard = check_guard.next_level()?; - for (index, expected) in tuple.get_types().iter().enumerate() { - let key = LuaMemberKey::Integer(index as i64 + 1); - let actual = context - .model - .member_type(source, &key) - .unwrap_or(LuaType::Nil); - super::check_general_type_compact( - &mut element_context, - &actual, - expected, - next_guard, - )?; - } - return Ok(()); + return check_all( + tuple + .get_types() + .iter() + .enumerate() + .map(|(index, expected)| { + let key = LuaMemberKey::Integer(index as i64 + 1); + let actual = context + .require_complete(|| context.model.member_type(source, &key))? + .unwrap_or(LuaType::Nil); + super::check_general_type_compact( + &mut element_context, + &actual, + expected, + ) + }), + ); } if matches!( compact_type, @@ -92,16 +92,13 @@ pub fn check_simple_type_compact( } LuaType::Boolean => { // Both values must fit, possibly through different target union members. - let next_guard = check_guard.next_level()?; - for value in [true, false] { + return check_all([true, false].into_iter().map(|value| { super::check_general_type_compact( context, &LuaType::BooleanConst(value), compact_type, - next_guard, - )?; - } - return Ok(()); + ) + })); } LuaType::BooleanConst(value) | LuaType::DocBooleanConst(value) => match compact_type { LuaType::Boolean => return Ok(()), @@ -109,19 +106,12 @@ pub fn check_simple_type_compact( return Ok(()); } LuaType::Union(union) => { - for component in union.iter() { - match super::check_general_type_compact( - context, - source, - component, - check_guard.next_level()?, - ) { - Ok(()) => return Ok(()), - Err(err) if err.is_type_not_match() => {} - Err(err) => return Err(err), - } - } - return Err(context.mismatch(source, compact_type)); + return check_alternatives( + context.mismatch(source, compact_type), + union.iter().map(|component| { + super::check_general_type_compact(context, source, component) + }), + ); } _ => {} }, @@ -134,7 +124,7 @@ pub fn check_simple_type_compact( return Ok(()); } LuaType::Ref(_) => { - match check_base_type_for_ref_compact(context, source, compact_type, check_guard) { + match check_base_type_for_ref_compact(context, source, compact_type) { Ok(_) => return Ok(()), Err(err) if err.is_type_not_match() => {} Err(err) => return Err(err), @@ -158,7 +148,7 @@ pub fn check_simple_type_compact( return Ok(()); } LuaType::Ref(_) => { - match check_base_type_for_ref_compact(context, source, compact_type, check_guard) { + match check_base_type_for_ref_compact(context, source, compact_type) { Ok(_) => return Ok(()), Err(err) if err.is_type_not_match() => {} Err(err) => return Err(err), @@ -176,7 +166,7 @@ pub fn check_simple_type_compact( return Ok(()); } LuaType::Ref(_) => { - match check_base_type_for_ref_compact(context, source, compact_type, check_guard) { + match check_base_type_for_ref_compact(context, source, compact_type) { Ok(_) => return Ok(()), Err(err) if err.is_type_not_match() => {} Err(err) => return Err(err), @@ -201,7 +191,7 @@ pub fn check_simple_type_compact( return Ok(()); } LuaType::Ref(_) => { - match check_base_type_for_ref_compact(context, source, compact_type, check_guard) { + match check_base_type_for_ref_compact(context, source, compact_type) { Ok(_) => return Ok(()), Err(err) if err.is_type_not_match() => {} Err(err) => return Err(err), @@ -249,7 +239,7 @@ pub fn check_simple_type_compact( } LuaType::Ref(_) => { // M0: doc constants are checked against custom integer types by nominal base type (integer). - return check_base_type_for_ref_compact(context, source, compact_type, check_guard); + return check_base_type_for_ref_compact(context, source, compact_type); } _ => {} }, @@ -283,7 +273,7 @@ pub fn check_simple_type_compact( } } LuaType::Variadic(source_type) => { - return check_variadic_type_compact(context, source_type, compact_type, check_guard); + return check_variadic_type_compact(context, source_type, compact_type); } LuaType::Language(lang_str) => match compact_type { LuaType::Language(compact_lang_str) => { @@ -300,25 +290,24 @@ pub fn check_simple_type_compact( } if let LuaType::Union(union) = compact_type { - for sub_compact in union.iter() { - check_simple_type_compact(context, source, sub_compact, check_guard.next_level()?)?; - } - return Ok(()); + return check_all( + union + .iter() + .map(|sub_compact| super::check_general_type_compact(context, source, sub_compact)), + ); } Err(context.mismatch(source, compact_type)) } -/// Base-type matching against custom types (scenarios like `---@alias integer = ...`; when semantic has no origin, compare by base type name). +/// Match primitive values against named base types and their descendants. fn check_base_type_for_ref_compact( context: &mut TypeCheckContext, source: &LuaType, compact_type: &LuaType, - check_guard: TypeCheckGuard, ) -> TypeCheckResult { if let LuaType::Ref(type_decl_id) = compact_type { - // Alias expansion (semantic has no origin type, so use the base type name). - if let Some(base_type) = base_type_name_of_ref(context, type_decl_id, check_guard) { + if let Some(base_type) = base_type_name_of_ref(type_decl_id) { if let Some(source_name) = base_type_name(source) && source_name == base_type { @@ -335,15 +324,7 @@ fn check_base_type_for_ref_compact( Err(context.mismatch(source, compact_type)) } -/// Alias → base type name (walk the alias chain to find a non-alias base name). Since semantic has no origin type, fall back to direct name matching. -fn base_type_name_of_ref( - context: &mut TypeCheckContext, - id: &LuaTypeDeclId, - _check_guard: TypeCheckGuard, -) -> Option<&'static str> { - if context.is_alias(id) { - return None; - } +fn base_type_name_of_ref(id: &LuaTypeDeclId) -> Option<&'static str> { match id.get_name() { "integer" => Some("integer"), "number" => Some("number"), @@ -359,7 +340,6 @@ fn check_variadic_type_compact( context: &mut TypeCheckContext, source_type: &VariadicType, compact_type: &LuaType, - check_guard: TypeCheckGuard, ) -> TypeCheckResult { let mut element_context = TypeCheckContext { mode: match context.mode { @@ -376,43 +356,34 @@ fn check_variadic_type_compact( .get_max_len() .or_else(|| source_type.get_max_len()) .unwrap_or(1); - for index in 0..count { + return check_all((0..count).map(|index| { let source = source_type.get_type(index).cloned().unwrap_or(LuaType::Nil); let compact = compact_variadic .get_type(index) .cloned() .unwrap_or(LuaType::Nil); - super::check_general_type_compact( - &mut element_context, - &source, - &compact, - check_guard.next_level()?, - )?; - } + super::check_general_type_compact(&mut element_context, &source, &compact) + })); } VariadicType::Multi(compact_multi) => { - for (index, compact_type) in compact_multi.iter().enumerate() { - let source = if compact_type.contain_multi_return() { - LuaType::Variadic(Rc::new(source_type.get_new_variadic_from(index))) - } else { - source_type.get_type(index).cloned().unwrap_or(LuaType::Nil) - }; - super::check_general_type_compact( - &mut element_context, - &source, - compact_type, - check_guard.next_level()?, - )?; - } + return check_all(compact_multi.iter().enumerate().map( + |(index, compact_type)| { + let source = if compact_type.contain_multi_return() { + LuaType::Variadic(Rc::new(source_type.get_new_variadic_from(index))) + } else { + source_type.get_type(index).cloned().unwrap_or(LuaType::Nil) + }; + super::check_general_type_compact( + &mut element_context, + &source, + compact_type, + ) + }, + )); } }, _ => { - check_simple_type_compact( - context, - source_base, - compact_type, - check_guard.next_level()?, - )?; + super::check_general_type_compact(context, source_base, compact_type)?; } }, VariadicType::Multi(_) => {} diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/test.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/test.rs index 7f9f339d2..13a07fa35 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/test.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/test.rs @@ -10,9 +10,7 @@ use crate::{ use super::check_type_detail; use super::context::TypeCheckContext; -use super::{ - TypeCheckFailReason, check_general_type_compact, guard::TypeCheckGuard, is_compatible, -}; +use super::{TypeCheckFailReason, check_general_type_compact, is_compatible}; /// Model-less context (structural tests don't need resolution). fn with_ctx(f: impl FnOnce(&mut TypeCheckContext<'_>) -> R) -> R { @@ -23,9 +21,7 @@ fn with_ctx(f: impl FnOnce(&mut TypeCheckContext<'_>) -> R) -> R { } fn ok(source: &LuaType, target: &LuaType) -> bool { - with_ctx(|context| { - check_general_type_compact(context, source, target, TypeCheckGuard::new()).is_ok() - }) + with_ctx(|context| check_general_type_compact(context, source, target).is_ok()) } fn doc_fun(params: Vec>, ret: LuaType) -> LuaType { diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_eval.rs b/crates/emmylua_code_analysis/src/semantic_model/type_eval.rs index 09a88734b..68338e954 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_eval.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_eval.rs @@ -12,12 +12,20 @@ use smol_str::SmolStr; use crate::{ GenericParam, GenericTpl, GenericTplId, LuaAliasCallKind, LuaAliasCallType, LuaArrayType, LuaConditionalType, LuaFunctionType, LuaGenericType, LuaIntersectionType, LuaMappedType, - LuaMemberKey, LuaObjectType, LuaTupleStatus, LuaTupleType, LuaType, LuaTypeDeclId, - LuaUnionType, TypeDef, TypeDefKind, VariadicType, + LuaMemberKey, LuaObjectType, LuaTupleStatus, LuaTupleType, LuaType, LuaTypeNode, LuaUnionType, + TypeDef, TypeDefKind, VariadicType, }; use super::SemanticModel; use super::infer::unify; +use super::type_check::{RequiredChecks, TypeCheckFailReason, TypeCheckResult, check_all}; + +mod alias_expansion; +use alias_expansion::AliasPath; +pub(crate) use alias_expansion::{expand_alias_branches, expand_alias_head, with_expanded_alias}; + +#[cfg(test)] +mod tests; /// Extract keys usable as object fields from literal types (strings/integers, including unions). pub fn literal_keys(ty: &LuaType) -> Vec<(LuaMemberKey, LuaType)> { @@ -133,42 +141,79 @@ pub fn member_key_from_literal(ty: &LuaType) -> Option { /// Try to concretely evaluate `T[K]` (K as literal / union / keyof / alias). pub fn eval_index_access(model: &SemanticModel, base: &LuaType, key: &LuaType) -> Option { - match key { - LuaType::Call(call) if call.get_call_kind() == LuaAliasCallKind::KeyOf => { - let key_base = call.get_operands().first()?; - let infos = model.member_infos(key_base); - if infos.is_empty() { - return None; + enum Task { + Enter(LuaType), + Leave, + } + let mut path = AliasPath::new(model)?; + let mut pending = vec![Task::Enter(key.clone())]; + let mut keys = Vec::new(); + while let Some(task) = pending.pop() { + let key = match task { + Task::Leave => { + path.leave(); + continue; } - Some(LuaType::from_vec( - infos.into_iter().map(|info| info.typ).collect(), - )) - } - LuaType::Ref(id) | LuaType::Def(id) => { - let def = model.type_def_of(id)?; - let target = model.alias_target(&def)?; - eval_index_access(model, base, &target) - } - LuaType::Union(union) => { - let mut types = Vec::new(); - for component in union.into_vec() { - if let Some(ty) = eval_index_access(model, base, &component) { - if !types.contains(&ty) { - types.push(ty); - } + Task::Enter(key) => key, + }; + match &key { + LuaType::Generic(_) => { + let expanded = expand_alias_generic(model, &key); + if expanded != key && path.enter(&key).is_some() { + pending.push(Task::Leave); + pending.push(Task::Enter(expanded)); } + continue; } - if types.is_empty() { - None - } else { - Some(LuaType::from_vec(types)) + LuaType::Ref(id) | LuaType::Def(id) => { + let Some(def) = model.type_def_of(id) else { + continue; + }; + if path.enter(&key).is_none() { + continue; + } + if let Some(target) = model.alias_target(&def) { + pending.push(Task::Leave); + pending.push(Task::Enter(target)); + } else { + path.leave(); + } + continue; } + LuaType::Union(union) => { + for component in union.into_vec().into_iter().rev() { + pending.push(Task::Enter(component)); + } + continue; + } + _ => {} } - _ => { - let member_key = member_key_from_literal(key)?; - model.member_type(base, &member_key) + keys.push(key); + } + // Key expansion is complete. Base lookup follows an independent alias path. + let keys = keys.iter().flat_map(|key| match key { + LuaType::Call(call) if call.get_call_kind() == LuaAliasCallKind::KeyOf => call + .get_operands() + .first() + .map(|base| keyof_keys(model, base)) + .unwrap_or_default(), + _ => literal_keys(key), + }); + let mut types = Vec::new(); + for (key, _) in keys { + let ty = match (base, &key) { + (LuaType::Tuple(tuple), LuaMemberKey::Integer(i)) => { + tuple.get_types().get(*i as usize).cloned() + } + _ => model.member_type(base, &key), + }; + if let Some(ty) = ty + && !types.contains(&ty) + { + types.push(ty); } } + (!types.is_empty()).then(|| LuaType::from_vec(types)) } /// Expand `Call(Index)` into field values/unions (semantic layer). @@ -176,50 +221,10 @@ pub fn expand_index_call(model: &SemanticModel, call: &LuaAliasCallType) -> Opti if call.get_call_kind() != LuaAliasCallKind::Index { return None; } - let operands = call.get_operands(); - if operands.len() != 2 { + let [base, key] = call.get_operands().as_slice() else { return None; - } - let base = &operands[0]; - let key = &operands[1]; - if let LuaType::Call(key_call) = key - && key_call.get_call_kind() == LuaAliasCallKind::KeyOf - && let Some(key_base) = key_call.get_operands().first() - { - let infos = model.member_infos(key_base); - if infos.is_empty() { - return None; - } - return Some(LuaType::from_vec( - infos.into_iter().map(|info| info.typ).collect(), - )); - } - let keys = literal_keys(key); - if keys.is_empty() { - return None; - } - let mut types = Vec::new(); - for (_, key_ty) in keys { - if let Some(member_key) = member_key_from_literal(&key_ty) { - let ty = match base { - LuaType::Tuple(tuple) => match &member_key { - LuaMemberKey::Integer(i) => tuple.get_types().get(*i as usize).cloned(), - _ => None, - }, - _ => model.member_type(base, &member_key), - }; - if let Some(ty) = ty - && !types.contains(&ty) - { - types.push(ty); - } - } - } - if types.is_empty() { - None - } else { - Some(LuaType::from_vec(types)) - } + }; + eval_index_access(model, base, key) } /// Normalize parameter-name references in mapped values (usually `Ref("P")`) into `TplRef(P)`. @@ -234,70 +239,11 @@ fn bind_mapped_param_ref(ty: &LuaType, param_id: GenericTplId, param_name: &str) false, None, ))), - Array(array) => Array(Rc::new(LuaArrayType::from_base_type( - bind_mapped_param_ref(array.get_base(), param_id, param_name), - ))), - Tuple(tuple) => Tuple(Rc::new(LuaTupleType::new( - tuple - .get_types() - .iter() - .map(|t| bind_mapped_param_ref(t, param_id, param_name)) - .collect(), - tuple.status, - ))), - Union(union) => Union(Rc::new(LuaUnionType::from_vec( - union - .into_vec() - .iter() - .map(|t| bind_mapped_param_ref(t, param_id, param_name)) - .collect(), - ))), - Object(object) => Object(Rc::new(LuaObjectType::new_with_fields( - object - .get_fields() - .iter() - .map(|(k, v)| (k.clone(), bind_mapped_param_ref(v, param_id, param_name))) - .collect(), - object - .get_index_access() - .iter() - .map(|(k, v)| { - ( - bind_mapped_param_ref(k, param_id, param_name), - bind_mapped_param_ref(v, param_id, param_name), - ) - }) - .collect(), - ))), - Variadic(variadic) => Variadic(Rc::new(match variadic.as_ref() { - VariadicType::Base(base) => { - VariadicType::Base(bind_mapped_param_ref(base, param_id, param_name)) - } - VariadicType::Repeat(base) => { - VariadicType::Repeat(bind_mapped_param_ref(base, param_id, param_name)) - } - VariadicType::Multi(types) => VariadicType::Multi( - types - .iter() - .map(|t| bind_mapped_param_ref(t, param_id, param_name)) - .collect(), - ), - })), - Call(call) => Call(Rc::new(LuaAliasCallType::new( - call.get_call_kind(), - call.get_operands() - .iter() - .map(|t| bind_mapped_param_ref(t, param_id, param_name)) - .collect(), - ))), - Conditional(conditional) => Conditional(Rc::new(LuaConditionalType::new( - bind_mapped_param_ref(conditional.get_checked_type(), param_id, param_name), - bind_mapped_param_ref(conditional.get_extends_type(), param_id, param_name), - bind_mapped_param_ref(conditional.get_true_type(), param_id, param_name), - bind_mapped_param_ref(conditional.get_false_type(), param_id, param_name), - conditional.get_infer_params().to_vec(), - conditional.has_new, - ))), + Array(_) | Tuple(_) | Union(_) | Object(_) | Variadic(_) | Call(_) | Conditional(_) => { + map_alias_children(ty, &mut |child| { + bind_mapped_param_ref(child, param_id, param_name) + }) + } _ => ty.clone(), } } @@ -321,8 +267,8 @@ fn eval_mapped_value( value } -/// Unbound generic parameters in call results must not leak as bare `TplRef`; uniformly degrade them to `Unknown`. -/// This is the semantic rule after type instantiation: generics that cannot be determined at the call site should no longer be exposed. +/// Replace unbound template references in call results with `Unknown`. +/// Nested functions keep their own generic context. pub fn sanitize_unresolved_generics( ty: &LuaType, allowed: &std::collections::HashSet, @@ -330,237 +276,62 @@ pub fn sanitize_unresolved_generics( use LuaType::*; match ty { TplRef(tpl) if !allowed.contains(&tpl.get_tpl_id()) => Unknown, - Array(array) => Array(Rc::new(LuaArrayType::from_base_type( - sanitize_unresolved_generics(array.get_base(), allowed), - ))), - Tuple(tuple) => Tuple(Rc::new(LuaTupleType::new( - tuple - .get_types() + TableGeneric(params) => TableGeneric(Rc::new( + params .iter() - .map(|t| sanitize_unresolved_generics(t, allowed)) - .collect(), - tuple.status, - ))), - Union(union) => Union(Rc::new(LuaUnionType::from_vec( - union - .into_vec() - .iter() - .map(|t| sanitize_unresolved_generics(t, allowed)) - .collect(), - ))), - Object(object) => Object(Rc::new(LuaObjectType::new_with_fields( - object - .get_fields() - .iter() - .map(|(k, v)| (k.clone(), sanitize_unresolved_generics(v, allowed))) - .collect(), - object - .get_index_access() - .iter() - .map(|(k, v)| { - ( - sanitize_unresolved_generics(k, allowed), - sanitize_unresolved_generics(v, allowed), - ) - }) - .collect(), - ))), - TableGeneric(generic) => TableGeneric(Rc::new( - generic - .iter() - .map(|t| sanitize_unresolved_generics(t, allowed)) + .map(|ty| sanitize_unresolved_generics(ty, allowed)) .collect(), )), - Variadic(variadic) => Variadic(Rc::new(match variadic.as_ref() { - VariadicType::Base(base) => { - VariadicType::Base(sanitize_unresolved_generics(base, allowed)) - } - VariadicType::Repeat(base) => { - VariadicType::Repeat(sanitize_unresolved_generics(base, allowed)) - } - VariadicType::Multi(types) => VariadicType::Multi( - types - .iter() - .map(|t| sanitize_unresolved_generics(t, allowed)) - .collect(), - ), - })), - Call(call) => Call(Rc::new(LuaAliasCallType::new( - call.get_call_kind(), - call.get_operands() - .iter() - .map(|t| sanitize_unresolved_generics(t, allowed)) - .collect(), - ))), - // Do not backfill nested function bodies: inner functions may have their own generic context. - DocFunction(_) => ty.clone(), + Array(_) | Tuple(_) | Union(_) | Object(_) | Variadic(_) | Call(_) => { + map_alias_children(ty, &mut |child| { + sanitize_unresolved_generics(child, allowed) + }) + } _ => ty.clone(), } } -/// Clean up call results: first perform model-free generic sanitization, then degrade unbound generic parameters that appear as `Ref`/`Def` in "generic parameter positions" (e.g. `T` in `CountObservable` has no matching TypeDef) to `Unknown`. Bare `Ref("A")` in return types, string templates, etc. remain unchanged. +/// Clean up call results, also replacing unresolved names inside generic arguments. +/// Bare named return types and nested function scopes remain unchanged. pub fn sanitize_unresolved_generics_with_model( model: &SemanticModel, ty: &LuaType, allowed: &std::collections::HashSet, ) -> LuaType { use LuaType::*; - let ty = sanitize_unresolved_generics(ty, allowed); match ty { - Generic(generic) => { - let params: Vec = generic - .get_params() - .iter() - .map(|param| sanitize_generic_param(model, param)) - .collect(); - Generic(Rc::new(LuaGenericType::new( - generic.get_base_type_id().clone(), - params, - ))) - } - Array(array) => Array(Rc::new(LuaArrayType::from_base_type( - sanitize_unresolved_generics_with_model(model, array.get_base(), allowed), - ))), - Tuple(tuple) => Tuple(Rc::new(LuaTupleType::new( - tuple - .get_types() - .iter() - .map(|ty| sanitize_unresolved_generics_with_model(model, ty, allowed)) - .collect(), - tuple.status, - ))), - Union(union) => Union(Rc::new(LuaUnionType::from_vec( - union - .into_vec() - .iter() - .map(|ty| sanitize_unresolved_generics_with_model(model, ty, allowed)) - .collect(), - ))), - Object(object) => Object(Rc::new(LuaObjectType::new_with_fields( - object - .get_fields() - .iter() - .map(|(key, ty)| { - ( - key.clone(), - sanitize_unresolved_generics_with_model(model, ty, allowed), - ) - }) - .collect(), - object - .get_index_access() - .iter() - .map(|(key, ty)| { - ( - sanitize_unresolved_generics_with_model(model, key, allowed), - sanitize_unresolved_generics_with_model(model, ty, allowed), - ) - }) - .collect(), - ))), - TableGeneric(generic) => TableGeneric(Rc::new( - generic + TplRef(tpl) if !allowed.contains(&tpl.get_tpl_id()) => Unknown, + Generic(_) => map_alias_children(ty, &mut |child| sanitize_generic_param(model, child)), + TableGeneric(params) => TableGeneric(Rc::new( + params .iter() .map(|ty| sanitize_unresolved_generics_with_model(model, ty, allowed)) .collect(), )), - Variadic(variadic) => Variadic(Rc::new(match variadic.as_ref() { - VariadicType::Base(base) => VariadicType::Base( - sanitize_unresolved_generics_with_model(model, base, allowed), - ), - VariadicType::Repeat(base) => VariadicType::Repeat( - sanitize_unresolved_generics_with_model(model, base, allowed), - ), - VariadicType::Multi(types) => VariadicType::Multi( - types - .iter() - .map(|ty| sanitize_unresolved_generics_with_model(model, ty, allowed)) - .collect(), - ), - })), - Call(call) => Call(Rc::new(LuaAliasCallType::new( - call.get_call_kind(), - call.get_operands() - .iter() - .map(|ty| sanitize_unresolved_generics_with_model(model, ty, allowed)) - .collect(), - ))), - other => other, + Array(_) | Tuple(_) | Union(_) | Object(_) | Variadic(_) | Call(_) => { + map_alias_children(ty, &mut |child| { + sanitize_unresolved_generics_with_model(model, child, allowed) + }) + } + _ => ty.clone(), } } -/// Recursively sanitize generic parameter positions: `T` (no matching TypeDef) degrades to `Unknown`. +/// Replace unresolved named types inside generic arguments with `Unknown`. fn sanitize_generic_param(model: &SemanticModel, ty: &LuaType) -> LuaType { use LuaType::*; match ty { - Ref(id) | Def(id) if model.type_def_of(&id).is_none() => Unknown, - Generic(generic) => Generic(Rc::new(LuaGenericType::new( - generic.get_base_type_id().clone(), - generic - .get_params() - .iter() - .map(|param| sanitize_generic_param(model, param)) - .collect(), - ))), - Array(array) => Array(Rc::new(LuaArrayType::from_base_type( - sanitize_generic_param(model, array.get_base()), - ))), - Tuple(tuple) => Tuple(Rc::new(LuaTupleType::new( - tuple - .get_types() - .iter() - .map(|ty| sanitize_generic_param(model, ty)) - .collect(), - tuple.status, - ))), - Union(union) => Union(Rc::new(LuaUnionType::from_vec( - union - .into_vec() - .iter() - .map(|ty| sanitize_generic_param(model, ty)) - .collect(), - ))), - Object(object) => Object(Rc::new(LuaObjectType::new_with_fields( - object - .get_fields() - .iter() - .map(|(key, ty)| (key.clone(), sanitize_generic_param(model, ty))) - .collect(), - object - .get_index_access() - .iter() - .map(|(key, ty)| { - ( - sanitize_generic_param(model, key), - sanitize_generic_param(model, ty), - ) - }) - .collect(), - ))), - TableGeneric(generic) => TableGeneric(Rc::new( - generic + Ref(id) | Def(id) if model.type_def_of(id).is_none() => Unknown, + TableGeneric(params) => TableGeneric(Rc::new( + params .iter() .map(|ty| sanitize_generic_param(model, ty)) .collect(), )), - Variadic(variadic) => Variadic(Rc::new(match variadic.as_ref() { - VariadicType::Base(base) => VariadicType::Base(sanitize_generic_param(model, base)), - VariadicType::Repeat(base) => VariadicType::Repeat(sanitize_generic_param(model, base)), - VariadicType::Multi(types) => VariadicType::Multi( - types - .iter() - .map(|ty| sanitize_generic_param(model, ty)) - .collect(), - ), - })), - Call(call) => Call(Rc::new(LuaAliasCallType::new( - call.get_call_kind(), - call.get_operands() - .iter() - .map(|ty| sanitize_generic_param(model, ty)) - .collect(), - ))), - other => other.clone(), + Generic(_) | Array(_) | Tuple(_) | Union(_) | Object(_) | Variadic(_) | Call(_) => { + map_alias_children(ty, &mut |child| sanitize_generic_param(model, child)) + } + _ => ty.clone(), } } @@ -614,361 +385,169 @@ pub fn class_generic_map(model: &SemanticModel, ty: &LuaType) -> Option concrete argument). +/// Substitute class generics referenced by name without traversing replacements. pub fn substitute_named_refs(ty: &LuaType, map: &HashMap) -> LuaType { + if !ty.any_type(|ty| match ty { + LuaType::Ref(id) | LuaType::Def(id) => map.contains_key(id.get_name()), + _ => false, + }) { + return ty.clone(); + } + map_type(ty, &mut |ty| match ty { + LuaType::Ref(id) | LuaType::Def(id) => map.get(id.get_name()).cloned(), + _ => None, + }) +} + +/// Rewrite the existing type tree iteratively, without visiting replacements. +pub(super) fn map_type( + ty: &LuaType, + replace: &mut impl FnMut(&LuaType) -> Option, +) -> LuaType { + enum Task<'a> { + Enter(&'a LuaType), + Rebuild(&'a LuaType, usize), + } + let mut pending = vec![Task::Enter(ty)]; + let mut values = Vec::new(); + while let Some(task) = pending.pop() { + match task { + Task::Enter(ty) => { + if let Some(result) = replace(ty) { + values.push(result); + continue; + } + let mut children = Vec::new(); + map_alias_children(ty, &mut |child| { + children.push(child); + child.clone() + }); + pending.push(Task::Rebuild(ty, children.len())); + pending.extend(children.into_iter().rev().map(Task::Enter)); + } + Task::Rebuild(ty, count) => { + let mut children = values.split_off(values.len() - count).into_iter(); + values.push(map_alias_children(ty, &mut |_| { + children.next().expect("each child has a result") + })); + } + } + } + values.pop().expect("type traversal must produce a result") +} + +/// Normalize existing structures and resolve computations to their outer shape. +/// Fields produced by an alias stay lazy until a consumer reads them. +pub fn expand_alias_generic(model: &SemanticModel, ty: &LuaType) -> LuaType { + let Some(evaluation) = crate::semantic_model::cache::TypeEvaluationScope::new(&model.cache) + else { + return ty.clone(); + }; + let result = map_type(ty, &mut |current| { + if !evaluation.visit(current) { + return Some(current.clone()); + } + match current { + LuaType::TableGeneric(_) => Some(current.clone()), + LuaType::Generic(_) + | LuaType::Call(_) + | LuaType::Conditional(_) + | LuaType::Mapped(_) + if alias_expansion::needs_reduction(model, current) => + { + Some(expand_alias_branches(model, current).unwrap_or_else(|| current.clone())) + } + _ => None, + } + }); + if model.cache.borrow().evaluation_exhausted { + ty.clone() + } else { + result + } +} + +/// Rebuild the structures used by alias and conditional evaluation. +/// The caller supplies children; this helper does not recurse or evaluate queries. +fn map_alias_children<'a>( + ty: &'a LuaType, + map: &mut impl FnMut(&'a LuaType) -> LuaType, +) -> LuaType { use LuaType::*; match ty { - Ref(id) | Def(id) => map - .get(id.get_name()) - .cloned() - .unwrap_or_else(|| ty.clone()), - Array(array) => Array(Rc::new(LuaArrayType::from_base_type( - substitute_named_refs(array.get_base(), map), + TableGeneric(types) => TableGeneric(Rc::new(types.iter().map(&mut *map).collect())), + Generic(generic) => Generic(Rc::new(LuaGenericType::new( + generic.get_base_type_id(), + generic.get_params().iter().map(&mut *map).collect(), ))), + Array(array) => Array(Rc::new(LuaArrayType::from_base_type(map(array.get_base())))), Tuple(tuple) => Tuple(Rc::new(LuaTupleType::new( - tuple - .get_types() - .iter() - .map(|t| substitute_named_refs(t, map)) - .collect(), + tuple.get_types().iter().map(&mut *map).collect(), tuple.status, ))), - DocFunction(fun) => DocFunction(Rc::new(LuaFunctionType::new( - fun.get_async_state(), - fun.is_colon_define(), - fun.is_variadic(), - fun.get_params() - .iter() - .map(|(name, ty)| { - ( - name.clone(), - ty.as_ref().map(|t| substitute_named_refs(t, map)), - ) - }) - .collect(), - substitute_named_refs(fun.get_ret(), map), - Some(fun.get_generic_params().to_vec()), + Union(union) => Union(Rc::new(LuaUnionType::from_vec( + union.iter().map(&mut *map).collect(), + ))), + Intersection(intersection) => Intersection(Rc::new(LuaIntersectionType::new( + intersection.get_types().iter().map(&mut *map).collect(), ))), Object(object) => Object(Rc::new(LuaObjectType::new_with_fields( object .get_fields() .iter() - .map(|(key, ty)| (key.clone(), substitute_named_refs(ty, map))) + .map(|(k, v)| (k.clone(), map(v))) .collect(), object .get_index_access() .iter() - .map(|(key, ty)| { - ( - substitute_named_refs(key, map), - substitute_named_refs(ty, map), - ) - }) + .map(|(k, v)| (map(k), map(v))) .collect(), ))), - Union(union) => Union(Rc::new(LuaUnionType::from_vec( - union - .into_vec() - .iter() - .map(|t| substitute_named_refs(t, map)) - .collect(), - ))), - Intersection(intersection) => Intersection(Rc::new(LuaIntersectionType::new( - intersection - .get_types() - .iter() - .map(|t| substitute_named_refs(t, map)) - .collect(), - ))), - Generic(generic) => Generic(Rc::new(LuaGenericType::new( - generic.get_base_type_id(), - generic - .get_params() + DocFunction(fun) => DocFunction(Rc::new(LuaFunctionType::new( + fun.get_async_state(), + fun.is_colon_define(), + fun.is_variadic(), + fun.get_params() .iter() - .map(|t| substitute_named_refs(t, map)) + .map(|(name, ty)| (name.clone(), ty.as_ref().map(&mut *map))) .collect(), + map(fun.get_ret()), + Some(fun.get_generic_params().to_vec()), ))), - TableGeneric(generic) => TableGeneric(Rc::new( - generic - .iter() - .map(|t| substitute_named_refs(t, map)) - .collect(), - )), Variadic(variadic) => Variadic(Rc::new(match variadic.as_ref() { - VariadicType::Base(base) => VariadicType::Base(substitute_named_refs(base, map)), - VariadicType::Repeat(base) => VariadicType::Repeat(substitute_named_refs(base, map)), - VariadicType::Multi(types) => VariadicType::Multi( - types - .iter() - .map(|t| substitute_named_refs(t, map)) - .collect(), - ), + VariadicType::Base(base) => VariadicType::Base(map(base)), + VariadicType::Repeat(base) => VariadicType::Repeat(map(base)), + VariadicType::Multi(types) => { + VariadicType::Multi(types.iter().map(&mut *map).collect()) + } })), Call(call) => Call(Rc::new(LuaAliasCallType::new( call.get_call_kind(), - call.get_operands() - .iter() - .map(|t| substitute_named_refs(t, map)) - .collect(), - ))), - Conditional(conditional) => Conditional(Rc::new(LuaConditionalType::new( - substitute_named_refs(conditional.get_checked_type(), map), - substitute_named_refs(conditional.get_extends_type(), map), - substitute_named_refs(conditional.get_true_type(), map), - substitute_named_refs(conditional.get_false_type(), map), - conditional.get_infer_params().to_vec(), - conditional.has_new, + call.get_operands().iter().map(&mut *map).collect(), ))), Mapped(mapped) => Mapped(Rc::new(LuaMappedType::new( ( mapped.param.0, GenericParam::new( mapped.param.1.name.clone(), - mapped - .param - .1 - .constraint - .as_ref() - .map(|t| substitute_named_refs(t, map)), - mapped - .param - .1 - .default - .as_ref() - .map(|t| substitute_named_refs(t, map)), + mapped.param.1.constraint.as_ref().map(&mut *map), + mapped.param.1.default.as_ref().map(&mut *map), mapped.param.1.is_const, mapped.param.1.attributes.clone(), ), ), - substitute_named_refs(&mapped.value, map), + map(&mapped.value), mapped.is_readonly, mapped.is_optional, ))), - _ => ty.clone(), - } -} - -/// Expand alias calls represented as `LuaType::Generic` (`ExtractFoo` -> alias target) and recursively evaluate conditional types. -/// After call-site generic bindings are substituted, calling this avoids leaking return types as `Alias<...>`. -pub fn expand_alias_generic(model: &SemanticModel, ty: &LuaType) -> LuaType { - let mut visited = Vec::new(); - expand_alias_generic_inner(model, ty, &mut visited) -} - -fn expand_alias_generic_inner( - model: &SemanticModel, - ty: &LuaType, - visited: &mut Vec, -) -> LuaType { - use LuaType::*; - match ty { - Generic(generic) => { - let base_id = generic.get_base_type_id(); - // `std.RawGet`: expand via type-level raw get into member types. - if matches!(base_id.get_name(), "std.RawGet" | "RawGet") { - let params = generic.get_params().to_vec(); - if params.len() == 2 { - let owner = expand_alias_generic_inner(model, ¶ms[0], visited); - let key = expand_alias_generic_inner(model, ¶ms[1], visited); - if let Some(ty) = eval_index_access(model, &owner, &key) { - return ty; - } - } - } - // Built-in `Merge` exists as `LuaGenericType` (no user alias definition). - if base_id.get_name() == "Merge" { - let operands: Vec = generic - .get_params() - .iter() - .map(|param| expand_alias_generic_inner(model, param, visited)) - .collect(); - if operands.len() >= 2 - && let Some(merged) = eval_merge_call( - model, - &LuaAliasCallType::new(LuaAliasCallKind::Merge, operands), - ) - { - return merged; - } - } - let def = model.type_def_of(&base_id); - if let Some(def) = &def - && def.kind == TypeDefKind::Alias - && !visited.contains(&base_id) - { - visited.push(base_id.clone()); - if let Some(target) = model.alias_target(def) { - let bindings: HashMap = generic - .get_params() - .iter() - .enumerate() - .map(|(index, param)| (GenericTplId::Type(index as u32), param.clone())) - .collect(); - // When rich projection does not carry the alias generic context, it projects `T` into a named reference; - // substitute by name once more here to ensure `checked_type` is also substituted. - let name_bindings: HashMap<::std::string::String, LuaType> = def - .generic_params - .iter() - .enumerate() - .filter_map(|(index, param)| { - generic - .get_params() - .get(index) - .map(|value| (param.name.to_string(), value.clone())) - }) - .collect(); - if let Conditional(cond) = &target { - // When generic arguments are not yet substituted (e.g. `T` in `MockParameters` is still a - // class generic), do not expand the conditional alias: evaluating early would treat unbound parameters as - // non-matching and incorrectly fold to `false`/`never`. Expand after concrete arguments are substituted. - if generic - .get_params() - .iter() - .any(contains_unbound_function_tpl) - { - visited.pop(); - let params = generic - .get_params() - .iter() - .map(|param| expand_alias_generic_inner(model, param, visited)) - .collect(); - return Generic(Rc::new(LuaGenericType::new(base_id, params))); - } - let evaluated = - eval_conditional_alias(model, cond, &bindings, &name_bindings); - let expanded = expand_alias_generic_inner(model, &evaluated, visited); - visited.pop(); - return expanded; - } - let named = substitute_named_refs(&target, &name_bindings); - let substituted = unify::substitute(&named, &bindings); - let expanded = expand_alias_generic_inner(model, &substituted, visited); - visited.pop(); - return expanded; - } - visited.pop(); - } - let params = generic - .get_params() - .iter() - .map(|param| expand_alias_generic_inner(model, param, visited)) - .collect(); - Generic(Rc::new(LuaGenericType::new(base_id, params))) - } - Array(array) => Array(Rc::new(LuaArrayType::from_base_type( - expand_alias_generic_inner(model, array.get_base(), visited), - ))), - Tuple(tuple) => Tuple(Rc::new(LuaTupleType::new( - tuple - .get_types() - .iter() - .map(|t| expand_alias_generic_inner(model, t, visited)) - .collect(), - tuple.status, - ))), - Union(union) => Union(Rc::new(LuaUnionType::from_vec( - union - .into_vec() - .iter() - .map(|t| expand_alias_generic_inner(model, t, visited)) - .collect(), - ))), - Intersection(intersection) => Intersection(Rc::new(LuaIntersectionType::new( - intersection - .get_types() - .iter() - .map(|t| expand_alias_generic_inner(model, t, visited)) - .collect(), - ))), - Object(object) => Object(Rc::new(LuaObjectType::new_with_fields( - object - .get_fields() - .iter() - .map(|(k, v)| (k.clone(), expand_alias_generic_inner(model, v, visited))) - .collect(), - object - .get_index_access() - .iter() - .map(|(k, v)| { - ( - expand_alias_generic_inner(model, k, visited), - expand_alias_generic_inner(model, v, visited), - ) - }) - .collect(), + Conditional(conditional) => Conditional(Rc::new(LuaConditionalType::new( + map(conditional.get_checked_type()), + map(conditional.get_extends_type()), + map(conditional.get_true_type()), + map(conditional.get_false_type()), + conditional.get_infer_params().to_vec(), + conditional.has_new, ))), - Variadic(variadic) => Variadic(Rc::new(match variadic.as_ref() { - VariadicType::Base(base) => { - VariadicType::Base(expand_alias_generic_inner(model, base, visited)) - } - VariadicType::Repeat(base) => { - VariadicType::Repeat(expand_alias_generic_inner(model, base, visited)) - } - VariadicType::Multi(types) => VariadicType::Multi( - types - .iter() - .map(|t| expand_alias_generic_inner(model, t, visited)) - .collect(), - ), - })), - Call(call) => { - let call = LuaAliasCallType::new( - call.get_call_kind(), - call.get_operands() - .iter() - .map(|t| expand_alias_generic_inner(model, t, visited)) - .collect(), - ); - if call.get_call_kind() == LuaAliasCallKind::Merge - && let Some(merged) = eval_merge_call(model, &call) - { - return merged; - } - Call(Rc::new(call)) - } - Mapped(mapped) => { - if let Some(expanded) = expand_mapped(model, mapped) { - expand_alias_generic_inner(model, &expanded, visited) - } else { - Mapped(Rc::new(LuaMappedType::new( - ( - mapped.param.0, - GenericParam::new( - mapped.param.1.name.clone(), - mapped - .param - .1 - .constraint - .as_ref() - .map(|t| expand_alias_generic_inner(model, t, visited)), - mapped - .param - .1 - .default - .as_ref() - .map(|t| expand_alias_generic_inner(model, t, visited)), - mapped.param.1.is_const, - mapped.param.1.attributes.clone(), - ), - ), - expand_alias_generic_inner(model, &mapped.value, visited), - mapped.is_readonly, - mapped.is_optional, - ))) - } - } - Conditional(conditional) => eval_conditionals_inner( - model, - &Conditional(Rc::new(LuaConditionalType::new( - expand_alias_generic_inner(model, conditional.get_checked_type(), visited), - expand_alias_generic_inner(model, conditional.get_extends_type(), visited), - expand_alias_generic_inner(model, conditional.get_true_type(), visited), - expand_alias_generic_inner(model, conditional.get_false_type(), visited), - conditional.get_infer_params().to_vec(), - conditional.has_new, - ))), - &mut Vec::new(), - ), _ => ty.clone(), } } @@ -979,173 +558,77 @@ fn eval_conditional_alias( conditional: &LuaConditionalType, bindings: &HashMap, name_bindings: &HashMap, + resolved_bound: LuaType, ) -> LuaType { - let checked = conditional.get_checked_type(); - let naked_tpl: Option<(GenericTplId, String)> = match checked { + let naked_tpl = match conditional.get_checked_type() { LuaType::TplRef(tpl) => Some((tpl.get_tpl_id(), tpl.get_name().to_string())), - LuaType::Ref(id) | LuaType::Def(id) => name_bindings - .get(id.get_name()) - .map(|_| (GenericTplId::Type(0), id.get_name().to_string())), + LuaType::Ref(id) | LuaType::Def(id) if name_bindings.contains_key(id.get_name()) => { + Some((GenericTplId::Type(0), id.get_name().to_string())) + } _ => None, }; - if let Some((tpl_id, tpl_name)) = naked_tpl { - let bound = bindings - .get(&tpl_id) - .or_else(|| name_bindings.get(&tpl_name)) - .cloned(); - if let Some(bound) = bound { - // The generic argument itself may be a named alias (`T = Procedure`); expand the alias first, - // otherwise `T extends (fun(...: infer P))` will use `Ref(Procedure)` as the function source - // and incorrectly go to the false branch. - let expanded = match &bound { - LuaType::Ref(id) | LuaType::Def(id) => model - .type_def_of(id) - .filter(|def| def.kind == TypeDefKind::Alias) - .and_then(|def| model.alias_target(&def)) - .unwrap_or_else(|| bound.clone()), - other => other.clone(), - }; - let bound = expand_alias_generic(model, &expanded); - if bound.is_never() { - return LuaType::Never; + let evaluate = |bound: LuaType| { + let mut bindings = bindings.clone(); + let mut names = name_bindings.clone(); + if let Some((id, name)) = &naked_tpl { + bindings.insert(*id, bound.clone()); + names.insert(name.clone(), bound.clone()); + } + let raw = LuaType::Conditional(Rc::new(conditional.clone())); + let substituted = substitute_named_refs(&unify::substitute(&raw, &bindings), &names); + match substituted { + LuaType::Conditional(conditional) => { + let conditional = LuaConditionalType::new( + bound, + conditional.get_extends_type().clone(), + conditional.get_true_type().clone(), + conditional.get_false_type().clone(), + conditional.get_infer_params().to_vec(), + conditional.has_new, + ); + eval_conditional_once(model, &conditional) } - if let LuaType::Union(union) = &bound { - let mut results = Vec::new(); - for member in union.into_vec() { - let mut member_bindings = bindings.clone(); - member_bindings.insert(tpl_id, member.clone()); - let mut member_names = name_bindings.clone(); - member_names.insert(tpl_name.clone(), member.clone()); - let raw = LuaType::Conditional(Rc::new(conditional.clone())); - let substituted = unify::substitute(&raw, &member_bindings); - let substituted = substitute_named_refs(&substituted, &member_names); - let mut cond_visiting = Vec::new(); - let result = match substituted { - LuaType::Conditional(c) => { - eval_conditional_once(model, c.as_ref(), &mut cond_visiting) - } - other => other, - }; - if !result.is_never() && !results.contains(&result) { - results.push(result); - } - } - return LuaType::from_vec(results); + other => other, + } + }; + // Only a naked parameter distributes bindings into each union branch. + if naked_tpl.is_some() + && let LuaType::Union(union) = &resolved_bound + { + let mut results = Vec::new(); + for member in union.into_vec() { + let result = evaluate(member); + if !result.is_never() && !results.contains(&result) { + results.push(result); } - // Non-union: write the expanded argument back into bindings, then use the general conditional evaluation. - let mut resolved_bindings = bindings.clone(); - resolved_bindings.insert(tpl_id, bound.clone()); - let mut resolved_names = name_bindings.clone(); - resolved_names.insert(tpl_name.clone(), bound.clone()); - let raw = LuaType::Conditional(Rc::new(conditional.clone())); - let substituted = unify::substitute(&raw, &resolved_bindings); - let substituted = substitute_named_refs(&substituted, &resolved_names); - let mut cond_visiting = Vec::new(); - return match substituted { - LuaType::Conditional(c) => { - eval_conditional_once(model, c.as_ref(), &mut cond_visiting) - } - other => other, - }; } - } - - let raw = LuaType::Conditional(Rc::new(conditional.clone())); - let substituted = unify::substitute(&raw, bindings); - let substituted = substitute_named_refs(&substituted, name_bindings); - let mut cond_visiting = Vec::new(); - match substituted { - LuaType::Conditional(c) => eval_conditional_once(model, c.as_ref(), &mut cond_visiting), - other => other, + LuaType::from_vec(results) + } else { + evaluate(resolved_bound) } } /// Evaluate conditional types (internal `infer` pattern matching). Recursively handle conditional types in structures. pub fn eval_conditionals(model: &SemanticModel, ty: &LuaType) -> LuaType { - eval_conditionals_inner(model, ty, &mut Vec::new()) + let Some(evaluation) = crate::semantic_model::cache::TypeEvaluationScope::new(&model.cache) + else { + return ty.clone(); + }; + if !evaluation.visit(ty) { + return ty.clone(); + } + eval_conditionals_inner(model, ty) } -fn eval_conditionals_inner( - model: &SemanticModel, - ty: &LuaType, - visiting: &mut Vec, -) -> LuaType { - use LuaType::*; - match ty { - Conditional(conditional) => eval_conditional_once(model, conditional, visiting), - Array(array) => Array(Rc::new(LuaArrayType::from_base_type( - eval_conditionals_inner(model, array.get_base(), visiting), - ))), - Tuple(tuple) => Tuple(Rc::new(LuaTupleType::new( - tuple - .get_types() - .iter() - .map(|t| eval_conditionals_inner(model, t, visiting)) - .collect(), - tuple.status, - ))), - Union(union) => Union(Rc::new(LuaUnionType::from_vec( - union - .into_vec() - .iter() - .map(|t| eval_conditionals_inner(model, t, visiting)) - .collect(), - ))), - Intersection(intersection) => Intersection(Rc::new(LuaIntersectionType::new( - intersection - .get_types() - .iter() - .map(|t| eval_conditionals_inner(model, t, visiting)) - .collect(), - ))), - Object(object) => Object(Rc::new(LuaObjectType::new_with_fields( - object - .get_fields() - .iter() - .map(|(k, v)| (k.clone(), eval_conditionals_inner(model, v, visiting))) - .collect(), - object - .get_index_access() - .iter() - .map(|(k, v)| { - ( - eval_conditionals_inner(model, k, visiting), - eval_conditionals_inner(model, v, visiting), - ) - }) - .collect(), - ))), - Generic(generic) => Generic(Rc::new(LuaGenericType::new( - generic.get_base_type_id(), - generic - .get_params() - .iter() - .map(|t| eval_conditionals_inner(model, t, visiting)) - .collect(), - ))), - Variadic(variadic) => Variadic(Rc::new(match variadic.as_ref() { - VariadicType::Base(base) => { - VariadicType::Base(eval_conditionals_inner(model, base, visiting)) - } - VariadicType::Repeat(base) => { - VariadicType::Repeat(eval_conditionals_inner(model, base, visiting)) - } - VariadicType::Multi(types) => VariadicType::Multi( - types - .iter() - .map(|t| eval_conditionals_inner(model, t, visiting)) - .collect(), - ), - })), - Call(call) => Call(Rc::new(LuaAliasCallType::new( - call.get_call_kind(), - call.get_operands() - .iter() - .map(|t| eval_conditionals_inner(model, t, visiting)) - .collect(), - ))), - _ => ty.clone(), +fn eval_conditionals_inner(model: &SemanticModel, ty: &LuaType) -> LuaType { + if !ty.any_type(|ty| matches!(ty, LuaType::Conditional(_))) { + return ty.clone(); } + map_type(ty, &mut |ty| match ty { + LuaType::Conditional(conditional) => Some(eval_conditional_once(model, conditional)), + LuaType::Mapped(_) | LuaType::TableGeneric(_) => Some(ty.clone()), + _ => None, + }) } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -1153,13 +636,10 @@ enum ConditionalCheck { True, False, Both, + Unresolved, } -fn eval_conditional_once( - model: &SemanticModel, - conditional: &LuaConditionalType, - visiting: &mut Vec, -) -> LuaType { +fn eval_conditional_once(model: &SemanticModel, conditional: &LuaConditionalType) -> LuaType { let checked = conditional.get_checked_type(); let extends = conditional.get_extends_type(); // Do not decide before function-level generics are substituted: `T extends (fun(...: infer P))` should not @@ -1180,7 +660,7 @@ fn eval_conditional_once( conditional.get_infer_params().to_vec(), conditional.has_new, ); - let member_result = eval_conditional_once(model, &member_conditional, visiting); + let member_result = eval_conditional_once(model, &member_conditional); if !member_result.is_never() && !results.contains(&member_result) { results.push(member_result); } @@ -1193,6 +673,7 @@ fn eval_conditional_once( } if contains_conditional_infer(extends) { + let revision = model.cache.borrow().revision(); // `new (fun(...: infer P): any)`: when the source is a constructor type/class table, extract its constructor signature first. let infer_source = if conditional.has_new { match try_constructor_function(model, checked) { @@ -1202,45 +683,39 @@ fn eval_conditional_once( } else { checked.clone() }; - // `T[K] extends Wrapper`: first evaluate the index access into tuple/object members, then collect infer. - let infer_source = if let LuaType::Call(call) = &infer_source - && let Some(expanded) = expand_index_call(model, call) - { - expanded - } else { - infer_source - }; + if !model.cache.borrow().is_complete(revision) { + return LuaType::Conditional(Rc::new(conditional.clone())); + } let mut infer_assignments: HashMap = HashMap::new(); - if collect_infer_assignments( + let matched = collect_infer_assignments( model, &infer_source, extends, &mut infer_assignments, InferVariance::Covariant, - ) { + ); + if matched.is_ok() { let bindings: HashMap = infer_assignments .into_iter() .filter_map(|(id, candidates)| candidates.finalize(model).map(|ty| (id, ty))) .collect(); let true_ty = unify::substitute(conditional.get_true_type(), &bindings); - eval_conditionals_inner(model, &true_ty, visiting) + eval_conditionals_inner(model, &true_ty) + } else if matched.is_err_and(|err| err.is_type_not_match()) { + eval_conditionals_inner(model, conditional.get_false_type()) } else { - eval_conditionals_inner(model, conditional.get_false_type(), visiting) + LuaType::Conditional(Rc::new(conditional.clone())) } } else { match check_conditional_extends(model, checked, extends) { - ConditionalCheck::True => { - eval_conditionals_inner(model, conditional.get_true_type(), visiting) - } - ConditionalCheck::False => { - eval_conditionals_inner(model, conditional.get_false_type(), visiting) - } + ConditionalCheck::True => eval_conditionals_inner(model, conditional.get_true_type()), + ConditionalCheck::False => eval_conditionals_inner(model, conditional.get_false_type()), ConditionalCheck::Both => { - let true_ty = eval_conditionals_inner(model, conditional.get_true_type(), visiting); - let false_ty = - eval_conditionals_inner(model, conditional.get_false_type(), visiting); + let true_ty = eval_conditionals_inner(model, conditional.get_true_type()); + let false_ty = eval_conditionals_inner(model, conditional.get_false_type()); LuaType::from_vec(vec![true_ty, false_ty]) } + ConditionalCheck::Unresolved => LuaType::Conditional(Rc::new(conditional.clone())), } } } @@ -1301,32 +776,39 @@ fn check_conditional_extends( for member in union.into_vec() { result = merge_conditional_check(result, check_conditional_extends(model, &member, target)); - if result == ConditionalCheck::Both { + if result == ConditionalCheck::Unresolved { break; } } return result; } if let LuaType::Union(union) = target { + let mut unresolved = false; for member in union.into_vec() { - if matches!( - check_conditional_extends(model, source, &member), - ConditionalCheck::True | ConditionalCheck::Both - ) { - return ConditionalCheck::True; + match check_conditional_extends(model, source, &member) { + ConditionalCheck::True | ConditionalCheck::Both => return ConditionalCheck::True, + ConditionalCheck::Unresolved => unresolved = true, + ConditionalCheck::False => {} } } - return ConditionalCheck::False; + return if unresolved { + ConditionalCheck::Unresolved + } else { + ConditionalCheck::False + }; } - if model.type_check(source, target) { - ConditionalCheck::True - } else { - ConditionalCheck::False + match model.type_check_cached(source, target, false) { + Some(true) => ConditionalCheck::True, + Some(false) => ConditionalCheck::False, + None => ConditionalCheck::Unresolved, } } fn merge_conditional_check(left: ConditionalCheck, right: ConditionalCheck) -> ConditionalCheck { match (left, right) { + (ConditionalCheck::Unresolved, _) | (_, ConditionalCheck::Unresolved) => { + ConditionalCheck::Unresolved + } (ConditionalCheck::True, ConditionalCheck::True) => ConditionalCheck::True, (ConditionalCheck::False, ConditionalCheck::False) => ConditionalCheck::False, _ => ConditionalCheck::Both, @@ -1452,112 +934,21 @@ fn primitive_intersection_is_empty( } pub fn contains_conditional_infer(ty: &LuaType) -> bool { - use LuaType::*; - match ty { - TplRef(tpl) => tpl.get_tpl_id().is_conditional_infer(), - Array(array) => contains_conditional_infer(array.get_base()), - Tuple(tuple) => tuple.get_types().iter().any(contains_conditional_infer), - Union(union) => union.into_vec().iter().any(contains_conditional_infer), - Intersection(intersection) => intersection - .get_types() - .iter() - .any(contains_conditional_infer), - Object(object) => { - object.get_fields().values().any(contains_conditional_infer) - || object - .get_index_access() - .iter() - .any(|(k, v)| contains_conditional_infer(k) || contains_conditional_infer(v)) - } - DocFunction(fun) => { - fun.get_params() - .iter() - .any(|(_, ty)| ty.as_ref().is_some_and(contains_conditional_infer)) - || contains_conditional_infer(fun.get_ret()) - } - Generic(generic) => generic.get_params().iter().any(contains_conditional_infer), - TableGeneric(generic) => generic.iter().any(contains_conditional_infer), - Variadic(variadic) => match variadic.as_ref() { - VariadicType::Base(base) | VariadicType::Repeat(base) => { - contains_conditional_infer(base) - } - VariadicType::Multi(types) => types.iter().any(contains_conditional_infer), - }, - Call(call) => call.get_operands().iter().any(contains_conditional_infer), - Conditional(conditional) => { - contains_conditional_infer(conditional.get_checked_type()) - || contains_conditional_infer(conditional.get_extends_type()) - || contains_conditional_infer(conditional.get_true_type()) - || contains_conditional_infer(conditional.get_false_type()) + let mut pending = vec![ty]; + while let Some(ty) = pending.pop() { + match ty { + LuaType::TplRef(tpl) if tpl.get_tpl_id().is_conditional_infer() => return true, + // Mapped patterns are reduced by type comparison. Their nested + // conditionals do not bind the enclosing condition. + LuaType::Mapped(_) | LuaType::TypeGuard(_) | LuaType::MultiLineUnion(_) => {} + _ => ty.push_direct_children(&mut pending), } - _ => false, } + false } fn contains_unbound_function_tpl(ty: &LuaType) -> bool { - use LuaType::*; - match ty { - TplRef(tpl) => !tpl.get_tpl_id().is_conditional_infer(), - Array(array) => contains_unbound_function_tpl(array.get_base()), - Tuple(tuple) => tuple.get_types().iter().any(contains_unbound_function_tpl), - Union(union) => union.into_vec().iter().any(contains_unbound_function_tpl), - Intersection(intersection) => intersection - .get_types() - .iter() - .any(contains_unbound_function_tpl), - Object(object) => { - object - .get_fields() - .values() - .any(contains_unbound_function_tpl) - || object.get_index_access().iter().any(|(k, v)| { - contains_unbound_function_tpl(k) || contains_unbound_function_tpl(v) - }) - } - DocFunction(fun) => { - fun.get_params() - .iter() - .any(|(_, ty)| ty.as_ref().is_some_and(contains_unbound_function_tpl)) - || contains_unbound_function_tpl(fun.get_ret()) - } - Generic(generic) => generic - .get_params() - .iter() - .any(contains_unbound_function_tpl), - TableGeneric(generic) => generic.iter().any(contains_unbound_function_tpl), - Variadic(variadic) => match variadic.as_ref() { - VariadicType::Base(base) | VariadicType::Repeat(base) => { - contains_unbound_function_tpl(base) - } - VariadicType::Multi(types) => types.iter().any(contains_unbound_function_tpl), - }, - Call(call) => call - .get_operands() - .iter() - .any(contains_unbound_function_tpl), - Conditional(conditional) => { - contains_unbound_function_tpl(conditional.get_checked_type()) - || contains_unbound_function_tpl(conditional.get_extends_type()) - || contains_unbound_function_tpl(conditional.get_true_type()) - || contains_unbound_function_tpl(conditional.get_false_type()) - } - Mapped(mapped) => { - contains_unbound_function_tpl(&mapped.value) - || mapped - .param - .1 - .constraint - .as_ref() - .is_some_and(contains_unbound_function_tpl) - || mapped - .param - .1 - .default - .as_ref() - .is_some_and(contains_unbound_function_tpl) - } - _ => false, - } + ty.any_type(|ty| matches!(ty, LuaType::TplRef(tpl) if !tpl.get_tpl_id().is_conditional_infer())) } /// `has_new` condition: resolve class tables/named types into constructor signatures (`---@overload fun(...)`). @@ -1597,7 +988,7 @@ fn collect_infer_assignments( pattern: &LuaType, assignments: &mut HashMap, variance: InferVariance, -) -> bool { +) -> TypeCheckResult { use LuaType::*; if let TplRef(tpl) = pattern && tpl.get_tpl_id().is_conditional_infer() @@ -1606,8 +997,23 @@ fn collect_infer_assignments( .entry(tpl.get_tpl_id()) .or_default() .insert(variance, source); - return true; + return Ok(()); } + // Preserve direct generic argument inference. Other patterns need the + // source's outer constructor, including aliases in lazily produced fields. + let source = if matches!((source, pattern), (Generic(s), Generic(p)) + if s.get_base_type_id() == p.get_base_type_id()) + { + source.clone() + } else { + let revision = model.cache.borrow().revision(); + let source = expand_alias_head(model, source).ok_or(TypeCheckFailReason::Unresolved)?; + if !model.cache.borrow().is_complete(revision) { + return Err(TypeCheckFailReason::Unresolved); + } + source + }; + let source = &source; match pattern { Array(pattern_array) => match source { Array(source_array) => collect_infer_assignments( @@ -1617,24 +1023,23 @@ fn collect_infer_assignments( assignments, variance, ), - _ => false, + _ => Err(TypeCheckFailReason::TypeNotMatch), }, Tuple(pattern_tuple) => match source { Tuple(source_tuple) if pattern_tuple.get_types().len() == source_tuple.get_types().len() => { - for (p, s) in pattern_tuple - .get_types() - .iter() - .zip(source_tuple.get_types().iter()) - { - if !collect_infer_assignments(model, s, p, assignments, variance) { - return false; - } - } - true + check_all( + pattern_tuple + .get_types() + .iter() + .zip(source_tuple.get_types()) + .map(|(p, s)| { + collect_infer_assignments(model, s, p, assignments, variance) + }), + ) } - _ => false, + _ => Err(TypeCheckFailReason::TypeNotMatch), }, Variadic(pattern_variadic) => match source { Variadic(source_variadic) => { @@ -1665,30 +1070,29 @@ fn collect_infer_assignments( variance, ) } - _ => false, + _ => Err(TypeCheckFailReason::TypeNotMatch), } } - _ => false, + _ => Err(TypeCheckFailReason::TypeNotMatch), }, Generic(pattern_generic) => match source { Generic(source_generic) => { if pattern_generic.get_base_type_id() != source_generic.get_base_type_id() || pattern_generic.get_params().len() != source_generic.get_params().len() { - return false; - } - for (p, s) in pattern_generic - .get_params() - .iter() - .zip(source_generic.get_params().iter()) - { - if !collect_infer_assignments(model, s, p, assignments, variance) { - return false; - } + return Err(TypeCheckFailReason::TypeNotMatch); } - true + check_all( + pattern_generic + .get_params() + .iter() + .zip(source_generic.get_params()) + .map(|(p, s)| { + collect_infer_assignments(model, s, p, assignments, variance) + }), + ) } - _ => false, + _ => Err(TypeCheckFailReason::TypeNotMatch), }, Object(pattern_object) => match source { Object(source_object) => infer_from_object_to_object( @@ -1701,19 +1105,23 @@ fn collect_infer_assignments( Ref(_) | Def(_) | TableConst(_) => { infer_from_class_to_object(model, source, pattern_object, assignments, variance) } - _ => false, + _ => Err(TypeCheckFailReason::TypeNotMatch), }, DocFunction(pattern_fun) => match source { DocFunction(source_fun) => { collect_infer_from_function(model, source_fun, pattern_fun, assignments, variance) } - _ => false, + _ => Err(TypeCheckFailReason::TypeNotMatch), }, _ => { if contains_conditional_infer(pattern) { - false + Err(TypeCheckFailReason::TypeNotMatch) } else { - source == pattern || model.type_check(source, pattern) + match model.type_check_cached(source, pattern, false) { + Some(true) => Ok(()), + Some(false) => Err(TypeCheckFailReason::TypeNotMatch), + None => Err(TypeCheckFailReason::Unresolved), + } } } } @@ -1725,25 +1133,24 @@ fn infer_from_object_to_object( pattern_object: &LuaObjectType, assignments: &mut HashMap, variance: InferVariance, -) -> bool { +) -> TypeCheckResult { + let mut checks = RequiredChecks::default(); for (key, pattern_field_ty) in pattern_object.get_fields() { match source_object.get_fields().get(key) { - Some(source_field_ty) => { - if !collect_infer_assignments( - model, - source_field_ty, - pattern_field_ty, - assignments, - variance, - ) { - return false; - } + Some(source_field_ty) => checks.require(collect_infer_assignments( + model, + source_field_ty, + pattern_field_ty, + assignments, + variance, + ))?, + None if contains_conditional_infer(pattern_field_ty) => { + return Err(TypeCheckFailReason::TypeNotMatch); } - None if contains_conditional_infer(pattern_field_ty) => return false, _ => {} } } - true + checks.finish() } fn infer_from_class_to_object( @@ -1752,23 +1159,36 @@ fn infer_from_class_to_object( pattern_object: &LuaObjectType, assignments: &mut HashMap, variance: InferVariance, -) -> bool { +) -> TypeCheckResult { + let mut checks = RequiredChecks::default(); for (key, pattern_field_ty) in pattern_object.get_fields() { - if let Some(source_field_ty) = model.member_type(source, key) { - if !collect_infer_assignments( + let revision = model.cache.borrow().revision(); + let source_field_ty = model.member_type(source, key); + if !model.cache.borrow().is_complete(revision) { + checks.require(Err(TypeCheckFailReason::Unresolved))?; + continue; + } + if let Some(source_field_ty) = source_field_ty { + checks.require(collect_infer_assignments( model, &source_field_ty, pattern_field_ty, assignments, variance, - ) { - return false; - } + ))?; } else if contains_conditional_infer(pattern_field_ty) { - return false; + return Err(TypeCheckFailReason::TypeNotMatch); } } - true + checks.finish() +} + +fn infer_fixed_param_count(function: &LuaFunctionType) -> usize { + let params = function.get_params(); + let has_variadic = params + .last() + .is_some_and(|(name, ty)| name == "..." || ty.as_ref().is_some_and(|ty| ty.is_variadic())); + params.len() - usize::from(has_variadic) } fn collect_infer_from_function( @@ -1777,37 +1197,29 @@ fn collect_infer_from_function( pattern_fun: &LuaFunctionType, assignments: &mut HashMap, variance: InferVariance, -) -> bool { +) -> TypeCheckResult { let pattern_params = pattern_fun.get_params(); let source_params = source_fun.get_params(); - let has_variadic = pattern_params - .last() - .is_some_and(|(name, ty)| name == "..." || ty.as_ref().is_some_and(|ty| ty.is_variadic())); - let normal_param_len = if has_variadic { - pattern_params.len().saturating_sub(1) - } else { - pattern_params.len() - }; + let normal_param_len = infer_fixed_param_count(pattern_fun); + let has_variadic = normal_param_len < pattern_params.len(); if !has_variadic && source_params.len() > normal_param_len { - return false; + return Err(TypeCheckFailReason::TypeNotMatch); } + let mut checks = RequiredChecks::default(); for (i, (_, pattern_param)) in pattern_params.iter().take(normal_param_len).enumerate() { let source_param = source_params.get(i).and_then(|(_, ty)| ty.as_ref()); let pattern_ty = pattern_param.as_ref(); match (source_param, pattern_ty) { - (Some(source_ty), Some(pattern_ty)) => { - if !collect_infer_assignments( - model, - source_ty, - pattern_ty, - assignments, - variance.flip(), - ) { - return false; - } + (Some(source_ty), Some(pattern_ty)) => checks.require(collect_infer_assignments( + model, + source_ty, + pattern_ty, + assignments, + variance.flip(), + ))?, + (None, Some(pattern_ty)) if contains_conditional_infer(pattern_ty) => { + return Err(TypeCheckFailReason::TypeNotMatch); } - (Some(_), None) => {} - (None, Some(pattern_ty)) if contains_conditional_infer(pattern_ty) => return false, _ => {} } } @@ -1842,18 +1254,24 @@ fn collect_infer_from_function( LuaTupleStatus::InferResolve, ))), }; - return collect_infer_assignments(model, &ty, pattern_ty, assignments, variance.flip()); + checks.require(collect_infer_assignments( + model, + &ty, + pattern_ty, + assignments, + variance.flip(), + ))?; + return checks.finish(); } let pattern_ret = pattern_fun.get_ret(); if contains_conditional_infer(pattern_ret) { - collect_infer_assignments( + checks.require(collect_infer_assignments( model, source_fun.get_ret(), pattern_ret, assignments, variance, - ) - } else { - true + ))?; } + checks.finish() } diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_eval/alias_expansion.rs b/crates/emmylua_code_analysis/src/semantic_model/type_eval/alias_expansion.rs new file mode 100644 index 000000000..1d8a8ec24 --- /dev/null +++ b/crates/emmylua_code_analysis/src/semantic_model/type_eval/alias_expansion.rs @@ -0,0 +1,323 @@ +//! Resolve computations to their outer constructor; data fields remain lazy. + +use super::*; + +/// Resolve outer type computations, leaving fields and array elements intact. +pub(crate) fn expand_alias_head(model: &SemanticModel, ty: &LuaType) -> Option { + use crate::semantic_model::cache::{CacheEntry, QueryCacheScope}; + + if !needs_reduction(model, ty) { + return Some(ty.clone()); + } + + let key = (model.file_id(), ty.clone()); + { + let cache = model.cache.borrow(); + if let Some(entry) = cache.alias_heads.get(&key) { + return entry.get(&cache).cloned().flatten(); + } + } + let _query = QueryCacheScope::new(&model.cache); + let revision = model.cache.borrow().revision(); + model + .cache + .borrow_mut() + .alias_heads + .insert(key.clone(), CacheEntry::InProgress); + let result = expand(model, ty).filter(|result| !needs_reduction(model, result)); + let mut cache = model.cache.borrow_mut(); + if result.is_none() { + cache.mark_incomplete(); + } + let complete = cache.is_complete(revision); + cache + .alias_heads + .insert(key, CacheEntry::finish(result.clone(), complete)); + result +} + +/// Keep an alias active while a member query consumes its outer structure. +pub(crate) fn with_expanded_alias( + model: &SemanticModel, + ty: &LuaType, + consume: impl FnOnce(&LuaType) -> T, +) -> Option { + let expanded = expand_alias_head(model, ty)?; + let mut path = AliasPath::new(model)?; + path.enter(ty)?; + Some(consume(&expanded)) +} + +/// Keep exact alias instances active across nested semantic queries. +pub(super) struct AliasPath<'a, 'db> { + model: &'a SemanticModel<'db>, + path: Vec, + evaluation: crate::semantic_model::cache::TypeEvaluationScope<'a>, +} + +impl<'a, 'db> AliasPath<'a, 'db> { + pub(super) fn new(model: &'a SemanticModel<'db>) -> Option { + Some(Self { + model, + path: Vec::new(), + evaluation: crate::semantic_model::cache::TypeEvaluationScope::new(&model.cache)?, + }) + } + + pub(super) fn enter(&mut self, ty: &LuaType) -> Option<()> { + if !self.evaluation.visit(ty) { + return None; + } + let mut cache = self.model.cache.borrow_mut(); + if !cache.expanding_aliases.insert(ty.clone()) { + cache.mark_incomplete(); + return None; + } + self.path.push(ty.clone()); + Some(()) + } + + pub(super) fn leave(&mut self) { + let ty = self.path.pop().expect("alias leave must match an entry"); + self.model.cache.borrow_mut().expanding_aliases.remove(&ty); + } +} + +impl Drop for AliasPath<'_, '_> { + fn drop(&mut self) { + while !self.path.is_empty() { + self.leave(); + } + } +} + +enum Task { + Enter(LuaType), + Conditional( + LuaType, + Rc, + unify::TplBindings, + HashMap, + ), + Leave, +} + +/// Union and intersection branches affect the outer shape. Resolve their +/// aliases too, but stop at data constructors so recursive fields stay lazy. +pub(crate) fn expand_alias_branches(model: &SemanticModel, ty: &LuaType) -> Option { + enum Task { + Enter(LuaType), + Rebuild(LuaType, usize), + Leave, + } + let mut path = AliasPath::new(model)?; + let mut pending = vec![Task::Enter(ty.clone())]; + let mut values = Vec::new(); + while let Some(task) = pending.pop() { + // Do not rebuild an unbounded union after the shared budget is spent. + if model.cache.borrow().evaluation_exhausted { + return None; + } + match task { + Task::Leave => path.leave(), + Task::Rebuild(ty, count) => { + let children = values.split_off(values.len() - count); + values.push(match ty { + LuaType::Union(_) => LuaType::from_vec(children), + LuaType::Intersection(_) => { + let children = children + .into_iter() + .flat_map(|child| match child { + LuaType::Intersection(intersection) => { + intersection.get_types().to_vec() + } + child => vec![child], + }) + .collect(); + LuaType::Intersection(Rc::new(LuaIntersectionType::new(children))) + } + _ => unreachable!("only unions and intersections have branches"), + }); + } + Task::Enter(current) => { + let expanded = if needs_reduction(model, ¤t) { + let Some(expanded) = expand(model, ¤t) else { + values.push(current); + continue; + }; + if matches!(expanded, LuaType::Union(_) | LuaType::Intersection(_)) { + if path.enter(¤t).is_none() { + values.push(current); + continue; + } + pending.push(Task::Leave); + } + expanded + } else { + current + }; + if matches!(expanded, LuaType::Union(_) | LuaType::Intersection(_)) { + let mut children = Vec::new(); + map_alias_children(&expanded, &mut |child| { + children.push(child.clone()); + child.clone() + }); + pending.push(Task::Rebuild(expanded, children.len())); + pending.extend(children.into_iter().rev().map(Task::Enter)); + } else { + values.push(expanded); + } + } + } + } + values.pop() +} + +pub(super) fn expand(model: &SemanticModel, ty: &LuaType) -> Option { + let mut path = AliasPath::new(model)?; + let mut pending = vec![Task::Enter(ty.clone())]; + let mut value = None; + while let Some(task) = pending.pop() { + if let Task::Enter(ty) = &task + && !path.evaluation.visit(ty) + { + return None; + } + match task { + Task::Leave => path.leave(), + Task::Conditional(current, conditional, bindings, names) => { + let bound = value.take()?; + // Predicate queries and following the selected branch are separate + // reductions. A branch that returns a constructor is already done. + let result = eval_conditional_alias(model, &conditional, &bindings, &names, bound); + path.leave(); + if needs_reduction(model, &result) { + path.enter(¤t)?; + pending.push(Task::Leave); + pending.push(Task::Enter(result)); + } else { + value = Some(result); + } + } + Task::Enter(current) => { + if !needs_reduction(model, ¤t) { + value = Some(current); + continue; + } + let id = match ¤t { + LuaType::Ref(id) | LuaType::Def(id) => Some(id.clone()), + LuaType::Generic(generic) => Some(generic.get_base_type_id()), + _ => None, + }; + let def = id + .filter(|id| !matches!(id.get_name(), "RawGet" | "std.RawGet" | "Merge")) + .and_then(|id| model.type_def_of(&id)) + .filter(|def| def.kind == TypeDefKind::Alias); + if let Some(def) = def { + let target = model.alias_target(&def)?; + let (bindings, names) = match ¤t { + LuaType::Generic(generic) => ( + generic + .get_params() + .iter() + .enumerate() + .map(|(i, ty)| (GenericTplId::Type(i as u32), ty.clone())) + .collect(), + def.generic_params + .iter() + .zip(generic.get_params()) + .map(|(param, ty)| (param.name.to_string(), ty.clone())) + .collect(), + ), + _ => (HashMap::new(), HashMap::new()), + }; + if let LuaType::Conditional(conditional) = &target { + if contains_unbound_function_tpl(¤t) { + value = Some(current); + continue; + } + let bound = unify::substitute(conditional.get_checked_type(), &bindings); + let bound = substitute_named_refs(&bound, &names); + path.enter(¤t)?; + pending.push(Task::Conditional( + current, + conditional.clone(), + bindings, + names, + )); + pending.push(Task::Enter(bound)); + continue; + } + let result = + unify::substitute(&substitute_named_refs(&target, &names), &bindings); + if needs_reduction(model, &result) { + path.enter(¤t)?; + pending.push(Task::Leave); + pending.push(Task::Enter(result)); + } else { + value = Some(result); + } + } else { + path.enter(¤t)?; + let result = match ¤t { + LuaType::Conditional(_) => Some(eval_conditionals(model, ¤t)), + LuaType::Mapped(mapped) => expand_mapped(model, mapped), + LuaType::Call(call) if call.get_call_kind() == LuaAliasCallKind::Index => { + expand_index_call(model, call) + } + _ => evaluate_builtin(model, ¤t), + }; + if let Some(result) = result.filter(|result| *result != current) { + pending.push(Task::Leave); + pending.push(Task::Enter(result)); + } else { + path.leave(); + value = Some(current); + } + } + } + } + } + value +} + +pub(super) fn needs_reduction(model: &SemanticModel, ty: &LuaType) -> bool { + let id = match ty { + LuaType::Ref(id) | LuaType::Def(id) => id.clone(), + LuaType::Generic(generic) => { + let id = generic.get_base_type_id(); + if matches!(id.get_name(), "RawGet" | "std.RawGet" | "Merge") { + return true; + } + id + } + LuaType::Call(_) | LuaType::Conditional(_) | LuaType::Mapped(_) => return true, + _ => return false, + }; + model + .type_def_of(&id) + .is_some_and(|def| def.kind == TypeDefKind::Alias) +} + +fn evaluate_builtin(model: &SemanticModel, ty: &LuaType) -> Option { + match ty { + LuaType::Generic(generic) => { + let id = generic.get_base_type_id(); + let params = generic.get_params(); + if matches!(id.get_name(), "std.RawGet" | "RawGet") && params.len() == 2 { + return eval_index_access(model, ¶ms[0], ¶ms[1]); + } + if id.get_name() == "Merge" && params.len() >= 2 { + return eval_merge_call( + model, + &LuaAliasCallType::new(LuaAliasCallKind::Merge, params.to_vec()), + ); + } + } + LuaType::Call(call) if call.get_call_kind() == LuaAliasCallKind::Merge => { + return eval_merge_call(model, call); + } + _ => {} + } + None +} diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_eval/tests.rs b/crates/emmylua_code_analysis/src/semantic_model/type_eval/tests.rs new file mode 100644 index 000000000..1cf1b4a3d --- /dev/null +++ b/crates/emmylua_code_analysis/src/semantic_model/type_eval/tests.rs @@ -0,0 +1,858 @@ +use super::*; +use crate::semantic_model::type_check::check_type; +use crate::{LuaTypeDeclId, VirtualWorkspace}; + +#[test] +fn recursive_mapped_members_do_not_depend_on_query_order() { + let mut ws = VirtualWorkspace::new(); + let file = ws.def( + r#" + ---@alias Copy {[K in keyof T]: T[K]} + ---@alias Node {value: T, copy: Copy>} + "#, + ); + let ty = ws.ty("Node"); + for expand_first in [true, false] { + let model = ws.analysis.semantic_model(file); + if expand_first { + expand_alias_generic(&model, &ty); + } + for _ in 0..2 { + let members = model.member_infos(&ty); + assert_eq!(members.len(), 2); + let value = members + .iter() + .find(|info| info.key == LuaMemberKey::Name("value".into())); + assert_eq!(value.map(|info| &info.typ), Some(&LuaType::String)); + expand_alias_generic(&model, &ty); + } + } +} + +#[test] +fn mapped_conditionals_keep_template_scopes() { + let mut ws = VirtualWorkspace::new(); + let file = ws.def(""); + for (source, expected) in [ + ("{value: string}", LuaType::Integer), + ("integer", LuaType::Boolean), + ] { + let conditional = ws.ty(&format!( + "{source} extends {{[K in 'value']: (string extends infer U and U or never)}} and integer or boolean" + )); + let model = ws.analysis.semantic_model(file); + assert_eq!( + eval_conditionals(&model, &conditional), + expected, + "{source}" + ); + } + let pending = + ws.ty("{[K in 'value']: (fun(value: T): T)} extends table and integer or boolean"); + let model = ws.analysis.semantic_model(file); + assert_eq!(eval_conditionals(&model, &pending), pending); +} + +#[test] +fn unresolved_conditional_comparisons_stay_deferred() { + let mut ws = VirtualWorkspace::new(); + let file = ws.def( + r#" + ---@alias Cycle Other + ---@alias Other Cycle + ---@alias Source [Source, string] + ---@alias Branch [Fail, string] | [Branch, Cycle] + ---@alias Fail [Delay, integer] + ---@alias Delay [Fail, Cycle] + "#, + ); + let unresolved = [ + ws.ty("Cycle extends string and boolean or integer"), + ws.ty("string extends (Cycle | integer) and boolean or integer"), + ws.ty("Cycle extends {value: infer T} and T or integer"), + ws.ty("Branch extends Source and boolean or integer"), + ]; + let accepted = ws.ty("string extends string and boolean or integer"); + let rejected = ws.ty("integer extends string and boolean or integer"); + let known_union_branch = ws.ty("string extends (Cycle | string) and boolean or integer"); + let deferred_field = ws.ty("{value: Cycle extends string and boolean or integer}"); + let target = ws.ty("{value: string}"); + let branch = ws.ty("Branch"); + let source = ws.ty("Source"); + let model = ws.analysis.semantic_model(file); + assert!(!model.type_check(&branch, &source)); + model.type_check( + &LuaType::Ref(LuaTypeDeclId::global("Cycle")), + &LuaType::String, + ); + for unresolved in unresolved { + assert!(matches!(&unresolved, LuaType::Conditional(_))); + assert_eq!(eval_conditionals(&model, &unresolved), unresolved); + } + assert_eq!( + crate::semantic_model::type_check::check_type_subtype(&model, &deferred_field, &target), + Err(TypeCheckFailReason::Unresolved) + ); + assert_eq!(eval_conditionals(&model, &accepted), LuaType::Boolean); + assert_eq!(eval_conditionals(&model, &rejected), LuaType::Integer); + assert_eq!( + eval_conditionals(&model, &known_union_branch), + LuaType::Boolean + ); +} + +#[test] +fn incomplete_alternatives_do_not_hide_complete_comparison_proofs() { + let mut ws = VirtualWorkspace::new(); + let file = ws.def("---@alias Cycle Other\n---@alias Other Cycle"); + for source in ["Cycle & true", "true & Cycle"] { + let ty = ws.ty(source); + let tuple = ws.ty(&format!("[{source}]")); + let tuple_target = ws.ty("[boolean]"); + let conditional = ws.ty(&format!("({source}) extends boolean and string or integer")); + let inferred = ws.ty(&format!( + "[{source}, string] extends [boolean, infer T] and T or integer" + )); + let unresolved = ws.ty(&format!("({source}) extends string and boolean or integer")); + let model = ws.analysis.semantic_model(file); + for _ in 0..2 { + assert_eq!(eval_conditionals(&model, &inferred), LuaType::String); + assert!(model.type_check(&ty, &LuaType::Boolean), "{source}"); + assert!(model.type_check_subtype(&ty, &LuaType::Boolean), "{source}"); + assert!(model.type_check(&tuple, &tuple_target), "{source}"); + assert_eq!(eval_conditionals(&model, &conditional), LuaType::String); + assert_eq!(eval_conditionals(&model, &unresolved), unresolved); + } + } +} + +#[test] +fn definite_mismatches_decide_required_comparisons() { + let mut ws = VirtualWorkspace::new(); + let file = ws.def("---@alias Cycle Other\n---@alias Other Cycle"); + for (source, target) in [ + ("[Cycle, integer]", "[string, string]"), + ("[integer, Cycle]", "[string, string]"), + ("table", "table"), + ("table", "table"), + ("integer[]", "[Cycle, string]"), + ( + "(fun(a: Cycle, b: integer): string)", + "(fun(a: string, b: string): string)", + ), + ("(fun(a: Cycle): integer)", "(fun(a: string): string)"), + ("[Cycle, integer, boolean]", "[string, string, infer T]"), + ("[integer, Cycle, boolean]", "[string, string, infer T]"), + ( + "(fun(a: Cycle, b: integer): boolean)", + "(fun(a: string, b: string): infer T)", + ), + ] { + let conditional = ws.ty(&format!("{source} extends {target} and boolean or number")); + let model = ws.analysis.semantic_model(file); + for _ in 0..2 { + assert_eq!( + eval_conditionals(&model, &conditional), + LuaType::Number, + "{source}" + ); + } + } + for condition in [ + "[Cycle, string] extends [string, string] and boolean or number", + "table extends table and boolean or number", + "[Cycle, string] extends [boolean, infer T] and T or integer", + "(fun(a: Cycle): string) extends (fun(a: string): string) and boolean or number", + ] { + let conditional = ws.ty(condition); + let model = ws.analysis.semantic_model(file); + assert_eq!(eval_conditionals(&model, &conditional), conditional); + } +} + +#[test] +fn unfinished_union_members_are_not_matches_or_mismatches() { + let mut ws = VirtualWorkspace::new(); + let file = ws.def("---@alias Cycle Other\n---@alias Other Cycle"); + for target in ["Cycle | true", "true | Cycle"] { + let ty = ws.ty(target); + let conditional = ws.ty(&format!("[true] extends [{target}] and string or integer")); + let unresolved = ws.ty(&format!("[false] extends [{target}] and string or integer")); + let model = ws.analysis.semantic_model(file); + for _ in 0..2 { + assert!(model.type_check(&LuaType::BooleanConst(true), &ty)); + assert!(!model.type_check(&LuaType::BooleanConst(false), &ty)); + assert_eq!(eval_conditionals(&model, &conditional), LuaType::String); + assert_eq!(eval_conditionals(&model, &unresolved), unresolved); + } + } +} + +#[test] +#[ntest::timeout(5000)] +fn recursive_alternatives_keep_only_proven_answers() { + for operator in ["|", "&"] { + let mut ws = VirtualWorkspace::new(); + let file = ws.def(&format!( + "---@alias Branch Left {operator} Right\n---@alias Left Branch\n---@alias Right Branch\n---@alias Productive Loop {operator} integer\n---@alias Loop Productive" + )); + let ty = ws.ty("Branch"); + let conditional = ws.ty("Branch extends integer and string or boolean"); + let productive = ws.ty("Productive extends integer and string or boolean"); + let model = ws.analysis.semantic_model(file); + assert!(!model.type_check(&ty, &LuaType::Integer)); + assert_eq!(eval_conditionals(&model, &conditional), conditional); + assert_eq!(eval_conditionals(&model, &productive), LuaType::String); + } +} + +#[test] +fn growing_computations_report_excessive_evaluation() { + let mut ws = VirtualWorkspace::new(); + let file = ws.def( + r#" + ---@alias Grow T | Grow + ---@alias Left Right + ---@alias Right Left + ---@alias Productive Grow | integer + ---@alias InferGrow T extends infer U and InferGrow or never + ---@alias ObjectGrow T extends {value: infer U} and ObjectGrow<{value: {value: U}}> or never + ---@class Box + ---@field value T + ---@alias BoxGrow T extends Box and BoxGrow>> or never + ---@alias ReturnGrow T extends (fun(): infer U) and ReturnGrow or never + ---@alias RestGrow T extends (fun(...: infer U)) and RestGrow or never + "#, + ); + for name in [ + "Grow", + "Left", + "Right", + "InferGrow", + "ObjectGrow<{value: string}>", + "BoxGrow>", + "ReturnGrow", + "RestGrow", + ] { + let source = ws.ty(name); + let conditional = ws.ty(&format!("{name} extends integer and boolean or string")); + let model = ws.analysis.semantic_model(file); + assert!( + matches!( + check_type(&model, &source, &LuaType::Integer), + Err(TypeCheckFailReason::TooComplex) + ), + "{name}" + ); + assert_eq!(eval_conditionals(&model, &conditional), conditional); + } + let known = ws.ty("Productive extends integer and boolean or string"); + let model = ws.analysis.semantic_model(file); + assert_eq!(eval_conditionals(&model, &known), LuaType::Boolean); +} + +#[test] +fn recursive_alias_arguments_can_change_within_a_finite_set() { + let mut ws = VirtualWorkspace::new(); + let file = ws.def( + r#" + ---@alias Change T | Change + ---@alias Switch {value: A, next: Switch} + ---@alias Tail {value: integer} + ---@class Fixed + ---@field value A + ---@alias Replace T | Replace[]> + "#, + ); + let candidate = ws.ty("Replace"); + let changed = ws.ty("Change"); + let switch = ws.ty("Switch"); + let accepted = ws.ty("{value: string, next: Tail}"); + let rejected = ws.ty("{value: string, next: {value: boolean}}"); + let model = ws.analysis.semantic_model(file); + assert_eq!( + model.member_type(&candidate, &LuaMemberKey::Name("value".into())), + Some(LuaType::Integer) + ); + assert!(model.type_check(&changed, &LuaType::Integer)); + assert!(model.type_check_subtype(&switch, &accepted)); + assert!(!model.type_check_subtype(&switch, &rejected)); +} + +#[test] +fn inferred_components_can_consume_recursive_alias_arguments() { + let mut ws = VirtualWorkspace::new(); + let file = ws.def( + r#" + ---@alias Flatten T extends (infer U)[] and Flatten or T + ---@alias Unwrap T extends {value: infer U} and Unwrap or T + ---@alias Items T[] + ---@alias WrappedItems {items: Items} + ---@alias Element T extends {items: (infer U)[]} and U or boolean + ---@class Box + ---@field value T + ---@alias Unbox T extends Box and Unbox or T + ---@alias Returns T extends (fun(): infer U) and Returns or T + ---@alias Argument T extends (fun(value: infer U)) and Argument or T + ---@alias FirstArgument T extends (fun(...: [infer U])) and FirstArgument or T + ---@class Peel + ---@field value Flatten + ---@alias ProjectedFlatten RawGet, 'value'> + ---@class Route + ---@field next RawGet, 'value'> + ---@field value T + ---@alias ReadRoute RawGet, 'next'> + ---@class Start + ---@field next Read> + ---@class Finish + ---@field next T + ---@alias Read RawGet + ---@alias ReadElement RawGet extends (infer U)[] and U or never + ---@class RecursiveBox + ---@field value RecursiveBox[] + ---@alias Wrapped T | Reset + ---@alias Reset Wrapped + "#, + ); + for (source, expected) in [ + ("ReadRoute", "string[]"), + ("Read>", "string[]"), + ("ReadElement>", "string"), + ("Flatten", "string"), + ("Element>", "string"), + ("Element<{items: integer}>", "boolean"), + ("ProjectedFlatten", "string"), + ("Unwrap<{value: {value: string}}>", "string"), + ("Unwrap", "RecursiveBox[]"), + ("Unbox>>", "string"), + ("Returns", "string"), + ("Argument", "string"), + ("FirstArgument", "string"), + ] { + let source = ws.ty(source); + let expected = ws.ty(expected); + let model = ws.analysis.semantic_model(file); + assert_eq!( + expand_alias_generic(&model, &source), + expected, + "{source:?}" + ); + } + let wrapped = ws.ty("Wrapped"); + let model = ws.analysis.semantic_model(file); + assert!(model.type_check(&wrapped, &LuaType::Integer)); +} + +#[test] +fn recursive_json_aliases_keep_finite_nested_values() { + let mut ws = VirtualWorkspace::new(); + let file = ws.def( + r#" + ---@alias Json nil | boolean | number | string | Json[] | table + ---@alias JsonOf T | JsonOf[] | table> + "#, + ); + for alias in ["Json", "JsonOf"] { + let target = ws.ty(alias); + for source in [ + "string", + "boolean", + "number[][]", + "table", + "fun()", + "thread", + ] { + let source_type = ws.ty(source); + let model = ws.analysis.semantic_model(file); + let result = crate::semantic_model::type_check::check_assign_type_detail( + &model, + &source_type, + &target, + ); + if matches!(source, "fun()" | "thread") { + assert!( + result.is_err_and(|err| err.is_type_not_match()), + "{alias}: {source}" + ); + } else { + assert!(result.is_ok(), "{alias}: {source}: {result:?}"); + } + } + } +} + +#[test] +fn recursive_matches_ignore_unused_union_alternatives() { + let mut ws = VirtualWorkspace::new(); + let file = ws.def( + r#" + ---@alias Link T | Link[] + ---@alias Other T | Other[] + ---@alias Grow T | Grow + "#, + ); + let source = ws.ty("Link"); + for target in [ + "Other", + "Other | Grow", + "Grow | Other", + ] { + let target = ws.ty(target); + let model = ws.analysis.semantic_model(file); + assert_eq!( + crate::semantic_model::type_check::check_assign_type_detail(&model, &source, &target), + Ok(()), + "{target:?}" + ); + } +} + +#[test] +fn finite_comparisons_have_no_depth_cutoff() { + let mut ws = VirtualWorkspace::new(); + let file = ws.def("---@alias Nested string | Nested[]"); + let target = ws.ty("Nested"); + for (leaf, accepted) in [("string", true), ("(fun())", false)] { + let source = ws.ty(&format!("{leaf}{}", "[]".repeat(256))); + let model = ws.analysis.semantic_model(file); + let result = + crate::semantic_model::type_check::check_assign_type_detail(&model, &source, &target); + assert_eq!(result.is_ok(), accepted, "{leaf}"); + if !accepted { + assert!(result.is_err_and(|err| err.is_type_not_match())); + } + } +} + +#[test] +fn function_parameters_keep_independent_alias_paths() { + let mut ws = VirtualWorkspace::new(); + let file = ws.def("---@alias Identity T\n---@alias Num integer"); + for (source, target, expected) in [ + ("Identity", "fun(x: Identity)", true), + ( + "Identity", + "fun(x: Identity)", + false, + ), + ("Identity", "fun(x: Identity)", true), + ( + "Identity", + "fun(x: Identity)", + false, + ), + ] { + let source_type = ws.ty(source); + let target_type = ws.ty(target); + let model = ws.analysis.semantic_model(file); + assert_eq!(model.type_check(&source_type, &target_type), expected); + assert_eq!( + model.type_check_subtype(&source_type, &target_type), + expected + ); + assert_eq!( + crate::semantic_model::type_check::check_assign_type_detail( + &model, + &source_type, + &target_type + ) + .is_ok(), + expected + ); + } +} + +#[test] +fn recursive_alias_comparisons_check_fields_before_accepting_cycles() { + let mut ws = VirtualWorkspace::new(); + let aliases = (0..32) + .map(|i| format!("---@alias Delay{i} Delay{}\n", i + 1)) + .collect::(); + let file = ws.def(&format!( + r#" + ---@alias Link {{value: T, next: Link}} + ---@alias Other {{value: T, next: Other}} + ---@alias Delayed {{value: Delay0, next: Delayed}} + {aliases} + ---@alias Delay32 integer + "#, + )); + let source = ws.ty("Link"); + for (target, accepted) in [ + ("Other", true), + ("Other", false), + ("Delayed", false), + ("{value: string, next: {value: string}}", true), + ("{value: string, next: {value: integer}}", false), + ] { + let target = ws.ty(target); + let model = ws.analysis.semantic_model(file); + for _ in 0..2 { + let result = + crate::semantic_model::type_check::check_type_subtype(&model, &source, &target); + assert_eq!(result.is_ok(), accepted, "{target:?}"); + if !accepted { + assert!(result.is_err_and(|err| err.is_type_not_match())); + } + } + } +} + +#[test] +fn lazy_computed_fields_keep_precise_subtype_checks() { + let mut ws = VirtualWorkspace::new(); + let file = ws.def( + r#" + ---@alias Indexed {value: T['value']} + ---@alias Conditional {value: T extends string and integer or boolean} + ---@alias MappedField {value: {[K in 'name']: T[K]}} + "#, + ); + for (computed, resolved, rejected) in [ + ( + "Indexed<{value: string}>", + "{value: string}", + "{value: integer}", + ), + ( + "Conditional", + "{value: integer}", + "{value: boolean}", + ), + ( + "MappedField<{name: string}>", + "{value: {name: string}}", + "{value: {name: integer}}", + ), + ] { + let computed = ws.ty(computed); + let resolved = ws.ty(resolved); + let rejected = ws.ty(rejected); + let model = ws.analysis.semantic_model(file); + for (source, target, accepted) in [ + (&computed, &resolved, true), + (&resolved, &computed, true), + (&computed, &rejected, false), + (&rejected, &computed, false), + ] { + let result = + crate::semantic_model::type_check::check_type_subtype(&model, source, target); + assert_eq!( + result.is_ok(), + accepted, + "{source:?} -> {target:?}: {result:?}" + ); + if !accepted { + assert!(result.is_err_and(|err| err.is_type_not_match())); + } + } + } +} + +#[test] +fn indexed_aliases_can_use_the_same_alias_for_base_and_key() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + let file = ws.def("---@alias Key 'len'\n---@alias Identity T"); + let types = [ + ws.ty("RawGet"), + ws.ty("RawGet"), + ws.ty("RawGet<'len', Key>"), + ws.ty("RawGet, Key>"), + ws.ty("RawGet>"), + ws.ty("RawGet>>"), + ]; + let model = ws.analysis.semantic_model(file); + let expected = model + .member_type(&LuaType::String, &LuaMemberKey::Name("len".into())) + .expect("string.len must be defined"); + for ty in types { + assert_eq!(expand_alias_generic(&model, &ty), expected); + } +} + +#[test] +fn recursive_structures_keep_definite_compatibility_answers() { + let mut ws = VirtualWorkspace::new(); + let file = ws.def( + r#" + ---@alias Leaf {value: T} + ---@alias Tree Leaf | Tree[] + ---@alias Copy {[K in keyof T]: T[K]} + ---@alias Node {value: T, copy: Copy>} + "#, + ); + let types = [ws.ty("Tree"), ws.ty("Node")]; + let conditional = ws.ty("Node extends table and boolean or integer"); + let model = ws.analysis.semantic_model(file); + for ty in types { + for _ in 0..2 { + assert!(model.type_check(&ty, &LuaType::Table)); + assert!(!model.type_check(&ty, &LuaType::Integer)); + } + } + assert_eq!(eval_conditionals(&model, &conditional), LuaType::Boolean); +} + +#[test] +fn shrinking_aliases_and_independent_fields_resolve() { + let mut ws = VirtualWorkspace::new(); + let file = ws.def("---@alias Identity T\n---@alias Pair {left: T, right: T}"); + let ty = ws.ty("Pair>>"); + let targets = [ + (ws.ty("{left: string, right: string}"), true), + (ws.ty("{left: integer, right: string}"), false), + (ws.ty("{left: string, right: integer}"), false), + ]; + let model = ws.analysis.semantic_model(file); + let expanded = expand_alias_generic(&model, &ty); + for (target, accepted) in targets { + assert_eq!( + model.type_check_subtype(&expanded, &target), + accepted, + "{target:?}" + ); + } +} + +#[test] +fn conditional_aliases_resolve_shrinking_arguments_and_union_members() { + let mut ws = VirtualWorkspace::new(); + let file = ws.def("---@alias Strings T extends string and T or never"); + let ty = ws.ty("Strings | integer>"); + let model = ws.analysis.semantic_model(file); + assert_eq!(expand_alias_generic(&model, &ty), LuaType::String); +} + +#[test] +fn alias_cycles_stop_and_leave_other_queries_usable() { + for (definitions, name) in [ + ("---@alias Cycle Cycle", "Cycle"), + ("---@alias Cycle Other\n---@alias Other Cycle", "Cycle"), + ("---@alias Cycle Cycle", "Cycle"), + ( + "---@alias Cycle Other\n---@alias Other Cycle", + "Cycle", + ), + ("---@alias Cycle Cycle", "Cycle"), + ( + "---@alias Cycle Other>\n---@alias Other T extends string and T or T[]", + "Cycle", + ), + ] { + let mut ws = VirtualWorkspace::new(); + let file = ws.def(&format!("{definitions}\n---@alias Identity T")); + let cycle = if name.contains('<') { + ws.ty(name) + } else { + LuaType::Ref(LuaTypeDeclId::global(name)) + }; + let valid = ws.ty("Identity>"); + let model = ws.analysis.semantic_model(file); + assert!( + matches!( + expand_alias_head(&model, &cycle), + None | Some(LuaType::Any | LuaType::Unknown) + ), + "{definitions}" + ); + assert_eq!(expand_alias_generic(&model, &cycle), cycle, "{definitions}"); + // A failed path must release its state before an independent query. + assert_eq!(expand_alias_generic(&model, &valid), LuaType::String); + } +} + +#[test] +fn conditional_recursion_can_finish_after_argument_growth() { + let mut ws = VirtualWorkspace::new(); + let file = ws.def( + r#" + ---@alias Build T extends string[][][][] and T or Build + ---@alias Accumulate T extends (infer U)[] and Accumulate or A + "#, + ); + for (source, expected) in [ + ("Build", "string[][][][]"), + ("Accumulate", "integer[][][][][][]"), + ] { + let ty = ws.ty(source); + let expected = ws.ty(expected); + let model = ws.analysis.semantic_model(file); + assert_eq!(expand_alias_generic(&model, &ty), expected, "{source}"); + } +} + +#[test] +fn long_valid_alias_chains_resolve_to_their_final_type() { + for kind in ["named", "generic", "conditional", "union"] { + let generic = kind != "named"; + let mut ws = VirtualWorkspace::new(); + let mut source = String::new(); + for i in 0..512 { + if kind == "conditional" { + source.push_str(&format!( + "---@alias Alias{i} T extends string and Alias{} or never\n", + i + 1 + )); + } else if kind == "union" { + source.push_str(&format!("---@alias Alias{i} Alias{} | T\n", i + 1)); + } else if generic { + source.push_str(&format!("---@alias Alias{i} Alias{}\n", i + 1)); + } else { + source.push_str(&format!("---@alias Alias{i} Alias{}\n", i + 1)); + } + } + source.push_str(if generic { + "---@alias Alias512 T" + } else { + "---@alias Alias512 string" + }); + let file = ws.def(&source); + let ty = ws.ty(if generic { "Alias0" } else { "Alias0" }); + let model = ws.analysis.semantic_model(file); + if kind == "union" { + assert_eq!(expand_alias_generic(&model, &ty), LuaType::String); + } else { + assert_eq!(expand_alias_head(&model, &ty), Some(LuaType::String)); + } + } +} + +#[test] +fn long_alias_chains_through_fields_resolve_the_leaf() { + let mut ws = VirtualWorkspace::new(); + let mut source = (0..256) + .map(|i| format!("---@alias Alias{i} {{child: Alias{}}}\n", i + 1)) + .collect::(); + source.push_str("---@alias Alias256 T"); + let file = ws.def(&source); + let ty = ws.ty("Alias0"); + let model = ws.analysis.semantic_model(file); + let mut leaf = ty; + for _ in 0..256 { + leaf = model + .member_type(&leaf, &LuaMemberKey::Name("child".into())) + .expect("child must resolve"); + } + assert_eq!(expand_alias_generic(&model, &leaf), LuaType::String); +} + +#[test] +fn operator_forwarding_resolves_long_chains_and_stops_cycles() { + let mut ws = VirtualWorkspace::new(); + let mut source = (0..512) + .map(|i| format!("---@alias Alias{i} Alias{}\n", i + 1)) + .collect::(); + source.push_str("---@class Alias512\n---@operator len: string\n---@alias Cycle Other\n---@alias Other Cycle"); + let file = ws.def(&source); + let model = ws.analysis.semantic_model(file); + assert_eq!( + model.operator_type("len", &LuaType::Ref(LuaTypeDeclId::global("Alias0"))), + Some(LuaType::String) + ); + assert_eq!( + model.operator_type("len", &LuaType::Ref(LuaTypeDeclId::global("Cycle"))), + None + ); +} + +#[test] +fn alias_index_keys_resolve_finite_paths_and_stop_cycles() { + let mut ws = VirtualWorkspace::new(); + let mut source = (0..512) + .map(|i| format!("---@alias Key{i} Key{}\n", i + 1)) + .collect::(); + source.push_str("---@alias Key512 'name'\n---@alias Keys 'name' | Keys\n---@alias Cycle Cycle\n---@class Record\n---@field name string"); + let file = ws.def(&source); + let key_source = ws.ty("{name: integer}"); + let model = ws.analysis.semantic_model(file); + let record = LuaType::Ref(LuaTypeDeclId::global("Record")); + for (key, expected) in [ + ("Key0", Some(LuaType::String)), + ("Keys", Some(LuaType::String)), + ("Cycle", None), + ] { + let key_type = LuaType::Ref(LuaTypeDeclId::global(key)); + assert_eq!( + eval_index_access(&model, &record, &key_type), + expected, + "{key}" + ); + let call = LuaAliasCallType::new(LuaAliasCallKind::Index, vec![record.clone(), key_type]); + assert_eq!(expand_index_call(&model, &call), expected, "{key}"); + } + let key = LuaType::Call(Rc::new(LuaAliasCallType::new( + LuaAliasCallKind::KeyOf, + vec![key_source], + ))); + assert_eq!( + eval_index_access(&model, &record, &key), + Some(LuaType::String) + ); + let call = LuaAliasCallType::new(LuaAliasCallKind::Index, vec![record, key]); + assert_eq!(expand_index_call(&model, &call), Some(LuaType::String)); +} + +#[test] +fn recursive_alias_member_queries_keep_concrete_alternatives() { + let mut ws = VirtualWorkspace::new(); + let file = ws.def( + r#" + ---@alias Growing Growing + ---@alias GrowingUnion GrowingUnion | nil + ---@alias Conditional Filter> + ---@alias Filter T extends string and T or T[] + ---@alias Leaf {value: T} + ---@alias Tree Leaf | Tree[] + ---@alias Branch Branch | Leaf + ---@alias NamedTree Leaf | NamedTree[] + ---@class Owner + ---@field value T + ---@field leaf Leaf + ---@field next Project + ---@alias Project RawGet, 'next'> + ---@alias Lookup RawGet + ---@class Indirect + ---@field next Lookup> + ---@alias Nest {value: T, next: Nest} + ---@alias Field RawGet, 'value'> + ---@class Computed + ---@field next RawGet, 'next'> + ---@alias ComputedProject RawGet, 'next'> + "#, + ); + let cycles = [ + "Growing", + "GrowingUnion", + "Conditional", + "Project", + "ComputedProject", + "Lookup>", + ] + .map(|name| ws.ty(name)); + let field = ws.ty("Field"); + let leaf = ws.ty("RawGet, 'leaf'>"); + let expected_leaf = ws.ty("{value: string}"); + let nest = ws.ty("Nest"); + let tree = ws.ty("Tree"); + let branch = ws.ty("Branch"); + let named_tree = LuaType::Ref(LuaTypeDeclId::global("NamedTree")); + let model = ws.analysis.semantic_model(file); + let key = LuaMemberKey::Name("value".into()); + assert_eq!(expand_alias_generic(&model, &field), LuaType::String); + assert_eq!(expand_alias_generic(&model, &leaf), expected_leaf); + for cycle in cycles { + let expanded = expand_alias_generic(&model, &cycle); + assert_eq!(model.member_type(&expanded, &key), None); + assert_eq!(model.member_type(&cycle, &key), None); + assert!(model.member_infos(&cycle).is_empty()); + } + assert_eq!(model.member_type(&nest, &key), Some(LuaType::String)); + for tree in [tree, named_tree, branch] { + let expanded = expand_alias_generic(&model, &tree); + assert_eq!(model.member_type(&expanded, &key), Some(LuaType::String)); + assert_eq!(model.member_type(&tree, &key), Some(LuaType::String)); + let members = model.member_infos(&tree); + assert_eq!(members.len(), 1); + assert_eq!(members[0].typ, LuaType::String); + } +} diff --git a/crates/emmylua_code_analysis/src/semantic_model/types.rs b/crates/emmylua_code_analysis/src/semantic_model/types.rs index e1d205ae5..c6f3a3b01 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/types.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/types.rs @@ -4,6 +4,15 @@ use crate::semantic_db::TypeDefList; use crate::semantic_model::type_eval::substitute_named_refs; impl<'db> SemanticModel<'db> { + pub(crate) fn type_evaluation_errors(&self) -> Vec> { + self.cache + .borrow() + .evaluation_errors + .iter() + .cloned() + .collect() + } + // -- Types -- /// 按当前文件的命名空间和可见性规则解析类型名称. @@ -111,21 +120,26 @@ impl<'db> SemanticModel<'db> { SemanticId::Member(key) => key.file_id, _ => self.view.file_id(), }; + if cache_file != self.file_id() { + return self.model_for(cache_file).type_of_decl(decl); + } let key = (cache_file, decl.clone()); - match self.cache.borrow().decl_type.get(&key) { - Some(cache::CacheEntry::Ready(cached)) => return cached.clone(), - Some(cache::CacheEntry::InProgress) => return None, - None => {} + { + let cache = self.cache.borrow(); + if let Some(entry) = cache.decl_type.get(&key) { + return entry.get_inference_seed(&cache).cloned().flatten(); + } } + let _query = cache::QueryCacheScope::new(&self.cache); self.cache .borrow_mut() .decl_type .insert(key.clone(), cache::CacheEntry::InProgress); + let revision = self.cache.borrow().revision(); let result = self.type_of_decl_impl(decl); - self.cache - .borrow_mut() - .decl_type - .insert(key, cache::CacheEntry::Ready(result.clone())); + let mut cache = self.cache.borrow_mut(); + let entry = cache::CacheEntry::finish(result.clone(), cache.is_complete(revision)); + cache.decl_type.insert(key, entry); result } pub(crate) fn type_of_decl_impl(&self, decl: &SemanticId) -> Option { @@ -311,19 +325,7 @@ impl<'db> SemanticModel<'db> { } } } - // Cross-file declarations: query by the file carried in the declaration (the Decl key includes file_id). - // Lazy type execution: `decl_type_lua` is keyed by the declaring file, entering only defining files, not consumer models. - let decl_file = match decl { - SemanticId::Decl(key) => key.file_id, - _ => self.view.file_id(), - }; - if decl_file != self.view.file_id() - && let foreign_model = SemanticModel::new(self.db(), decl_file) - && let Some(foreign_ty) = foreign_model.type_of_decl(decl) - { - return Some(foreign_ty); - } - self.analysis().decl_type_lua(decl_file, decl.clone()) + self.analysis().decl_type_lua(self.file_id(), decl.clone()) } /// Parameter declarations may lose generic constraints in `type_of_decl`'s fallback path; re-attach them here. pub(crate) fn attach_param_decl_constraint(&self, decl: &SemanticId, ty: LuaType) -> LuaType { @@ -793,41 +795,37 @@ impl<'db> SemanticModel<'db> { SemanticId::Member(key) => key.file_id, _ => self.view.file_id(), }; + if cache_file != self.file_id() { + return self.model_for(cache_file).type_of_member(member); + } let key = (cache_file, member.clone()); - match self.cache.borrow().member_type.get(&key) { - Some(cache::CacheEntry::Ready(cached)) => return cached.clone(), - Some(cache::CacheEntry::InProgress) => return None, - None => {} + { + let cache = self.cache.borrow(); + if let Some(entry) = cache.member_type.get(&key) { + return entry.get_inference_seed(&cache).cloned().flatten(); + } } + let _query = cache::QueryCacheScope::new(&self.cache); self.cache .borrow_mut() .member_type .insert(key.clone(), cache::CacheEntry::InProgress); + let revision = self.cache.borrow().revision(); let result = self.type_of_member_impl(member); - self.cache - .borrow_mut() - .member_type - .insert(key, cache::CacheEntry::Ready(result.clone())); + let mut cache = self.cache.borrow_mut(); + let entry = cache::CacheEntry::finish(result.clone(), cache.is_complete(revision)); + cache.member_type.insert(key, entry); result } pub(crate) fn type_of_member_impl(&self, member: &SemanticId) -> Option { - // Members are keyed by declaring file: take the file from the Member key. - let member_file = match member { - SemanticId::Member(key) => key.file_id, - _ => self.view.file_id(), - }; - // Cross-file members are uniformly delegated to the member file's own model, keeping VM replay and cycle guards on the same model. - if member_file != self.view.file_id() { - let foreign_model = self.model_for(member_file); - return foreign_model.type_of_member(member); - } + let member_file = self.file_id(); // Literal integers in `---@enum` table fields stay constant (`severity.ERROR` -> IntegerConst(1)). if let Some(enum_const) = self.enum_member_const(member_file, member) { return Some(enum_const); } // Runtime member assignments prefer VM projection: the TypeShell path does not perform higher-order generic call inference, // and would keep `E.foo_wrapped = wrap(function(a) ... end)` as `fun(...: T...)`. - // Cycles/reentry are handled by `CacheEntry::InProgress` in the per-model cache. + // Cycles/reentry are handled by `CacheEntry::InProgress` in the shared cache. if let Some(facts) = self.file_facts_of(member_file) && let Some(member_def) = facts.member_by_id(member) && !matches!(member_def.owner, SemanticId::TypeDef(_)) @@ -835,7 +833,7 @@ impl<'db> SemanticModel<'db> { { // Table-literal fields keep the expression type from construction (`[key] = 1`, named fields // `foo = 123` keep `IntegerConst`; flow/type matching widens when `number` is needed). - // Reentry is handled by `CacheEntry::InProgress` in the per-model cache. + // Reentry is handled by `CacheEntry::InProgress` in the shared cache. let vm_ty = (|| { let tree = self.syntax_tree_of(member_file)?; let node = value_syntax.to_node_from_root(&tree.get_red_root())?; @@ -863,8 +861,6 @@ impl<'db> SemanticModel<'db> { let ty = self .analysis() .type_shell_lua_in(member_file, &shell, &generic_names); - let ty = type_eval::expand_alias_generic(self, &ty); - let ty = type_eval::eval_conditionals(self, &ty); let ty = if let Some(facts) = self.file_facts_of(member_file) && let Some(member_def) = facts.member_by_id(member) && member_def.doc_type_syntax.is_none() @@ -990,14 +986,15 @@ impl<'db> SemanticModel<'db> { } /// All members of a prefix type (completion candidates; `@field` + inheritance + runtime values, with generic substitution). pub fn member_infos(&self, prefix_type: &LuaType) -> Vec { - if let Some(cached) = self.cache.borrow().member_infos.get(prefix_type) { + let key = (self.file_id(), prefix_type.clone()); + if let Some(cached) = self.cache.borrow().member_infos.get(&key) { return cached.clone(); } + let revision = self.cache.borrow().revision(); let value = member::member_infos(self, prefix_type); - self.cache - .borrow_mut() - .member_infos - .insert(prefix_type.clone(), value.clone()); + self.cache.publish_if_complete(revision, |cache| { + cache.member_infos.insert(key, value.clone()); + }); value } /// The specified-key member of a prefix type (first match). @@ -1006,19 +1003,15 @@ impl<'db> SemanticModel<'db> { prefix_type: &LuaType, key: &LuaMemberKey, ) -> Option { - if let Some(value) = self - .cache - .borrow() - .member_info - .get(&(prefix_type.clone(), key.clone())) - { + let cache_key = (self.file_id(), prefix_type.clone(), key.clone()); + if let Some(value) = self.cache.borrow().member_info.get(&cache_key) { return value.clone(); } + let revision = self.cache.borrow().revision(); let value = member::member_info(self, prefix_type, key); - self.cache - .borrow_mut() - .member_info - .insert((prefix_type.clone(), key.clone()), value.clone()); + self.cache.publish_if_complete(revision, |cache| { + cache.member_info.insert(cache_key, value.clone()); + }); value } /// Replaces `TplRef` in a type with a set of generic arguments (for hover/display-layer call-site projection). @@ -1236,12 +1229,14 @@ impl<'db> SemanticModel<'db> { if let Some(cached) = cached { return self.sanitize_global_generic_decl(decl, cached); } + let revision = self.cache.borrow().revision(); let ty = flow::type_of_decl_at(self, decl, offset); if let Some(start) = start { - self.cache - .borrow_mut() - .flow_decl - .insert((self.view.file_id(), decl.clone(), start), ty.clone()); + self.cache.publish_if_complete(revision, |cache| { + cache + .flow_decl + .insert((self.view.file_id(), decl.clone(), start), ty.clone()); + }); } self.sanitize_global_generic_decl(decl, ty) } @@ -1282,23 +1277,15 @@ impl<'db> SemanticModel<'db> { self.type_of_member_at_impl(member, offset) } pub(crate) fn type_of_member_at_impl(&self, member: &SemanticId, offset: TextSize) -> LuaType { - let cache_file = match member { - SemanticId::Member(key) => key.file_id, - _ => self.view.file_id(), - }; - if let Some(cached) = - self.cache - .borrow() - .member_type_at - .get(&(cache_file, member.clone(), offset)) - { + let key = (self.file_id(), member.clone(), offset); + if let Some(cached) = self.cache.borrow().member_type_at.get(&key) { return cached.clone(); } + let revision = self.cache.borrow().revision(); let ty = flow::type_of_member_at(self, member, offset); - self.cache - .borrow_mut() - .member_type_at - .insert((cache_file, member.clone(), offset), ty.clone()); + self.cache.publish_if_complete(revision, |cache| { + cache.member_type_at.insert(key, ty.clone()); + }); ty } /// Decl type before the flow node at offset (for assignment checks: this assignment does not participate in the target type). @@ -1343,42 +1330,106 @@ impl<'db> SemanticModel<'db> { expr_syntax: LuaSyntaxId, offset: TextSize, ) -> LuaType { + // Flow reads must not turn a long binary/member chain into native + // recursion. Evaluate each prefix/operand before its parent, as the VM does. + enum Task { + Enter(LuaExpr), + Finish(LuaExpr, u64), + } let file_id = self.view.file_id(); - if let Some(cached) = self - .cache - .borrow() - .expr_type_at - .get(&(file_id, expr_syntax, offset)) + let root_key = (file_id, expr_syntax, offset); { - return cached.clone(); + let cache = self.cache.borrow(); + if let Some(entry) = cache.expr_type_at.get(&root_key) { + return entry.get(&cache).cloned().unwrap_or(LuaType::Unknown); + } } - let ty = flow::type_of_expr_at(self, expr_syntax, offset); - self.cache - .borrow_mut() + let Some(expr) = self + .syntax_tree() + .and_then(|tree| expr_syntax.to_node_from_root(&tree.get_red_root())) + .and_then(LuaExpr::cast) + else { + return self.type_of_expr(expr_syntax); + }; + let _query = cache::QueryCacheScope::new(&self.cache); + let mut pending = vec![Task::Enter(expr)]; + while let Some(task) = pending.pop() { + match task { + Task::Enter(expr) => { + let key = (file_id, expr.get_syntax_id(), offset); + if self.cache.borrow().expr_type_at.contains_key(&key) { + continue; + } + self.cache + .borrow_mut() + .expr_type_at + .insert(key, cache::CacheEntry::InProgress); + pending.push(Task::Finish(expr.clone(), self.cache.borrow().revision())); + match expr { + LuaExpr::BinaryExpr(binary) => { + if let Some((left, right)) = binary.get_exprs() { + pending.push(Task::Enter(right)); + pending.push(Task::Enter(left)); + } + } + LuaExpr::IndexExpr(index) => { + if let Some(prefix) = index.get_prefix_expr() { + pending.push(Task::Enter(prefix)); + } + } + _ => {} + } + } + Task::Finish(expr, revision) => { + let syntax = expr.get_syntax_id(); + let is_index = matches!(expr, LuaExpr::IndexExpr(_)); + let ty = flow::type_of_expr_at_node(self, expr, offset); + let ty = if is_index { + let ty = type_eval::expand_alias_generic(self, &ty); + type_eval::eval_conditionals(self, &ty) + } else { + ty + }; + let mut cache = self.cache.borrow_mut(); + let entry = cache::CacheEntry::finish(ty, cache.is_complete(revision)); + cache.expr_type_at.insert((file_id, syntax, offset), entry); + } + } + } + let cache = self.cache.borrow(); + cache .expr_type_at - .insert((file_id, expr_syntax, offset), ty.clone()); - ty + .get(&root_key) + .and_then(|entry| entry.get(&cache)) + .cloned() + .unwrap_or(LuaType::Unknown) } /// Operator-overload return type: when operand is a named type with `---@operator`, look up the return type by operator name. pub fn operator_type(&self, op_name: &str, operand: &LuaType) -> Option { - let decl_id = match operand { - LuaType::Ref(id) | LuaType::Def(id) => id, - _ => return None, - }; - let def = self.type_def_of(decl_id)?; - // Alias forwarding: `AliasType` -> `Origin`, where the operator is defined on Origin. - if def.kind == TypeDefKind::Alias - && let Some(target) = self.alias_target(&def) - { - return self.operator_type(op_name, &target); + let mut current = operand.clone(); + let mut visited = HashSet::new(); + loop { + let decl_id = match ¤t { + LuaType::Ref(id) | LuaType::Def(id) => id, + _ => return None, + }; + if !visited.insert(decl_id.clone()) { + return None; + } + let def = self.type_def_of(decl_id)?; + if def.kind == TypeDefKind::Alias { + current = self.alias_target(&def)?; + continue; + } + let facts = self.file_facts_of(def.file_id)?; + let op = facts.operator_of(&def.id, op_name)?; + let returns = + self.analysis() + .doc_type_lua(def.file_id, op.returns, &def.generic_params); + return (!matches!(returns, LuaType::Unknown)).then_some(returns); } - let facts = self.file_facts_of(def.file_id)?; - let op = facts.operator_of(&def.id, op_name)?; - let returns = self - .analysis() - .doc_type_lua(def.file_id, op.returns, &def.generic_params); - (!matches!(returns, LuaType::Unknown)).then_some(returns) } + /// All type definitions for a scope + full name (cross-file; used by checkers / inheritance chains). pub fn type_defs_in_scope(&self, scope: TypeScope, full_name: &str) -> TypeDefList { self.analysis().type_defs_in_scope(scope, full_name)