Skip to content

Conversation

@eureka-cpu
Copy link
Contributor

@eureka-cpu eureka-cpu commented Jul 15, 2022

Closes #2300
Closes #992
Closes #2467

Currently not all expressions have vertical handling, but they do have horizontal. There could also be more tests, but it may be a good idea to start getting reviews now since this has grown beyond the scope of what it was originally. This also fixes an issue of accidentally adding single line formatting for non-literal structures. There are also some changes to Shape that made multiline formatting easier. I've gone ahead and removed/added features that we weren't/are using.

@eureka-cpu eureka-cpu added forc big this task is hard and will take a while formatter labels Jul 15, 2022
@eureka-cpu eureka-cpu added this to the swayfmt-v2 milestone Jul 15, 2022
@eureka-cpu eureka-cpu self-assigned this Jul 15, 2022
Copy link
Member

@kayagokalp kayagokalp left a comment

Choose a reason for hiding this comment

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

Hey @eureka-cpu, I am taking a quick look and wanted to have a little comment. First of all, this seems really great 🔥, for the tests of comments at #2311, I couldn't add comments with expr test because we didn't have expr formatting back then and the way comment stuff works were duplicating comments next to exprs. With this PR, we should be able to add expr comments as well, so it might be a good idea to add a test_expr_comments with this introduction of expr formatting.

I don't know if you were planning to add such tests but wanted to give a heads-up on that 👍

@eureka-cpu eureka-cpu marked this pull request as ready for review August 8, 2022 23:05
@eureka-cpu eureka-cpu requested a review from sezna August 8, 2022 23:19
@eureka-cpu eureka-cpu added the enhancement New feature or request label Aug 8, 2022
Copy link
Contributor

@mitchmindtree mitchmindtree left a comment

Choose a reason for hiding this comment

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

Looking super clean and readable @eureka-cpu, nice work!

I've left a few nits that would be good to address, but no showstoppers. I'll leave it up to you whether we want to address them in this PR or if we want to handle them in smaller follow-up PRs.


Shape changes

I have a slight hesitation about exposing the formatter's Shape state as done in this PR as I can imagine it might lead to cases where a Format implementation could easily forget to reset the line style or width heuristics at the end of an item's formatting, leading to following items being mysteriously incorrect only on occasion.

We could potentially avoid this with methods on Formatter that scope these temporary shaping changes, e.g.

formatter.with_line_style(new_style, |formatter| {
    // Do some formatting with `new_style`.
});
// Automatically revert to original style after scope exited.

With an API along these lines, a Format implementation could never accidentally leave the Formatter's inner state in some unexpected state.

All that said, this might require some more significant redesign, and we can investigate diving into this in future issues/PRs. Your existing approach appears to be working so happy to land it!

assert(balance_test_contract_balance == 3);
true
}"#;
Copy link
Contributor

Choose a reason for hiding this comment

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

In a future PR it might be useful to split this test up into (or create more) small tests for each kind of expression, as it might make it easier to spot the cause of a failing test in CI when we inevitably break formatting by mistake in the future.

Copy link
Member

Choose a reason for hiding this comment

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

While splitting to smaller tests we could also introduce a commented version of those new tests, just like we have for other types

Just like:

fn test_enum_comments() {

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 test is actually specifically what @mohammadfawaz gave me for formatting method calls, hence why it was used. But I can see that smaller tests would be better, since we may only end up breaking one specific thing like inline method calls.

});

let mut value_pairs_iter = value_pairs.iter().enumerate().peekable();
for (var_index, (type_field, comma_token)) in value_pairs_iter.clone() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Ahh I'm just noticing this line might not be quite what we're after. We call value_pairs_iter.clone() here and then use value_pairs_iter.peek() later on during the loop. As a result, the value_pairs_iter.peek() will only ever return the first value which I think is probably not what we want?

To fix this, we could change this line to:

Suggested change
for (var_index, (type_field, comma_token)) in value_pairs_iter.clone() {
while let Some((var_index, (type_field, comma_token))) = value_pairs_iter.next() {

Using while let allows us to avoid consuming the iterator upon starting iteration, and as a result is often used when working with peekable iterators.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was curious of that, I believe this has been there for some time

});

let mut value_pairs_iter = value_pairs.iter().enumerate().peekable();
for (var_index, (type_field, comma_token)) in value_pairs_iter.clone() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here (w.r.t. using while let instead)

let mut collected_spans = vec![ByteSpan::from(self.register.span())];
if let Some(value) = &self.value_opt {
collected_spans.append(&mut value.leaf_spans());
// TODO: determine if we are allowing comments between `:` and expr
Copy link
Contributor

Choose a reason for hiding this comment

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

Ideally we should support comments everywhere that the parser supports them. Perhaps we should open dedicated issues that we can link to in these TODOs so its easy to keep track.

Copy link
Member

Choose a reason for hiding this comment

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

Currently we are supporting this kind of comments as long as it is parsed.

Example

// This can be parsed and supported
let number: /* this number is for counting */ u64 = 10;

// This cannot be parsed and not supported
let number: // this number is for counting u64 = 10;

Weird thing is I thought I get rid of these comments with #2416 and it seems like we do not have it currently (here). Would it be possible while rebasing this PR, those come back accidentally?

To summarize we should be currently supporting all of those TODOs and after we decided to support every possible place the comments are deleted as they were no longer relevant 🤔

let mut collected_spans = vec![ByteSpan::from(self.register.span())];
if let Some(ty) = &self.ty_opt {
collected_spans.append(&mut ty.leaf_spans());
// TODO: determine if we are allowing comments between `:` and ty
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here

Copy link
Member

Choose a reason for hiding this comment

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

^

write!(formatted_code, "{} ", match_token.span().as_str())?;
value.format(formatted_code, formatter)?;
MatchBranch::open_curly_brace(formatted_code, formatter)?;
let branches = branches.clone().into_inner();
Copy link
Contributor

Choose a reason for hiding this comment

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

I've noticed there are quite a lot of places where we clone() in order to call into_inner(), even though we only need the inner type by reference.

In this case, the Braces type has a get method which allows us to use the existing instance directly rather than cloning the whole structure.

It might be worth checking all our uses of .clone().into_inner() for similar cases to this as we can likely avoid quite a bit of unnecessary allocation.

}

/// Collects various expr field's ByteSpans.
fn visit_expr(expr: &Expr) -> Vec<ByteSpan> {
Copy link
Contributor

Choose a reason for hiding this comment

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

This function might be better named expr_leaf_spans as we are just matching on the expr to collect leaf spans, not really doing visitor traversal or anything like that.


// TODO:
// path, literal, abicast, struct, tuple, parens, block, array, asm, return, if, match, while, func app, index, field projection, tuple field projection, ref, deref, operators, bitwise stuff, comp operators
// logical operators, reassignment
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we open an issue for these and link it 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.

sure! I have taken care of a few of them. I believe we would need all of them as tests before this PR can be approved.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

*at least the horizontal ones

keywords::CommaToken, punctuated::Punctuated, token::PunctKind, StorageField, TypeField,
};
use sway_types::{Ident, Spanned};

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change

In general I think it's good to keep use statements together as it allows rustfmt to automatically order them, but not that important.

Copy link
Member

@kayagokalp kayagokalp left a comment

Choose a reason for hiding this comment

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

Looks great 🚀. Left couple of nits regarding some possible rebasing issue, and expanded @mitchmindtree's great review around the usage of while let.

If you would like to merge this and address changes in a separate PR, just ping me and I can happily approve as they are not blocking issues. I am not approving rn just to give you a chance to look to the reviews 🙌

let mut collected_spans = vec![ByteSpan::from(self.register.span())];
if let Some(value) = &self.value_opt {
collected_spans.append(&mut value.leaf_spans());
// TODO: determine if we are allowing comments between `:` and expr
Copy link
Member

Choose a reason for hiding this comment

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

Currently we are supporting this kind of comments as long as it is parsed.

Example

// This can be parsed and supported
let number: /* this number is for counting */ u64 = 10;

// This cannot be parsed and not supported
let number: // this number is for counting u64 = 10;

Weird thing is I thought I get rid of these comments with #2416 and it seems like we do not have it currently (here). Would it be possible while rebasing this PR, those come back accidentally?

To summarize we should be currently supporting all of those TODOs and after we decided to support every possible place the comments are deleted as they were no longer relevant 🤔

let mut collected_spans = Vec::new();
for instruction in &self.instructions {
collected_spans.append(&mut instruction.leaf_spans());
// TODO: probably we shouldn't allow for comments in between the instruction and comma since it may/will result in build failure after formatting
Copy link
Member

Choose a reason for hiding this comment

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

^

let mut collected_spans = vec![ByteSpan::from(self.register.span())];
if let Some(ty) = &self.ty_opt {
collected_spans.append(&mut ty.leaf_spans());
// TODO: determine if we are allowing comments between `:` and ty
Copy link
Member

Choose a reason for hiding this comment

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

^

assert(balance_test_contract_balance == 3);
true
}"#;
Copy link
Member

Choose a reason for hiding this comment

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

While splitting to smaller tests we could also introduce a commented version of those new tests, just like we have for other types

Just like:

fn test_enum_comments() {

LineStyle::Multiline => {
writeln!(formatted_code)?;
let mut value_pairs_iter = self.value_separator_pairs.iter().peekable();
for (type_field, comma_token) in value_pairs_iter.clone() {
Copy link
Member

Choose a reason for hiding this comment

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

^ Same (w.r.t usage of while let)

match formatter.shape.code_line.line_style {
LineStyle::Normal => {
let value_pairs = &self.value_separator_pairs;
for (type_field, punctuation) in value_pairs.iter() {
Copy link
Member

Choose a reason for hiding this comment

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

I guess #2338 (comment) is also relevant here

LineStyle::Inline => {
write!(formatted_code, " ")?;
let mut value_pairs_iter = self.value_separator_pairs.iter().peekable();
for (type_field, punctuation) in value_pairs_iter.clone() {
Copy link
Member

Choose a reason for hiding this comment

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

^ Same (w.r.t usage of while let)

let mut collected_spans = vec![ByteSpan::from(self.field_name.span())];
if let Some((colon_token, expr)) = &self.expr_opt {
collected_spans.push(ByteSpan::from(colon_token.span()));
// TODO: determine if we are allowing comments between `:` and expr
Copy link
Member

Choose a reason for hiding this comment

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

^ Same (w.r.t old comment (deleted in #2416) and does not exists currently re-spawning)

collected_spans.push(ByteSpan::from(expr.0.span()));

@kayagokalp kayagokalp self-requested a review August 9, 2022 18:25
Copy link
Member

@kayagokalp kayagokalp left a comment

Choose a reason for hiding this comment

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

🚀

@eureka-cpu
Copy link
Contributor Author

Looking super clean and readable @eureka-cpu, nice work!

I've left a few nits that would be good to address, but no showstoppers. I'll leave it up to you whether we want to address them in this PR or if we want to handle them in smaller follow-up PRs.

Shape changes

I have a slight hesitation about exposing the formatter's Shape state as done in this PR as I can imagine it might lead to cases where a Format implementation could easily forget to reset the line style or width heuristics at the end of an item's formatting, leading to following items being mysteriously incorrect only on occasion.

We could potentially avoid this with methods on Formatter that scope these temporary shaping changes, e.g.

formatter.with_line_style(new_style, |formatter| {
    // Do some formatting with `new_style`.
});
// Automatically revert to original style after scope exited.

With an API along these lines, a Format implementation could never accidentally leave the Formatter's inner state in some unexpected state.

All that said, this might require some more significant redesign, and we can investigate diving into this in future issues/PRs. Your existing approach appears to be working so happy to land it!

I'm curious if this way will solve the problem of having nested formats, for instance in a let statement where the inner value is an expr that has another expr within it. We would need a way to continually update the state of the shape to fit its formatting needs. I took this approach with this in mind, but if the way you mentioned also takes this into account I'd rather that!

@eureka-cpu
Copy link
Contributor Author

I will create some issues around these comments and address them in separate PRs, thanks so much!

@eureka-cpu eureka-cpu merged commit 0a4ac5d into master Aug 9, 2022
@eureka-cpu eureka-cpu deleted the eureka-cpu/2300 branch August 9, 2022 19:31
@eureka-cpu
Copy link
Contributor Author

I will create some issues around these comments and address them in separate PRs, thanks so much!

#2493 #2494 #2495 #2496 #2497 #2498 #2499 #2500 #2501

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

big this task is hard and will take a while enhancement New feature or request forc formatter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Only allow single line formatting for structure literals in sfv2 Add expr formatting to sway-fmt-v2 Formatting contract call arguments

4 participants