Bug - Fixed "new" is weeeeeiiird with floats and ints

fredg1

Member
little refresher on how "new" works/what it is/is for: https://wiki.kolmafia.us/index.php?title=New

now, what's the issue? Let's start with mafia's "normal" behavior:
Code:
> ash int x; x=3.5; return x

Returned: 3
and
Code:
> ash float x; x=3; return x

Returned: 3.0

Everything's fine here. You send a float when an int is expected? the decimal part is cropped. You send an int when a float is expected? it adds ".0" to it.

So, what's the issue? Well, as you may know, "new" assigns the parameters in the order they are place IN THE RECORD. This means that
Code:
record example { string str; location loc;} a_record_that_WILL_see_the_light_of_day = new example("aaa", $location[the dire warren]);
will return:
Returned: record example
str => aaa
loc => The Dire Warren

BUT
Code:
record example { string str; location loc;} a_record_that_will_NEVER_see_the_light_of_day = new example($location[the dire warren], "aaa");
will return:
string found when location expected for field #2 (loc) ()
Returned: void



As you can see, it doesn't "look for a match" when assigning, it just matches the parameters in order. If it can't, it aborts.

However, when talking about int <=> float, it doesn't seem to have that "problem" (the "it aborts if it fails")... at all??? (in a bad way; the "there's no way something isn't broken" kind of way)

Code:
record test_record_type {
    int i;
    float flt;
} my_record = new test_record_type(2.5, 10);

return my_record;

returns:
Returned: record test_record_type
i => 2.5
flt => 10


...the integer field got assigned a float; the float field got assigned an integer


Here's a way to showcase this: (these code blocks will be grouped 2 by 2. Between them, only the function at the start (and its call) will be different)

float => int:
Code:
void function_who_wants_a_float(float a_float)
{    print("The value of a_float is: " + a_float);}
//function which expects a float, and prints it

record a_record_containing_only_a_float_field {
    float flt;
} record_name = new a_record_containing_only_a_float_field(53);
//This record's only field (a float) just got filled with an integer... not a big deal, right? should just be turned into 53.0, right?




function_who_wants_a_float( record_name.flt );
return record_name;
returns:
The value of a_float is: 53.0
Returned: record a_record_containing_only_a_float_field
flt => 53


Code:
void function_who_wants_an_integer(int an_integer)
{    print("The value of an_integer is: " + an_integer);}
//function which expects an integer, and prints it

record a_record_containing_only_a_float_field {
    float flt;
} record_name = new a_record_containing_only_a_float_field(53);
//This record's only field (a float) just got filled with an integer... not a big deal, right? should just be turned into 53.0, right?




function_who_wants_an_integer( record_name.flt );
return record_name;
returns:
The value of an_integer is: 53
Returned: record a_record_containing_only_a_float_field
flt => 53



int => float:
Code:
void function_who_wants_an_integer(int an_integer)
{    print("The value of an_integer is: " + an_integer);}
//function which expects an integer, and prints it

record a_record_containing_only_an_integer_field {
    int i;
} record_name = new a_record_containing_only_an_integer_field(41.7);
//This record's only field (an integer) just got filled with a float... It'll be converted to 41, right?




function_who_wants_an_integer( record_name.i );
return record_name;
returns:
The value of an_integer is: 41
Returned: record a_record_containing_only_an_integer_field
i => 41.7


Code:
void function_who_wants_a_float(float a_float)
{    print("The value of a_float is: " + a_float);}
//function which expects a float, and prints it

record a_record_containing_only_an_integer_field {
    int i;
} record_name = new a_record_containing_only_an_integer_field(41.7);
//This record's only field (an integer) just got filled with a float... It'll be converted to 41, right?




function_who_wants_a_float( record_name.i );
return record_name;
returns:
The value of a_float is: 41.7
Returned: record a_record_containing_only_an_integer_field
i => 41.7




Overall, all this would just be a "fun little fact" if it wasn't for memory-related worries: the values are either stored as they should with their datatype, or not.
What this means: if they are stored correctly, the maximum values just changed: for ints, the maximum is no longer 2 147 483 647.
If they are NOT, then this is an issue regarding memory. We're talking about OVERFLOW and UNDERFLOW: a float which became an int may be modified/may modify the previous thing stored in memory, because of the possibly added 16 bits of memory. For an int, it may impact/be impacted by the next, because of the possible 16 bits "added" to account for the decimal part.
 

xKiv

Active member
a float which became an int may be modified/may modify the previous thing stored in memory,

Nope, because that's not how Value [1] is stored internally.
You have (effectively) a pointer at an int, or a pointer at a float, and those are always allocated correctly. It's literally impossible in java to access a value [2] as a wrong type. (you can try assigning incompatible object value into a variable that cannot hold it, but you will get an exception)

[1] with capital V - internal mafia type for storing values of ash variables and intermediate values [2]
[2] in the normal sense

So what this means is that mafia lacks type validation of values assigned when creating a record literal with new TYPE(...). Declared types are seemingly just ... suggestions.
(and, AFAIK, all operations are performed internally based on the real type of the stored Value, not the declared type of the variable)
 

Veracity

Developer
Staff member
(Declared types are seemingly just ... suggestions.)
I have not yet studied your wall of text, but I think this is the explanation.

If the record field is a "int" or "float" or a "string". the "new" constructor will attempt to coerce anything whatsoever into the required type.
The field is an int? 1 => 1, 1.0 => 1, "1"" -=> 1.

I'll have to read your wall of text and try to understand it, but I think this will be the resoulution.

If so, then I will suggest that people writing ASH scripts provide the expected data type to each field in a "new" operator.
If it expects an int, give it an int.
If you give it a string and it parses it into an int, what is the problem?
You have a string and it wants an int and it doesn't coerce it the way you expect?
Then fix your program to actually pass in an int, converted the way you want.
 

gausie

D̰͕̝͚̤̥̙̐̇̑͗̒e͍͔͎͈͔ͥ̉̔̅́̈l̠̪̜͓̲ͧ̍̈́͛v̻̾ͤe͗̃ͥ̐̊ͬp̔͒ͪ
Staff member
This should be the tl;dr, Veracity

Code:
record test_record_type {
    int i;
    float flt;
} my_record = new test_record_type(2.5, 10);

return my_record;

returns:
Returned: record test_record_type
i => 2.5
flt => 10


...the integer field got assigned a float; the float field got assigned an integer

Which is actually somewhat worrying
 

fredg1

Member
I have not yet studied your wall of text, but I think this is the explanation.

If the record field is a "int" or "float" or a "string". the "new" constructor will attempt to coerce anything whatsoever into the required type.
The field is an int? 1 => 1, 1.0 => 1, "1"" -=> 1.

If you give it a string and it parses it into an int, what is the problem?

This actually doesn't happen; the "issue" is ONLY with floats and integers, strings are NOT part of the problem (...or so I thought!!).

for string => int:
Code:
record a_record_containing_only_an_integer_field {
    int i;
} record_name = new a_record_containing_only_an_integer_field("41");
string found when int expected for field #1 (i) ()
Returned: void​

It does not even try to parse the string as an int.

for int => string (which is where it got weird):
Code:
record a_record_containing_only_a_string_field {
    string str;
} record_name = new a_record_containing_only_a_string_field(2);

return record_name.str + record_name.str;
Returned: 22​

So even though I passed an int, it got registered as a string, phew...
Just for the records, here is what happens if it had been an int:
Code:
record a_record_containing_only_an_integer_field {
    int i;
} record_name = new a_record_containing_only_an_integer_field(2);

return record_name.i + record_name.i;
Returned: 4​

So... it's correctly a string! Yaaaaaay...

Just to be sure, let's run another test, this time with a * (multiplication) instead of + (which can be addition or string concatenation). It'll give an error, right?

Code:
record a_record_containing_only_a_string_field {
    string str;
} record_name = new a_record_containing_only_a_string_field(2);

return record_name.str * record_name.str;
Returned: 4​


Wait, what..?
...
...
...
OOOOoooohhh... Silly me, the * operator must have the capacity to silently convert the "2"s, which are strings, into 2s, which are integers, right..?
Code:
return "2" * "2"
Operator '*' applied to string operands ()
Returned: void​

... ... ... Wait what..?
Code:
record a_record_containing_only_a_string_field {
    string str;
} record_name = new a_record_containing_only_a_string_field("2");

return record_name.str * record_name.str;
Operator '*' applied to string operands ()
Returned: void​

... ... ... Wait what..?

Code:
record a_record_containing_only_a_string_field {
    string str;
} record_name = new a_record_containing_only_a_string_field(2);

return record_name.str * record_name.str + record_name.str * record_name.str;
Returned: 44​

... ... ... Wait... what... the... actual... [PROFANITY]!!???


If you pass an int to a record waiting for a string, it passes around as an int, but always triggers string concatenation out of + ?????

HTML:
> ash record a_record_containing_only_a_string_field { string str; } record_name = new a_record_containing_only_a_string_field(2); return 3 + record_name.str * record_name.str;

Returned: 34

> ash record a_record_containing_only_a_string_field { string str; } record_name = new a_record_containing_only_a_string_field(2); return record_name.str * record_name.str + 2;

Returned: 42

> ash record a_record_containing_only_a_string_field { string str; } record_name = new a_record_containing_only_a_string_field(2); return record_name.str * 3 + 2;

Returned: 8

> ash record a_record_containing_only_a_string_field { string str; } record_name = new a_record_containing_only_a_string_field(2); return 3 * record_name.str + 2;

Cannot apply operator * to 3 (int) and record_name[] (string) ()
Returned: void

This. makes. no. sense.
 
Last edited:

xKiv

Active member
This. makes. no. sense.

It makes exactly the sense I already made out of it. If you say new anything(2, 2) then the values you are storing are integers. The declared types are irrelevant.
When you do
Code:
record a_record_containing_only_a_string_field {
    string str;
} record_name = new a_record_containing_only_a_string_field(2);

return record_name.str * record_name.str;

It multiplies the Integer objects that are actually stored there. There is no conversion to string happening anywhere.
It's like any dynamically typed language, even though it looks like it is strongly typed (and usually checks declared types at compile time).
I think.
 

fredg1

Member
First:
I may not know what "dynamically typed" and "strongly typed" languages are, but when you said:
If you say new anything(2, 2) then the values you are storing are integers. The declared types are irrelevant.

It multiplies the Integer objects that are actually stored there. There is no conversion to string happening anywhere.
Doesn't the last part, of the last example that I provided, point towards the opposite?
> ash record a_record_containing_only_a_string_field { string str; } record_name = new a_record_containing_only_a_string_field(2); return 3 * record_name.str + 2;

Cannot apply operator * to 3 (int) and record_name[] (string) ()

Returned: void



Second:
If you say new anything(2, 2) then the values you are storing are integers. The declared types are irrelevant.
When you do
Code:
record a_record_containing_only_a_string_field {
    string str;
} record_name = new a_record_containing_only_a_string_field(2);

return record_name.str * record_name.str;

It multiplies the Integer objects that are actually stored there. There is no conversion to string happening anywhere.

This was not the weird example, though. THIS was:
Code:
record a_record_containing_only_a_string_field {
    [U][B][COLOR="#008000"]string str;[/COLOR][/B][/U]
} record_name = new a_record_containing_only_a_string_field[COLOR="#008000"][U][B](2)[/B][/U][/COLOR];

return record_name.str [COLOR="#008000"][U][B]*[/B][/U][/COLOR] record_name.str [COLOR="#008000"][U][B]+[/B][/U][/COLOR] record_name.str [COLOR="#008000"][U][B]*[/B][/U][/COLOR] record_name.str;

Returned: 44

Here, just like you said, it multiplies the Integer objects that are actually stored there.
2 (Integer) * 2 (Integer) + 2 (Integer) * 2 (Integer)

This gives 4 (Integer) + 4 (Integer)

(Integer) + (Integer) = (Integer). Simple enough, right? So 8 (Integer)?
No, 44 (string).


A few examples/test results:
(Yes, I know, this is/was/will be barely readable... If only you could write stuff in parallel, it would be so much simpler; I'm doing my best with the tools I have...)

All of the examples below start with
Code:
record a_record_containing_only_a_string_field {
    string str;
} record_name = new a_record_containing_only_a_string_field(2);


  • Code:
    return (record_name.str * record_name.str + record_name.str * record_name.str) *2;
    Code:
    return (record_name.str * record_name.str + 1) *2;
    Code:
    return (1 + record_name.str * record_name.str) *2;
    Operator '*' applied to string operands ()
    Returned: void​

    All 3 return this error.

    This proves that the "44" from earlier is really a string. Everything that is inside the parenthesis is Integers, but the value that comes out of the parenthesis, no matter the case, is a string.
    ( 4 (Integer?) + 4 (Integer?) ) * 2 (Integer)
    = "44" (String?) * 2 (Integer)
    = Error

    ( 4 (Integer?) + 1 (Integer) ) * 2 (Integer)
    = "41" (String?) * 2 (Integer)
    = Error

    ( 1 (Integer) + 4 (Integer?) ) * 2 (Integer)
    = "14" (String?) * 2 (Integer)
    = Error
  • Code:
    return record_name.str * 2;
    Returned: 4​

    2 (Integer?) * 2 (Integer)
    = 4 (Integer)
  • Code:
    return 2 * record_name.str;
    Cannot apply operator * to 2 (int) and record_name[] (string) ()
    Returned: void​

    2 (Integer) * 2 (Integer?)
    = Error

???
 

xKiv

Active member
So apparently, it goes through declared types of variables to check what operations are allowed in the first place, but the actual implementations depend on actual types of values.
And there's a special case for string + in Parser.parseExpression that overrides this when the apparent types of both sides are string.

So record_name.str + record_name.str gets a (compile-time) pass because it looks like a string concatenation
and + that looks like a string concatenation is special cased to always do string concatenation

record_name.str * record_name.str with 2 gets a (compile-time) pass because 1) the declared types are the same so neither side needs to be coerced to a different type + 2) it's not any of the special cases like << or & that explicitly check both sides for a specific type + 3) the declared types are the same, which is apparently enough for *
but then it is executed it sees that it is an arithmetic operator, which expects numbers ... and it actually gets numbers so it works
but initialized with "2" instead it fails when executed, because it sees that it is an arithmetic operator, which expects numbers ... but get strings

return 2 * record_name.str; fails at compile time because 2 and record_name.str do not have the same declared type, and cannot be both coerced to a type acceptable by *
 

Veracity

Developer
Staff member
If the record field is a "int" or "float" or a "string". the "new" constructor will attempt to coerce anything whatsoever into the required type.
That is the expected behavior - when we parse the "new" constructor, we see if the expression for a field is coercable (as an "assign") and errors if not.
Unfortunately, the actual record initializer does not do the expected (allowed) coercion.

Revision 20320 will make the "new" record constructor coerce values into fields as if they were assignments.
 
Top