[Amendment] SE-0368: StaticBigInt

The review for an amendment to SE-0368: StaticBigInt begins now and runs through February 13th, 2023.

This amendment formalizes eliminating an unintentional source-breaking change from the original proposal for SE-0368 by removing the prefix operator + on StaticBigInt.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager by email or DM. When contacting the review manager directly, please keep the proposal link at the top of the message and put "SE-0368" in the subject line.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at:

https://github.com/apple/swift-evolution/blob/main/process.md

Thank you,

Holly Borla
Review Manager

14 Likes

Unfortunate but necessary. +1

1 Like

+1

A quick read.

I’ve had to work around the lack of this functionality before, and it would help immensely when writing tests and new functionality for arbitrary precision rationals (as I’ve used them in my app Kalkyl).

I appreciate the simpler solutions put forth and included in the proposal by @xwu

The omission of rational or fractional literals is great. The proposal itself provides great value.

Note that this review is only concerning the removal of the prefix operator + on StaticBigInt (see the last bullet under "alternatives considered' for details); the rest of the proposal has already been accepted.

Note also that this change has no effect on existing code, and minimal effect on new code, since integer literals are usually used in a context where the type is inferred to be something other than StaticBigInt.

5 Likes

It bears mentioning that there is an alternative (among other possible ones) which would preserve the ability to use prefix + with StaticBigInt—indeed, all types expressible by integer literals. It would also fix the shortcoming of the initially proposed solution (namely, that StaticBigInt(+42) wasn't supported even though +42 as StaticBigInt was):

extension ExpressibleByIntegerLiteral {
  @_alwaysEmitIntoClient @_disfavoredOverload @inline(__always)
  public init(_ value: Self) { self = value }

  @_alwaysEmitIntoClient @_disfavoredOverload @inline(__always)
  public static prefix func + (x: Self) -> Self { x }
}

Pros are that it actually works, has no ABI footprint, and makes this a non-issue not just for StaticBigInt but for all types to come that may step into this problem inadvertently if left to attempt implementing prefix func + for themselves.

Cons are that it vends an extension to ExpressibleByIntegerLiteral (albeit a defensible one in that it is semantically something that all integer literal–expressible types can notionally support) so is a bigger change than originally proposed.

The less elaborate choice of just removing the functionality is entirely fine by me, but it bears clarifying for folks whose takeaway from reading the amendment is that it's an "unfortunate but necessary" decision that there are options.

3 Likes

I continue to think that prefix + is of pretty limited utility, but if we want to support it on literals as a way to clarify a positive sign, we should just handle it in the parser the same way we do prefix -.

17 Likes

I will quote Fabian Giesen: "the more I think about it, the more I become convinced that 'we added a purely functional do-nothing operator then discovered it had side effects' is the most computer science thing that could have happened in that situation." We don't need to repeat that experiment without a compelling reason.

16 Likes

I would point out that the side effect which we had to discover the hard way emerges from the design of Swift's type inference and overload resolution rules and merely becomes latent but doesn't go away when we remove the operator.

Because this scenario is the "most computer science thing" ever, if the standard library had never provided prefix + in the first place, we can be assured that someone (many someones) would have tried to write it—and then stepped into the same problem.

The utility, then, of providing prefix + in the standard library is arguably exactly to assure that what happened to us doesn't happen again to others and speaks to the criteria, incidentally, we've reiterated as the bar for inclusion into the standard library: to provide for something commonly attempted that, if not done correctly in the standard library, folks will try to implement themselves but often incorrectly.

So I would differ in that I think there is at least a case to be made for the operator to exist in the standard library. If I were totally assured that an alternative library solution like the above doesn't have any possible source-breaking effects, then I would come down on the side of maintaining the functionality. For me, the sticking point is rather that I'm not totally sure that the solution is benign and don't know that it's even possible to verify that.

2 Likes

I think the best outcome is to remove the prefix + operator for StaticBigInt, with the plan of later adding + prefix parsing to literals.

4 Likes

speaking for myself only, i would not consider prefix + to be useful enough that i would bother reimplementing it in a project if it were removed from the standard library.

2 Likes

This is from the amendment link:

  It was later discovered to be a source-breaking change. For example:

   ```swift
   let a = -7     // inferred as `a: Int`
   let b = +6     // inferred as `b: StaticBigInt`
   let c = a * b

I wonder why "a = -7" is NOT inferred as StaticBigInt (cp with "let b = +6" which is)?

1 Like

I believe this is because, after parsing and during AST construction, negative numeric literals have special treatment; the prefix - is wiped away and the literal itself is flagged as negative, so there's no "operator" involved anymore during type checking:

$ echo '-5' | swiftc -dump-ast -
(source_file "<stdin>"
  (top_level_code_decl range=[<stdin>:1:1 - line:1:2]
    (brace_stmt implicit range=[<stdin>:1:1 - line:1:2]
      (integer_literal_expr type='Int' location=<stdin>:1:1 range=[<stdin>:1:1 - line:1:2] negative value=5 builtin_initializer=Swift.(file).Int.init(_builtinIntegerLiteral:) initializer=**NULL**))))

With unary +, on the other hand, the operator remains as an explicit invocation:

$ echo '+5' | swiftc -dump-ast -
(source_file "<stdin>"
  (top_level_code_decl range=[<stdin>:1:1 - line:1:2]
    (brace_stmt implicit range=[<stdin>:1:1 - line:1:2]
      (prefix_unary_expr type='Int' location=<stdin>:1:1 range=[<stdin>:1:1 - line:1:2] nothrow
        (dot_syntax_call_expr implicit type='(Int) -> Int' location=<stdin>:1:1 range=[<stdin>:1:1 - line:1:1] nothrow
          (declref_expr type='(Int.Type) -> (Int) -> Int' location=<stdin>:1:1 range=[<stdin>:1:1 - line:1:1] decl=Swift.(file).AdditiveArithmetic extension.+ [with (substitution_map generic_signature=<Self where Self : AdditiveArithmetic> (substitution Self -> Int))] function_ref=single)
          (argument_list implicit
            (argument
              (type_expr implicit type='Int.Type' location=<stdin>:1:1 range=[<stdin>:1:1 - line:1:1] typerepr='Int'))
          ))
        (argument_list implicit
          (argument
            (integer_literal_expr type='Int' location=<stdin>:1:2 range=[<stdin>:1:2 - line:1:2] value=5 builtin_initializer=Swift.(file).Int.init(_builtinIntegerLiteral:) initializer=**NULL**))
        )))))
5 Likes

Very interesting, and somewhat unexpected. Example:

struct OnesComplement: ExpressibleByIntegerLiteral {
    init(integerLiteral value: Int) {
        self.value = value
    }
    init(value: Int) {
        self.value = value
    }
    var value: Int
    
    static prefix func - (a: Self) -> Self {
        return Self(value: ~a.value)
    }
}

let one: OnesComplement = 1
print("1 ==", one.value)        // 1 ==  1
let a: OnesComplement = -one
print("-2 ==", a.value)         // -2 == -2
let b: OnesComplement = -(1 as OnesComplement)
print("-2 ==", b.value)         // -2 == -2
let c: OnesComplement = -1
print("-2 ==", c.value)         // -2 == -1  🤔?!
print()
Edit: the workaround is simple of course.
    init(integerLiteral value: Int) {
        self.value = value < 0 ? ~(-value) : value
    }
1 Like

Like Tony said, this is because the prefix - is part of the literal in the Swift grammar, rather than an operator (I don't think that this is documented correctly in the Swift grammar reference, however).

Grammatically, it's a prefix operator whose operand happens to be a literal, and it happens to be treated as a special case in the type checker. (In the implementation, we actually handle this at the parser level, but formally that isn't a real difference.)

3 Likes

The special-casing leads to interesting (read: unexpected) behavior even without contriving any new types:

let x: Float = -0   // +0.0
let y: Float = -(0) // -0.0

Handling + the same way wouldn't have the same issue but, as always with operators and literals, I'm not sure how one would verify that there isn't some corner case where the behavior is undesirable.

4 Likes

:cry:

Could this be changed or is this too deep in Swift DNA now?

My :100: vote would be to treat prefix "-" the same way as any other prefix operator (in other words remove any kind of special / privileged treatment of it).

I'd never seen that particular example, that's cute. Not sure there's a good solution to that; we intentionally don't preserve negative zero as a value in integer literals, so the only thing I can see would be to preferentially interpret construct (certain?) integer literals as floating-point literals when possible.

Among other things, that would make it impossible to write -128 as an initial value for Int8, since 128 is not valid for that type. You'd have to write something like -127-1.

3 Likes

Good catch, you win some, you lose some. I'd probably write var x: Int8 = .min in that case.
What are the other things?

Tangential, but isn't this already a problem in C? My copy of limits.h contains definitions such as

#define INT_MIN     (-2147483647 - 1)

I still think it would be an unfortunate behavior to have in Swift.

Even if it only affected Double, it could still be source-breaking in some edge cases:

var x: Double = 0
var y: Double = -0
let identical = (memcmp(&x, &y, MemoryLayout<Double>.size) == 0)
// true before, false if we change integer literal behavior