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?
-
$\begingroup$ I would go for negative literals, for the reasons specified in the answer. $\endgroup$– user16217248Jul 10 at 21:47
-
$\begingroup$ @bigyihsuan, Looks like pros and cons either way. What ever choice is made, some folks are unhappy. $\endgroup$– chux - Reinstate MonicaJul 14 at 4:47
6 Answers
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.
-
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 make0.0
and-0.0
two different values. $\endgroup$ Jul 11 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$– MiralJul 12 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 at 5:09
-
$\begingroup$ This
-0
can easily be linted against, though. Nothing simpler than syntactic linters, after all. $\endgroup$ Jul 12 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$– occipitaJul 15 at 15:22
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, but0−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
-
6$\begingroup$ one of those times when the solution is worse than the problem! $\endgroup$ Jul 11 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$ Jul 11 at 11:53
-
7$\begingroup$ @Neinstein APL is designed to use a custom keyboard/font anyway. See this image search for examples. $\endgroup$– KRyanJul 11 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$ Jul 13 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$– Bbrk24Jul 13 at 20:05
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.
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
, buta + -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.
-
3$\begingroup$ Incidentally, GHC has a syntactic extension that avoids the complications you mention (allowing to write e.g.
9 + -8
andmap (- 2) [10..20]
andabs -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 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 tonegate
. $\endgroup$ Jul 11 at 11:03
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
.
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
is2147483648
(231).All decimal literals from
0
to2147483647
may appear anywhere an int literal may appear. The decimal literal2147483648
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 typeint
is larger than2147483648
(231).
There is a similar rule for decimal literals of type long
.