Skip to content
Open
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
1 change: 1 addition & 0 deletions crates/starpls_hir/src/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ impl DisplayWithDb for TyKind {
let text = match self {
TyKind::Unbound => "Unbound",
TyKind::Unknown => "Unknown",
TyKind::EmptyUnspecified => "EmptyUnspecified",
TyKind::Any => "Any",
TyKind::Never => "Never",
TyKind::None => "None",
Expand Down
76 changes: 68 additions & 8 deletions crates/starpls_hir/src/typeck.rs
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,10 @@ impl Ty {
TyKind::Unknown.intern()
}

pub(crate) fn empty_unspecified() -> Ty {
TyKind::EmptyUnspecified.intern()
}

pub(crate) fn any() -> Ty {
TyKind::Any.intern()
}
Expand Down Expand Up @@ -654,20 +658,72 @@ impl Ty {
TyKind::Dict(key_ty, value_ty, known_keys).intern()
}

/// Handles unions of containers such as `list[EmptyUnspecified] | list[int]` == `list[int]`.
/// If the types were merged or are equal, returns a single combined type, otherwise nothing.
/// The return type may not be one of the original types. For example, one edge case is
/// ([], [""]) or ([1], []) , which would resolve to `tuple[list[int], list[str]]`.
fn resolve_empty_unspecified_type_union(ty1: &Ty, ty2: &Ty) -> Option<Ty> {
let ty1 = ty1.clone().normalize();
let ty2 = ty2.clone().normalize();
match (ty1.kind(), ty2.kind()) {
(TyKind::EmptyUnspecified, _) => Some(ty2.clone()),
(_, TyKind::EmptyUnspecified) => Some(ty1.clone()),
(TyKind::List(v1), TyKind::List(v2)) => {
Ty::resolve_empty_unspecified_type_union(v1, v2).map(Ty::list)
}
(TyKind::Dict(k1, v1, _), TyKind::Dict(k2, v2, _)) => {
// Note: the only key type that can even be container-like is a tuple. Dicts and lists
// are not allowed in dict keys.
// The only case the key covers if if you have something like `{} | {1: 2}`, since both
// key and value will be EmptyUnspecified.
let key = Ty::resolve_empty_unspecified_type_union(k1, k2);
let value = Ty::resolve_empty_unspecified_type_union(v1, v2);
match (key, value) {
// TODO: known keys?
(Some(k), Some(v)) => Some(Ty::dict(k, v, None)),
_ => None,
}
}
// Variadics are always explicit annotations, so they should never have EmptyUnspecified types.
(TyKind::Tuple(Tuple::Simple(tys1)), TyKind::Tuple(Tuple::Simple(tys2))) => {
if tys1.len() != tys2.len() {
None
} else {
let mut newtys: SmallVec<[Ty; 2]> = smallvec![];
for (t1, t2) in tys1.iter().zip(tys2) {
newtys.push(Ty::resolve_empty_unspecified_type_union(t1, t2)?);
}
Some(TyKind::Tuple(Tuple::Simple(newtys)).intern())
}
}
_ => {
if Ty::eq(&ty1, &ty2) {
Some(ty1.clone())
} else {
None
}
}
}
}

pub(crate) fn union(tys: impl Iterator<Item = Ty>) -> Ty {
let mut unique_tys = smallvec![];
// Type spelled out to help the rust LSP
let mut unique_tys: SmallVec<[Ty; 2]> = smallvec![];

// Deduplicate types. Dicts and structs are handled separately because the metadata
// that they store // (for their declarations, etc.) is not relevant to determining
// whether or not types are duplicates.
for ty in tys {
let mut check_unique = |ty: Ty| {
let ty = ty.normalize();
if unique_tys
.iter()
.any(|unique_ty: &Ty| Ty::eq(&ty, &unique_ty.clone().normalize()))
{
return;
for unique_ty in unique_tys.iter_mut() {
if let Some(new_ty) = Ty::resolve_empty_unspecified_type_union(
&ty,
&unique_ty.clone().normalize(),
) {
*unique_ty = new_ty;
return;
}
}
unique_tys.push(ty);
};
Expand Down Expand Up @@ -1234,6 +1290,9 @@ pub(crate) enum TyKind {
/// function.
Unknown,

/// Used when a collection is empty, so its element type is not specified.
EmptyUnspecified,

/// Similar to `Unknown`, but not necessarily the result of failed
/// type inference.
Any,
Expand Down Expand Up @@ -2002,10 +2061,11 @@ pub(crate) fn resolve_builtin_type_ref_opt(db: &dyn Db, type_ref: Option<TypeRef
pub(crate) fn assign_tys(db: &dyn Db, source: &Ty, target: &Ty) -> bool {
use Protocol::*;

// Assignments involving "Any", "Unknown", or "Unbound" at the top-level
// Assignments involving "Any", "EmptyUnspecified", "Unknown", or "Unbound" at the top-level
// are always valid to avoid confusion.
match (source.kind(), target.kind()) {
(TyKind::Any | TyKind::Unknown, _) | (_, TyKind::Any | TyKind::Unknown) => true,
(TyKind::Any | TyKind::EmptyUnspecified | TyKind::Unknown, _)
| (_, TyKind::Any | TyKind::EmptyUnspecified | TyKind::Unknown) => true,
(
TyKind::List(source),
TyKind::List(target) | TyKind::Protocol(Iterable(target) | Sequence(target)),
Expand Down
26 changes: 22 additions & 4 deletions crates/starpls_hir/src/typeck/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -403,13 +403,23 @@ impl TyContext<'_> {
Expr::List { exprs } => {
// Determine the full type of the list. If all of the specified elements are of the same type T, then
// we assign the list the type `list[T]`. Otherwise, we assign it the type `list[Unknown]`.
TyKind::List(self.get_common_type(file, exprs.iter().cloned(), self.unknown_ty()))
.intern()
// In the special case where the list is empty, we emit `list[EmptyUnspecified]`, since it may
// be resolved by more clues later.
TyKind::List(self.get_common_type(
file,
exprs.iter().cloned(),
if exprs.is_empty() {
self.empty_unspecified_ty()
} else {
self.unknown_ty()
},
))
.intern()
}
Expr::ListComp { expr, .. } => TyKind::List(self.infer_expr(file, *expr)).intern(),
Expr::Dict { entries } => {
let key_ty = match entries.len() {
0 => Ty::unknown(),
0 => Ty::empty_unspecified(),
len if len > 32 => {
return self.set_expr_type(
file,
Expand All @@ -422,7 +432,11 @@ impl TyContext<'_> {
let value_ty = self.get_common_type(
file,
entries.iter().map(|entry| entry.value),
self.unknown_ty(),
if entries.is_empty() {
self.empty_unspecified_ty()
} else {
self.unknown_ty()
},
);

// Determine the list of known string keys from the entries.
Expand Down Expand Up @@ -2348,6 +2362,10 @@ impl TyContext<'_> {
self.types().unknown.clone()
}

fn empty_unspecified_ty(&self) -> Ty {
self.types().empty_unspecified.clone()
}

fn none_ty(&self) -> Ty {
self.types().none.clone()
}
Expand Down
2 changes: 2 additions & 0 deletions crates/starpls_hir/src/typeck/intrinsics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub(crate) struct IntrinsicTypes {
pub(crate) any: Ty,
pub(crate) unbound: Ty,
pub(crate) unknown: Ty,
pub(crate) empty_unspecified: Ty,
pub(crate) none: Ty,
pub(crate) bool: Ty,
pub(crate) int: Ty,
Expand All @@ -50,6 +51,7 @@ impl Default for IntrinsicTypes {
any: Ty::any(),
unbound: TyKind::Unbound.intern(),
unknown: Ty::unknown(),
empty_unspecified: Ty::empty_unspecified(),
none: TyKind::None.intern(),
bool: Ty::bool(),
int: Ty::int(),
Expand Down
Loading