26
$\begingroup$

Are there benefits to number literal tokens including negative numbers? Or is the string -1234 being parsed as unary negation of positive number literal 1234 more beneficial?

$\endgroup$
2
  • $\begingroup$ I would go for negative literals, for the reasons specified in the answer. $\endgroup$
    – CPlus
    Jul 10, 2023 at 21:47
  • $\begingroup$ @bigyihsuan, Looks like pros and cons either way. What ever choice is made, some folks are unhappy. $\endgroup$ Jul 14, 2023 at 4:47

6 Answers 6

31
$\begingroup$

INT_MIN

In C, negative literals are processed as unary negation on a literal. So, INT_MIN must be defined as -2147483647 - 1 rather than -2147483648.

-0

In Swift, negative literals can be a single token. However, this means -0 as Double and -0.0 are different. -0.0 and -(0 as Double), however, are the same.

However, Swift isn't even consistent about this. When method calls or postfix operators are involved, they bind more tightly than the unary minus, so -0.foo() is parsed as -(0.foo()). As I mentioned in a comment, this means that in some cases, -0.foo(), (-0).foo(), and (-(0)).foo() are all different.

$\endgroup$
6
  • 6
    $\begingroup$ +1 for the INT_MIN edge case, as 2147483648 is not a valid 32-bit signed integer, but -2147483648 should be. Whereas the second part is more about the (poor?) IEEE choice to make 0.0 and -0.0 two different values. $\endgroup$ Jul 11, 2023 at 7:00
  • 4
    $\begingroup$ Signed zero does have some purpose -- if you take a negative value and divide it by a sufficiently large value to completely reduce its mantissa to 0 (due to limited bit precision), you have not lost the fact that it was originally negative. This is occasionally important. $\endgroup$
    – Miral
    Jul 12, 2023 at 4:08
  • 3
    $\begingroup$ Seems like you can avoid the former issue by just allowing the compiler to read (say) bigints, and then have logic that deals with the case where the literal describes a number too large to fit into whatever you're trying to store it in. $\endgroup$ Jul 12, 2023 at 5:09
  • $\begingroup$ This -0 can easily be linted against, though. Nothing simpler than syntactic linters, after all. $\endgroup$ Jul 12, 2023 at 16:08
  • $\begingroup$ The INT_MIN issue may or may not be a problem for all languages, of course. For example, part of my current language design is to have literals and expressions involving them default to an infinite-precision type until they interact with something that has a defined precision, e.g. by being assigned to a variable or used in an expression alongside something that can't be computed at compile time. In such a language, this is no issue at all: const INT_MIN = -(2147483648) works just fine, but just needs a little more memory to compile. OTOH, I do have negative literals.... $\endgroup$
    – occipita
    Jul 15, 2023 at 15:22
17
$\begingroup$

Do both (avoid abiguity or parenthesis)

The overloading of - for negative literals and negation causes ambiguity:W

In mathematics and most programming languages, the rules for the order of operations mean that −52 is equal to −25: Exponentiation binds more strongly than the unary minus, which binds more strongly than multiplication or division. However, in some programming languages (Microsoft Excel in particular), unary operators bind strongest, so in those cases −5^2 is 25, but 0−5^2 is −25.

To avoid this (and to allow adjacent numerical tokens not causing subtraction) a language can use a separate character for negative literals. APL uses ¯ while J uses _.W

$\endgroup$
6
  • 6
    $\begingroup$ one of those times when the solution is worse than the problem! $\endgroup$ Jul 11, 2023 at 8:27
  • $\begingroup$ off, but why would a programming language select an uncommon unicode token that's not present in usual keyboard layouts for such a commonly used sign as the unary minus? Seems to be a nightmare to type it. $\endgroup$
    – Neinstein
    Jul 11, 2023 at 11:53
  • 7
    $\begingroup$ @Neinstein APL is designed to use a custom keyboard/font anyway. See this image search for examples. $\endgroup$
    – KRyan
    Jul 11, 2023 at 13:10
  • $\begingroup$ See also in Javascript: -64.0.toString(16) evaluates to the number -40 vs (-64.0).toString(16) evaluates to the string '-40' $\endgroup$
    – corvus_192
    Jul 13, 2023 at 15:29
  • 1
    $\begingroup$ @corvus_192 I'll do you one worse: in Swift, it's possible to devise a method foo for which -0.foo() , (-0).foo(), and (-(0)).foo() are all different. Proof by example $\endgroup$
    – Bbrk24
    Jul 13, 2023 at 20:05
12
$\begingroup$

The advantage of “unary negated positive literals” means avoiding a special case in your parser. You can treat the - in -1234 the same way that you treat the - in -x or -(x * y + z). It is for this reason that Jack Crenshaw's Let's Build a Compiler chose that approach (as well as rewriting unary -x to binary 0-x).

It does theoretically make the generated machine code slightly less efficient, but a simple constant folding optimization will turn the expression into the desired negative constant.

$\endgroup$
0
10
$\begingroup$

Referential transparency

This is probably the big one. If you have a negation operator rather than negative literals, you can replace -5 with -a (where a is an integer variable) everywhere, and nothing else about the program will change.

This isn't true if -5 is a token.

However...

Interaction with other syntax

To quote from the Haskell 2010 report, section 10.6:

The handling of the prefix negation operator, -, complicates matters only slightly. Recall that prefix negation has the same fixity as infix negation: left-associative with precedence 6. The operator to the left of -, if there is one, must have precedence lower than 6 for the expression to be legal. The negation operator itself may left-associate with operators of the same fixity (e.g. +). So for example -a + b is legal and resolves as (-a) + b, but a + -b is illegal.

The negation operator also interacts with function application in ways that may be non-obvious to those coming from other languages; f -1 doesn't mean f (-1), it means f - 1.

To complicate things even further, Haskell doesn't have a unary plus operator, so f (-1) means negative one passed to the function f, but f (+1) means the operator section (\x -> x + 1) passed to f.

This is not unique to Haskell, but it's made more obvious because of the (otherwise extremely convenient) function call and operator section syntax. Some variants of Prolog have related issues. Now that I think of it, I doubt there are two Prolog implementations which parse the same way.

For comparison, C has 13 levels of operator precedence and many operators which can be both prefix and infix. Most C programmers never seem to find themselves needing to put parentheses around a negative integer, but the same can't be said of the prefix * operator.

$\endgroup$
2
  • 3
    $\begingroup$ Incidentally, GHC has a syntactic extension that avoids the complications you mention (allowing to write e.g. 9 + -8 and map (- 2) [10..20] and abs -pi). Basically, what it does is to treat - as unary negation iff it has an expression to its right without whitespace in between, but not an expression to its left without whitespace in between. Else, it's treated as infix minus operator. ... $\endgroup$ Jul 11, 2023 at 10:58
  • $\begingroup$ ...And if the expression to the right of the unary minus - is a number literal, the whole thing is syntactically treated as a single negative-valued literal, else it becomes a call to negate. $\endgroup$ Jul 11, 2023 at 11:03
5
$\begingroup$

Exponentiation doesn't like negative literals

Languages with an exponentiation operator prefer to parse negative numbers as unary negation, because exponentiation has higher precedence than unary negation: -3^2 should evaluate to -9, not to +9.

$\endgroup$
2
$\begingroup$

If - is a binary operator in your language, then you probably shouldn't make -1 a single token. If you do, then x-1 will be lexed as two tokens ─ an identifier x followed by a literal -1 ─ and then you can't parse it as a binary operator expression like x - 1, unless your parser has a weird special case for treating negative number literals like postfix operators.

Yes, you also need a special case if you want to reject out-of-bounds integer literals as syntax errors while accepting INT_MIN, whose absolute value is out-of-bounds. But that's a considerably easier special case. For example, here's how the Java Language Specification handles it:

The largest decimal literal of type int is 2147483648 (231).

All decimal literals from 0 to 2147483647 may appear anywhere an int literal may appear. The decimal literal 2147483648 may appear only as the operand of the unary minus operator - (§15.15.4).

It is a compile-time error if the decimal literal 2147483648 appears anywhere other than as the operand of the unary minus operator; or if a decimal literal of type int is larger than 2147483648 (231).

There is a similar rule for decimal literals of type long.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .