Skip to content

WIP: Improve type checking records#141

Closed
gomoripeti wants to merge 5 commits into
josefs:masterfrom
gomoripeti:improve_record_check
Closed

WIP: Improve type checking records#141
gomoripeti wants to merge 5 commits into
josefs:masterfrom
gomoripeti:improve_record_check

Conversation

@gomoripeti
Copy link
Copy Markdown
Collaborator

This is a work in progress. I have two questions:

I started to improve handling undefined records and record fields. But then I started to think how Gradualizer is used. In case it will be used as a parse transform (that takes place after pre-processing but before any other compiler steps) then sure these errors can happen. However if Gradualizer is used as other steps like xref, eunit, dialyzer, they are run after successful compilation. If Gradualizer works from a beam file this is surely that case. So why duplicate checks that the compiler already does. Maybe Gradualizer could just run the erlang linter (after epp) in case it works from a source file and the rest of the code can assume there are no linting errors in the AST. What do you think? (There are eg unexported functions in the should_pass examples which result in compiler warnings but Gradualizer happily checks them. I introduced undefined records but compilation fails, that's how I started to think about this)

It is possible to override/refine the types provided in the record definitions in types. eg:

-record(rec, {f1 :: atom()}).
-spec f(#rec{f1 :: foo}, term()) -> #rec{f1 :: boolean()}.
f(R, T) ->
  R#rec{f1 = is_integer(T)}.

In this case we could make use somehow of the boolean() type. Either type-check is_integer/1 both against atom() (from the record definition) and against boolean() (from the expected type) ie the intersection of atom() and boolean(). Or infer the type of the whole record update and check that against the whole ResTy.
So in general I think there are multiple flows how to verify the extra info in type-inference and type-checking flows.
Also I'm not sure if this syntax is a pure override or field type (in the above case boolean()) must strictly be a subtype of the type from the rec def (ie atom()) and whether this also needs to be checked?

My main intention is to catch undefined record and record field, the
precise type is an "if we can, why not" extra.
It is possible to override/refine the types provided in the record
definitions.

So far Gradualizer only handled types like `#rec{}` - now it can also
handle types like `#rec{f1 :: t()}`. But currently it ignores the
refined field type information.
@Zalastax
Copy link
Copy Markdown
Collaborator

It makes sense to me that we should not need to do the same checks as the compiler already does. With that said, warnings are not errors and Gradualizer should not reject typechecking a module just because there are unexported functions (it should even check unexported functions in my opinion).

Copy link
Copy Markdown
Collaborator

@zuiderkwast zuiderkwast left a comment

Choose a reason for hiding this comment

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

Very nice stuff, especially the precise types.

"if we can, why not"

Well, OK, if it doesn't add complexity.

For milestone 1, I think we should really focus on code that compiles without a warning or error. With this reasoning we can postpone things like variable shadowing because it gives a compiler warning by default.

Have you considered #40 as well? I seems to be related.

Comment thread src/typechecker.erl
type_check_expr(Env, {record_index, Anno, Name, FieldNameAtom}) ->
%% we still fetch record field index even if infer=false
%% to be able to report undefined record or record field.
%% ("we do our best for the customer")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

In other words, we're checking things that we don't need, to be able to report an error that the compiler would also report.

In principle, I think it's the wrong approach, but in this case, I don't see that there might be any performance impact and the complexity of the code is the same either way, so keep it!

Reporting an exact type for record index when inference is on is very good!

-export([f/0, g/0]).
-export([f/0, g/0,
undefined_record/0,
undefined_record_infer/0,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

These are not automatically tested as only one fail per module is enough for should_fail. We should really fix #129.

Comment thread src/typechecker.erl
end.

expect_record_type(Record, {type, _, record, [{atom, _, Name}]}, _TEnv) ->
expect_record_type(Record, {type, _, record, [{atom, _, Name}|_]}, _TEnv) ->
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

In general, please add a TODO comment when adding code to ignore unimplemented stuff.

@gomoripeti
Copy link
Copy Markdown
Collaborator Author

thank you guys for the feedback. I split this into multiple PRs

@gomoripeti
Copy link
Copy Markdown
Collaborator Author

closing this as usable parts are merged in separate PRs. There is work left in refined record types

@gomoripeti gomoripeti closed this Jan 27, 2019
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