From 106bcc287ed39bf9be64346fb513db0ac5903578 Mon Sep 17 00:00:00 2001 From: Neil Brown Date: Fri, 24 Jul 2026 11:05:36 +0200 Subject: [PATCH 1/2] Add regression tests for pyi generic-container return types combined with for-loop iteration and cross-function parameter inference Covers list[Actor]/set[Actor]/tuple[Actor, ...] declared in a .pyi stub, iterated in a for-loop, and passed into another function whose parameter type is inferred from that call site - a combination not exercised by the existing completer_pyi_generic_* tests, which only check direct indexing/unpacking. --- .../completer_pyi_generic_list_paraminfer.py | 15 +++++++++++++++ .../completer_pyi_generic_set_paraminfer.py | 15 +++++++++++++++ .../completer_pyi_generic_tuple_paraminfer.py | 15 +++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 tpParser/shared/src/test/programs/completer/completer_pyi_generic_list_paraminfer.py create mode 100644 tpParser/shared/src/test/programs/completer/completer_pyi_generic_set_paraminfer.py create mode 100644 tpParser/shared/src/test/programs/completer/completer_pyi_generic_tuple_paraminfer.py diff --git a/tpParser/shared/src/test/programs/completer/completer_pyi_generic_list_paraminfer.py b/tpParser/shared/src/test/programs/completer/completer_pyi_generic_list_paraminfer.py new file mode 100644 index 0000000..ad222bf --- /dev/null +++ b/tpParser/shared/src/test/programs/completer/completer_pyi_generic_list_paraminfer.py @@ -0,0 +1,15 @@ +# pyi:pyi.generic_list_paraminfer +# class Actor: +# def is_at_edge(self) -> None: ... +# def remove(self) -> None: ... +# def get_x(self) -> None: ... +# def get_actors() -> list[Actor]: ... +# 76 +# get_x;is_at_edge;remove +from pyi.generic_list_paraminfer import * + +def move(particle): + particle. + +for p in get_actors(): + move(p) diff --git a/tpParser/shared/src/test/programs/completer/completer_pyi_generic_set_paraminfer.py b/tpParser/shared/src/test/programs/completer/completer_pyi_generic_set_paraminfer.py new file mode 100644 index 0000000..a3de6b5 --- /dev/null +++ b/tpParser/shared/src/test/programs/completer/completer_pyi_generic_set_paraminfer.py @@ -0,0 +1,15 @@ +# pyi:pyi.generic_set_paraminfer +# class Actor: +# def is_at_edge(self) -> None: ... +# def remove(self) -> None: ... +# def get_x(self) -> None: ... +# def get_actors() -> set[Actor]: ... +# 75 +# get_x;is_at_edge;remove +from pyi.generic_set_paraminfer import * + +def move(particle): + particle. + +for p in get_actors(): + move(p) diff --git a/tpParser/shared/src/test/programs/completer/completer_pyi_generic_tuple_paraminfer.py b/tpParser/shared/src/test/programs/completer/completer_pyi_generic_tuple_paraminfer.py new file mode 100644 index 0000000..57a8d56 --- /dev/null +++ b/tpParser/shared/src/test/programs/completer/completer_pyi_generic_tuple_paraminfer.py @@ -0,0 +1,15 @@ +# pyi:pyi.generic_tuple_paraminfer +# class Actor: +# def is_at_edge(self) -> None: ... +# def remove(self) -> None: ... +# def get_x(self) -> None: ... +# def get_actors() -> tuple[Actor, ...]: ... +# 77 +# get_x;is_at_edge;remove +from pyi.generic_tuple_paraminfer import * + +def move(particle): + particle. + +for p in get_actors(): + move(p) From 0a0e9c330c309b7fb2127141de2e016649dfc0e2 Mon Sep 17 00:00:00 2001 From: Neil Brown Date: Fri, 24 Jul 2026 11:06:34 +0200 Subject: [PATCH 2/2] Fix parameter type annotations losing all member completion A parameter annotation (e.g. `def foo(w: Widget)`, `def foo(items: set[Widget])`) resolved via TypeAstWalker.getType to the raw ClassType/PrimitiveType itself. Real instance members live in getInstanceFields, which only an Instance wrapper exposes - the raw type's own getFields is always empty. Every other place that produces a value of a user's type (constructor calls, function return values, for-loop targets) already wraps it in Instance; parameter annotations were the one gap, silently losing all completions for any annotated parameter. Also fills in two related gaps in annotation parsing that were masking the fix for two of the three container kinds: `set[X]` had no case at all (fell through to ANY_TYPE), and `tuple[X, ...]` (variadic) was misparsed as a fixed 2-arity tuple instead of a VarTupleType - both now mirror the handling already used for .pyi-declared types. --- .../scala/tigerpython/utilities/scopes/AstWalker.scala | 10 +++++++++- .../tigerpython/utilities/types/TypeAstWalker.scala | 6 ++++++ .../completer/completer_annotation_generic_set.py | 9 +++++++++ .../completer_annotation_generic_tuple_ellipsis.py | 9 +++++++++ .../completer/completer_annotation_param_class.py | 8 ++++++++ 5 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 tpParser/shared/src/test/programs/completer/completer_annotation_generic_set.py create mode 100644 tpParser/shared/src/test/programs/completer/completer_annotation_generic_tuple_ellipsis.py create mode 100644 tpParser/shared/src/test/programs/completer/completer_annotation_param_class.py diff --git a/tpParser/shared/src/main/scala/tigerpython/utilities/scopes/AstWalker.scala b/tpParser/shared/src/main/scala/tigerpython/utilities/scopes/AstWalker.scala index bed334f..fbfc6f2 100644 --- a/tpParser/shared/src/main/scala/tigerpython/utilities/scopes/AstWalker.scala +++ b/tpParser/shared/src/main/scala/tigerpython/utilities/scopes/AstWalker.scala @@ -555,7 +555,15 @@ class AstWalker(val scope: Scope) { case AstNode.NameParameter(_, name, annotation) => val dataType = if (annotation != null) - getType(annotation) + // A bare class (or a container built from one, e.g. `list[Widget]`) resolves + // via getType to the raw ClassType/PrimitiveType itself - its real members + // live in getInstanceFields, which only an Instance wrapper exposes (see + // Instance.apply's docs). Every other call-site that produces a value of a + // user's own type (constructor calls, function-call return values, `for` + // targets) already goes through this same wrapping; a param annotation was + // the one place that didn't, silently losing all member completion for any + // annotated parameter. + Instance(getType(annotation)) else if (i >= delta) { getType(params.defaults(i - delta)._1) match { case BuiltinTypes.NONE_TYPE => BuiltinTypes.ANY_TYPE diff --git a/tpParser/shared/src/main/scala/tigerpython/utilities/types/TypeAstWalker.scala b/tpParser/shared/src/main/scala/tigerpython/utilities/types/TypeAstWalker.scala index fc37f51..f8e7920 100644 --- a/tpParser/shared/src/main/scala/tigerpython/utilities/types/TypeAstWalker.scala +++ b/tpParser/shared/src/main/scala/tigerpython/utilities/types/TypeAstWalker.scala @@ -68,8 +68,14 @@ class TypeAstWalker { (getType(subscript.base), subscript.slice) match { case (BuiltinTypes.LIST_TYPE, Index(_, value)) => ListType(getType(value)) + case (BuiltinTypes.SET_TYPE, Index(_, value)) => + SetType(getType(value)) case (BuiltinTypes.DICT_TYPE, MultiSlice(_, Array(Index(_, key), Index(_, value)))) => new DictType(getType(key), getType(value)) + // `tuple[X, ...]`: a homogeneous, variable-length tuple (as opposed to `tuple[X, Y]`, + // a fixed-arity tuple where each position has its own type). + case (BuiltinTypes.TUPLE_TYPE, MultiSlice(_, Array(Index(_, elt), Index(_, _: AstNode.Ellipsis)))) => + new VarTupleType(getType(elt)) case (BuiltinTypes.TUPLE_TYPE, MultiSlice(_, elements)) if elements.forall { case Index(_, _) => true case _ => false diff --git a/tpParser/shared/src/test/programs/completer/completer_annotation_generic_set.py b/tpParser/shared/src/test/programs/completer/completer_annotation_generic_set.py new file mode 100644 index 0000000..ed25d3c --- /dev/null +++ b/tpParser/shared/src/test/programs/completer/completer_annotation_generic_set.py @@ -0,0 +1,9 @@ +# 107 +# draw +class Widget: + def draw(self): + pass + +def foo(items: set[Widget]): + for w in items: + w. diff --git a/tpParser/shared/src/test/programs/completer/completer_annotation_generic_tuple_ellipsis.py b/tpParser/shared/src/test/programs/completer/completer_annotation_generic_tuple_ellipsis.py new file mode 100644 index 0000000..b5a98ce --- /dev/null +++ b/tpParser/shared/src/test/programs/completer/completer_annotation_generic_tuple_ellipsis.py @@ -0,0 +1,9 @@ +# 114 +# draw +class Widget: + def draw(self): + pass + +def foo(items: tuple[Widget, ...]): + for w in items: + w. diff --git a/tpParser/shared/src/test/programs/completer/completer_annotation_param_class.py b/tpParser/shared/src/test/programs/completer/completer_annotation_param_class.py new file mode 100644 index 0000000..405486f --- /dev/null +++ b/tpParser/shared/src/test/programs/completer/completer_annotation_param_class.py @@ -0,0 +1,8 @@ +# 74 +# draw +class Widget: + def draw(self): + pass + +def foo(w: Widget): + w.