Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Type check lists and comprehensions #69

Merged
merged 14 commits into from
Jan 16, 2024
Merged

Conversation

mark-koch
Copy link
Collaborator

No description provided.

@mark-koch mark-koch changed the title feat: Add list and comprehension type checking feat: Type check lists and comprehensions Dec 20, 2023
@mark-koch mark-koch requested a review from croyzor January 3, 2024 10:23
node,
)
if exp_ty:
assert var is not None
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the argument to this function should then be exp_ty: tuple[FunctionType, FreeTypeVar] | None

)
if exp_ty:
assert var is not None
exp_ty = cast(FunctionType, exp_ty.substitute({var: ty}))
Copy link
Collaborator

@croyzor croyzor Jan 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think things would be neater if this substitution happened in the caller and there was a comment in this function saying what is assumed

# The rest is checked in a new nested context to ensure that variables don't escape
# their scope
inner_locals = Locals({}, parent_scope=ctx.locals)
inner_ctx = Context(ctx.globals, inner_locals)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be inheriting type variables from ctx as well?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably yes, but we're not tracking them in the Context yet since we don't have polymorphic FuncDefns yet. (This is related to the discussion in the polymorphism PR #61 (comment))

guppy/cfg/bb.py Outdated Show resolved Hide resolved
guppy/checker/core.py Outdated Show resolved Hide resolved
@@ -240,6 +292,22 @@ def visit_Tuple(self, node: ast.Tuple) -> tuple[ast.expr, GuppyType]:
node.elts = [n for n, _ in elems]
return node, TupleType([ty for _, ty in elems])

def visit_List(self, node: ast.List) -> tuple[ast.expr, GuppyType]:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My inclination is to get rid of this and see if we need it. Lists seem like more of a checkable thing anyway

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that morally, lists feel checkable. But in practice, I think it's good to be able to synthesize a type whenever possible. For example, we want to allow users to write

xs = [1, 2, 3]

instead of always having to provide an annotation

xs: list[int] = [1, 2, 3]

But this only works if we can synthesize lists.

Copy link
Collaborator

@croyzor croyzor Jan 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the more involved thing to do is come up with a fresh type variable, T for the list element, and when it's used in a comprehension or function application, there will be a synthesised type A -> B such that you can unify T with A.

But in the absence of that (I think this is a later stage in the polymorphism timeline), synthesising lists works!


def visit_IterNext(self, node: IterNext) -> tuple[ast.expr, GuppyType]:
var = FreeTypeVar.new("Iter", False)
exp_ty = FunctionType([var], TupleType([FreeTypeVar.new("T", True), var]))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really sure why the "T" variable is linear here. It seems like we would maybe want its linearity to be a function of whether it came from a comprehension over a list or a linst?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, should we not make some note that "T" is the element type of "Iter"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really sure why the "T" variable is linear here

Good point, we shouldn't require linearity here. In fact, a user could even write a custom iterator on a linst of qubits that yields classical measurement results in __next__. Or the other way round, a custom iterator on a classical list could return a newly allocated Qubit in __next__. So we cannot infer linearity of the element type from the linearity of the iterator.

Also, should we not make some note that "T" is the element type of "Iter"

You mean storing this as an annotation somewhere? I guess we're already keeping track of it in the synthesized type that is returned

Copy link
Collaborator

@croyzor croyzor Jan 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean storing this as an annotation somewhere? I guess we're already keeping track of it in the synthesized type that is returned

I mean, the function type here seems to be forall Iter. Iter -> forall T. [T, Iter]. It seems odd that there's no recorded relationship between T and Iter, and afaict nothing to say that all of the Ts for a given iterator should be the same (maybe in python they don't need to be??)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that we have a FreeTypeVar (now called ExistentialTypeVar) here, so no quantifiers in the type.

Following the change in f51f0bf, we check that the type of the Iter.__next__ function that the user has written for the type Iter can be unified with Iter -> (?T, Iter). This is the only restriction, so there is indeed no direct relationship between the Iter type and the element type T.

However, there can only be a single __next__ method for each type, so the signature of the associated __next__ function uniquely determines the element type for the iterator. Thus, the returned T should be the same for each iteration.

@ss2165 ss2165 added this to the Release v0.1 milestone Jan 10, 2024
Base automatically changed from lists/build to feat/lists January 16, 2024 12:57
@mark-koch mark-koch merged commit e22fde7 into feat/lists Jan 16, 2024
1 check passed
@mark-koch mark-koch deleted the lists/check branch January 16, 2024 13:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants