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

Add annotation printer #3007

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft

Conversation

omprakaash
Copy link
Contributor

@omprakaash omprakaash commented Apr 18, 2024

Adds an annotation printer class that is to be used for inlay hints/code annotations in the language server. This is similar to the pretty_printer, but also context aware.
Ref: #2525

@omprakaash omprakaash changed the title Add LSP Printer Add annotation printer Apr 20, 2024

impl<'a> AnnotationPrinter<'a> {
pub fn new(
type_aliases: &'a HashMap<(EcoString, EcoString), EcoString>,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would it be better if we make the constructor just take in the module as an argument ? We could then construct the necessary context in the constructor.

Copy link
Member

Choose a reason for hiding this comment

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

Could be good, however I think that would make it a lot harder to write the tests as there would be a great deal more information that would be needed to construct this class.

Copy link
Member

@lpil lpil left a comment

Choose a reason for hiding this comment

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

Wicked! Thank you. I've left a handful of comments inline. Let me know if anything is unclear.

}

pub fn print(&mut self, typ: &Type) -> String {
let mut typ_str = String::new();
Copy link
Member

Choose a reason for hiding this comment

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

We can use an ecostring here!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh yes, will change the buffer to an EcoString.

let key = (module.clone(), name.clone());

if let Some(typ_alias) = self.type_aliases.get(&key) {
// Type has been aliased. eg: import mod1.{type Cat as c} or type UserId = Int
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// Type has been aliased. eg: import mod1.{type Cat as c} or type UserId = Int
// Type has been aliased. eg: import mod1.{type Cat as C} or type UserId = Int

Typo!

Comment on lines 21 to 23
// Mapping of module.type to type alias -
// eg: import mod1.{type Cat as c} -> Key: (mod1, Cat), Value: c
// eg: type UserId = Int -> Key: (mod, UserId), Value: Int
Copy link
Member

Choose a reason for hiding this comment

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

Sorry, bit confused by these. Could you use Rust syntax for the key values and explain with text what each example is showing. Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh the field type_aliases contains mapping of pairs of types and the module in which they are defined to their alias in the current module. (Sorry the second example is actually flipped since the value has to be UserId).

So for example, lets say there is an import statement in a module like so import mod1.{type Tiger as Tig}. This would result in a map entry with the key being ("mod1", "Tiger") and the value being Tig. -> type_aliases.insert(("mod1", "Tiger"), "Tig")

Another example is that of an type alias statement like so in a module named foo: type UserId = Int. Since Int has been aliased by UserId, an entry in the map is inserted, with they key being a pair of (foo, Int) and the value being UserId. (type_aliases.insert(("foo", "Int"), "UserId"). So any occurrences of the above types in a module which are not already annotated by the user, could be annotated with their aliased types.

Copy link
Member

Choose a reason for hiding this comment

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

I think that type alias one is going to break for generics

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm sorry, not quite sure on how this would break generics. Could you give me an example ?
Edit: This field does not deal with generics. The field type_parameters helps deal with them

// eg: type UserId = Int -> Key: (mod, UserId), Value: Int
type_aliases: &'a HashMap<(EcoString, EcoString), EcoString>,

// Mapping of module alias to module name
Copy link
Member

Choose a reason for hiding this comment

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

Could you give an example for this too

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is just used to keep track of module aliases.
eg: import testmod as test
So the key would be "testmod" and the vale "test"

Comment on lines 28 to 101
type_parameters: &'a HashMap<u64, TypeAst>,
current_module: EcoString,
Copy link
Member

Choose a reason for hiding this comment

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

What are these?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sry, type_parameters is probably not a clear name for this. What this does it keeps track of user annotations for generic variables in a function.
eg 1:

fn foo(arg1: gen, arg2){
    arg1 == arg2
}

-> Here the annotation that will be generated for arg2 will be gen

eg2:

fn identity(x) {
    let z = x
    let y: value = x
    y
}

The type annotations generated for x and z will be 'value'.

}
typ_str.push_str(name.as_str());
} else {
// Always qualify types from other modules
Copy link
Member

Choose a reason for hiding this comment

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

If the type has been imported in an unqualified fashion then we want to use the unqualified constructor, not always qualify it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh Yes. Will change this. Might have to keep track of all unqualified imports in the module then.

}
}

fn args_to_string(&mut self, args: &[Arc<Type>]) -> String {
Copy link
Member

Choose a reason for hiding this comment

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

Rather than build an extra string all methods should push to the existing string buffer

}
}

fn name_clashes_if_unqualified(&mut self, type_: &EcoString, module: &str) -> bool {
Copy link
Member

Choose a reason for hiding this comment

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

What does this function mean? I'm not sure what it is doing looking at the previously printed types.

I think it's here to work out if a prelude type has been shadowed by a type defined in this module, but you can't work that out by looking at what has been printed. Rather, we need to know the names of all the types that are in scope in this module, both defined and imported.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep. this is meant to check to check if a prelude type is shadowed. I see, yeah I think this depends on the order of printing being just right to figure out if it is shadowed. Will include info of all types in scope of the module to check for any shadowing.

}
}

pub fn print(&mut self, typ: &Type) -> String {
Copy link
Member

Choose a reason for hiding this comment

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

This method constructs a great many strings- instead let's have it push strs onto the mutable buffer so it only ever allocates one and then grows that as needed.

I'd do this by having a public method that returns a string, and internally it resets an buffer and then calls a private printing function which recursively calls itself and each call will append to the buffer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Gotcha Thank you !

};

assert_eq!(printer.print(&typ), "T");
}
Copy link
Member

Choose a reason for hiding this comment

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

Nice! Let's have tests for all the other possible paths through the type printer. We want full coverage to make sure that we are generating correct code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Gotcha. Will add more tests.

Copy link
Member

Choose a reason for hiding this comment

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

Are you working on these? 🙏

@omprakaash
Copy link
Contributor Author

Hey, I changed the logic for annotation generation based on the earlier review and added some comments as well. I am still a bit unclear on how the current use of type_aliases breaks generics (#3007 (comment)). Will add more test cases as soon that is fixed.

}
}

pub fn print_type(&mut self, typ: &Type) -> String {
Copy link
Member

Choose a reason for hiding this comment

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

We use EcoString, not String. If conversion is needed to interact with some library we should do it at the boundary of the system, not internally

let mut generic_annotations = HashMap::new();

let type_var = TypeAst::Var(crate::ast::TypeAstVar {
name: EcoString::from("T"),
Copy link
Member

Choose a reason for hiding this comment

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

This isn't valid Gleam, it should be lowercase for a variable

};

assert_eq!(printer.print(&typ), "T");
}
Copy link
Member

Choose a reason for hiding this comment

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

Are you working on these? 🙏

@lpil lpil added this to the LS02 milestone May 3, 2024
@omprakaash omprakaash force-pushed the lsp-printer branch 2 times, most recently from 92186e3 to 2e295ee Compare May 4, 2024 15:41
Copy link
Member

@lpil lpil left a comment

Choose a reason for hiding this comment

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

Nice! Once we have all the tests we can merge this I think

package: EcoString::from(""),
};

assert_eq!(printer.print_type(&typ), "Animals.Cat");
Copy link
Member

Choose a reason for hiding this comment

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

This is not valid Gleam. What happened here?

@raquentin
Copy link
Contributor

Is this stable? I can complete #3001 if so

@lpil lpil force-pushed the lsp-printer branch 2 times, most recently from 83747b1 to 07a0cc5 Compare May 24, 2024 20:48
@lpil lpil self-assigned this May 24, 2024
@lpil lpil marked this pull request as draft May 24, 2024 21:19
@lpil
Copy link
Member

lpil commented May 24, 2024

Thank you!! Gunna use this as a dev branch while v1.2.0 is worked through.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Unfinished
Development

Successfully merging this pull request may close these issues.

None yet

3 participants