implement scalar conversion functions #19
Labels
No Milestone
No Assignees
5 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: M-Labs/nac3#19
Loading…
Reference in New Issue
There is no content yet.
Delete Branch "%!s(<nil>)"
Deleting a branch is permanent. Although the deleted branch may exist for a short time before cleaning up, in most cases it CANNOT be undone. Continue?
They are ubiquituous in ARTIQ:
int32()
,int64()
,float()
,round()
.We also probably want to special-case
int64(round())
.Float to integer conversion that overflows or has other problems (NaN etc.) should raise an exception to match Numpy behavior.
bool()
is not very important but it makes sense to implement it at the same time.Though using the saturating LLVM intrinsics would give us constant folding/propagation for free, which is an important feature - for example, many delays in ARTIQ code are fixed and specified in SI (e.g. 1e-6 seconds), and we want the conversion to machine units to happen at compilation time to improve runtime performance.
Otherwise LLVM cannot do it and we have to reimplement constant propagation ourselves.
@lriesebos @dhslichter @cjbe @bradbqc thoughts?
Or we can implement the whole function (with the overflow check) in LLVM IR and make sure that SCCP/DCE passes take care of the overflow check at compilation time.
IMHO deviating from the Numpy behavior here for significantly better performance / ease of implementation is totally worth it - from the nature of the compiler there are already, and always will be, deviations, so the user already has to consider this "similar but different" from standard python/numpy behaviour.
I also think it is fine to deviate from numpy behavior in this case, especially with the mentioned benefits in mind.
Also note the following behavior regarding the overflow exception:
I suppose this is because it converts to int64 internally with overflow check, and then int64 -> int32 is allowed to overflow. Doesn't make much sense from a usability POV, so maybe we just shouldn't bother. The consistent saturating behavior of the LLVM intrinsic is actually more usable.
Agreed that we don't need to match numpy behavior, as long as we are explicit in documenting what the behavior will be. We just want to make sure that this change in behavior doesn't cause silent failures of higher-level functions (especially built-in numpy-like kernel math functions) that might call these sorts of type cast operations.
Round is hard to deal with. From the documentation:
Our type system does not allow such ad-hoc polymorphism. Special-case
int64(round(x))
would also be ugly. The simplest way might be to allow the return type to be either float/int32/int64, and choose according to the type inference result. (This would be impossible to implement in the language but we can do it with custom codegen)Example:
Num
is a type variable that can range over int32/int64/float. Note that round to n decimal places is not implemented.Just implement
round(float) -> int32
andint64(round(float)) -> int64
. The current compiler does not implementndigits
and nobody asked for it.If the special case is so ugly, we can also just have another
round64()
function. Though it seems to me that the special case can be dealt with using a simple AST pattern matching that turnsint64(round())
intoround64()
internally.Is that special case substantially different from
int64(numeric_constant)
?Nope, but you would need a pretty long (and deep) pattern matching. Just wondering why do we need to add special case everywhere.
It's just syntactic sugar, nothing wrong with having this kind of special case IMHO. Implement a separate
round64()
function instead if you don't like it.round64()
would need a Python implementation too for proper simulation. from that standpoint, it would be better to limit the number of "builtin" functions and not add a new one.for
round()
withndigits
, we do not have any desires for that and I do not think I can imagine any future use cases at this moment.Implemented in
1d2a32b140
int64(round(x))
is not yet implemented. Around64
is provided for this.ok well if we stick with
round64()
, then we need to provide the Python implementation in the ARTIQ library. Not sure if I can still convince people ofint64(round(x))
, but that is still the preferred solution for me because it does not introduce a new builtin and is therefore immediately simulatable.they are different things.
round(x)
has this implicit number of digits, and could reasonably be expected to produce a float, whileround64(x)
would be explicitly rounding and converting to anint64
. Numpy has the functionrint(x)
that rounds to an integer, if people prefer that nomenclature.Providing a Python implementation of
round64()
seems like a pretty simple thing to do?No,
round()
withoutndigits
always produces an integer in Python.Correct, but it can take the optional ndigits argument and produce a float if ndigits != 0. And it may be desirable to have
round()
produce a float for some use cases. My sentiment is that if one is making a rounding method that always rounds to an integer, it's better to name it something other thanround()
to reduce potential for confusion. Which is why I like theround64()
idea.Well, as mentioned earlier,
ndigits
does not seem to be on the list to be implemented, so that would meanround()
will always produce an integer.int64(round())
also seems completely clear to me without introducing new functions that users have to learn. And even ifndigits
would be implemented, that would not interfere with theint64(round())
construct, its semantics are still clear. So personally, I do not see the need to add a new function.compiler wise, if having
round64()
would make a huge difference, then that is fine with me.I guess my point is that Python users may interpret
round()
as the usual "makes a float or an int, depending" function, and that if we named it something different (e.g.rint()
orround64()
), then it would be more readily apparent to the casual user that it is different from the usual Pythonround()
. I think one of the big problems that ARTIQ has is that people try things they expect to work a certain way in Python, and then discover that ARTIQ-Python is different. If we can reduce the chance for this confusion it's nice. However, it's not something I want to fight strongly for if it's not a big deal to others.The behavior is exactly the same as
round()
, we just reject thendigits
argument if it is passed.Most of the time people will not use it AFAICT and it will just work the same as Python. If the function is named differently on the other hand then there will be a small surprise.
They still have to learn that
round()
may overflow and that the trick is to addint64()
around it. A purist may also think that theint64()
fix isn't obvious since according to function call semantics it would only get the value after the overflow has already occurred.Anyway enough bike-shedding. Summarizing the current status of this issue:
bool()
is still missing.int64(round())
vs.round64()
I don't really mind either way. @pca006132 seems to have strong opinion about havinground64()
and it's less/simpler code, so let's keep it this way. Addinground64
to CPython is trivial.Actually, the idea I have in mind is to make round output either an int32 or an int64 depending on the usage of its result. Although there would be a quirk due to the inference rules, that
1 + round(1.0)
can be inferred butround(1.0) + 1
cannot be inferred, as + is treated as a function and the lhs is the object.Just leave it as it is now.