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

dialyzer: add global variable binding #7717

Open
wants to merge 11 commits into
base: master
Choose a base branch
from

Conversation

kikofernandez
Copy link
Contributor

Make typed argument names and type variables global in their declaration.

Before this PR, the following example would make the linter to complain about singleton variable used:

-foo(X :: integer()) -> X.
foo(X) ->
    X.

So the linter would complain with the following message:

type variable 'X' is only used once (is unbound)

With this pull request, an argument name given to a type becomes bound globally and usable anywhere in the type specification.

Furthermore, bound type annotations cannot receive different types, which was something that we never took care of, and duplicate annotation names are also disallowed.

Example:

-spec id(X) -> X when
    X :: string(),
    X :: integer().   # notice that this was allowed

-spec id(X :: term()) -> X :: term(). # throws error since annotation X is bound multiple times.

This code throws a linter error if such declaration of duplicate names with different types appear.

Make typed argument names and type variables global in their
declaration. Before this PR, the following example would make the linter
to complain about singleton variable used:

```erlang
-foo(X :: integer()) -> X.
foo(X) ->
    X.
```

So the linter would complain with the following message:
```
type variable 'X' is only used once (is unbound)
```

With this pull request, an argument name given to a type becomes bound
globally and usable anywhere in the type specification.

bound type annotations cannot receive different types, which was
something that we never took care of.

Example:

```
-spec id(X) -> X when
    X :: string(),
    X :: integer().   # notice that this was allowed
```

This PR throws a linter error if such declaration of duplicate names
with different types appear. That said, in cases where there are
multiple type annotations with different types, the PR simply performs
type equality of the types (such as nominal typing), and does not try to
infer their structure (structural typing).

This means that the following is an error, even if their internal
structure represent the same thing:

```
-spec id(X :: string()) -> X :: [char()].
```

The main reason for not detecting  this type equivalence is that, if one
really is going  to duplicate the name, then it  should be explicit that
the types assigned to the same type annotation are identical.
@kikofernandez kikofernandez requested a review from jhogberg October 4, 2023 13:16
@kikofernandez kikofernandez changed the title Kiko/erl lint/duplicate annotated variable name/erierl 935/gh 7583/otp 18726 dialyzer: add global variable binding Oct 4, 2023
@kikofernandez kikofernandez linked an issue Oct 4, 2023 that may be closed by this pull request
@github-actions
Copy link
Contributor

github-actions bot commented Oct 4, 2023

CT Test Results

No tests were run for this PR. This is either because the build failed, or the PR is based on a branch without GH actions tests configured.

Results for commit c0275b4

To speed up review, make sure that you have read Contributing to Erlang/OTP and that all checks pass.

See the TESTING and DEVELOPMENT HowTo guides for details about how to run test locally.

Artifacts

// Erlang/OTP Github Action Bot

@kikofernandez kikofernandez self-assigned this Oct 4, 2023
@kikofernandez kikofernandez added team:VM Assigned to OTP team VM team:PS Assigned to OTP team PS bug Issue is reported as a bug types The issue is related to types testing currently being tested, tag is used by OTP internal CI labels Oct 4, 2023
@kikofernandez kikofernandez force-pushed the kiko/erl_lint/duplicate_annotated_variable_name/ERIERL-935/GH-7583/OTP-18726 branch from 90ee1bc to 88a89c8 Compare October 5, 2023 11:24
@kikofernandez kikofernandez removed the testing currently being tested, tag is used by OTP internal CI label Oct 5, 2023
@kikofernandez kikofernandez added testing currently being tested, tag is used by OTP internal CI and removed testing currently being tested, tag is used by OTP internal CI labels Oct 6, 2023
kikofernandez added a commit to kikofernandez/getopt that referenced this pull request Oct 6, 2023
as per OTP pull request
[#7717](erlang/otp#7717), duplicate annotation
types will not be allowed in OTP-27. this commit simply re-writes the
type specification so that it is compliant with the change.
kikofernandez added a commit to kikofernandez/ssl_verify_fun.erl that referenced this pull request Oct 6, 2023
as per OTP pull request
[#7717](erlang/otp#7717), duplicate annotation
types will not be allowed in OTP-27.

this PR simply re-writes the type specification so that it is compliant
with the OTP change.
@kikofernandez
Copy link
Contributor Author

Stalled until a new tag for getopt is created
jcomellas/getopt#54

@IngelaAndin IngelaAndin added the stalled waiting for input by the Erlang/OTP team label Oct 11, 2023
@kikofernandez
Copy link
Contributor Author

Stalled until the following PR is merged
deadtrickster/ssl_verify_fun.erl#30

@ilya-klyuchnikov
Copy link
Contributor

I have a concern that this PR changes the semantics how type specs are interpreted:

Right now - as documentation says - such syntax is used for documentation purposes. - It's not the same as type variables.

https://www.erlang.org/doc/reference_manual/typespec.html#specifications-for-functions

Also, for documentation purposes, argument names can be given:
-spec Function(ArgName1 :: Type1, ..., ArgNameN :: TypeN) -> ReturnType.

And currently for dialyzer (and for eqWAlizer, and I believe for Gradualizer as well) -spec Function(ArgName1 :: Type1, ..., ArgNameN :: TypeN) -> ReturnType. is equivalent to just -spec Function(Type1, ..., TypeN) -> ReturnType.


To me, as a documentation reader, argument names and type variables are different concepts. This PR makes documentation confusing - as the addition to documentation (see below) implies that "argument names" and "type variables" are the same concepts.

Since OTP-27, argument names become global in the type specification. That is, the following examples are all equivalent

@kikofernandez
Copy link
Contributor Author

I am not sure that this PR will go through, due to breaking existing 3rd party code.

That said:

To me, as a documentation reader, argument names and type variables are different concepts. This PR makes documentation confusing - as the addition to documentation (see below) implies that "argument names" and "type variables" are the same concepts.

Type variables and argument names are different. This PR only binds argument names to types, so that one can reuse the name.

Example:

-spec foo(X :: integer(), Y) -> {X, Y}.

Argument X is bound to integer(), so one can assume that the following is its equivalent:

-spec foo(X :: integer(), Y) -> {X :: integer(), Y}.

The type variable Y is still a type variable. Any type variable without a type is a type variable (as always). Argument names are bounded to a type and can be reused just by using their name, without having to duplicate its type. AFAIK this is fixing code where clients assumed that:

-spec foo(X :: integer(), Y) -> {X, Y}.

The first X was bound to integer() and the second X was also bound to integer(), when in reality, the second X is a free type variable (unbound). If you will, this is the same as re-writing the following:

-spec foo(X, Y) -> {X, Y} when X :: integer().

The only difference is that you do not need to use the when to do the binding of X "global" on the scope of the type declaration.

@kikofernandez
Copy link
Contributor Author

kikofernandez commented Dec 7, 2023

This example below has tripped some customers thinking that X :: integer() in any place they used X.

-spec foo(X :: integer(), Y) -> {X, Y}.

which is not true. Here the second Y is a free (type) variable.
Since it does not appear as an input argument, it is illegal code from a standard type system perspective (AFAIK) since it is a singleton type variable happening at the return side.

Does this make sense?

@IngelaAndin IngelaAndin removed the team:PS Assigned to OTP team PS label Dec 19, 2023
@kikofernandez
Copy link
Contributor Author

I will continue stalling this until we know this is correct and dependencies to which I submitted PRs have been merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Issue is reported as a bug stalled waiting for input by the Erlang/OTP team team:VM Assigned to OTP team VM types The issue is related to types
Projects
None yet
Development

Successfully merging this pull request may close these issues.

dialyzer: Duplicate named variables are not detected
3 participants