From 8ec14d5eb427a228cfb4368f59b2a9dfcc84bc9f Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Fri, 16 Oct 2026 14:55:52 +0100 Subject: [PATCH] fix(semantic): make dependent queries safe to re-enter Dependent semantic queries can revisit unfinished work. A provisional fallback can then be cached as a final answer, which makes later queries depend on evaluation order. Recursive expression and type evaluation can also overflow the native stack. Query caching Publish in-progress entries before calling dependencies and release cache borrows before those calls. Track results that read unfinished work as provisional. Reuse them within the outer query, then discard them so a later query can retry. Publish dependent cache entries only when their inputs are complete. Preserve the existing seeds for recursive inference. Type evaluation Use a work list for alias instantiation and conditional continuations. Share the active alias path across nested semantic queries, and release it before independent branches. Resolve outer type computations while keeping fields and array elements lazy. Substitute member arguments at lookup, then evaluate the result where it is consumed. This supports recursive data types without unfolding their complete structure. Use work lists for long flow-expression chains, alias index keys, and operator forwarding. Share type reconstruction and named substitution between evaluation, generic result cleanup, and rendering. Type comparison Replace the recursive depth guard with a dependency graph and work queue. Keep matches, mismatches, and unresolved comparisons distinct. Recheck parents when a dependency changes. Accept a recursive assumption only when the cycle passes through data constructors, then finish dependent checks before publishing the result. Alias forwarding alone is no proof. A complete union alternative can prove a match despite an unresolved alternative. A definite mismatch can reject a required set of checks despite another unfinished check. Keep conditional types deferred when neither outcome is proven, including during conditional inference. Resource exhaustion Exact cycle checks cannot stop computations whose arguments keep growing: ---@alias Input Filter> ---@alias Filter T extends string and T or T[] Share a work budget across alias evaluation and type comparison. Retain a stack guard for nested semantic calls that still use native recursion. Report exhaustion with a type-complexity diagnostic instead of treating it as a type mismatch. Keep unrelated queries and diagnostics available. Coverage Cover recursive JSON types, structural cycles with mismatched fields, shrinking and finitely changing arguments, growing computations, query order, and independent branches. Check long valid alias, expression, and comparison chains, plus deferred conditionals and unrelated diagnostics. --- .../src/check/checker/attribute_check.rs | 40 +- .../src/check/checker/mod.rs | 19 + .../src/check/diagnostic_code.rs | 2 + .../check/test/assign_type_mismatch_test.rs | 35 +- .../src/check/test/mod.rs | 1 + .../src/check/test/param_type_check_test.rs | 42 +- .../src/check/test/reentry_test.rs | 334 ++++ .../src/semantic_model/access.rs | 56 +- .../src/semantic_model/cache.rs | 210 ++- .../src/semantic_model/doc_types.rs | 18 +- .../src/semantic_model/flow.rs | 61 +- .../legacy_tests/semantic_generic_test.rs | 23 +- .../src/semantic_model/infer/test.rs | 30 + .../src/semantic_model/infer/vm.rs | 34 +- .../src/semantic_model/member.rs | 84 +- .../src/semantic_model/mod.rs | 98 +- .../src/semantic_model/render/mod.rs | 119 +- .../src/semantic_model/render/tests.rs | 19 +- .../src/semantic_model/signature.rs | 75 +- .../src/semantic_model/type_check/assign.rs | 234 ++- .../semantic_model/type_check/comparison.rs | 219 +++ .../semantic_model/type_check/complex_type.rs | 114 +- .../src/semantic_model/type_check/context.rs | 35 +- .../semantic_model/type_check/fail_reason.rs | 10 +- .../semantic_model/type_check/func_type.rs | 112 +- .../semantic_model/type_check/generic_type.rs | 104 +- .../src/semantic_model/type_check/guard.rs | 27 - .../src/semantic_model/type_check/mod.rs | 269 +-- .../src/semantic_model/type_check/ref_type.rs | 60 +- .../semantic_model/type_check/simple_type.rs | 147 +- .../src/semantic_model/type_check/test.rs | 8 +- .../src/semantic_model/type_eval.rs | 1478 +++++------------ .../type_eval/alias_expansion.rs | 252 +++ .../src/semantic_model/type_eval/tests.rs | 844 ++++++++++ .../src/semantic_model/types.rs | 257 +-- 35 files changed, 3286 insertions(+), 2184 deletions(-) create mode 100644 crates/emmylua_code_analysis/src/check/test/reentry_test.rs create mode 100644 crates/emmylua_code_analysis/src/semantic_model/type_check/comparison.rs delete mode 100644 crates/emmylua_code_analysis/src/semantic_model/type_check/guard.rs create mode 100644 crates/emmylua_code_analysis/src/semantic_model/type_eval/alias_expansion.rs create mode 100644 crates/emmylua_code_analysis/src/semantic_model/type_eval/tests.rs 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/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 8def71eb7..e77617d60 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/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..0841f5be6 --- /dev/null +++ b/crates/emmylua_code_analysis/src/check/test/reentry_test.rs @@ -0,0 +1,334 @@ +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 in ["Choose", "Identity>"] { + 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 + ---@class Box + ---@field callback {field} + ---@param box Box + local function call(box) + return box.callback({argument}) + end + return call + "#, + )); + assert_eq!( + diagnostics.iter().map(|d| d.code).collect::>(), + expected, + "{field}, {argument}: {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_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 8fd7eadeb..8a40e1fed 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!( @@ -1508,6 +1499,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, @@ -1515,15 +1507,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 } @@ -3625,12 +3625,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..e5666f61f --- /dev/null +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/comparison.rs @@ -0,0 +1,219 @@ +//! 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::{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 { + if let Some(id) = self.index.get(key) { + return *id; + } + let id = self.nodes.len(); + self.index.insert(key.clone(), 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 = std::mem::take(&mut self.reads) + .into_iter() + .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 { + 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..f9b18399d 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_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(alias_expansion::expand(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..1a2eba1f5 --- /dev/null +++ b/crates/emmylua_code_analysis/src/semantic_model/type_eval/alias_expansion.rs @@ -0,0 +1,252 @@ +//! 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, +} + +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..db880d83d --- /dev/null +++ b/crates/emmylua_code_analysis/src/semantic_model/type_eval/tests.rs @@ -0,0 +1,844 @@ +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"] { + 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 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); + 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 Conditional Filter> + ---@alias Filter T extends string and T or T[] + ---@alias Leaf {value: T} + ---@alias Tree Leaf | Tree[] + ---@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", + "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 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 { + 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] { + 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)