Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions crates/emmylua_code_analysis/src/check/test/need_check_nil_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,4 +294,40 @@ mod test {
"#,
));
}

#[test]
fn test_and_narrows_after_merge_of_earlier_guard() {
// After `if prev and ok()` merges back to `St?`, the later `prev and prev.sig` still
// narrows its right side, even when an unguarded read of `prev` memoized the merge
// first. Needs the std library and every check: with less, nothing reads `prev`
// before the guarded read, and the test passes without the fix.
let mut ws = VirtualWorkspace::new_with_init_std_lib();
let file_id = ws.def(
r#"
---@class St
---@field sig string

---@return St?
local function decode() end

---@return boolean
local function ok() return true end

local prev = decode()
if prev and ok() then end
print(prev and prev.sig)
"#,
);
let diagnostics = ws
.analysis
.diagnose_file(file_id, CancellationToken::new())
.unwrap_or_default();
let code = Some(NumberOrString::String(
DiagnosticCode::NeedCheckNil.get_name().to_string(),
));
assert!(
!diagnostics.iter().any(|diag| diag.code == code),
"{diagnostics:#?}"
);
}
}
7 changes: 3 additions & 4 deletions crates/emmylua_code_analysis/src/semantic_model/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,9 @@ pub(crate) enum TraceSubject {
/// Original-signature traces keep incoming PathState empty and memoize each
/// node's transfer during unwind, so a cached node never contains later casts.
///
/// 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
/// guards, so the path is a function of the node. [`super::flow`] consults this
/// memo only in that case and recomputes otherwise.
/// The state is the one for an empty incoming path: [`super::flow`] applies the
/// guards a query collected before reaching the node on top of it. The memo is
/// consulted only for nodes with no loop behind them, and recomputed otherwise.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct TraceStateKey {
pub(crate) file_id: FileId,
Expand Down
27 changes: 24 additions & 3 deletions crates/emmylua_code_analysis/src/semantic_model/flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2144,16 +2144,37 @@ fn trace_decl(
visited: &mut HashSet<FlowId>,
path: &mut PathState,
) -> Option<LuaType> {
// On a loop-free graph every route to a flow node crosses the same guards, so
// a state's value depends on the node and the purpose alone — never on the
// route. That makes the state cacheable, which matters because a nested
// On a loop-free graph a state for an empty incoming path depends on the node and
// the purpose alone. That makes it cacheable, which matters because a nested
// conditional chain otherwise re-derives the same states along exponentially
// many routes. Only nodes that can reach a loop backwards keep the uncached
// path (see the type note on `cache::TraceStateKey`), so a loop in one function
// does not disable memoization for the rest of the file.
if tree.is_loop_reachable(flow_id) {
return trace_decl_uncached(model, decl, tree, flow_id, purpose, mode, visited, path);
}
// The memo holds the state for an empty incoming path. Guards collected between the query
// and this node narrow that state: they apply in program order and distribute over branch
// merges. Incoming casts and correlated narrowings do not commute that way.
if !path.narrowings.is_empty()
&& path.casts.is_empty()
&& !path
.narrowings
.iter()
.any(|narrowing| matches!(narrowing, Narrowing::Correlated { .. }))
{
let state = trace_decl(
model,
decl,
tree,
flow_id,
purpose,
mode,
visited,
&mut PathState::default(),
)?;
return Some(finalize(model, state, path));
}
let key = decl_trace_key(model, decl, flow_id, purpose, mode);
{
let cache = model.cache.borrow();
Expand Down
Loading