Skip to content

Commit b86b64f

Browse files
committed
WIP: Add explicitly wrapping versions of integer arithmetic
This adds operators `+%`, `-%`, `*%`, which are equivalent to the non-`%` versions, but indicate an explicit semantic expectation that twos completement wrapping behavior is expected and correct. As discussed at JuliaCon 2014 and every year since, users have often requested a way to opt into explicit overflow checking of arithmetic, whether for debugging or because they have regulatory or procedural requirements that expect to be able to do this. Having explicit operators for overflowing semantics allows use cases that depend on overflow behavior for correct functioning to explicitly opt-out of any such checking. I want to explicitly emphasize that there are no plans to change the default behavior of arithmetic in Julia, neither by introducing error checking nor by making it undefined behavior (as in C). The general consensus here is that while overflow checking can be useful, and would be a fine default, even if hardware supported it efficiently (which it doesn't), the performance costs of performing the check (through inhibition of other optimization) is too high. In our experience it also tends to be relatively harmless, even if it can be a very rude awakeing to users coming from Python or other languages with big-default integers. The idea here is simply to give users another tool in their arsenal for checking correctness. Think sanitizers, not language change. This PR includes a macro `@Base.Experimental.make_all_arithmetic_checked`, that will define overrides to make arithmetic checked, but does not include any mechanism (e.g. #50239) to make this fast. What is included in this PR: - Flisp parser changes to parse the new operators - Definitions of the new operators - Some basic replacements in base to give a flavor for using the new operator and make sure it works Still to be done: - [] Parser changes in JuliaSyntax - [] Correct parsing for `+%` by itself, which currently parses as `+(%)` The places to change in base were found by using the above-mentioned macro and running the test suite. I did not work through the tests exhaustively. We have many tests that explicitly expect overflow and many others that we should go through on a case by case basis. The idea here is merely to give an idea of the kind of changes that may be required if overflow checking is enabled. I think they can broadly be classed into: - Crypto and hashing code that explicitly want modular arithmetic - Bit twidelling code for arithmetic tricks (though many of these, particularly in Ryu, could probably be replaced with better abstractions). - UInt8 range checks written by Stefan - Misc
1 parent 210c5b5 commit b86b64f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+312
-133
lines changed

base/abstractarray.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3405,7 +3405,7 @@ pushfirst!(A, a, b, c...) = pushfirst!(pushfirst!(A, c...), a, b)
34053405

34063406
const hash_abstractarray_seed = UInt === UInt64 ? 0x7e2d6fb6448beb77 : 0xd4514ce5
34073407
function hash(A::AbstractArray, h::UInt)
3408-
h += hash_abstractarray_seed
3408+
h +%= hash_abstractarray_seed
34093409
# Axes are themselves AbstractArrays, so hashing them directly would stack overflow
34103410
# Instead hash the tuple of firsts and lasts along each dimension
34113411
h = hash(map(first, axes(A)), h)

base/abstractset.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ max_values(T::Union{map(X -> Type{X}, BitIntegerSmall_types)...}) = 1 << (8*size
9393
function max_values(T::Union)
9494
a = max_values(T.a)::Int
9595
b = max_values(T.b)::Int
96-
return max(a, b, a + b)
96+
r, o = add_with_overflow(a, b)
97+
o && return typemax(Int)
98+
return r
9799
end
98100
max_values(::Type{Bool}) = 2
99101
max_values(::Type{Nothing}) = 1

base/bool.jl

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,12 +160,17 @@ isone(x::Bool) = x
160160

161161
## do arithmetic as Int ##
162162

163-
+(x::Bool) = Int(x)
164-
-(x::Bool) = -Int(x)
165-
166-
+(x::Bool, y::Bool) = Int(x) + Int(y)
167-
-(x::Bool, y::Bool) = Int(x) - Int(y)
163+
+(x::Bool) = Int(x)
164+
+%(x::Bool) = Int(x)
165+
-(x::Bool) = -%(Int(x))
166+
-%(x::Bool) = -%(Int(x))
167+
168+
+(x::Bool, y::Bool) = Int(x) +% Int(y)
169+
-(x::Bool, y::Bool) = Int(x) -% Int(y)
170+
+%(x::Bool, y::Bool) = Int(x) +% Int(y)
171+
-%(x::Bool, y::Bool) = Int(x) -% Int(y)
168172
*(x::Bool, y::Bool) = x & y
173+
*%(x::Bool, y::Bool) = x & y
169174
^(x::Bool, y::Bool) = x | !y
170175
^(x::Integer, y::Bool) = ifelse(y, x, one(x))
171176

base/char.jl

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,9 @@ isless(x::AbstractChar, y::AbstractChar) = isless(Char(x), Char(y))
223223
hash(x::AbstractChar, h::UInt) = hash(Char(x), h)
224224
widen(::Type{T}) where {T<:AbstractChar} = T
225225

226+
@inline -%(x::AbstractChar, y::AbstractChar) = Int(x) -% Int(y)
226227
@inline -(x::AbstractChar, y::AbstractChar) = Int(x) - Int(y)
227-
@inline function -(x::T, y::Integer) where {T<:AbstractChar}
228+
@inline function -%(x::T, y::Integer) where {T<:AbstractChar}
228229
if x isa Char
229230
u = Int32((bitcast(UInt32, x) >> 24) % Int8)
230231
if u >= 0 # inline the runtime fast path
@@ -234,7 +235,7 @@ widen(::Type{T}) where {T<:AbstractChar} = T
234235
end
235236
return T(Int32(x) - Int32(y))
236237
end
237-
@inline function +(x::T, y::Integer) where {T<:AbstractChar}
238+
@inline function +%(x::T, y::Integer) where {T<:AbstractChar}
238239
if x isa Char
239240
u = Int32((bitcast(UInt32, x) >> 24) % Int8)
240241
if u >= 0 # inline the runtime fast path
@@ -244,7 +245,11 @@ end
244245
end
245246
return T(Int32(x) + Int32(y))
246247
end
247-
@inline +(x::Integer, y::AbstractChar) = y + x
248+
@inline +%(x::Integer, y::AbstractChar) = y + x
249+
250+
-(x::AbstractChar, y::Integer) = x -% y
251+
+(x::AbstractChar, y::Integer) = x +% y
252+
+(x::Integer, y::AbstractChar) = x +% y
248253

249254
# `print` should output UTF-8 by default for all AbstractChar types.
250255
# (Packages may implement other IO subtypes to specify different encodings.)

base/checked.jl

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import Core.Intrinsics:
2121
checked_srem_int,
2222
checked_uadd_int, checked_usub_int, checked_umul_int, checked_udiv_int,
2323
checked_urem_int
24-
import ..no_op_err, ..@inline, ..@noinline, ..checked_length
24+
import ..no_op_err, ..@inline, ..@noinline, ..checked_length, ..BitInteger
2525

2626
# define promotion behavior for checked operations
2727
checked_add(x::Integer, y::Integer) = checked_add(promote(x,y)...)
@@ -98,7 +98,7 @@ throw_overflowerr_negation(x) = (@noinline;
9898
throw(OverflowError(Base.invokelatest(string, "checked arithmetic: cannot compute -x for x = ", x, "::", typeof(x)))))
9999
if BrokenSignedInt != Union{}
100100
function checked_neg(x::BrokenSignedInt)
101-
r = -x
101+
r = -%(x)
102102
(x<0) & (r<0) && throw_overflowerr_negation(x)
103103
r
104104
end
@@ -140,11 +140,11 @@ Calculates `r = x+y`, with the flag `f` indicating whether overflow has occurred
140140
function add_with_overflow end
141141
add_with_overflow(x::T, y::T) where {T<:SignedInt} = checked_sadd_int(x, y)
142142
add_with_overflow(x::T, y::T) where {T<:UnsignedInt} = checked_uadd_int(x, y)
143-
add_with_overflow(x::Bool, y::Bool) = (x+y, false)
143+
add_with_overflow(x::Bool, y::Bool) = (x +% y, false)
144144

145145
if BrokenSignedInt != Union{}
146146
function add_with_overflow(x::T, y::T) where T<:BrokenSignedInt
147-
r = x + y
147+
r = x +% y
148148
# x and y have the same sign, and the result has a different sign
149149
f = (x<0) == (y<0) != (r<0)
150150
r, f
@@ -154,7 +154,7 @@ if BrokenUnsignedInt != Union{}
154154
function add_with_overflow(x::T, y::T) where T<:BrokenUnsignedInt
155155
# x + y > typemax(T)
156156
# Note: ~y == -y-1
157-
x + y, x > ~y
157+
x +% y, x > ~y
158158
end
159159
end
160160

@@ -171,7 +171,11 @@ The overflow protection may impose a perceptible performance penalty.
171171
"""
172172
function checked_add(x::T, y::T) where T<:Integer
173173
@inline
174-
z, b = add_with_overflow(x, y)
174+
zb = add_with_overflow(x, y)
175+
# Avoid use of tuple destructuring, which uses aritmetic internally,
176+
# so that this can be used as a replacement for +
177+
z = getfield(zb, 1)
178+
b = getfield(zb, 2)
175179
b && throw_overflowerr_binaryop(:+, x, y)
176180
z
177181
end
@@ -206,7 +210,7 @@ sub_with_overflow(x::Bool, y::Bool) = (x-y, false)
206210

207211
if BrokenSignedInt != Union{}
208212
function sub_with_overflow(x::T, y::T) where T<:BrokenSignedInt
209-
r = x - y
213+
r = x -% y
210214
# x and y have different signs, and the result has a different sign than x
211215
f = (x<0) != (y<0) == (r<0)
212216
r, f
@@ -215,7 +219,7 @@ end
215219
if BrokenUnsignedInt != Union{}
216220
function sub_with_overflow(x::T, y::T) where T<:BrokenUnsignedInt
217221
# x - y < 0
218-
x - y, x < y
222+
x -% y, x < y
219223
end
220224
end
221225

@@ -242,7 +246,7 @@ Calculates `r = x*y`, with the flag `f` indicating whether overflow has occurred
242246
function mul_with_overflow end
243247
mul_with_overflow(x::T, y::T) where {T<:SignedInt} = checked_smul_int(x, y)
244248
mul_with_overflow(x::T, y::T) where {T<:UnsignedInt} = checked_umul_int(x, y)
245-
mul_with_overflow(x::Bool, y::Bool) = (x*y, false)
249+
mul_with_overflow(x::Bool, y::Bool) = (x *% y, false)
246250

247251
if BrokenSignedIntMul != Union{} && BrokenSignedIntMul != Int128
248252
function mul_with_overflow(x::T, y::T) where T<:BrokenSignedIntMul
@@ -273,14 +277,14 @@ if Int128 <: BrokenSignedIntMul
273277
else
274278
false
275279
end
276-
x*y, f
280+
x *% y, f
277281
end
278282
end
279283
if UInt128 <: BrokenUnsignedIntMul
280284
# Avoid BigInt
281285
function mul_with_overflow(x::T, y::T) where T<:UInt128
282286
# x * y > typemax(T)
283-
x * y, y > 0 && x > fld(typemax(T), y)
287+
x *% y, y > 0 && x > fld(typemax(T), y)
284288
end
285289
end
286290

base/deprecated.jl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,3 +384,14 @@ macro pure(ex)
384384
end
385385

386386
# END 1.10 deprecations
387+
388+
# BEGIN 1.11 deprecations
389+
390+
# These operators are new in 1.11, but these fallback methods are added for
391+
# compatibility while packages adjust to defining both operators, to allow
392+
# Base and other packages to start using these.
393+
*%(a::T, b::T) where {T} = *(a, b)
394+
+%(a::T, b::T) where {T} = +(a, b)
395+
-%(a::T, b::T) where {T} = -(a, b)
396+
397+
# END 1.11 deprecations

base/docs/basedocs.jl

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2644,6 +2644,30 @@ julia> +(1, 20, 4)
26442644
"""
26452645
(+)(x, y...)
26462646

2647+
"""
2648+
+%(x::Integer, y::Integer...)
2649+
2650+
Addition operator with semantic wrapping. In the default Julia environment, this
2651+
is equivalent to the regular addition operator `+`. However, some users may choose to overwrite
2652+
`+` in their local environment to perform checked arithmetic instead (e.g. using
2653+
[`Experimental.@make_all_arithmetic_checked`](@ref)). The `+%` operator may be used to indicate
2654+
that wrapping behavior is semantically expected and correct and should thus be exempted from
2655+
any opt-in overflow checking.
2656+
2657+
# Examples
2658+
```jldoctest
2659+
julia> 1 +% 20 +% 4
2660+
25
2661+
2662+
julia> +%(1, 20, 4)
2663+
25
2664+
2665+
julia> typemax(Int) +% 1
2666+
-9223372036854775808
2667+
```
2668+
"""
2669+
(+%)(x, y...)
2670+
26472671
"""
26482672
-(x)
26492673
@@ -2683,6 +2707,27 @@ julia> -(2, 4.5)
26832707
"""
26842708
-(x, y)
26852709

2710+
"""
2711+
-%(x::Integer, y::Integer...)
2712+
2713+
Subtraction operator with semantic wrapping. In the default Julia environment, this
2714+
is equivalent to the regular subtraction operator `-`. However, some users may choose to overwrite
2715+
`-` in their local environment to perform checked arithmetic instead (e.g. using
2716+
[`Experimental.@make_all_arithmetic_checked`](@ref)). The `-%` operator may be used to indicate
2717+
that wrapping behavior is semantically expected and correct and should thus be exempted from
2718+
any opt-in overflow checking.
2719+
2720+
# Examples
2721+
```jldoctest
2722+
julia> 2 -% 3
2723+
-1
2724+
2725+
julia> -(typemin(Int))
2726+
-9223372036854775808
2727+
```
2728+
"""
2729+
(-%)(x, y...)
2730+
26862731
"""
26872732
*(x, y...)
26882733
@@ -2699,6 +2744,30 @@ julia> *(2, 7, 8)
26992744
"""
27002745
(*)(x, y...)
27012746

2747+
"""
2748+
*%(x::Integer, y::Integer, z::Integer...)
2749+
2750+
Multiplication operator with semantic wrapping. In the default Julia environment, this
2751+
is equivalent to the regular multiplication operator `*`. However, some users may choose to overwrite
2752+
`*` in their local environment to perform checked arithmetic instead (e.g. using
2753+
[`Experimental.@make_all_arithmetic_checked`](@ref)). The `*%` operator may be used to indicate
2754+
that wrapping behavior is semantically expected and correct and should thus be exempted from
2755+
any opt-in overflow checking.
2756+
2757+
# Examples
2758+
```jldoctest
2759+
julia> 2 *% 7 *% 8
2760+
112
2761+
2762+
julia> *(2, 7, 8)
2763+
112
2764+
2765+
julia> 0xff *% 0xff
2766+
0x01
2767+
```
2768+
"""
2769+
(*%)(x, y, z...)
2770+
27022771
"""
27032772
/(x, y)
27042773

base/experimental.jl

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,4 +368,25 @@ adding them to the global method table.
368368
"""
369369
:@MethodTable
370370

371+
"""
372+
Experimental.@make_all_arithmetic_checked()
373+
374+
This macro defines methods that overwrite the base definition of basic arithmetic (+,-,*),
375+
to use their checked variants instead. Explicitly overflowing arithmetic operators (+%,-%,*%)
376+
are not affected.
377+
378+
!!! warning
379+
This macro is temporary and will likely be replaced by a more complete mechanism in the
380+
future. It is subject to change or removal without notice.
381+
"""
382+
macro make_all_arithmetic_checked()
383+
esc(quote
384+
Base.:(-)(x::BitInteger) = Base.Checked.checked_neg(x)
385+
Base.:(-)(x::T, y::T) where {T<:BitInteger} = Base.Checked.checked_sub(x, y)
386+
Base.:(+)(x::T, y::T) where {T<:BitInteger} = Base.Checked.checked_add(x, y)
387+
Base.:(*)(x::T, y::T) where {T<:BitInteger} = Base.Checked.checked_mul(x, y)
388+
Base.:(-)(x::AbstractChar, y::AbstractChar) = Int(x) - Int(y)
389+
end)
390+
end
391+
371392
end

base/exports.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,11 @@ export
183183
÷,
184184
&,
185185
*,
186+
*%,
186187
+,
188+
+%,
187189
-,
190+
-%,
188191
/,
189192
//,
190193
<,

base/filesystem.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,9 +200,10 @@ end
200200

201201
function read(f::File, ::Type{Char})
202202
b0 = read(f, UInt8)
203-
l = 0x08 * (0x04 - UInt8(leading_ones(b0)))
203+
lo = UInt8(leading_ones(b0))
204204
c = UInt32(b0) << 24
205-
if l 0x10
205+
if 0x02 lo 0x04
206+
l = 0x08 * (0x04 - lo)
206207
s = 16
207208
while s l && !eof(f)
208209
# this works around lack of peek(::File)

0 commit comments

Comments
 (0)