diff --git a/src/InlineStrings.jl b/src/InlineStrings.jl index c3b946e..7a9b925 100644 --- a/src/InlineStrings.jl +++ b/src/InlineStrings.jl @@ -71,9 +71,61 @@ end const SmallInlineStrings = Union{String1, String3, String7, String15} -# used to zero out n lower bytes of an inline string -clear_n_bytes(s, n) = Base.shl_int(Base.lshr_int(s, 8 * n), 8 * n) -_bswap(x::T) where {T <: InlineString} = Base.bswap_int(x) + +@inline get_byte(x::T, i::Int) where {T <: InlineString} = + Base.trunc_int(UInt8, Base.lshr_int(x, 8 * (i - 1))) + +@inline function set_byte(x::T, i::Int, b::UInt8) where {T <: InlineString} + bit_pos = 8 * (i - 1) + mask = Base.not_int(Base.shl_int(Base.zext_int(T, 0xff), bit_pos)) + cleared = Base.and_int(x, mask) + return Base.or_int(cleared, Base.shl_int(Base.zext_int(T, b), bit_pos)) +end + +@inline get_capacity_byte(x::T) where {T <: InlineString} = + Base.trunc_int(UInt8, Base.lshr_int(x, 8 * (sizeof(T) - 1))) + +@inline function set_capacity_byte(x::T, b::UInt8) where {T <: InlineString} + bit_pos = 8 * (sizeof(T) - 1) + mask = Base.not_int(Base.shl_int(Base.zext_int(T, 0xff), bit_pos)) + cleared = Base.and_int(x, mask) + return Base.or_int(cleared, Base.shl_int(Base.zext_int(T, b), bit_pos)) +end + +@inline function clear_suffix_bytes(x::T, n::Int) where {T <: InlineString} + n == 0 && return x + n >= sizeof(T) && return create_with_length(T, 0) + result = create_with_length(T, 0) + keep_bytes = sizeof(T) - n + for i in 1:keep_bytes + result = set_byte(result, i, get_byte(x, i)) + end + return result +end + +@inline function clear_prefix_bytes(x::T, n::Int) where {T <: InlineString} + n == 0 && return x + capacity = get_capacity_byte(x) + data_only = Base.and_int(x, Base.not_int(Base.shl_int(Base.zext_int(T, 0xff), 8 * (sizeof(T) - 1)))) + shifted_data = Base.lshr_int(data_only, 8 * n) + return set_capacity_byte(shifted_data, capacity) +end + +@inline function create_with_length(::Type{T}, length::Int) where {T <: InlineString} + capacity_byte = trailing_byte(T, length) + return Base.shl_int(Base.zext_int(T, capacity_byte), 8 * (sizeof(T) - 1)) +end + +@inline function get_string_data(x::T) where {T <: InlineString} + capacity_mask = Base.shl_int(Base.zext_int(T, 0xff), 8 * (sizeof(T) - 1)) + return Base.and_int(x, Base.not_int(capacity_mask)) +end + +@inline function resize_string_data(x::S, ::Type{T}) where {S <: InlineString, T <: InlineString} + sizeof(T) == sizeof(S) && return x + data = get_string_data(x) + return sizeof(T) > sizeof(S) ? Base.zext_int(T, data) : Base.trunc_int(T, data) +end const InlineStringTypes = Union{InlineString1, InlineString3, @@ -113,18 +165,20 @@ Base.widen(::Type{InlineString63}) = InlineString127 Base.widen(::Type{InlineString127}) = InlineString255 Base.widen(::Type{InlineString255}) = String -Base.ncodeunits(x::InlineString) = Int(Base.trunc_int(UInt8, x)) +trailing_byte(::Type{T}, len) where {T <: InlineString} = UInt8(sizeof(T) - len - 1) + +Base.ncodeunits(x::InlineString) = Core.sizeof(x) - Int(get_capacity_byte(x)) - 1 Base.codeunit(::InlineString) = UInt8 Base.@propagate_inbounds function Base.codeunit(x::T, i::Int) where {T <: InlineString} @boundscheck checkbounds(Bool, x, i) || throw(BoundsError(x, i)) - return Base.trunc_int(UInt8, Base.lshr_int(x, 8 * (sizeof(T) - i))) + return get_byte(x, i) end function Base.String(x::T) where {T <: InlineString} len = ncodeunits(x) out = Base._string_n(len) - ref = Ref{T}(_bswap(x)) + ref = Ref{T}(x) GC.@preserve ref out begin ptr = convert(Ptr{UInt8}, Base.unsafe_convert(Ptr{T}, ref)) unsafe_copyto!(pointer(out), ptr, len) @@ -133,17 +187,17 @@ function Base.String(x::T) where {T <: InlineString} end function Base.Symbol(x::T) where {T <: InlineString} - ref = Ref{T}(_bswap(x)) + ref = Ref{T}(x) return ccall(:jl_symbol_n, Ref{Symbol}, (Ref{T}, Int), ref, sizeof(x)) end Base.cconvert(::Type{Ptr{UInt8}}, x::T) where {T <: InlineString} = - Ref{T}(_bswap(clear_n_bytes(x, 1))) + Ref{T}(x) Base.cconvert(::Type{Ptr{Int8}}, x::T) where {T <: InlineString} = - Ref{T}(_bswap(clear_n_bytes(x, 1))) + Ref{T}(x) function Base.cconvert(::Type{Cstring}, x::T) where {T <: InlineString} - ref = Ref{T}(_bswap(clear_n_bytes(x, 1))) + ref = Ref{T}(x) Base.containsnul(Ptr{Int8}(pointer_from_objref(ref)), sizeof(x)) && throw(ArgumentError("embedded NULs are not allowed in C strings: $x")) return ref @@ -173,25 +227,24 @@ function Base.show(io::IO, s::InlineString) # So `repr` shows how to recreate ` end end -# add a codeunit to end of string method function addcodeunit(x::T, b::UInt8) where {T <: InlineString} - len = Base.trunc_int(UInt8, x) + len = Base.trunc_int(UInt8, ncodeunits(x)) sz = Base.trunc_int(UInt8, sizeof(T)) - shf = Base.zext_int(Int16, max(0x01, sz - len - 0x01)) << 3 - x = Base.or_int(x, Base.shl_int(Base.zext_int(T, b), shf)) - return Base.add_int(x, Base.zext_int(T, 0x01)), (len + 0x01) >= sz + x = set_byte(x, len + 1, b) + x = set_capacity_byte(x, get_capacity_byte(x) - 0x01) + return x, (len + 0x01) >= sz end for T in (:InlineString1, :InlineString3, :InlineString7, :InlineString15, :InlineString31, :InlineString63, :InlineString127, :InlineString255) - @eval $T() = Base.zext_int($T, 0x00) - + @eval $T() = create_with_length($T, 0) @eval function $T(x::AbstractString) if typeof(x) === String && sizeof($T) <= sizeof(UInt) len = sizeof(x) len < sizeof($T) || stringtoolong($T, len) y = GC.@preserve x unsafe_load(convert(Ptr{$T}, pointer(x))) - sz = 8 * (sizeof($T) - len) - return Base.or_int(Base.shl_int(Base.lshr_int(_bswap(y), sz), sz), Base.zext_int($T, UInt8(len))) + # Clear unused bytes and set capacity byte + cleared = clear_suffix_bytes(y, sizeof($T) - len) + return set_capacity_byte(cleared, trailing_byte($T, len)) else len = ncodeunits(x) len < sizeof($T) || stringtoolong($T, len) @@ -219,8 +272,9 @@ for T in (:InlineString1, :InlineString3, :InlineString7, :InlineString15, :Inli return y else y = GC.@preserve buf unsafe_load(convert(Ptr{$T}, pointer(buf, pos))) - sz = 8 * (sizeof($T) - len) - return Base.or_int(Base.shl_int(Base.lshr_int(_bswap(y), sz), sz), Base.zext_int($T, UInt8(len))) + # Clear unused bytes and set capacity byte + cleared = clear_suffix_bytes(y, sizeof($T) - len) + return set_capacity_byte(cleared, trailing_byte($T, len)) end end @@ -253,12 +307,12 @@ for T in (:InlineString1, :InlineString3, :InlineString7, :InlineString15, :Inli # trying to compress len = sizeof(x) len > (sizeof($T) - 1) && stringtoolong($T, len) - y = Base.trunc_int($T, Base.lshr_int(x, 8 * (sizeof(S) - sizeof($T)))) - return Base.add_int(y, Base.zext_int($T, UInt8(len))) + y = resize_string_data(x, $T) + return set_capacity_byte(y, trailing_byte($T, len)) else # promoting smaller InlineString to larger - y = Base.shl_int(Base.zext_int($T, Base.lshr_int(x, 8)), 8 * (sizeof($T) - sizeof(S) + 1)) - return Base.add_int(y, Base.zext_int($T, UInt8(sizeof(x)))) + y = resize_string_data(x, $T) + return set_capacity_byte(y, trailing_byte($T, sizeof(x))) end end end @@ -291,7 +345,7 @@ end Base.:(==)(x::T, y::T) where {T <: InlineString} = Base.eq_int(x, y) function Base.:(==)(x::String, y::T) where {T <: InlineString} sizeof(x) == sizeof(y) || return false - ref = Ref{T}(_bswap(y)) + ref = Ref{T}(y) GC.@preserve x begin return ccall(:memcmp, Cint, (Ptr{UInt8}, Ref{T}, Csize_t), pointer(x), ref, sizeof(x)) == 0 @@ -299,14 +353,29 @@ function Base.:(==)(x::String, y::T) where {T <: InlineString} end Base.:(==)(y::InlineString, x::String) = x == y -Base.cmp(a::T, b::T) where {T <: InlineString} = - Base.eq_int(a, b) ? 0 : Base.ult_int(a, b) ? -1 : 1 +function Base.cmp(a::T, b::T) where {T <: InlineString} + Base.eq_int(a, b) && return 0 + + len_a = ncodeunits(a) + len_b = ncodeunits(b) + min_len = min(len_a, len_b) + + for i in 1:min_len + byte_a = get_byte(a, i) + byte_b = get_byte(b, i) + if byte_a != byte_b + return byte_a < byte_b ? -1 : 1 + end + end + + return len_a < len_b ? -1 : (len_a > len_b ? 1 : 0) +end @static if isdefined(Base, :hash_bytes) function Base.hash(x::T, h::UInt) where {T <: InlineString} len = ncodeunits(x) - ref = Ref{T}(_bswap(x)) + ref = Ref{T}(x) GC.@preserve ref begin ptr = convert(Ptr{UInt8}, Base.unsafe_convert(Ptr{T}, ref)) return Base.hash_bytes(ptr, len, UInt64(h), Base.HASH_SECRET) % UInt @@ -317,7 +386,7 @@ else function Base.hash(x::T, h::UInt) where {T <: InlineString} h += Base.memhash_seed - ref = Ref{T}(_bswap(x)) + ref = Ref{T}(x) return ccall(Base.memhash, UInt, (Ref{T}, Csize_t, UInt32), ref, sizeof(x), h % UInt32) + h @@ -347,7 +416,7 @@ function Base.read(s::IO, ::Type{T}) where {T <: InlineString} end function Base.print(io::IO, x::T) where {T <: InlineString} - ref = Ref{T}(_bswap(x)) + ref = Ref{T}(x) return GC.@preserve ref begin ptr = convert(Ptr{UInt8}, Base.unsafe_convert(Ptr{T}, ref)) unsafe_write(io, ptr, sizeof(x)) @@ -357,14 +426,9 @@ end function Base.isascii(x::T) where {T <: InlineString} len = ncodeunits(x) - x = Base.lshr_int(x, 8 * (sizeof(T) - len)) - for _ = 1:(len >> 2) - y = Base.trunc_int(UInt32, x) - (y & 0xff000000) >= 0x80000000 && return false - (y & 0x00ff0000) >= 0x00800000 && return false - (y & 0x0000ff00) >= 0x00008000 && return false - (y & 0x000000ff) >= 0x00000080 && return false - x = Base.lshr_int(x, 32) + for i in 1:len + byte_val = get_byte(x, i) + byte_val >= 0x80 && return false end return true end @@ -382,12 +446,14 @@ function Base.chop(s::InlineString; head::Integer = 0, tail::Integer = 1) return _subinlinestring(s, i, j) end + # `i`, `j` must be `isvalid` string indexes @inline function _subinlinestring(s::T, i::Integer, j::Integer) where {T <: InlineString} new_n = max(0, nextind(s, j) - i) # new ncodeunits jx = nextind(s, j) - 1 # last codeunit to keep - s = clear_n_bytes(s, sizeof(typeof(s)) - jx) - return Base.or_int(Base.shl_int(s, (i - 1) * 8), _oftype(typeof(s), new_n)) + s = clear_suffix_bytes(s, sizeof(typeof(s)) - jx) + s = clear_prefix_bytes(s, (i - 1)) + return set_capacity_byte(s, trailing_byte(T, new_n)) end Base.getindex(s::InlineString, r::AbstractUnitRange{<:Integer}) = getindex(s, Int(first(r)):Int(last(r))) @@ -433,9 +499,8 @@ end new_n = n - nprefix # call `nextind` for each "character" (not codeunit) in prefix i = min(n + 1, max(nextind(s, firstindex(s), lprefix), 1)) - s = clear_n_bytes(s, 1) # clear out the length bits - s = Base.shl_int(s, (i - 1) * 8) # clear out prefix - return Base.or_int(s, _oftype(typeof(s), new_n)) + s = clear_prefix_bytes(s, (i - 1)) + return set_capacity_byte(s, trailing_byte(typeof(s), new_n)) end throw_strip_argument_error() = @@ -479,8 +544,8 @@ _chopsuffix(s::InlineString, suffix::AbstractString) = _chopsuffix(s, ncodeunits @inline function _chopsuffix(s::InlineString, nsuffix::Int) n = ncodeunits(s) new_n = n - nsuffix - s = clear_n_bytes(s, sizeof(typeof(s)) - new_n) - return Base.or_int(s, _oftype(typeof(s), new_n)) + s = clear_suffix_bytes(s, sizeof(typeof(s)) - new_n) + return set_capacity_byte(s, trailing_byte(typeof(s), new_n)) end function Base.rstrip(f, s::InlineString) @@ -503,16 +568,19 @@ function Base.chomp(s::InlineString) if i < 1 || codeunit(s, i) != 0x0a return s elseif i < 2 || codeunit(s, i - 1) != 0x0d - return Base.or_int(clear_n_bytes(s, sizeof(typeof(s)) - i + 1), _oftype(typeof(s), len - 1)) + s = clear_suffix_bytes(s, sizeof(typeof(s)) - i + 1) + return set_capacity_byte(s, trailing_byte(typeof(s), len - 1)) else - return Base.or_int(clear_n_bytes(s, sizeof(typeof(s)) - i + 2), _oftype(typeof(s), len - 2)) + s = clear_suffix_bytes(s, sizeof(typeof(s)) - i + 2) + return set_capacity_byte(s, trailing_byte(typeof(s), len - 2)) end end function Base.first(s::T, n::Integer) where {T <: InlineString} newlen = nextind(s, min(lastindex(s), nextind(s, 0, n))) - 1 i = sizeof(T) - newlen - return Base.or_int(clear_n_bytes(s, i), _oftype(typeof(s), newlen)) + s = clear_suffix_bytes(s, i) + return set_capacity_byte(s, trailing_byte(T, newlen)) end function Base.last(s::T, n::Integer) where {T <: InlineString} @@ -520,39 +588,41 @@ function Base.last(s::T, n::Integer) where {T <: InlineString} i = max(1, prevind(s, nc, n)) i == 1 && return s newlen = nc - i - # clear out the length bits before shifting left - s = clear_n_bytes(s, 1) - return Base.or_int(Base.shl_int(s, (i - 1) * 8), _oftype(typeof(s), newlen)) + s = clear_prefix_bytes(s, (i - 1)) + return set_capacity_byte(s, trailing_byte(T, newlen)) end Base.reverse(x::String1) = x function Base.reverse(s::T) where {T <: InlineString} nc = ncodeunits(s) + nc <= 1 && return s + + result = create_with_length(T, nc) + if isascii(s) - len = Base.zext_int(T, Base.trunc_int(UInt8, s)) - x = Base.or_int(Base.shl_int(_bswap(s), 8 * (sizeof(T) - nc)), len) - return x - end - x = Base.zext_int(T, Base.trunc_int(UInt8, s)) - i = 1 - while i <= nc - j = nextind(s, i) - _x = Base.lshr_int(s, 8 * (sizeof(T) - (j - 1))) - n = j - i - _x = Base.and_int(_x, n == 1 ? Base.zext_int(T, 0xff) : - n == 2 ? Base.zext_int(T, 0xffff) : - n == 3 ? Base.zext_int(T, 0xffffff) : - Base.zext_int(T, 0xffffffff)) - _x = Base.shl_int(_x, 8 * (sizeof(T) - (nc - (i - 1)))) - x = Base.or_int(x, _x) - i = j + for i in 1:nc + result = set_byte(result, nc - i + 1, get_byte(s, i)) + end + else + dest_offs = nc + 1 + src_pos = 1 + + for c in s + char_len = ncodeunits(c) + dest_offs -= char_len + for i in 1:char_len + result = set_byte(result, dest_offs + i - 1, get_byte(s, src_pos + i - 1)) + end + src_pos += char_len + end end - return x + + return result end @inline function Base.__unsafe_string!(out, x::T, offs::Integer) where {T <: InlineString} n = sizeof(x) - ref = Ref{T}(_bswap(x)) + ref = Ref{T}(x) GC.@preserve ref out begin ptr = convert(Ptr{UInt8}, Base.unsafe_convert(Ptr{T}, ref)) unsafe_copyto!(pointer(out, offs), ptr, n) @@ -594,11 +664,24 @@ function _string(a::Ta, b::Tb) where {Ta <: SmallInlineStrings, Tb <: SmallInlin T = summed_type(Ta, Tb) len_a = sizeof(a) len_b = sizeof(b) - # Remove length byte (lshr), grow to new size (zext), move chars forward (shl). - a2 = Base.shl_int(Base.zext_int(T, Base.lshr_int(a, 8)), 8 * (sizeof(T) - sizeof(Ta) + 1)) - b2 = Base.shl_int(Base.zext_int(T, Base.lshr_int(b, 8)), 8 * (sizeof(T) - sizeof(Tb) + 1 - len_a)) - lb = _oftype(T, len_a + len_b) # new length byte - return Base.or_int(Base.or_int(a2, b2), lb) + total_len = len_a + len_b + + # Create result with correct capacity + result = create_with_length(T, total_len) + + # Copy bytes from first string + for i in 1:len_a + byte_val = get_byte(a, i) + result = set_byte(result, i, byte_val) + end + + # Copy bytes from second string + for i in 1:len_b + byte_val = get_byte(b, i) + result = set_byte(result, len_a + i, byte_val) + end + + return result end summed_type(::Type{InlineString1}, ::Type{InlineString1}) = InlineString3 @@ -624,7 +707,7 @@ function Base.repeat(x::T, r::Integer) where {T <: InlineString} ccall(:memset, Ptr{Cvoid}, (Ptr{UInt8}, Cint, Csize_t), out, b, r) else for i = 0:r-1 - ref = Ref{T}(_bswap(x)) + ref = Ref{T}(x) GC.@preserve ref out begin ptr = convert(Ptr{UInt8}, Base.unsafe_convert(Ptr{T}, ref)) unsafe_copyto!(pointer(out, i * n + 1), ptr, n) @@ -640,7 +723,7 @@ Base.startswith(a::InlineString, b::InlineString) = invoke(startswith, Tuple{Abs function Base.startswith(a::T, b::Union{String, SubString{String}}) where {T <: InlineString} cub = ncodeunits(b) ncodeunits(a) < cub && return false - ref = Ref{T}(_bswap(a)) + ref = Ref{T}(a) return GC.@preserve ref begin ptr = convert(Ptr{UInt8}, Base.unsafe_convert(Ptr{T}, ref)) if Base._memcmp(ptr, b, sizeof(b)) == 0 @@ -657,7 +740,7 @@ function Base.endswith(a::T, b::Union{String, SubString{String}}) where {T <: In cub = ncodeunits(b) astart = ncodeunits(a) - ncodeunits(b) + 1 astart < 1 && return false - ref = Ref{T}(_bswap(a)) + ref = Ref{T}(a) return GC.@preserve ref begin ptr = convert(Ptr{UInt8}, Base.unsafe_convert(Ptr{T}, ref)) if Base._memcmp(ptr + (astart - 1), b, sizeof(b)) == 0 @@ -851,6 +934,7 @@ sortvalue(o::Perm, i::Int) = sortvalue(o.order, o.data[i]) sortvalue(o::Lt, x ) = error("sortvalue does not work with general Lt Orderings") sortvalue(rev::ReverseOrdering, x) = Base.not_int(sortvalue(rev.fwd, x)) sortvalue(::Base.ForwardOrdering, x) = x +sortvalue(::Base.ForwardOrdering, x::InlineString) = Base.bswap_int(get_string_data(x)) _oftype(::Type{T}, x::S) where {T, S} = sizeof(T) == sizeof(S) ? Base.bitcast(T, x) : sizeof(T) > sizeof(S) ? Base.zext_int(T, x) : Base.trunc_int(T, x) diff --git a/test/runtests.jl b/test/runtests.jl index ba91609..0a4b037 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -573,6 +573,94 @@ end @test inlinestrings(["a", "b", ""]) == [String1("a"), String1("b"), String1("")] @test String1("") == "" +@testset "C-compatibility" begin + @testset "Basic C string functions" begin + for S in SUBTYPES + data = randstring(Core.sizeof(S) - 1) + str = S(data) + @test (@ccall strlen(str::Cstring)::Csize_t) == length(data) + end + end + + @testset "C string comparison (strcmp)" begin + # Test equal strings + s1 = InlineString15("hello") + s2 = InlineString15("hello") + regular_str = "hello" + + @test (@ccall strcmp(s1::Cstring, s2::Cstring)::Cint) == 0 + @test (@ccall strcmp(s1::Cstring, regular_str::Cstring)::Cint) == 0 + + # Test different strings + s3 = InlineString15("hello") + s4 = InlineString15("world") + result = @ccall strcmp(s3::Cstring, s4::Cstring)::Cint + @test result < 0 # "hello" < "world" + + result2 = @ccall strcmp(s4::Cstring, s3::Cstring)::Cint + @test result2 > 0 # "world" > "hello" + + # Test with different lengths + s5 = InlineString15("test") + s6 = InlineString15("testing") + result3 = @ccall strcmp(s5::Cstring, s6::Cstring)::Cint + @test result3 < 0 # "test" < "testing" + end + + @testset "C string functions with special characters" begin + # Test with empty string + empty_str = InlineString7("") + @test (@ccall strlen(empty_str::Cstring)::Csize_t) == 0 + + # Test with single character + single_char = InlineString3("a") + @test (@ccall strlen(single_char::Cstring)::Csize_t) == 1 + @test (@ccall strcmp(single_char::Cstring, "a"::Cstring)::Cint) == 0 + + # Test with numbers and special chars (but not null) + special_str = InlineString31("abc123!@#") + @test (@ccall strlen(special_str::Cstring)::Csize_t) == 9 + @test (@ccall strcmp(special_str::Cstring, "abc123!@#"::Cstring)::Cint) == 0 + + # Test case sensitivity + lower_str = InlineString7("hello") + upper_str = InlineString7("HELLO") + result = @ccall strcmp(lower_str::Cstring, upper_str::Cstring)::Cint + @test result > 0 # lowercase comes after uppercase in ASCII + end + + @testset "C compatibility across all InlineString types" begin + test_strings = ["a", "ab", "abc", "test", "hello world"] + + for test_str in test_strings + for S in SUBTYPES + if length(test_str) < Core.sizeof(S) + inline_str = S(test_str) + + # Test strlen + @test (@ccall strlen(inline_str::Cstring)::Csize_t) == length(test_str) + + # Test strcmp with original string + @test (@ccall strcmp(inline_str::Cstring, test_str::Cstring)::Cint) == 0 + + # Test that the string content is identical at byte level + @test (@ccall memcmp(inline_str::Ptr{UInt8}, test_str::Ptr{UInt8}, length(test_str)::Csize_t)::Cint) == 0 + end + end + end + end + + @testset "C string safety - no embedded nulls" begin + # Test that strings without embedded nulls work fine + safe_str = InlineString15("safe string") + @test (@ccall strlen(safe_str::Cstring)::Csize_t) == 11 + + # Test that attempting to create Cstring with embedded null throws + str_with_null = InlineString15("has\0null") + @test_throws ArgumentError Base.cconvert(Cstring, str_with_null) + end +end + # only test package extension on >= 1.9.0 if VERSION >= v"1.9.0" && Sys.WORD_SIZE == 64 include(joinpath(dirname(pathof(InlineStrings)), "../ext/tests.jl"))