Skip to content

Conversation

vermont42
Copy link
Contributor

Building on the work of @ahoppen on SR-11580, this PR adds convenience properties to get Double and Int values from FloatLiteralExprSyntax and IntegerLiteralExprSyntax, respectively.

The two new properties, floatingValue and intValue, use failable initializers of Double and Int: init?<S>(_ text: S) where S : StringProtocol and init?<S>(_ text: S, radix: Int) where S : StringProtocol. These initializers fail if the Strings passed in do not represent valid Doubles or Ints. The question arises: what should floatingValue and intValue return, if anything, if initialization fails? I considered the following possibilities, ultimately choosing the fourth.

  1. floatingValue could return Double.nan. This would make sense because, for example, "🍕.🌍" is truly not a number, floating-point or otherwise. The problem with this approach is that there is no corresponding Int.nan, which would prevent floatingValue and intValue from returning the same sort of value given invalid input. Decimal does have a nan value, so if intValue returned a Decimal rather than an Int, that would solve the problem, but I, as a consumer of the API, would find this counter-intuitive because "42" in a source file represents an Int, not a Decimal.

  2. The return types of floatingValue and intValue could be optional. If Double or Int initialization fails, floatingValue and intValue could therefore return nil. I preferred not to impose on API consumers the burden of unwrapping.

  3. The return types of floatingValue and intValue could be Result<Double, LiteralError> and Result<Int, LiteralError>, respectively. If Double or Int initialization fails, floatingValue and intValue could return .failure(.invalid). I preferred not to impose on API consumers the burden of switching on the returned value.

  4. The result of the Double or Int initialization can be force-unwrapped. This causes a crash if initialization fails. Crashing is considered harmful, though not by everyone. Force-unwrapping is the most convenient option for API consumers, which is why I chose it. I observe that force-unwrapping does appear elsewhere in SwiftSyntax, for example here, here, here, and here, and I suspect that the authors of this code made similar judgments about the convenience of force-unwrapping versus the crashing risk it imposes.

Feedback on this code occurred in this PR, which I closed due to a rebase error described here.

@ahoppen
Copy link
Member

ahoppen commented Sep 18, 2020

swiftlang/swift#33948

@swift-ci Please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - 7a450a8

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - 7a450a8

@akyrtzi
Copy link
Contributor

akyrtzi commented Sep 18, 2020

The failures are because the generated files differ:

***************
*** 1080 ****
!   public init?(_ build: (inout FloatLiteralExprSyntaxBuilder) -> Void) {
--- 1080 ----
!   public init(_ build: (inout FloatLiteralExprSyntaxBuilder) -> Void) {

Does the swift repo PR need update?

@ahoppen
Copy link
Member

ahoppen commented Sep 18, 2020

swiftlang/swift#33948

@swift-ci Please test

@vermont42 Could you squash the commits again?

@ahoppen
Copy link
Member

ahoppen commented Sep 18, 2020

swiftlang/swift#33948

@swift-ci Please test

@swiftlang swiftlang deleted a comment from swift-ci Sep 18, 2020
@swiftlang swiftlang deleted a comment from swift-ci Sep 18, 2020
@vermont42
Copy link
Contributor Author

vermont42 commented Sep 18, 2020

@ahoppen I will do so, but I just realized that because I was using a pre-concurrency version of ExprNodes.py, my generated files were incorrect. That is why CI failed recently. I am regenerating with an updated ExprNodes.py.

@ahoppen
Copy link
Member

ahoppen commented Sep 18, 2020

Ah, I see. Let me know when you have regenerated the files and I will trigger CI again.

@vermont42
Copy link
Contributor Author

@akyrtzi That is my conclusion as well. I modified those Swift-repo GYB files repo more than a month ago and didn't notice until this morning that they had changed in upstream. That said, I have verified that my current Swift PR is correct in that it includes the recent concurrency changes.

@ahoppen
Copy link
Member

ahoppen commented Sep 18, 2020

@vermont42 What you need to do, is to rebase your swift PR so that the version in your branch contains both the concurrency changes as well as your changes and then re-generate the files for this PR. If you look at the diff for the generated files in this PR they should contain no concurrency-related changes.

@vermont42
Copy link
Contributor Author

vermont42 commented Sep 18, 2020

swiftlang/swift#33948

@swift-ci Please test

I fixed the generated files to restore the recent concurrency enhancements, and tests passed locally. In the beginning of this comment, I attempt to invoke CI, not knowing whether I have permission to do so. (No prob if I don't.) When CI passes as a result of this invocation or someone else's, I will squash commits.

@akyrtzi
Copy link
Contributor

akyrtzi commented Sep 18, 2020

swiftlang/swift#33948

@swift-ci Please test

Copy link
Member

@rintaro rintaro left a comment

Choose a reason for hiding this comment

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

Looks good to me

@akyrtzi akyrtzi merged commit 94fc5ae into swiftlang:master Sep 19, 2020
$0 != "_"
}

return Int(textWithoutPrefixOrUnderscores, radix: radix)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm sorry to have missed the initial review, but this would crash for valid integer literals that are too large to be represented in Int. Is this intentional? If so, shouldn't this API be intValue and the floating-point API doubleValue and not integerValue/floatingValue (the latter implying that any valid integer or floating-point value would be returned)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

According to the Int initializer's documentation, "if the value [that the String parameter] denotes in the given radix is not representable, the result is nil." I confirmed this by running the following code:

  let goodInt = Int("42", radix: 10)
  let badInt = Int("42424242424242424242424242424242424242424242424242424242424242424242424242", radix: 10)
  print("@@@ \(goodInt) @@@ \(badInt) @@@")

The output was:

@@@ Optional(42) @@@ nil @@@

Copy link
Contributor

@xwu xwu Sep 19, 2020

Choose a reason for hiding this comment

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

Right, so force unwrapping that value will crash. Is this the intended behavior here for a valid integer literal?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A valid IntegerLiteralExprSyntax would not cause a crash.

The only way a crash would occur, via force unwrap, is if a client called integerValue on an invalid IntegerLiteralExprSyntax. However, there are guardrails in place to prevent clients from creating invalid IntegerLiteralExprSyntaxes. Those guardrails, not initially present in the PR, were the primary focus of feedback.

Copy link
Contributor

@xwu xwu Sep 20, 2020

Choose a reason for hiding this comment

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

I might be misunderstanding then. My assumption was that 'IntegerLiteralExprSyntax' can represent any valid integer literal? If so, then calling 'integerValue' on some valid 'IntegerLiteralExprSyntax' values would crash, because not all valid integer literals can be represented as an 'Int'. For example, '42424242424242424242424242424242424242424242424242424242424242424242424242' is a valid integer literal.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

IntegerLiteralExprSyntaxes represent Int literals in source files. A non-representable Int won't compile.

Screen Shot 2020-09-21 at 8 33 20 AM

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 quite accurate; IntegerLiteralExprSyntax is not limited to what can fit into the Int type; it's any sequence of characters that is syntactically a valid integer literal. The error you're showing occurs later on at the semantic analysis phase, but the compiler and SwiftSyntax will still parse it without error. If we try the following command:

echo 'let foo = 42424242424242424242424242424242424242424242424242424242424242424242424242' | \
  swiftc -frontend -emit-syntax -

we'll see this in the output:

...
{
  "id":6,
  "kind":"IntegerLiteralExpr",
    "layout":[{
      "id":5,
      "tokenKind": {
        "kind":"integer_literal",
        "text":"42424242424242424242424242424242424242424242424242424242424242424242424242"
      },

So potentialIntegerValue called on this would return nil, and integerValue would crash.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with @allevato, this "overflows" error is not even coming from typechecking, it's coming from SIL code generation.
Many thanks to @xwu for catching this, I believe we need to make integerValue return an optional.

Copy link
Contributor

@xwu xwu Sep 21, 2020

Choose a reason for hiding this comment

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

Right, integer literals are untyped; they are not “Int literals” and can now be arbitrarily large (previously, they were limited to something like 2048 bits).

The same applies for the floating-point types. They should not return rounded Double values if those values cannot represent the literal exactly.

I would recommend renaming them for clarity as well: the literals are called “integer literals” and “float literals,” so these APIs should be called “integerLiteralValue” and “floatLiteralValue.” Ideally, they would be generic over T: ExpressibleByIntegerLiteral and T: ExpressibleByFloatLiteral so users can get the value represented in any type they want.

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 now agree with @xwu's concern. Accordingly, I intend to create a story in the bug tracker and raise a follow-up PR.

@vermont42
Copy link
Contributor Author

Many thanks to Xiaodi, Rintaro, Harlan, and Αργύριος for their thoughtful review and input on the PR. Extra thanks to Alex for opening the starter bug, creating the initial implementation, providing me the GYB strategy, and answering my questions.

adevress pushed a commit to adevress/swift-syntax that referenced this pull request Jan 14, 2024
…ak_fix

Revert "Fix for newly optional return type of `withDigits`."
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.

7 participants