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

[internal] ComparableExpr (f)strings and bytes made invariant under concatenation #13301

Merged
merged 18 commits into from
Sep 25, 2024

Conversation

dylwil3
Copy link
Contributor

@dylwil3 dylwil3 commented Sep 10, 2024

Summary

This PR modifies the construction of ComparableExpr objects from Expr objects in order to ensure that the following equalities hold at the level of comparable AST nodes:

"a" "b" == "ab"
b"a" b"b" == b"ab"
"text" f"{foo} more" "text" f"{bar}" == f"text{foo} moretext{bar}"
  • ExprStringLiteral
  • ExprBytesLiteral
  • ExprFString

Closes #13291

Details
  • ExprStringLiteral: The comparable variant simply stores the concatenated string value using the to_str method
  • ExprBytesLiteral: The comparable variant stores a Cow<'ast, [u8]> which is owned for concatenated bytes expressions and borrowed otherwise.
  • ExprFString: The comparable variant stores the vector of FStringElement's obtained by joining together contiguous sequences of StringLiteral and FString elements. So, for example
"text" f"{foo} more" "text" f"{bar}"

would be interpreted as the vector with components"text", f"{foo}", " moretext", and f"{bar}".

Test Plan

  • Unit tests with hand-crafted AST nodes
  • Integration tests with parsed source code

@dylwil3 dylwil3 changed the title [internal] ComparableExpr (f)strings and bytes made invariant under concatenation [internal] [WIP] ComparableExpr (f)strings and bytes made invariant under concatenation Sep 10, 2024
Copy link
Contributor

github-actions bot commented Sep 11, 2024

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Formatter (stable)

✅ ecosystem check detected no format changes.

Formatter (preview)

✅ ecosystem check detected no format changes.

@dylwil3 dylwil3 changed the title [internal] [WIP] ComparableExpr (f)strings and bytes made invariant under concatenation [internal] ComparableExpr (f)strings and bytes made invariant under concatenation Sep 24, 2024
@dylwil3 dylwil3 marked this pull request as ready for review September 24, 2024 17:09
@zanieb zanieb added the internal An internal refactor or improvement label Sep 24, 2024
Copy link
Member

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

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

This is great and nice work on f-strings. I've a few suggestions on how we can reduce the code for the f-string transformation and I think it would be great if we can reduce the test-boilerplate to improve readability.

Comment on lines 679 to 695
ast::FStringPart::Literal(string_literal) => match &mut literal_accumulator {
None => {
literal_accumulator = Some(Cow::Borrowed(string_literal.as_str()));
}
Some(existing_literal) => match existing_literal {
Cow::Borrowed(s1) => {
let mut s = String::with_capacity(s1.len() + string_literal.len());
s.push_str(s1);
s.push_str(string_literal.as_str());
*existing_literal = Cow::Owned(s);
}
Cow::Owned(ref mut s1) => {
s1.push_str(string_literal.as_str());
}
},
},
ast::FStringPart::FString(fstring) => {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: We can simplify this a bit

Suggested change
ast::FStringPart::Literal(string_literal) => match &mut literal_accumulator {
None => {
literal_accumulator = Some(Cow::Borrowed(string_literal.as_str()));
}
Some(existing_literal) => match existing_literal {
Cow::Borrowed(s1) => {
let mut s = String::with_capacity(s1.len() + string_literal.len());
s.push_str(s1);
s.push_str(string_literal.as_str());
*existing_literal = Cow::Owned(s);
}
Cow::Owned(ref mut s1) => {
s1.push_str(string_literal.as_str());
}
},
},
ast::FStringPart::FString(fstring) => {
ast::FStringPart::Literal(string_literal) => {
literal_accumulator = Some(match literal_accumulator.take() {
None => Cow::Borrowed(string_literal),
Some(existing_literal) => {
let mut s = existing_literal.into_owned();
s.push_str(string_literal);
s.into()
}
});
},
ast::FStringPart::FString(fstring) => {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, but superseded by Collector.

Comment on lines 652 to 662
Some(existing_literal) => match existing_literal {
Cow::Borrowed(s1) => {
let mut s = String::with_capacity(s1.len() + literal.len());
s.push_str(s1);
s.push_str(literal);
*existing_literal = Cow::Owned(s);
}
Cow::Owned(ref mut s1) => {
s1.push_str(literal);
}
},
Copy link
Member

Choose a reason for hiding this comment

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

Nit: This can be simplified to

Suggested change
Some(existing_literal) => match existing_literal {
Cow::Borrowed(s1) => {
let mut s = String::with_capacity(s1.len() + literal.len());
s.push_str(s1);
s.push_str(literal);
*existing_literal = Cow::Owned(s);
}
Cow::Owned(ref mut s1) => {
s1.push_str(literal);
}
},
Some(existing_literal) => existing_literal.to_mut().push_str(&literal.value),

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done (but now inside Collector)

@@ -600,25 +602,115 @@ pub struct ComparableFString<'a> {
elements: Vec<ComparableFStringElement<'a>>,
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
elements: Vec<ComparableFStringElement<'a>>,
elements: Box<[ComparableFStringElement<'a>]>,

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

crates/ruff_python_ast/src/comparable.rs Show resolved Hide resolved
//
// So the idea of the code below is as follows:
// - Iterate over parts.
// - When a literal is encountered, push it to a stack.
Copy link
Member

Choose a reason for hiding this comment

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

The word stack here is a bit confusing because the implementation doesn't make use of a stack. It uses a single "slot" storing the last string literal.

I think I'm good with the above comment because the most important informatin that I wasn't aware of is that adjacent string literals need to be concatenated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reworded a bit.


use crate as ast;
#[test]
fn compare_concatenated_string_to_value() {
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for taking the time to write all those tests.

I don't have a great suggestion but I wonder if we should make them integration tests instead, because they're very verbose and are difficult to read. E.g. there's so much boilerplate that I find it difficult to pick up the important bits. I also assume that they were very annoying to write. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The integration tests have the same content so I elected to remove these verbose unit tests entirely. They were helpful for development (and for my own understanding of the internals of f-string nodes), but keeping them around is probably more trouble than they're worth!

Copy link
Member

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

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

This is great! Thank you!

@MichaReiser MichaReiser merged commit f27a8b8 into astral-sh:main Sep 25, 2024
20 checks passed
@dylwil3 dylwil3 deleted the comparable branch September 25, 2024 15:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
internal An internal refactor or improvement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[internal] ComparableExpr docstring does not match behavior
3 participants