float in boolean context

9 views
Skip to first unread message

Stephan Weinberger

unread,
Jan 15, 2022, 7:26:58 PM1/15/22
to LDMud Talk
Hallo,

in 3.6.5 there is a new warning:

+ Warn about floats used in a boolean context (#780).


That apparently also encompasses constructs like

    void foo(float bar) {
        bar ||= get_default_value();
        ...
    }


From the commit log i gather:
    Floats historically are always interpreted as true (only the
integer zero
    will be considered false), so even if 0.0 compares equal to (int)0
it is
    not seen as false.

I don't see the problem with '0.0' being 'true' and '0.0 == 0' _also_
being 'true'. There is no reason why "(boolean)0.0 == (boolean)0" must
have the same result as "(float)0.0 == (float)0". On the contrary: I'd
_expect_ them to be different. Why warn about expected behavior?


The only implementation that does not throw a warning now seems to be

    void foo(float bar) {
        if (bar == 0) bar = get_default_value();
        ...
    }

which is objectively worse, as it does not allow to pass '0.0' to the
function (because 0.0 == 0). In other words: There is no other way to
distinguish between 0.0 and 0 than using a boolean context.

"In boolean context, only the special value (int)0 evaluates to false"
is nicely consistent over all data types and a beautifully simple
concept. So why should it generate a warning for floats? The ability for
every type to hold (int)0, which is distinct from the type's own 'empty'
value, is a fundamental feature of LPC. Using this feature should not
generate warnings, especially when there is no other way to detect
'undefined' values (as it is the case with floats!).


In any case: If this change is here to stay, could we at least not use
"#pragma warn_deprecated" but have a separate pragma to activate this check?


cya
  Stephan

Zesstra

unread,
Jan 16, 2022, 5:06:05 AM1/16/22
to ldmud...@googlegroups.com
Hello Stephan,

before discussing deep stuff around this everlasting point of contention in
LPC: why not use one of the following constructs for solving this sort of task?

void foo(float bar=get_default_value());
and
if (intp(0)) bar = get_default_value();
or
if (!floatp(0)) bar = get_default_value();

Bye,
Zesstra
--
MorgenGrauen -
1 Welt, mehr als 200 Programmierer , mehr als 16000 Raeume,
viel mehr als 7000 unterschiedliche Figuren, 90 Quests, 13 Gilden,
ueber 5000 Waffen und Ruestungen, keine Umlaute und ein Haufen Verrueckter.
Existenz: mehr als 25 Jahre
http://mg.mud.de/

Stephan Weinberger

unread,
Jan 16, 2022, 9:14:51 AM1/16/22
to ldmud...@googlegroups.com
Hello Zesstra,


On 16.01.22 11:06, Zesstra wrote:
> Hello Stephan,
>
> before discussing deep stuff around this everlasting point of
> contention in LPC: why not use one of the following constructs for
> solving this sort of task?
>
> void foo(float bar=get_default_value());


Oh, I wasn't aware, that default values are actually "default
expressions" which are evaluated at runtime/per call. Seems like it's
not documented ;-)

That's nice, very nice indeed!

Yes, that would certainly help in most cases (when it's only about
variable initialization).



> and
> if (intp(0)) bar = get_default_value();
> or
> if (!floatp(0)) bar = get_default_value();


I guess you meant "intp(bar)" and "!floatp(bar)", respectively.

I'm not sure if I like that better. Yes it works, but it feels somewhat
strange to test a variable for being an int when it was just declared as
float one line above. I know that (for now?) types are more like
'suggestions' in LPC, and being able to just have (int)0 in any variable
is only possible because of that (i.e. it's unlikely to change anytime
soon).

But my main concern remains: why is float treated differently than the
other data types, which _can_ be used in boolean context without warning?


cya

  Stephan

Gnomi

unread,
Jan 16, 2022, 12:15:25 PM1/16/22
to ldmud...@googlegroups.com
Hi,

Stephan Weinberger wrote:
> But my main concern remains: why is float treated differently than the other
> data types, which _can_ be used in boolean context without warning?

Because float is the only other type that can be compared to integers.

Because float is the only type, which is not false if it equals to zero.

And also because float is the only type that has a different semantic in
a boolean context in LPC than in C.

The baseline here is, that there are ambiguities around floats in a boolean
context that make the current behavior unintuitive, and that is the reason
to treat it specially.

Greetings,
Gnomi.

Stephan Weinberger

unread,
Jan 16, 2022, 10:13:18 PM1/16/22
to ldmud...@googlegroups.com
Hi there!

On 16.01.22 18:15, Gnomi wrote:
> Because float is the only other type that can be compared to integers.

... where that integer it is implicitly cast to float.
So maybe the warning should rather be "warning: implicit cast in
operator ==" when comparing a float to (int)0. Because we all know how a
comparison like that can _really_ go wrong.


> And also because float is the only type that has a different semantic in
> a boolean context in LPC than in C.

In C (int)0 is indistinguishable from (float)0.0 - both have the same
bit pattern (at least for the usual implementation of floating point
numbers). But in LPC those two _are_ different, so different semantics
are to be expected.


> The baseline here is, that there are ambiguities around floats in a boolean
> context that make the current behavior unintuitive, and that is the reason
> to treat it specially.

Is it really so unintuitive? Once you accept that every type can hold
(int)0, (int)0 is distinguishable from (float)0.0, and (int)0 is the
only 'false' value, then this behavior makes perfect sense. I honestly
never had problems with this in my 25+ years of coding in LPC.

Yes, this behavior is different from C. But is it really a good solution
to replace one inconsistency (which only really exists when comparing
LPC with a different language) with another inconsistency (but this time
internal to the LPC type system)?

After all ambiguities like this happen quite often. E.g. in Python any
"empty" value - "", [], {}, () - is considered false in a boolean
context, but "[] == False" is _also_ false (as are "[] == None" and
"None == False", despite 'None' also evaluating to false in a boolean
context). So basically exactly the same inconsistency in reverse there,
because they decided on the fundamental rule that different types can
never be equal. A design decision just like LPC's fundamental rule that
only (int)0 is false.



That said: would it at least be possible to have a separate #pragma
other than 'warn_deprecated' for things like that? E.g. #pragma
warn_ambiguous_context.
Or do you plan to make a larger change to the type system in the future,
thereby actually 'deprecating' the current use of floats in boolean context?


cya
  Stephan

Gnomi

unread,
Jan 17, 2022, 4:12:10 AM1/17/22
to ldmud...@googlegroups.com
Hi,

Stephan Weinberger schrieb:
> On 16.01.22 18:15, Gnomi wrote:
> > Because float is the only other type that can be compared to integers.
>
> ... where that integer it is implicitly cast to float.
> So maybe the warning should rather be "warning: implicit cast in operator
> ==" when comparing a float to (int)0. Because we all know how a comparison
> like that can _really_ go wrong.

This is a little off-topic. But I don't understand what the problem is with
comparing against integers versus comparing against floats:

if (float_val == 0)
if (float_val == 0.0)

What can _really_ go wrong at the first one compared to the second one?
Why whould you warn only there?

> > And also because float is the only type that has a different semantic in
> > a boolean context in LPC than in C.
>
> In C (int)0 is indistinguishable from (float)0.0 - both have the same bit
> pattern (at least for the usual implementation of floating point numbers).

But that's not the reason for the C semantics. In C the compiler does very
well know if it has an int or float on its hand in a boolean context
(as does the LPC runtime). So even if the bit pattern would be different,
the semantics would be the same. Because the C standard mandates, that the
value is considered to be true "if the expression compares unequal to 0"
and we are not talking about bit comparisons. The C compiler really does
change
if (float_val)
to
if (float_val != 0.0)
And there the bit pattern is irrelevant.

> But in LPC those two _are_ different, so different semantics are to be
> expected.
>
> > The baseline here is, that there are ambiguities around floats in a boolean
> > context that make the current behavior unintuitive, and that is the reason
> > to treat it specially.
>
> Is it really so unintuitive? Once you accept that every type can hold
> (int)0, (int)0 is distinguishable from (float)0.0, and (int)0 is the only
> 'false' value, then this behavior makes perfect sense. I honestly never had
> problems with this in my 25+ years of coding in LPC.

An example:

float f;
if (f)
write("True.\n");

What result would you expect? Please try out after guessing.

> Yes, this behavior is different from C. But is it really a good solution to
> replace one inconsistency (which only really exists when comparing LPC with
> a different language) with another inconsistency (but this time internal to
> the LPC type system)?

I don't know of which replacement are you talking about? The warning is for
avoiding this kind of float usage alltogether. Not using one semantics or
another.

> After all ambiguities like this happen quite often. E.g. in Python any
> "empty" value - "", [], {}, () - is considered false in a boolean context,
> but "[] == False" is _also_ false (as are "[] == None" and "None == False",
> despite 'None' also evaluating to false in a boolean context). So basically
> exactly the same inconsistency in reverse there, because they decided on the
> fundamental rule that different types can never be equal. A design decision
> just like LPC's fundamental rule that only (int)0 is false.

The argument is not that the semantics isn't well defined. Python's
semantics is well defined (cast the expression to bool) as well as LPC's
semantics (type is int and value is 0).

The argument is that LPC's semantics is not intuitive (many LPC programmer's
forget the 'type is int' part) and runs against other design decisions in
LPC (see example above), which are hard to change now.

> That said: would it at least be possible to have a separate #pragma other
> than 'warn_deprecated' for things like that? E.g. #pragma
> warn_ambiguous_context.

One of the reason we didn't use an own pragma for it was that it is really
easy to avoid (checking for intp(value) or !floatp(value)). How many
occurrences of this warning do you have?

> Or do you plan to make a larger change to the type system in the future,
> thereby actually 'deprecating' the current use of floats in boolean context?

We can't change it in the near future, so currently we have no plans to
change it. But we don't like the current behavior either. That's why we
are deprecating it.

With best regards,
Gnomi

Stephan Weinberger

unread,
Jan 17, 2022, 2:32:37 PM1/17/22
to ldmud...@googlegroups.com
Howdy-ho,


On 17.01.22 10:12, Gnomi wrote:
> This is a little off-topic. But I don't understand what the problem is with
> comparing against integers versus comparing against floats:
>
> if (float_val == 0)
> if (float_val == 0.0)
>
> What can _really_ go wrong at the first one compared to the second one?
> Why whould you warn only there?

While equality comparisons on floats are generally not a good idea, in
LPC the case of explicitly comparing a float to (int)0 _would_ make
sense, since (int)0 is used as "undefined".

In practice however, unfortunately, "float_val == 0" implicitly casts
the 0 to 0.0, which makes the information about float_val being
"undefined" inaccessible. Thus the comparison does not actually do what
it might look like (that is, for someone aware of the workings of the
LPC type system)!
Additionally you cannot even enforce the desired behavior by writing
"float_val == (int)0", as that throws a "casting value of unknown type"
error. You _have_ to use the intp()/!floatp() method (or until 3.6.4
simply a boolean context) to check for "undefined".
Contrary, with "float_val == 0.0" the coder obviously is *not* trying to
check for (int)0; there the == operator will have no hidden behavior and
works as expected - albeit not necessarily as intended, if float_val is
only 'almost 0'.

So the only case where it *could* work reliably and make sense within
the context of the LPC type system - (int)0 - does *not* work as
expected. Hence a warning would make sense for this specific case; to 1)
point out to the "type-aware coder" that this doesn't work as intended,
and 2) convey to the "casual coder" that they should use the correct
type (just like assigning an integer other than 0 would throw a "Bad
assignment (float vs int)", instead of implicitly converting the type).


But OTOH, since checking floats for equality is a bad idea to begin
with, maybe a warning about *any* equality check on floats would be even
more adequate.


>
>> In C (int)0 is indistinguishable from (float)0.0 - both have the same bit
>> pattern (at least for the usual implementation of floating point numbers).
> But that's not the reason for the C semantics. In C the compiler does very
> well know if it has an int or float on its hand in a boolean context
> (as does the LPC runtime).

In C a float can not hold (int)0 in the first place. "float f = 0" and
"float f = 0.0" produce exactly the same value in C, whereas in LPC - by
design - they don't.

So either you make 0.0 also evaluate to 'false' (like in C), or embrace
being able to have two distinct 'zero' values - which implies different
semantics than in C, where this possibility doesn't exist.


I'm fine with both! My issue is, that the new warning is *neither* of
the two options; it basically warns me about utilizing a well-defined
(and useful) language feature.


>> Is it really so unintuitive? Once you accept that every type can hold
>> (int)0, (int)0 is distinguishable from (float)0.0, and (int)0 is the only
>> 'false' value, then this behavior makes perfect sense. I honestly never had
>> problems with this in my 25+ years of coding in LPC.
> An example:
>
> float f;
> if (f)
> write("True.\n");
>
> What result would you expect? Please try out after guessing.

It will print "True", because floats are initialized to 0.0, not 0 like
every other type. Which actually also is kind of an inconsistency.

But I get your point, you need to be aware of that quirk.


But then again: wouldn't it be better to just make 0.0 evaluate to
'false' as well (like in C), or alternatively change the initialization
to 0 (like with all other types, and also in line with missing varargs)?
Depending on which design philosophy - "like in C" or "hey, we have an
'undefined' value" - you want to follow.

The former shouldn't affect existing code too much, and it would be more
intuitive to the "casual coder". (It may raise the question why empty
strings, arrays or mappings are not 'false' as well - but at least there
would be a clear distinction between numeric and non-numeric types.)
The latter may be more problematic for existing code, because math
operations on a variable currently try to preserve the variable's actual
type instead of "upgrading" it to the declared type (which also might be
a design decision to reconsider). This can lead to unexpected runtime
errors already today - e.g. with missing varargs, where the int-type can
be dragged down into the function body and potentially "survive" math
operations, until it is used (and fails) in a type-aware expression; but
at least in this case the 'varargs' modifier should make you aware of
the issue.


>> That said: would it at least be possible to have a separate #pragma other
>> than 'warn_deprecated' for things like that? E.g. #pragma
>> warn_ambiguous_context.
> One of the reason we didn't use an own pragma for it was that it is really
> easy to avoid (checking for intp(value) or !floatp(value)). How many
> occurrences of this warning do you have?

Quite a lot, as we use this pattern all over the lib. Sure, in most
cases it can now be replaced with the new default values, but in some
instances we also use it for other purposes, like e.g.:

  float f = get_f(); // returns (int)0 if it cannot calculate a valid
result
  f ||= get_f_somehow_else(); // fallback, if get_f() didn't work
  f ||= 1.0; // if we still don't have a valid result

or short:

  float f = get_f() || get_f_shomehow_else() || 1.0;

which I will have to find and replace with the !floatp() method (and not
all of them are quite as obvious).


So, another refactoring-session for the weekend :-)


cya

  Stephan


Gnomi

unread,
Jan 17, 2022, 3:27:57 PM1/17/22
to ldmud...@googlegroups.com
Hi,

Stephan Weinberger wrote:
> But then again: wouldn't it be better to just make 0.0 evaluate to 'false'
> as well (like in C), or alternatively change the initialization to 0 (like
> with all other types, and also in line with missing varargs)? Depending on
> which design philosophy - "like in C" or "hey, we have an 'undefined' value"
> - you want to follow.

I'm open for both changes. Both changes would imho only change the behavior
of (defined/undefined) floats in a boolean context and - of course -
intp()/floatp() checks. (I don't know why the initalization to 0.0 was
introduced, so I guess there would be further consequences, but I fail to
see them.)

So if we would try to change one or both of these things, we should warn
beforehand, that you should not rely on the current behavior of floats in a
boolean context. And that exactly is what the deprecation warning is saying.

> The latter may be more problematic for existing code, because math
> operations on a variable currently try to preserve the variable's actual
> type instead of "upgrading" it to the declared type (which also might be a
> design decision to reconsider).

Could you give code for that?

Arithmetic operations like +=, -=, *= and /= do convert the left hand value
from int to float if the right hand is a float. That's why I don't think
that changing the initialization will do any harm. I would be curious for
examples that say otherwise.

Greetings,
Gnomi

Stephan Weinberger

unread,
Jan 17, 2022, 4:17:16 PM1/17/22
to ldmud...@googlegroups.com

On 17.01.22 21:27, Gnomi wrote:
>
>> The latter may be more problematic for existing code, because math
>> operations on a variable currently try to preserve the variable's actual
>> type instead of "upgrading" it to the declared type (which also might be a
>> design decision to reconsider).
> Could you give code for that?

eg.

float f = 0; // or "varargs void foo(float f) {" called without argument
f++;
f *= time();
f -= 1;
printf("f is %.1f\n", f);


-> (s)printf(): incorrect argument type to %f.


Despite the declaration as float the operators never change the type if
the right hand isn't a float.

Maybe that's part of the reason why floats are initialized to 0.0?

cya
  Stephan

Gnomi

unread,
Jan 17, 2022, 4:35:05 PM1/17/22
to ldmud...@googlegroups.com
Hi,

Stephan Weinberger wrote:
> float f = 0; // or "varargs void foo(float f) {" called without argument
> f++;
> f *= time();
> f -= 1;
> printf("f is %.1f\n", f);
>
> -> (s)printf(): incorrect argument type to %f.
>
> Despite the declaration as float the operators never change the type if the
> right hand isn't a float.
>
> Maybe that's part of the reason why floats are initialized to 0.0?

Maybe. Interesting.

So the problem is not doing floating point arithmetic, but doing integer
arithmethic with floats.

I would argue that the following invariant should hold for a variable of
type 'float' (similar for every type in LPC):
floatp(var) || var == 0

In your example all operations violate that invariant (independent of the
variable's initialization) and should imho be fixed. The initialization
only hides the problem in many cases.

But this would mean that x++ has different meaning whether x is declared as
mixed or float. Is this acceptable or a problem?

Greetings,
Gnomi.

Mika Niinistö

unread,
Jan 17, 2022, 5:39:45 PM1/17/22
to ldmud...@googlegroups.com

Seriously? This is the level of problems ldmud is facing?

 

Lähetetty Windowsin Sähköpostiista

 

Lähettäjä: Stephan Weinberger
Lähetetty: 17 January 2022 21:32
Vastaanottaja: ldmud...@googlegroups.com
Aihe: Re: [ldmud-talk] float in boolean context

--

You received this message because you are subscribed to the Google Groups "LDMud Talk" group.

To unsubscribe from this group and stop receiving emails from it, send an email to ldmud-talk+...@googlegroups.com.

To view this discussion on the web visit https://groups.google.com/d/msgid/ldmud-talk/33867244-acd4-ca8e-a930-950530535a21%40invisible.priv.at.

 

Stephan Weinberger

unread,
Jan 17, 2022, 5:54:33 PM1/17/22
to ldmud...@googlegroups.com


On 17.01.22 23:38, Mika Niinistö wrote:

Seriously? This is the level of problems ldmud is facing?


Well... LPC is a historically grown, weakly typed language, so problems like this are to be expected. Things that seemed like a good idea 30 or 35 years ago (e.g. "who needs types anyway?!") turned out to be not that good...

I really appreciate Gnomi's and Zesstra's efforts to bring some sanity into the madness, and there have already been substantial improvements over the last years; but unfortunately you can't just introduce a modern type system without breaking tons of existing code. Hence it will always be a compromise.

Mika Niinistö

unread,
Jan 17, 2022, 6:04:57 PM1/17/22
to ldmud...@googlegroups.com

My point here is that, this is … something that any coder can work around of. Shouldn’t the community try to focus on something bigger?

 

Lähetetty Windowsin Sähköpostiista

 

Lähettäjä: Stephan Weinberger
Lähetetty: 18 January 2022 00:54
Vastaanottaja: ldmud...@googlegroups.com
Aihe: Re: VS: [ldmud-talk] float in boolean context

--

You received this message because you are subscribed to the Google Groups "LDMud Talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to ldmud-talk+...@googlegroups.com.

Stephan Weinberger

unread,
Jan 17, 2022, 6:20:29 PM1/17/22
to ldmud...@googlegroups.com
On 18.01.22 00:03, Mika Niinistö wrote:

My point here is that, this is … something that any coder can work around of. Shouldn’t the community try to focus on something bigger?


*Should* coders have to "work around" stuff? Or shouldn't we rather try to make things better, more consistent, and more intuitive (which might be conflicting goals!), but with as little breaking of existing code as possible? And wouldn't an objectively better language be "something bigger" as well?

Sure I can "work around" this change (and I will, since Gnomi has convinced me of the "intuitiveness benefits"), but where is the problem in clarifying things (and in the process finding other flaws, that might be worth addressing) _before_ I invest a few days work to get rid of those new warnings?

This is just part of the process of improving things for everybody.

Stephan Weinberger

unread,
Jan 17, 2022, 9:33:49 PM1/17/22
to ldmud...@googlegroups.com
Ho,

On 17.01.22 22:35, Gnomi wrote:
>
> So the problem is not doing floating point arithmetic, but doing integer
> arithmethic with floats.

Well, internally the variable *is* an int (despite the declaration), so
all operators act accordingly.


> I would argue that the following invariant should hold for a variable of
> type 'float' (similar for every type in LPC):
> floatp(var) || var == 0
>
> In your example all operations violate that invariant (independent of the
> variable's initialization) and should imho be fixed. The initialization
> only hides the problem in many cases.

Yes, even with "#pragma rtt_checks" my example still works; the type is
only checked for the assignment operator! (This is actually documented
behavior.)

So if you assign an (int)0, but then only apply other operators with an
int RHS to the variable, you have essentially converted a float to an int.


> But this would mean that x++ has different meaning whether x is declared as
> mixed or float. Is this acceptable or a problem?

Phew... I completely forgot about mixed... :-/

But from a quick test, mixed already seems to handle this as one would
expect, by changing the type accordingly.

e.g.
mixed m = 0; // m is now T_NUMBER
m += 0.0; // m is now T_FLOAT
m += 1; // still T_FLOAT

In fact this is exactly the same behavior as for typed variables, but
for mixed it actually makes sense :-)
I guess internally all variables are stored as mixed, and the type
declarations just add some - but obviously not enough - checks, correct?

For non-mixed, the operators should probably always convert values to
the _declared_ type (or throw an error if that's not possible, of
course), except for the assignment of 0 (which would be passed through
without modification).
Once you start doing math you're very likely no longer interested in the
'undefined' aspect conveyed by (int)0.
Which conversions are considered "possible" would be up for debate (eg.
any->string might be conventient, but is currently prohibited when
enabling rtt_checks).


Longterm a dedicated 'undef' or 'None' value still seems like a good
idea, so that we no longer need to hijack (int)0 for this purpose. I
presume that this would automagically solve many of the inconsistencies,
which seem to mainly arise from the fact that every type can store
(int)0; but I'm also aware that this would be a massive change, and
support for undef == (int)0 would still be required for backwards
compatibility, likely indefinitely; so that would probably introduce
quite a bit of duplication in the driver :-(


regards,

  Stephan

Karl Tiedt

unread,
Jan 17, 2022, 10:15:12 PM1/17/22
to Ldmud Talk
"Longterm a dedicated 'undef' or 'None' value still seems like a good
idea, so that we no longer need to hijack (int)0 for this purpose."

Yes please! I never understood the idea behind not having a safe value for confirming that perhaps no argument was passed in after all.

-Karl Tiedt


--
You received this message because you are subscribed to the Google Groups "LDMud Talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to ldmud-talk+...@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages