`StringLiteral#to_i` with a number kind
StringLiteral#to_i converts its receiver using String#to_i64? (due to #16467 the effective range is currently even smaller), yet number literals can certainly go over Int64's range. I propose adding a separate overload that accepts a number kind, i.e. the symbol that would be returned by NumberLiteral#kind:
module Crystal::Macros
class StringLiteral
# Similar to `String#to_i`.
#
# *kind* is the returned literal's type as returned by `NumberLiteral#kind`.
# This type is always used, even if the numeric value of the string fits into
# a smaller integer type's range.
def to_i(kind : SymbolLiteral, base : NumberLiteral = 0) : NumberLiteral
end
end
end
{{ "123".to_i(:u64) }} # => 123_u64
{{ UInt64::MAX.to_number.stringify.to_i(:u64) }} # okay
{{ ("1" * 30).to_i(:i128) }} # okay
{{ "130".to_i(:i8) }} # error
Add a :+1: reaction to issues you find important.
Passing :f32 or :f64 to the kind parameter would be an error, but then it would be somewhat odd to add a separate StringLiteral#to_f that does a similar conversion, only erroring on integer kinds. Perhaps this should be called #to_num or #to_number instead?
The other alternative, of course, is to add the typed variants like #to_i32 and #to_f64 directly. IMO an overload taking a kind argument would be more flexible in a dynamically typed macro language.
While reading https://github.com/crystal-lang/crystal/issues/16467 I was wondering whether it would be useful to have a method for converting the kind of a NumberLiteral. Maybe NumberLiteral#to_kind(kind : SymbolLiteral).
Together with a generic StringLiteral#to_number this could perhaps even be an alternative to #to_i(kind) or #to_number(kind).
But I suppose it probably makes sense to define the kind directly in the parse method.
NumberLiteral#to_kind might still be a nice addition, though I'm not sure about concrete use cases.
Is there a need to diverge from the corelib methods? We could have #to_u8, #to_i (aka #to_i32), #to_f64, and so on that would behave like corelib (no surprises).
For flexibility, there could be a #to_num or #to_number method that would optionally take a number kind (:u8, :i32, :f64) and otherwise infer the best type; the explicit methods would just call it internally.
I suppose converting number literals between different types is probably less common than converting actual number values. So there is less incentive to provide these super convenient conversion methods. But then it probably doesn't hurt, and there is a benefit in mirroring the normal methods.
I wanted to outline that we should avoid making up a similar yet different syntax when we already have a good one, unless there's a good reason to.
I'm fine with #to_num(:i32), though #to_int(32) sounds better, and at this point it's just #to_i32 which we already know...
Yet, it's still different because we won't get an IntegerLiteral but a NumberLiteral so maybe putting an emphasis on the "number" word is better design-wise.
(And as you say, we likely don't need to care about bit sizes in macros... maybe arbitrary sized numbers as in Ruby would be a better choice).
Following https://github.com/crystal-lang/crystal/issues/16469#issuecomment-3601302288 and https://github.com/crystal-lang/crystal/pull/16468#issuecomment-3612573727, StringLiteral#to_number (or #to_num?) seems like the best choice to me: It converts any StringLiteral into an equivalent NumberLiteral. It works for any kind of number, including floating point.
We don't have IntegerLiteral or Int32Literal, only a generic NumberLiteral, so keeping the conversion generic makes sense.
Whether the conversion method directly accepts an optional target kind, or we need an extra NumberLiteral#to_kind, or both. May be up for debate.
We might keep StringLiteral#to_i as a shortcut, but as mentioned in https://github.com/crystal-lang/crystal/issues/16469#issuecomment-3614266727, I'm not sure about adding more methods like it.