-
-
Notifications
You must be signed in to change notification settings - Fork 873
Add suggestions for variables with the same name in imported modules when unknown variable #4810
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
base: main
Are you sure you want to change the base?
Add suggestions for variables with the same name in imported modules when unknown variable #4810
Conversation
e292510
to
53c6c08
Compare
68e6ce4
to
8e1b877
Compare
Hi @realraphrous! Thanks for making us your first open source PR 🎉 Sorry for the delay reviewing this, I have been busy with the v1.12 release. Looking now |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking great! Very nice work, especially for a first contribution.
I've left a bunch of small notes inline.
Could you add tests for all the different paths through this new feature please 🙏
RE naming: You've been using the words identifier and variable here to refer to public constants and functions in other modules. We don't have a meaning for the word identifier, and a variable is a local variable defined with let
, so it's something else. "values" fits generally.
Thanks again! Please un-draft this PR and ping me when you want another review, and do reply to any comments of mine that are unclear or need discussion.
CHANGELOG.md
Outdated
|
||
- The compiler now suggest public variables and functions from imported modules | ||
when the variable in unknown. These variables and functions are suggested | ||
based on name and arity. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, very clear explanation. Will be useful for me when I write the release post too!
Let's say "values" rather than "functions and variables" in this changelog.
compiler-core/src/type_/error.rs
Outdated
type_with_name_in_scope: bool, | ||
/// Filled with the name of imported modules when the module has public value | ||
/// with the same name as this variable | ||
imported_modules_with_same_public_variable_name: Vec<EcoString>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Descriptive, but a little long! Perhaps module_candidates
? possible_modules
? Something like that?
compiler-core/src/error.rs
Outdated
let consider_text = imported_modules_with_same_public_variable_name | ||
.iter() | ||
.fold( | ||
String::from("Consider using one of these variables:\n\n"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They're not really variables as they're definitions in a module. How about this?
Did you mean one of these?
- list.map
- dict.map
I think we tend to use the -
prefix with lists rendered in this module.
compiler-core/src/error.rs
Outdated
// If there are some suggestions about variables in imported modules | ||
// put a "consider" text after the main message | ||
if !imported_modules_with_same_public_variable_name.is_empty() { | ||
let consider_text = imported_modules_with_same_public_variable_name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Little stylistic thing: Can we use a for
loop here rather than fold
here please 🙏
The accumulator is being mutated, so prefer not to use the immutable accumulator passing pattern of fold
.
compiler-core/src/error.rs
Outdated
.fold( | ||
String::from("Consider using one of these variables:\n\n"), | ||
|mut acc, module_name| { | ||
acc.push_str(&wrap_format!(" {module_name}.{name}\n")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not wrap these items please 🙏
compiler-core/src/type_/pipe.rs
Outdated
UntypedExpr::Var { location, name } => self.expr_typer.infer_var( | ||
name, | ||
location, | ||
VarUsage::PipelineCall, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than have a special case for piped functions, let's use arguments.len() + 1
as the arity of the function, as that's by far the most common use of pipes.
compiler-core/src/type_.rs
Outdated
/// This is used to know when a variable is used as a value or as a call: | ||
/// function call, a pipeline or a custom type variant constructor | ||
#[derive(Debug, Copy, Clone, PartialEq, Eq)] | ||
pub enum VarUsage { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name says it's specific to variables, but the definition is a lot more generic! Any expression could could produce a function which is called or not.
Perhaps this is a ValueUsage
?
.iter() | ||
.filter_map(|(module_name, (_, module))| { | ||
module | ||
.get_public_value(name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps this is a get_public_function(name, arity)
?
} | ||
|
||
fn infer_var( | ||
pub(crate) fn infer_var( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not the biggest fan of how this function has become part of the public API. The internal ReferenceRegistration being something that the rest of the code needs to know about is something I'd like to avoid- it's not very clear what it does or how to use it.
Maybe introduce a public infer_called_var
function that takes the arity as an argument, leaving the existing functions with the same API as before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have done that, infer_var
stay private and I have introduced infer_called_var
has a public function.
UntypedExpr::Var { location, name } => { | ||
// Because we are not calling infer / infer_or_error directly, | ||
// we do not warn if there was a previous panic. Check for that | ||
// here. Not perfect, but works. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we not need to do this for infer_field_access
etc below?
What does the "not perfect bit" mean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without this kind of mitigation, the test unreachable_use_after_panic
failed, no warning are emitted:
pub fn wibble(_) { 1 }
pub fn main() {
panic
use <- wibble
1
}
I checked, there is no test for a similar program and, when tested, there is no warn for unreachable code:
// define: pub fn wibble(_) { 1 }
import mylibrary/mymodule
pub fn main() {
panic
use <- mymodule.wibble
1
}
Basically, I wanted to just pass this test, but it seems that there is a problem with use
expression.
Edit: It seems that previous_panics
is set to false in do_infer_call_with_known_fun
before the warning is registered. We have this call path :
infer_use
infer_call
do_infer_call
(depending on expression)
3.1. UntypedExpr::Var ->infer_called_var
3.2. UntypedExpr::FieldAccess ->infer_field_access
3.3. UntypedExpr::Fn ->infer_fn_with_call_context
3.4. Others ->infer
->infer_or_error
do_infer_call_with_known_fun
But, warnings for unreachable code are mainly registered from infer_or_error
. With these commits, I added the case for UntypedExpr::Var
in do_infer_call
and I am not going anymore in infer_or_error
. So I broke one test by not giving warning for the first case as stated above. It also explains why I don't have warnings in the second example.
Honnestly, I don't know where to correct that, there is too much possibilities. Did you have advice for that ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lpil I don't know if I need to ping you for comment, really sorry if not. I does not know anything about Github ...
4d340b6
to
3e2fc4d
Compare
Hi @lpil ! |
When a variable is not in the current scope, an `Error::UnknownVariable` is triggered. However the only suggestions were for variables in the scope with a similar name computed by the "did you mean" algorithm. With this commit, we also suggest public variables / identifiers (such as constants, functions or type variant constructor) in imported modules with the same name. For example with the following code: ```gleam import gleam/io pub fn main() -> Nil { println("Hello, World!") } ``` The suggestions are: ``` ┌─ /path/to/project/src/project.gleam:4:3 │ 4 │ println("Hello, World!") │ ^^^^^^^ The name `println` is not in scope here. Consider using one of these variables: io.println ``` However because we are only checking the name of the variable, we could have wrong suggestions, as in this case: ```gleam import gleam/float import gleam/int pub fn main() -> Nil { to_string(3) } ``` Here, it is clear that we want suggestions on a function named `to_string` and accepting one parameter of type `Int`, but this is not the case: ``` ┌─ /path/to/project/src/project.gleam:5:3 │ 5 │ to_string(3) │ ^^^^^^^^^ The name `to_string` is not in scope here. Consider using one of these implementations: float.to_string int.to_string ```
Before this commit, all functions / constructors were suggested even if they do not have the correct arity. With this change, only functions / constructors with the correct arity are suggested. However, even if the arity is correct the type of arguments can mismatch, resulting in incorrect suggestions. ```gleam // fn to_string(Int) -> String import gleam/int // fn to_string(Float) -> String import gleam/float pub fn main() -> Nil { to_string(1) } error: Unknown variable ┌─ /path/to/project/src/project.gleam:8:3 │ 8 │ to_string(1) │ ^^^^^^^^^ The name `to_string` is not in scope here. Consider using one of these variables: int.to_string float.to_string ```
At this stage, pipeline are not considered as `Call` so previous work on `UntypedExpr::Call` does not apply directly to pipeline. With this change, we handle the pipeline case when the function / constructor is not a `Call`. ```gleam 1 |> to_string ``` When there is a call in the pipeline, it is more delicate to address because something like that: ```gleam 1 |> add(2) ``` Can be desugared into two possibilities and so suggestions are not the same. The function `report_name_error` does not have enough context to make good suggestions: ```gleam add(1, 2) add(2)(1) ``` In this case, we just suggest every functions.
In this example, the name `zoo` exist in the module `wibble` which is imported. The compiler correctly suggest to use `wibble.zoo`.
3e2fc4d
to
aa35b39
Compare
Really sorry @lpil, did not saw that, with force-push, the review become tough ... I'm using Jujutsu and this is so common to just modify prior commit ... |
Rename all terms related to variables: variable / functions, Var, identifiers to value(s) in code, comment and CHANGELOG.md Rename the new field in `Error::UnknownVariable` from `imported_modules_with_same_public_variable_name` to `possible_modules`. The field name was too long. Change the fold pattern with immutable accumulator to a for loop mutating a variable. Rewrite the compiler message to a "Did you mean" sentence.
…ression. With previous changes, a test was broken when a panic was right before a use expression. The test was like that: ```gleam pub fn wibble(_) { 1 } pub fn main() { panic use <- wibble 1 } ``` This was the case because, before these changes, `UntypedExpr::Var` has not special call path in `do_infer_call` function. `infer` and `infer_or_error` was called has expected and a warning was emitted by `warn_for_unreachable_code` in case of a previous panic expression. Actually, `UntypedExpr::FieldAccess` has a special call path in `do_infer_call` and compiler does not warned about a program like that: ```gleam // Define pub fn wibble(_) { 1 } import mylib/mymod pub fn main() { panic use <- mymod.wibble 1 } ``` With this change, use expression are always warned after a panic expression.
aa35b39
to
e51319c
Compare
Looks like the build is failing! |
This is failling cause of a Deno fetching error : |
9edc508
to
e51319c
Compare
9edc508
to
0ea266c
Compare
@lpil, I amended the last commit without any edit just to relaunch the pipeline. This was a Deno fetching issue for sure, now everything is OK ! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good! I've left a couple more notes inline, but this can probably be merged soon.
CHANGELOG.md
Outdated
The name `println` is not in scope here. | ||
Consider using one of these variables: | ||
io.println |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this entry needs to be updated with the latest wording of the error message
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, had not spotted that !
compiler-core/src/error.rs
Outdated
did_you_mean_text.push_str(&format!(" - {module_name}.{name}\n")); | ||
} | ||
text.push('\n'); | ||
text.push_str(&did_you_mean_text); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason you are building did_you_mean_text
then pushing to text
rather than just appending directly to text
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No reason at all. I will change that to push to the current text
variable.
1 | ||
} | ||
" | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we get tests for:
- Modules imported using an alias (
import some_module as some_alias
) - Modules which have multiple segments (
import some/module
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ofc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great to me! Louis still needs to give it another look over before it's merged.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Super nice! The approach in the code looks flawless to me!
Couple tiny notes inline RE documentation and extracting a helper, and could you add these final tests please:
- Internal values from the same package are suggested
- Internal values from a different package are not suggested
(fun, arguments, type_) | ||
} | ||
|
||
pub fn infer_called_var( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add a detailed documentation comment explaining what this function doe and when it is to be used please 🙏
ValueUsage::Other => self | ||
.environment | ||
.imported_modules | ||
.iter() | ||
.filter_map(|(module_name, (_, module))| { | ||
module.get_public_value(name).map(|_| module_name) | ||
}) | ||
.cloned() | ||
.collect_vec(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This block of code is repeated a few times. Could you extract it and the block above to a helper function or method please.
Looks like one of the places this block exists is in the environment class, so maybe that's a good place for it.
/// This is used to know when a value is used as a call or not. | ||
#[derive(Debug, Copy, Clone, PartialEq, Eq)] | ||
pub enum ValueUsage { | ||
/// Used as `call(..)`, `Type(..)`, `left |> right` or `left |> right(..)` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Used as `call(..)`, `Type(..)`, `left |> right` or `left |> right(..)` | |
/// Used as `function(..)`, `Record(..)`, `left |> right` or `left |> right(..)` |
It's not a type, it's value, specifically a record.
} | ||
} | ||
|
||
pub fn get_public_function(&self, name: &str, arity: usize) -> Option<&ValueConstructor> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Document this function please 🙏
Does this return internal values too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this function also return internal values. This is based on the function get_public_value
that check for importable values, and importable values can be public and internal. I named this function like that to be consistent with other function of the same kind.
Also gonna document this function but get_public_value
, on which this function is based, is not documented ...
closes #2697
As stated on this Discord message, this is my first contributions to OSS and this project. I would really appreciate any feedback and improvement on my code, this pull request or commits that I made !
Summary
This PR add more suggestions when an unknown variable is encounter by listing all variables / identifier in imported modules that match the same name. Also, when the variable / identifier is called such as functions, pipeline steps or type constructors, the suggestions are based on the name and number of arguments provided to the call. (ref: #2697 (comment))
Examples
For example, take this invalid program:
The error message reported by the compiler:
Also, it works with variables, type constructor, constant etc. Consider this custom module and this program:
With these changes we have new suggestions about variables / identifier in imported modules:
Implementations
The current implementation uses a new enumeration
VarUsage
. This is similar to the enumFieldAccessUsage
and it helps the functionreport_name_error
by giving a little more context about the usage of this variable: a function call or something else. This function can now report about imported modules containing a variable with the same name as the unknown variable and, in case of function call, the same arity.I tried to experiment another implementation where the function
report_name_error
was not provided with more context. The function was keeped as simple as possible by reporting imported modules containing a variable with the same name, without checking if it was a call or something else. Instead, we were filtering at the call side, insideinfer_call
function for example, if the variable from the imported module was of type "function-like".Honestly it does not feel right. We needed to check if an
UnknownVariable
error was emitted, apply a filtering on imported modules that were reported and re-emit the same error with the filter applied. Also, we had to perform this manipulation at every location where a function call was possible, which likely implies code duplication, errors and omissions.Shortcomings
Wording
First thing first, the wording. I am not sure about the error message wording:
Consider using one of these variables
For many developers,
io.println
is a function anduri.empty
is a constant, not really variables. I wanted to make the error message like:Consider using on of these identifiers
But the term
identifiers
is not used in the compiler, in error messages that I found or the language tour so it does not feel right. As I can see in the compiler,Var
refer to what I callidentifier
so I thought it would be best to writevariables
.No type checking on arguments
Because we only check name and arity in the case of calls, the suggestions may be wrong because the type of the function parameters does not match the type of arguments provided with the call. If we want that, we need to give more context, such as arguments types, to the
report_name_error
function in order to find the right functions to suggest.Suggestions for pipeline calls like
1 |> add(2)
Also, I had a problem with function call on pipeline. To better explain the situation, take this program:
It can be desugared in two ways:
I didn't know how to implement this case. For sure, the compiler can report function with arity of 2 and a function with arity 1 that has a return type that is also a function with arity 1. I just didn't know what was the right choice ...
Tests
To conclude, theses commits does not add tests for now, it might be cool to have some !
Ending
Thank you in advance for reading and helping me on my journey into OSS. Please feel free to give me feedback on all this work! 💛