Project

General

Profile

Bug #16677

Negative integer powered (**) to a float number results in a complex

Added by CamilleDrapier (Camille Drapier) 3 months ago. Updated 29 days ago.

Status:
Closed
Priority:
Normal
Target version:
-
ruby -v:
2.5.7, 2.6.5, 2.7.0
[ruby-core:97390]

Description

Not sure if this is an unexpected behavior.

This works as I expect:

-2 ** 2.2 # => -4.59479341998814

But when I change the code a bit, it gives me a complex:

-2.to_i ** 2.2 # => (3.717265962412589+2.7007518095995273i)

a = -2; a ** 2.2 # => (3.717265962412589+2.7007518095995273i)

This seems to happen only with negative numbers and float powers. I think it might be related to how Fixnum is treated differently from other classes by the power function.


Related issues

Is duplicate of Ruby master - Bug #13152: Numeric parsing differences between ruby <-> crystalRejectedActions

Updated by CamilleDrapier (Camille Drapier) 3 months ago

Oh sorry, I just notice that this is an expected behaviour in the documentation (example) given in Integer.

I guess the to-i attribute assignment is a bit confusing that it changes the behaviour but probably not a bug!

Updated by Eregon (Benoit Daloze) 3 months ago

  • Status changed from Open to Closed

This is just operator precedence, ** has higher precedence than unary minus.

Updated by Dan0042 (Daniel DeLorme) 3 months ago

It's actually a bit more complicated than that.

-2.to_i ** 2.2 #=> (3.717265962412589+2.7007518095995264i)
x = 2
-x.to_i ** 2.2 #=> -4.59479341998814

So it looks like there's something special about how negative integers are parsed? I'm not really sure how to describe the above behavior.

#4

Updated by sawa (Tsuyoshi Sawada) 3 months ago

  • Description updated (diff)
#5

Updated by sawa (Tsuyoshi Sawada) 3 months ago

  • Status changed from Closed to Open
#6

Updated by sawa (Tsuyoshi Sawada) 3 months ago

  • Description updated (diff)

Updated by alanwu (Alan Wu) 3 months ago

So it looks like there's something special about how negative integers are parsed?

Negative integers are atomic tokens whereas the expression -x applies the unary - operator to x.

-x.to_i is parsed as -(x.to_i):

$ ruby --dump=parsetree -e '-x.to_i'
###########################################################
## Do NOT use this node dump for any purpose other than  ##
## debug and research.  Compatibility is not guaranteed. ##
###########################################################

# @ NODE_SCOPE (line: 1, location: (1,0)-(1,7))
# +- nd_tbl: (empty)
# +- nd_args:
# |   (null node)
# +- nd_body:
#     @ NODE_OPCALL (line: 1, location: (1,0)-(1,7))*
#     +- nd_mid: :-@
#     +- nd_recv:
#     |   @ NODE_CALL (line: 1, location: (1,1)-(1,7))
#     |   +- nd_mid: :to_i
#     |   +- nd_recv:
#     |   |   @ NODE_VCALL (line: 1, location: (1,1)-(1,2))
#     |   |   +- nd_mid: :x
#     |   +- nd_args:
#     |       (null node)
#     +- nd_args:
#         (null node)

while -2.to_i is parsed as (-2).to_i. I guess it's the typical "things that look the same are not always the same" thing in programming languages :)
Side note, it's a bit surprising to me that the doc for operator precedence does not mention the method call operator (the dot), but I suppose it's not really an operator?

Updated by Dan0042 (Daniel DeLorme) 3 months ago

Negative integers are atomic tokens

If that was the case then -2 ** 2.2 would be parsed as (-2) ** 2.2, which is not the case as shown above.

Updated by alanwu (Alan Wu) 3 months ago

Ah thanks for catching that.
Interesting, -2 ** 2.2 is parsed as -(2 ** 2.2) whereas -2.to_i ** 2.2 is parsed as ((-2).to_i) ** 2.2.
It looks like whether it is a literal of negative two changes depending on the presence of the method call.
-2 and -2.to_i look so similar on paper!

Updated by Eregon (Benoit Daloze) 3 months ago

It seems rather unexpected that -2 ** 2.2 is parsed as -(2 ** 2.2), I would expect (-2) ** 2.2 as well.

Updated by mrkn (Kenta Murata) 3 months ago

  • Assignee set to matz (Yukihiro Matsumoto)

I also expect (-2) ** 2.2 rather than -(2 ** 2.2).

How you think, matz (Yukihiro Matsumoto)?

Updated by mrkn (Kenta Murata) 3 months ago

I also expect (-2) ** 2.2 rather than -(2 ** 2.2).

Sorry, I reversed each of them. I expect the current behavior, that is -(2 ** 2.2), rather than (-2) ** 2.2.
The current interpretation seems to follow the rule we use for writing equations down by our hands.

Updated by Anonymous 3 months ago

As far as I know there is no strictly correct math rule for evaluating -2 ** 2.2 to -(2 ** 2.2) or (-2) ** 2.2. I expect the current behavior -(2 ** 2.2) since I think exponentiation takes higher precedence than unary minus operation.

Updated by Dan0042 (Daniel DeLorme) 3 months ago

In math exponentation is expressed as superscript; there's no exponentation "operator" per se, afaik. So -2² is -(2²) according to mathematical rules, and it feels quite obvious to me. It doesn't feel quite as right when written with an operator though; -2**2 doesn't have that same obviousness, and -2 ** 2 is downright deceptive.

But a quick search in gems shows things like Time.at(-2**63) where it's clearly intended as -(2**63). I think those precedence rules are ok, especially given that most languages work the same way (see table below). But in that case -2.to_i ** 2 should obey expected rules and parse as -(2.to_i ** 2). Although a quick search in gems shows a few things like -28.upto(28) or -5.hash that would break (mostly in tests/specs).

For reference, here's some other languages' precedence rules for exponentation and unary operators: (from high to low precedence)

language exp. note
Ruby ! ~ + ** - quite unique...
Perl ** ! ~ \ + -
Python ** ~ + -
Javascript ! ~ + - ** but -2**2 is a SyntaxError
F# + - **
Excel, Basic + - ^
Lua ^ -
R ^ + -

I kinda like how Javascript does it; just force people to use parentheses! :-)

Updated by mame (Yusuke Endoh) 3 months ago

Dan0042 (Daniel DeLorme) wrote in #note-14:

But a quick search in gems shows things like Time.at(-2**63) where it's clearly intended as -(2**63). I think those precedence rules are ok, especially given that most languages work the same way (see table below). But in that case -2.to_i ** 2 should obey expected rules and parse as -(2.to_i ** 2). Although a quick search in gems shows a few things like -28.upto(28) or -5.hash that would break (mostly in tests/specs).

Very good point. The current behavior is indeed a bit inconsistent, but reasonable. I vote for no change to keep the compatibility.

#16

Updated by shyouhei (Shyouhei Urabe) 3 months ago

  • Is duplicate of Bug #13152: Numeric parsing differences between ruby <-> crystal added

Updated by matz (Yukihiro Matsumoto) 3 months ago

  • Status changed from Open to Closed

I vote for keeping precedence, for compatibility's sake. All other things (e.g. consistency between languages) are trivial.

Matz.

Updated by Dan0042 (Daniel DeLorme) 3 months ago

matz (Yukihiro Matsumoto),
I find your statement a bit confusing. You vote for "keeping precedence" but the entire point of this bug report is that -2.to_i ** 2.2 does not respect the precedence rules. For example -2.to_s results in "-2" rather than frozen "2". So did you mean that we should keep the current inconsistent behavior, or fix it to always follow precedence rules?

Updated by sawa (Tsuyoshi Sawada) 3 months ago

The most confusing part of the current behaviour is that (it superficially looks like) the precedence relation between the three operations (i) -, (ii) typical method call (using a period), and (iii) ** does not follow transitivity, but is rather in a rock-paper-scissors relation.

(a) - has priority over a typical method call: -2.itself # => (-2).itself,
(b) a typical method call has priority over **: 2.itself ** 2 # => (2.itself) ** 2, and yet
(c) ** has priority over -: -2 ** 2 # => -(2 ** 2)

After close examination, we can tell that this is only superficial, and actually not due to precedence relation. (a) is due to the fact that - as a part of a literal works differently from the unary method -@. (b) is due to the fact that 2.(itself ** 2) does not make sense.

What is problematic is that we have to do close examination whenever we get lost using them. (b) is inevitable, and (c) matches our convention in mathematics. What has room of improvement is (a).

Updated by matz (Yukihiro Matsumoto) about 1 month ago

Dan0042 (Daniel DeLorme) To rephrase, I vote for changing nothing, keeping the current behavior. It may be inconsistent but not worth breaking existing code.

sawa (Tsuyoshi Sawada) Are you proposing something new? I couldn't read the concrete behavior proposed.

Matz.

Updated by sawa (Tsuyoshi Sawada) about 1 month ago

matz (Yukihiro Matsumoto) wrote in #note-20:

sawa (Tsuyoshi Sawada) Are you proposing something new? I couldn't read the concrete behavior proposed.

My proposal (which I have suggested not so clearly in my previous comment) is this:

-2.itself # => -(2.itself) (change from current behavior)

From this, it follows that:

-2.to_i ** 2.2 # => -(2.to_i ** 2.2) (change from current behavior)
-2.to_s # => frozen "2" (change from current behavior)

Updated by sawa (Tsuyoshi Sawada) about 1 month ago

As an argument for this proposal, unary operators like -@ and +@ look very similar to splat operators *, **, and & in the sense that they are located at the front-most position of an expression. Since the splat operators have the lowest operator precedence, it is natural for Ruby users to assume that that also applies to unary operators.

Updated by nobu (Nobuyoshi Nakada) about 1 month ago

sawa (Tsuyoshi Sawada) wrote in #note-21:

-2.itself # => -(2.itself) (change from current behavior)
-2.to_i ** 2.2 # => -(2.to_i ** 2.2) (change from current behavior)
-2.to_s # => frozen "2" (change from current behavior)

A space after - means same things.

Updated by Dan0042 (Daniel DeLorme) 30 days ago

nobu (Nobuyoshi Nakada) wrote in #note-23:

A space after - means same things.

Wow! So -2.to_s is different from - 2.to_s !?!?!
I find this really amazing. I'm just not sure if it's amazing in a good or a bad way.

Updated by sawa (Tsuyoshi Sawada) 30 days ago

nobu (Nobuyoshi Nakada) wrote in #note-23:

A space after - means same things.

Thank you for the information. That further strengthens the motivation for the proposal.

Updated by nobu (Nobuyoshi Nakada) 29 days ago

sawa (Tsuyoshi Sawada) wrote in #note-25:

A space after - means same things.

Thank you for the information. That further strengthens the motivation for the proposal.

Really?
It feels counter-motivation to me.

Also available in: Atom PDF