Repository navigation
math: fix exp_impl_fast Float64 nothrow inference - #61615
Merged
IanButterworth merged 1 commit intoApr 21, 2026
Merged
Conversation
Use `table_unpack(N)` in `exp_impl_fast` for Float64 instead of a raw `@inbounds J_TABLE[N&255 + 1]` indexing. The compiler cannot prove the `@inbounds` access is safe, causing the function to be inferred as `!n` (not nothrow) and `!u` (not noundef), unlike its non-fast counterpart `exp_impl` which already uses `table_unpack` (annotated with `@assume_effects :nothrow`). This affected `exp_fast`, `exp2_fast`, and `exp10_fast` for Float64 since they all go through `exp_impl_fast`. Co-authored-by: Claude <noreply@anthropic.com>
gbaraldi
marked this pull request as ready for review
April 20, 2026 16:43
JeffBezanson
approved these changes
Apr 20, 2026
IanButterworth
added a commit
that referenced
this pull request
May 6, 2026
Continuation of #61616 #61615 Developed with claude: --- Several `Char` predicates and a couple of `String` predicates in `base/strings/unicode.jl` and `base/strings/string.jl` are spuriously inferred as `nothrow=false`, even though they are provably total. This prevents the compiler from constant-folding them or from eliminating them when their result is unused. The root cause is that the compiler cannot prove that the `ismalformed` / `is_overlong_enc` guards rule out the throwing paths in their bodies — for example the `Int(::Cint)` conversion after the `utf8proc` ccall, or the `UInt32(::AbstractChar)` conversion in `category_code`. The `AbstractChar` contract guarantees that `UInt32(c)` only throws when `ismalformed(c)` returns `true`, and the `utf8proc_charwidth` result fits in a `Cint` so the `Int` conversion never throws `InexactError`. This PR annotates the affected definitions with `@assume_effects` and adds regression tests in `test/char.jl` and `test/strings/basic.jl`. ### Scope `Char`: - `textwidth`, `category_code`, `category_abbrev`, `category_string`, `isassigned`, `islowercase`, `isuppercase` `String`: - `isascii`, `lastindex`, `textwidth` `SubString{String}`: - `length`, `isascii`, `lastindex`, `textwidth` `String` / `SubString{String}` predicates: - `startswith`, `endswith`, `in(::Char, _)` Annotations are added on narrow concrete-type method overloads so they don't constrain user-defined `AbstractChar` / `AbstractString` subtypes. A static `@assert length(category_strings) == 32` is added so the `category_string(::Char)` annotation stays sound if the table is ever edited. The bodies of `startswith` / `endswith` are `_memcmp` byte-bounded by `sizeof(b) ≤ ncodeunits(a)` plus `nextind` / `thisind` calls on indices proven in range, so they cannot throw even on strings containing arbitrary, malformed UTF-8 byte sequences. A stress test exercises every annotated function on adversarial byte sequences and on `Char` values constructed from invalid bit-patterns to lock in the `:nothrow` soundness against future refactors. ### Effect on generated code `textwidth("hello world")` now constant-folds: **nightly:** ```julia-repl julia> code_typed(() -> textwidth("hello world"), ())[1] CodeInfo( 1 ─ %1 = invoke Base._foldl_impl( Base.MappingRF{typeof(textwidth), Base.BottomRF{typeof(+)}}( textwidth, Base.BottomRF{typeof(+)}(+))::Base.MappingRF{...}, 0::Int64, "hello world"::String)::Int64 └── return %1 ) => Int64 ``` **this PR:** ```julia-repl julia> code_typed(() -> textwidth("hello world"), ())[1] CodeInfo( 1 ─ return 11 ) => Int64 ``` Likewise, `startswith` / `endswith` / `in` over literal `String`s now constant-fold. **nightly:** ```julia-repl julia> code_typed(() -> startswith("https://example.com/foo", "https://"), ())[1] CodeInfo( 1 ─ %1 = invoke Main.startswith("https://example.com/foo"::String, "https://"::String)::Bool └── return %1 ) => Bool julia> code_typed(() -> endswith("foo.jl", ".jl"), ())[1] CodeInfo( 1 ─ %1 = invoke Main.endswith("foo.jl"::String, ".jl"::String)::Bool └── return %1 ) => Bool julia> code_typed(() -> in('a', "aeiou"), ())[1] CodeInfo( 1 ─ %1 = invoke Base.codeunits("aeiou"::String)::Base.CodeUnits{UInt8, String} │ %2 = $(Expr(:gc_preserve_begin, :(%1))) │ %3 = builtin Base.getfield(%1, :s)::String │ %4 = invoke Base.cconvert(Ptr{UInt8}::Type{Ptr{UInt8}}, %3::String)::String │ %5 = $(Expr(:foreigncall, :((:jl_string_ptr,)), Ptr{UInt8}, svec(Any), 0, :(:ccall), :(%4)))::Ptr{UInt8} │ %6 = intrinsic Base.add_ptr(%5, 0x0000000000000001)::Ptr{UInt8} │ %7 = intrinsic Base.sub_ptr(%6, 0x0000000000000001)::Ptr{UInt8} │ %8 = $(Expr(:foreigncall, :((:memchr,)), Ptr{UInt8}, svec(Ptr{UInt8}, Int32, UInt64), 0, :(:ccall), :(%7), 97, 0x0000000000000005, 0x0000000000000005, 97, :(%7)))::Ptr{UInt8} │ $(Expr(:gc_preserve_end, :(%2))) │ %10 = intrinsic Core.bitcast(Core.UInt, %8)::UInt64 │ %11 = builtin (%10 === 0x0000000000000000)::Bool └── goto #3 if not %11 2 ─ goto #4 3 ─ goto #4 4 ┄ %15 = φ (#2 => true, #3 => false)::Bool │ %16 = intrinsic Core.Intrinsics.not_int(%15)::Bool └── goto #5 5 ─ return %16 ) => Bool ``` **this PR:** ```julia-repl julia> code_typed(() -> startswith("https://example.com/foo", "https://"), ())[1] CodeInfo( 1 ─ return true ) => Bool julia> code_typed(() -> endswith("foo.jl", ".jl"), ())[1] CodeInfo( 1 ─ return true ) => Bool julia> code_typed(() -> in('a', "aeiou"), ())[1] CodeInfo( 1 ─ return true ) => Bool ``` And `SubString{String}` queries can now be DCE'd when their result is unused. With `describe(ss)` calling `length`, `lastindex`, `textwidth`, `isascii`: **nightly:** ```llvm define i64 @julia_caller_0(...) { top: %"sret::NamedTuple" = alloca [4 x i64], align 8 call void @j_describe_0(ptr ... sret(...) align 8 %"sret::NamedTuple", ...) ret i64 42 } ``` **this PR:** ```llvm define i64 @julia_caller_0(...) { top: ret i64 42 } ``` --------- Co-authored-by: Claude <noreply@anthropic.com>
hardikxk
pushed a commit
to hardikxk/julia
that referenced
this pull request
May 18, 2026
hardikxk
pushed a commit
to hardikxk/julia
that referenced
this pull request
May 18, 2026
…ang#61625) Continuation of JuliaLang#61616 JuliaLang#61615 Developed with claude: --- Several `Char` predicates and a couple of `String` predicates in `base/strings/unicode.jl` and `base/strings/string.jl` are spuriously inferred as `nothrow=false`, even though they are provably total. This prevents the compiler from constant-folding them or from eliminating them when their result is unused. The root cause is that the compiler cannot prove that the `ismalformed` / `is_overlong_enc` guards rule out the throwing paths in their bodies — for example the `Int(::Cint)` conversion after the `utf8proc` ccall, or the `UInt32(::AbstractChar)` conversion in `category_code`. The `AbstractChar` contract guarantees that `UInt32(c)` only throws when `ismalformed(c)` returns `true`, and the `utf8proc_charwidth` result fits in a `Cint` so the `Int` conversion never throws `InexactError`. This PR annotates the affected definitions with `@assume_effects` and adds regression tests in `test/char.jl` and `test/strings/basic.jl`. ### Scope `Char`: - `textwidth`, `category_code`, `category_abbrev`, `category_string`, `isassigned`, `islowercase`, `isuppercase` `String`: - `isascii`, `lastindex`, `textwidth` `SubString{String}`: - `length`, `isascii`, `lastindex`, `textwidth` `String` / `SubString{String}` predicates: - `startswith`, `endswith`, `in(::Char, _)` Annotations are added on narrow concrete-type method overloads so they don't constrain user-defined `AbstractChar` / `AbstractString` subtypes. A static `@assert length(category_strings) == 32` is added so the `category_string(::Char)` annotation stays sound if the table is ever edited. The bodies of `startswith` / `endswith` are `_memcmp` byte-bounded by `sizeof(b) ≤ ncodeunits(a)` plus `nextind` / `thisind` calls on indices proven in range, so they cannot throw even on strings containing arbitrary, malformed UTF-8 byte sequences. A stress test exercises every annotated function on adversarial byte sequences and on `Char` values constructed from invalid bit-patterns to lock in the `:nothrow` soundness against future refactors. ### Effect on generated code `textwidth("hello world")` now constant-folds: **nightly:** ```julia-repl julia> code_typed(() -> textwidth("hello world"), ())[1] CodeInfo( 1 ─ %1 = invoke Base._foldl_impl( Base.MappingRF{typeof(textwidth), Base.BottomRF{typeof(+)}}( textwidth, Base.BottomRF{typeof(+)}(+))::Base.MappingRF{...}, 0::Int64, "hello world"::String)::Int64 └── return %1 ) => Int64 ``` **this PR:** ```julia-repl julia> code_typed(() -> textwidth("hello world"), ())[1] CodeInfo( 1 ─ return 11 ) => Int64 ``` Likewise, `startswith` / `endswith` / `in` over literal `String`s now constant-fold. **nightly:** ```julia-repl julia> code_typed(() -> startswith("https://example.com/foo", "https://"), ())[1] CodeInfo( 1 ─ %1 = invoke Main.startswith("https://example.com/foo"::String, "https://"::String)::Bool └── return %1 ) => Bool julia> code_typed(() -> endswith("foo.jl", ".jl"), ())[1] CodeInfo( 1 ─ %1 = invoke Main.endswith("foo.jl"::String, ".jl"::String)::Bool └── return %1 ) => Bool julia> code_typed(() -> in('a', "aeiou"), ())[1] CodeInfo( 1 ─ %1 = invoke Base.codeunits("aeiou"::String)::Base.CodeUnits{UInt8, String} │ %2 = $(Expr(:gc_preserve_begin, :(%1))) │ %3 = builtin Base.getfield(%1, :s)::String │ %4 = invoke Base.cconvert(Ptr{UInt8}::Type{Ptr{UInt8}}, %3::String)::String │ %5 = $(Expr(:foreigncall, :((:jl_string_ptr,)), Ptr{UInt8}, svec(Any), 0, :(:ccall), :(%4)))::Ptr{UInt8} │ %6 = intrinsic Base.add_ptr(%5, 0x0000000000000001)::Ptr{UInt8} │ %7 = intrinsic Base.sub_ptr(%6, 0x0000000000000001)::Ptr{UInt8} │ %8 = $(Expr(:foreigncall, :((:memchr,)), Ptr{UInt8}, svec(Ptr{UInt8}, Int32, UInt64), 0, :(:ccall), :(%7), 97, 0x0000000000000005, 0x0000000000000005, 97, :(%7)))::Ptr{UInt8} │ $(Expr(:gc_preserve_end, :(%2))) │ %10 = intrinsic Core.bitcast(Core.UInt, %8)::UInt64 │ %11 = builtin (%10 === 0x0000000000000000)::Bool └── goto JuliaLang#3 if not %11 2 ─ goto JuliaLang#4 3 ─ goto JuliaLang#4 4 ┄ %15 = φ (JuliaLang#2 => true, JuliaLang#3 => false)::Bool │ %16 = intrinsic Core.Intrinsics.not_int(%15)::Bool └── goto JuliaLang#5 5 ─ return %16 ) => Bool ``` **this PR:** ```julia-repl julia> code_typed(() -> startswith("https://example.com/foo", "https://"), ())[1] CodeInfo( 1 ─ return true ) => Bool julia> code_typed(() -> endswith("foo.jl", ".jl"), ())[1] CodeInfo( 1 ─ return true ) => Bool julia> code_typed(() -> in('a', "aeiou"), ())[1] CodeInfo( 1 ─ return true ) => Bool ``` And `SubString{String}` queries can now be DCE'd when their result is unused. With `describe(ss)` calling `length`, `lastindex`, `textwidth`, `isascii`: **nightly:** ```llvm define i64 @julia_caller_0(...) { top: %"sret::NamedTuple" = alloca [4 x i64], align 8 call void @j_describe_0(ptr ... sret(...) align 8 %"sret::NamedTuple", ...) ret i64 42 } ``` **this PR:** ```llvm define i64 @julia_caller_0(...) { top: ret i64 42 } ``` --------- Co-authored-by: Claude <noreply@anthropic.com>
mkitti
pushed a commit
to mkitti/julia
that referenced
this pull request
May 22, 2026
mkitti
pushed a commit
to mkitti/julia
that referenced
this pull request
May 22, 2026
…ang#61625) Continuation of JuliaLang#61616 JuliaLang#61615 Developed with claude: --- Several `Char` predicates and a couple of `String` predicates in `base/strings/unicode.jl` and `base/strings/string.jl` are spuriously inferred as `nothrow=false`, even though they are provably total. This prevents the compiler from constant-folding them or from eliminating them when their result is unused. The root cause is that the compiler cannot prove that the `ismalformed` / `is_overlong_enc` guards rule out the throwing paths in their bodies — for example the `Int(::Cint)` conversion after the `utf8proc` ccall, or the `UInt32(::AbstractChar)` conversion in `category_code`. The `AbstractChar` contract guarantees that `UInt32(c)` only throws when `ismalformed(c)` returns `true`, and the `utf8proc_charwidth` result fits in a `Cint` so the `Int` conversion never throws `InexactError`. This PR annotates the affected definitions with `@assume_effects` and adds regression tests in `test/char.jl` and `test/strings/basic.jl`. ### Scope `Char`: - `textwidth`, `category_code`, `category_abbrev`, `category_string`, `isassigned`, `islowercase`, `isuppercase` `String`: - `isascii`, `lastindex`, `textwidth` `SubString{String}`: - `length`, `isascii`, `lastindex`, `textwidth` `String` / `SubString{String}` predicates: - `startswith`, `endswith`, `in(::Char, _)` Annotations are added on narrow concrete-type method overloads so they don't constrain user-defined `AbstractChar` / `AbstractString` subtypes. A static `@assert length(category_strings) == 32` is added so the `category_string(::Char)` annotation stays sound if the table is ever edited. The bodies of `startswith` / `endswith` are `_memcmp` byte-bounded by `sizeof(b) ≤ ncodeunits(a)` plus `nextind` / `thisind` calls on indices proven in range, so they cannot throw even on strings containing arbitrary, malformed UTF-8 byte sequences. A stress test exercises every annotated function on adversarial byte sequences and on `Char` values constructed from invalid bit-patterns to lock in the `:nothrow` soundness against future refactors. ### Effect on generated code `textwidth("hello world")` now constant-folds: **nightly:** ```julia-repl julia> code_typed(() -> textwidth("hello world"), ())[1] CodeInfo( 1 ─ %1 = invoke Base._foldl_impl( Base.MappingRF{typeof(textwidth), Base.BottomRF{typeof(+)}}( textwidth, Base.BottomRF{typeof(+)}(+))::Base.MappingRF{...}, 0::Int64, "hello world"::String)::Int64 └── return %1 ) => Int64 ``` **this PR:** ```julia-repl julia> code_typed(() -> textwidth("hello world"), ())[1] CodeInfo( 1 ─ return 11 ) => Int64 ``` Likewise, `startswith` / `endswith` / `in` over literal `String`s now constant-fold. **nightly:** ```julia-repl julia> code_typed(() -> startswith("https://example.com/foo", "https://"), ())[1] CodeInfo( 1 ─ %1 = invoke Main.startswith("https://example.com/foo"::String, "https://"::String)::Bool └── return %1 ) => Bool julia> code_typed(() -> endswith("foo.jl", ".jl"), ())[1] CodeInfo( 1 ─ %1 = invoke Main.endswith("foo.jl"::String, ".jl"::String)::Bool └── return %1 ) => Bool julia> code_typed(() -> in('a', "aeiou"), ())[1] CodeInfo( 1 ─ %1 = invoke Base.codeunits("aeiou"::String)::Base.CodeUnits{UInt8, String} │ %2 = $(Expr(:gc_preserve_begin, :(%1))) │ %3 = builtin Base.getfield(%1, :s)::String │ %4 = invoke Base.cconvert(Ptr{UInt8}::Type{Ptr{UInt8}}, %3::String)::String │ %5 = $(Expr(:foreigncall, :((:jl_string_ptr,)), Ptr{UInt8}, svec(Any), 0, :(:ccall), :(%4)))::Ptr{UInt8} │ %6 = intrinsic Base.add_ptr(%5, 0x0000000000000001)::Ptr{UInt8} │ %7 = intrinsic Base.sub_ptr(%6, 0x0000000000000001)::Ptr{UInt8} │ %8 = $(Expr(:foreigncall, :((:memchr,)), Ptr{UInt8}, svec(Ptr{UInt8}, Int32, UInt64), 0, :(:ccall), :(%7), 97, 0x0000000000000005, 0x0000000000000005, 97, :(%7)))::Ptr{UInt8} │ $(Expr(:gc_preserve_end, :(%2))) │ %10 = intrinsic Core.bitcast(Core.UInt, %8)::UInt64 │ %11 = builtin (%10 === 0x0000000000000000)::Bool └── goto JuliaLang#3 if not %11 2 ─ goto JuliaLang#4 3 ─ goto JuliaLang#4 4 ┄ %15 = φ (JuliaLang#2 => true, JuliaLang#3 => false)::Bool │ %16 = intrinsic Core.Intrinsics.not_int(%15)::Bool └── goto JuliaLang#5 5 ─ return %16 ) => Bool ``` **this PR:** ```julia-repl julia> code_typed(() -> startswith("https://example.com/foo", "https://"), ())[1] CodeInfo( 1 ─ return true ) => Bool julia> code_typed(() -> endswith("foo.jl", ".jl"), ())[1] CodeInfo( 1 ─ return true ) => Bool julia> code_typed(() -> in('a', "aeiou"), ())[1] CodeInfo( 1 ─ return true ) => Bool ``` And `SubString{String}` queries can now be DCE'd when their result is unused. With `describe(ss)` calling `length`, `lastindex`, `textwidth`, `isascii`: **nightly:** ```llvm define i64 @julia_caller_0(...) { top: %"sret::NamedTuple" = alloca [4 x i64], align 8 call void @j_describe_0(ptr ... sret(...) align 8 %"sret::NamedTuple", ...) ret i64 42 } ``` **this PR:** ```llvm define i64 @julia_caller_0(...) { top: ret i64 42 } ``` --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses the exp finding in #61394 (comment) @giordano
#61394
This PR
Developed with claude:
Use
table_unpack(N)inexp_impl_fastfor Float64 instead of a raw@inbounds J_TABLE[N&255 + 1]indexing. The compiler cannot prove the@inboundsaccess is safe, causing the function to be inferred as!n(not nothrow) and!u(not noundef), unlike its non-fast counterpartexp_implwhich already usestable_unpack(annotated with@assume_effects :nothrow). This affectedexp_fast,exp2_fast, andexp10_fastfor Float64 since they all go throughexp_impl_fast.