Another Arithemetic overflow problem

In the code below you see I explicitly set the shown values to be i64 64-bit values.
However, when it get close to the limit value, I notice some of them switching between i32 and i64 types. How can this happen? I set them to be (and always stay) i64 to explicitly to prevent overflow.

start = Time.monotonic
print("The self-describing numbers are:")
i  = 10i64  # self-describing number must end in 0
pw = 10i64  # power of 10
fd = 1i64   # first digit
sd = 1i64   # second digit
dg = 2i64   # number of digits
mx = 11i64  # maximum for current batch
lim = 9_100_000_001i64 # sum of digits can't be more than 10
while i < lim
  if selfDesc(i)
    secs = (Time.monotonic - start).total_seconds
    print("\n#{i} in #{secs} secs")
  end
  i += 10
  if i > mx
    fd += 1
    sd -= 1
    if sd >= 0
      i = fd * pw
    else
      pw *= 10
      dg += 1
      i  = pw
      fd = 1
      sd = dg - 1
    end
    puts " i = #{i.class}, sd = #{sd.class}, pw = #{pw.class}, mx = #{mx.class}"
    mx = i + sd * pw // 10
  end
end

I inserted that puts statement line before the line: mx = i + sd * pw // 10
because that’s where the error message said the over flow occurred.
Here’s the output just before it fails.

 i = Int32, sd = Int64, pw = Int64, mx = Int32
 i = Int32, sd = Int64, pw = Int64, mx = Int32
 i = Int32, sd = Int64, pw = Int64, mx = Int32
 i = Int32, sd = Int64, pw = Int64, mx = Int32
 i = Int64, sd = Int64, pw = Int64, mx = Int32
 i = Int32, sd = Int64, pw = Int64, mx = Int64

I finally figured out how to “fix” it by making all the constants in that inner loop i64s.
But why should I have to do this? Why do the number types change to lower types?

if i > mx
    fd += 1i64
    sd -= 1i64
    if sd >= 0
      i = fd * pw
    else
      pw *= 10i64
      dg += 1i64
      i  = pw
      fd = 1i64
      sd = dg - 1i64
    end
    #puts " i = #{i.class}, sd = #{sd.class}, pw = #{pw.class}, mx = #{mx.class}"
    mx = i + sd * pw // 10

There’s a fd = 1 in the else branch. That assigns a number of value 1 to fd and since no type is specified, it picks the default type Int32. Just changing this line to fd = 1i64 is necessary and fixes the problem.

Btw. you can add type restrictions to local variables, that would have immediately alerted you about the mismatching type (ideally it could probably even autocast assigned literals to Int64).

How do you do that? Please give examples.

Changing the fd = 1 to fd = i64 did work.
However, what I ended up doing is changing the order of multiplication, shown below, to have pw set the type precedence for the multiplication on that line, so fd = 1 now works.

if i > mx
    fd += 1
    sd -= 1
    if sd >= 0
      i = pw * fd    <- pw always i64, sets * type precedence
      #i = fd * pw
    else
      pw *= 10
      dg += 1
      i  = pw
      fd = 1         <- can remain the same 
      sd = dg - 1
    end
    #puts " i = #{i.class}, sd = #{sd.class}, pw = #{pw.class}, mx = #{mx.class}"
    mx = i + sd * pw // 10
  end

There’s needs to be a clear (and documented) way users can set variable types and know all math operations results will produce values of that type.

I was lucky here because I’ve run into this kind of thing enough now to know what to look to change, but I pity the poor soul who has no idea of what’s going on, let alone how to figure out how to fix it. :rage:

1 Like

NEVER DO THIS

it’s in the documentation under https://crystal-lang.org/reference/syntax_and_semantics/type_grammar.html then “declaring variables”

x = uninitialized Int32

x = 0
puts x

x = "test"
puts x

this produces :

 Showing last frame. Use --error-trace for full trace.

In test.cr:8:1

 8 | x = "test"
     ^
Error: type must be Int32, not (Int32 | String)

not sure if there is a different/better way.

No, please don’t use uninitialized. That’s unsafe!!

The way to declare the type of a variable is the same as you declare the type of anything:

x : Int64 = 1
# do stuff with x

tl;dr: never, ever use uninitialize

1 Like

I see that documentation on type restrictions is missing from the reference page on local variables. We’ll need to add that.

The result type of math operations doesn’t depend on the type restriction of the variable they are assigned to. It only depends on the return type of the methods that implements it. And this is actually well documented. For example the API docs for Int32#*(other : Int64) : Int32. Per convention, binary operators usually return the type of the first operand (see Operators - Crystal).

There’s needs to be a clear (and documented) way users can set variable types and know all math operations results will produce values of that type.

As straigh-shoota said, the type of a op b is always a. I think that’s a pretty easy thing to remember.

I was lucky here because I’ve run into this kind of thing enough now to know what to look to change, but I pity the poor soul who has no idea of what’s going on, let alone how to figure out how to fix it.

We are aware of the problem. Do you have ideas on how to improve the situation?

See also https://github.com/crystal-lang/crystal/issues/8872 (though not quite the same thing…)

4 posts were split to a new topic: Arithmethic overflow should not raise an exception

What if we automatically turn Int32 | Int64 into Int64? That is, you can’t really have a union of two integer types when one fits perfectly inside another one: you just get the bigger type. Same if you do Int8 | Int16, UInt32 | UInt64, etc. Then at least when mixing Int64 code with Int32 you’ll always get Int64, so correct code.

I’m going to try this next.

1 Like

Meh, maybe for 2.0. Until 1.0 I won’t experiment any more.

This may seem a naive question, but do you understand what Go is doing to be able to run the equivalent code with no extra type value declarations? Understanding first how it (et al) deal with this issue (design choices) may provide better clarity to go forward from.

package main
 
import (
    "fmt"
    "strconv"
    "strings"
    "time"
)
 
func selfDesc(n uint64) bool {
    if n >= 1e10 {
        return false
    }
    s := strconv.FormatUint(n, 10)
    for d, p := range s {
        if int(p)-'0' != strings.Count(s, strconv.Itoa(d)) {
            return false
        }
    }
    return true
}
 
func main() {
    start := time.Now()
    fmt.Println("The self-describing numbers are:")
    i := uint64(10)   // self-describing number must end in 0
    pw := uint64(10)  // power of 10
    fd := uint64(1)   // first digit
    sd := uint64(1)   // second digit
    dg := uint64(2)   // number of digits
    mx := uint64(11)  // maximum for current batch
    lim := uint64(9_100_000_001) // sum of digits can't be more than 10
    for i < lim {
        if selfDesc(i) {
            secs := time.Since(start).Seconds()
            fmt.Printf("%d (in %.1f secs)\n", i, secs)
        }
        i += 10
        if i > mx {
            fd++
            sd--
            if sd >= 0 {
                i = fd * pw
            } else {
                pw *= 10
                dg++
                i = pw
                fd = 1
                sd = dg - 1
            }
            mx = i + sd*pw/10
        }
    }
    osecs := time.Since(start).Seconds()
    fmt.Printf("\nTook %.1f secs overall\n", osecs)
}

Go has no union types.

As far as I know Golang doesn’t allow to change the type of a variable. So when you assign an untyped number literal 1 to a local variable, it seems to be autocased to the type of the varible.

fd := uint64(1)
fd = 1
fmt.Println(reflect.TypeOf(fd)) // => uint64
fd = uint(32) // cannot use uint32(1) (type uint32) as type uint64 in assignment

Is autocasting something that you can do too when appropriate, to use the larger type?