Repository: invenia/NamedDims.jl (julia)

Issue:
cat, hcat, vcat only work with numbered dimensions
I guess this could be added relatively easily? Would appreciate guidance but happy to give it a go.

```
julia> nda = NamedDimsArray(zeros(2, 5), (:x, :y))
2×5 NamedDimsArray{(:x, :y),Float64,2,Array{Float64,2}}:
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0

julia> cat(nda, ones(1, 5), dims=1)
3×5 NamedDimsArray{(:x, :y),Float64,2,Array{Float64,2}}:
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0
 1.0  1.0  1.0  1.0  1.0

julia> cat(nda, ones(1, 5), dims=x)
ERROR: UndefVarError: x not defined
...

julia> cat(nda, ones(1, 5), dims=:x)
ERROR: MethodError: no method matching isless(::Symbol, ::Int64)
...
```

Pull request:
Once completed, closes #123 

Here's my first attempt at the problem. The current state has a few improvements, namely:
1) Returns NamedDimsArray with correct dimensions
2) Errors when dimensions don't match
3) Can call cat with named dimensions

However, there are a few things that should be polished:
A) ~~Calling cat with `dims=:new` where `:new` is a new dimension errors.~~
B) ~~`cat` method is only defined for cases where there are two arrays (at least one of which is an NDA)~~
C) ~~Current naive hcat and vcat definitions create ambiguities, I think also because the Base methods accept arbitrary number of arguments.~~

Details on 1:
```
julia> a = [10 20; 31 40];

julia> nda = NamedDimsArray(a, (:x, :y));

julia> cat(nda, nda, dims=3)
2×2×2 NamedDimsArray{(:x, :y, :_),Int64,3,Array{Int64,3}}:
[:, :, 1] =
 10  20
 31  40

[:, :, 2] =
 10  20
 31  40

julia> cat(nda, nda, dims=:z)
ERROR: ArgumentError: Specified name (:z) does not match any dimension name ((:x, :y))
```

Details on 2:
```
julia> cat(nda, nda, nda', dims=2) # should error
2×6 NamedDimsArray{(:x, :y),Int64,2,Array{Int64,2}}:
 10  20  10  20  10  31
 31  40  31  40  20  40
```

Details on 3:
```
julia> hcat(nda, a)
ERROR: MethodError: hcat(::NamedDimsArray{(:x, :y),Int64,2,Array{Int64,2}}, ::Array{Int64,2}) is ambiguous. Candidates:
  hcat(A::Union{AbstractArray{T,1}, AbstractArray{T,2}}...) where T in Base at abstractarray.jl:1313
  hcat(a::NamedDimsArray, b::AbstractArray) in NamedDims at /Users/mzgubic/.julia/dev/NamedDims.jl/src/functions.jl:179
  hcat(A::Union{AbstractArray{T,1}, AbstractArray{T,2}} where T...) in Base at abstractarray.jl:1312
Possible fix, define
  hcat(::Union{NamedDimsArray{L,T,1,A} where A<:AbstractArray{T,1} where L, NamedDimsArray{L,T,1,A} where A<:AbstractArray{T,1} where L, NamedDimsArray{L,T,2,A} where A<:AbstractArray{T,2} where L, NamedDimsArray{L,T,2,A} where A<:AbstractArray{T,2} where L}, ::Union{AbstractArray{T,1}, AbstractArray{T,2}, AbstractArray{T,1}, AbstractArray{T,2}}) where T
Stacktrace:
 [1] top-level scope at REPL[186]:1
 [2] eval(::Module, ::Any) at ./boot.jl:331
 [3] eval_user_input(::Any, ::REPL.REPLBackend) at /Users/julia/buildbot/worker/package_macos64/build/usr/share/julia/stdlib/v1.4/REPL/src/REPL.jl:86
 [4] run_backend(::REPL.REPLBackend) at /Users/mzgubic/.julia/packages/Revise/BqeJF/src/Revise.jl:1184
 [5] top-level scope at none:0
```

Patch:
diff --git a/Project.toml b/Project.toml
index 61295b0..1125a3e 100644
--- a/Project.toml
+++ b/Project.toml
@@ -1,7 +1,7 @@
 name = "NamedDims"
 uuid = "356022a1-0364-5f58-8944-0da4b18d706f"
 authors = ["Invenia Technical Computing Corporation"]
-version = "0.2.23"
+version = "0.2.24"
 
 [deps]
 LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
diff --git a/src/NamedDims.jl b/src/NamedDims.jl
index 042e008..6f11733 100644
--- a/src/NamedDims.jl
+++ b/src/NamedDims.jl
@@ -26,6 +26,7 @@ include("broadcasting.jl")
 include("functions.jl")
 include("functions_dims.jl")
 include("functions_math.jl")
+include("cat.jl")
 
 @deprecate names dimnames false
 @deprecate refine_names NamedDimsArray true
diff --git a/src/cat.jl b/src/cat.jl
new file mode 100644
index 0000000..d76a11f
--- /dev/null
+++ b/src/cat.jl
@@ -0,0 +1,89 @@
+function Base.cat(a::NamedDimsArray{L}; dims) where L
+    newL = expand_dimnames(L, dims)
+    numerical_dims = dim(newL, dims)
+    data = Base.cat(parent(a); dims=numerical_dims) # Base.cat is type unstable
+    T = eltype(a)  # therefore the element type has to be inferred manually
+    N = length(newL)  # as must the size of the array
+    return NamedDimsArray{newL, T, N, Array{T,N}}(data)
+end
+
+# While the following two functions are covered by the general case below where splatted c
+# is empty, they are included because they reduce allocations to that of regular arrays.
+function Base.cat(a::NamedDimsArray{L}, b::AbstractArray; dims) where L
+    newL = expand_dimnames(L, dims)
+    numerical_dims = dim(newL, dims)
+    data = Base.cat(parent(a), b; dims=numerical_dims)
+    T = promote_type(eltype(a), eltype(b))
+    N = length(newL)
+    return NamedDimsArray{newL, T, N, Array{T,N}}(data)
+end
+
+function Base.cat(a::AbstractArray, b::NamedDimsArray{L}; dims) where L
+    newL = expand_dimnames(L, dims)
+    numerical_dims = dim(newL, dims)
+    data = Base.cat(a, parent(b); dims=numerical_dims)
+    T = promote_type(eltype(a), eltype(b))
+    N = length(newL)
+    return NamedDimsArray{newL, T, N, Array{T,N}}(data)
+end
+
+# to dispatch on the first _or the second_ argument being the NDA.
+for (T, S) in [
+    (:NamedDimsArray, :AbstractArray),
+    (:AbstractArray, :NamedDimsArray),
+    (:NamedDimsArray, :NamedDimsArray)
+    ]
+
+    @eval function Base.cat(a::$T, b::$S, c::AbstractArray...; dims)
+        combL = unify_names_shortest(dimnames(a), dimnames(b), ntuple(i->dimnames(c[i]), length(c))...)
+        newL = expand_dimnames(combL, dims)
+        numerical_dims = dim(newL, dims)
+        data = Base.cat(parent(a), parent(b), ntuple(i->parent(c[i]), length(c))...; dims=numerical_dims)
+        T = promote_type(eltype(a), eltype(b), ntuple(i->eltype(c[i]), length(c))...)
+        N = length(newL)
+        return NamedDimsArray{newL, T, N, Array{T,N}}(data)
+    end
+end
+
+# vcat and hcat
+function Base.hcat(a::NamedDimsArray{L}) where L
+    newL = expand_dimnames(L, 2)
+    data = Base.hcat(parent(a))
+    T = eltype(a)
+    N = length(newL)
+    return NamedDimsArray{newL, T, N, Array{T,N}}(data)
+end
+
+Base.vcat(a::NamedDimsArray{L}) where L = a
+
+# Base.hcat and Base.vcat specialise on this Union
+const AbsVecOrMat = Union{AbstractVector, AbstractMatrix}
+for (T, S) in [
+    (:NamedDimsArray, :AbsVecOrMat),
+    (:NamedDimsArray, :AbstractArray),
+    (:AbsVecOrMat, :NamedDimsArray),
+    (:AbstractArray, :NamedDimsArray),
+    (:NamedDimsArray, :NamedDimsArray)
+    ]
+
+    for (fun, d) in zip((:vcat, :hcat), (1, 2))
+        # this special case is needed to resolve ambiguities
+        @eval function Base.$fun(a::$T, b::$S)
+            combL = unify_names_shortest(dimnames(a), dimnames(b))
+            newL = expand_dimnames(combL, $d)
+            data = Base.$fun(parent(a), parent(b))
+            T = promote_type(eltype(a), eltype(b))
+            N = length(newL)
+            return NamedDimsArray{newL, T, N, Array{T,N}}(data)
+        end
+
+        @eval function Base.$fun(a::$T, b::$S, c::NamedDimsArray...)
+            combL = unify_names_shortest(dimnames(a), dimnames(b), ntuple(i->dimnames(c[i]), length(c))...)
+            newL = expand_dimnames(combL, $d)
+            data = Base.$fun(parent(a), parent(b), ntuple(i->parent(c[i]), length(c))...)
+            T = promote_type(eltype(a), eltype(b), ntuple(i->eltype(c[i]), length(c))...)
+            N = length(newL)
+            return NamedDimsArray{newL, T, N, Array{T,N}}(data)
+        end
+    end
+end
diff --git a/src/name_core.jl b/src/name_core.jl
index d3f44c0..5d23b9f 100644
--- a/src/name_core.jl
+++ b/src/name_core.jl
@@ -71,6 +71,40 @@ Base.@pure function dim_noerror(dimnames::Tuple{Vararg{Symbol, N}}, name::Symbol
     return 0
 end
 
+"""
+    expand_dimnames(dimnames, name)
+
+For `dimnames` being a tuple of names (symbols) for the dimensions.
+and `name` being a name.
+This expands the `dimnames` if `name` is not in `dimnames`.
+e.g. `expand_dimnames((:a, :b), :c) == (:a, :b, :c)`
+If `name` is already in `dimnames` then `dimnames` is returned.
+"""
+function expand_dimnames(dimnames::Tuple, name::Symbol)
+    if dim_noerror(dimnames, name) > 0  # name in dimnames, but optimised
+        return dimnames
+    else
+        return (dimnames..., name)
+    end
+end
+
+function expand_dimnames(dimnames::Tuple, name::Union{Colon, Tuple{}})
+    return dimnames
+end
+
+function expand_dimnames(dimnames::Tuple, name::Integer)
+    if name <= length(dimnames)
+        return dimnames
+    else
+        extra_length = name - length(dimnames)
+        new_dimnames = ntuple(i->:_, extra_length)
+        return (dimnames..., new_dimnames...)
+    end
+end
+
+function expand_dimnames(dimnames::Tuple, names)
+    expand_dimnames(expand_dimnames(dimnames, names[1]), names[2:end])
+end
 
 
 """
@@ -240,6 +274,10 @@ function unify_names_longest(names_a, names_b)
     return compile_time_return_hack(ret)
 end
 
+function unify_names_longest(names_a, names_b, names_c::Vararg{<:NTuple{Nd, Symbol} where Nd, N}) where N
+    return unify_names_longest(names_a, unify_names_longest(names_b, names_c...))
+end
+
 unify_names_shortest(names, ::Tuple{}) = ()
 unify_names_shortest(::Tuple{}, names) = ()
 unify_names_shortest(::Tuple{}, ::Tuple{}) = ()
@@ -260,6 +298,10 @@ function unify_names_shortest(names_a, names_b)
     return compile_time_return_hack(ret)
 end
 
+function unify_names_shortest(names_a, names_b, names_c::Vararg{<:NTuple{Nd, Symbol} where Nd, N}) where N
+    return unify_names_shortest(names_a, unify_names_shortest(names_b, names_c...))
+end
+
 # The following are helpers for remaining_dimnames_from_indexing
 # as a generated function it can get unhappy if asked to use anon functions
 # and it can only call function declared before it. So we declare them explictly here.


Test patch:
diff --git a/test/cat.jl b/test/cat.jl
new file mode 100644
index 0000000..71a7961
--- /dev/null
+++ b/test/cat.jl
@@ -0,0 +1,97 @@
+@testset "cat" begin
+    a = [10 20; 31 40]
+    nda = NamedDimsArray(a, (:x, :y))
+
+    @testset "basic functionality" begin
+        # check cat when NDAs are involved gives the same result as cat on arrays
+        for d in 1:3
+            @test cat(a; dims=d) ==
+                  parent(cat(nda; dims=d))
+            @test cat(a, a; dims=d) ==
+                  parent(cat(nda, a; dims=d)) ==
+                  parent(cat(a, nda; dims=d)) ==
+                  parent(cat(nda, nda; dims=d))
+            @test cat(a, a, a; dims=d) ==
+                  parent(cat(a, nda, nda; dims=d)) == 
+                  parent(cat(nda, a, nda; dims=d)) == 
+                  parent(cat(nda, nda, nda; dims=d))
+        end
+    end
+
+    @testset "dimensions requirements" begin
+        # check that conflicting dimensions are flagged as errors
+        for d in 1:3
+            @test_throws DimensionMismatch cat(nda, nda'; dims=d)
+            @test_throws DimensionMismatch cat(nda, nda, nda'; dims=d)
+            @test_throws DimensionMismatch cat(a, nda, nda'; dims=d)
+            @test_skip @test_throws DimensionMismatch cat(a, a, nda, nda'; dims=d)
+        end
+
+        # check that dimnames work as expected (for one, two, three args)
+        for d in 1:2
+            @test dimnames(cat(nda; dims=d)) == dimnames(nda)
+
+            @test dimnames(cat(nda, nda; dims=d)) == dimnames(nda)
+            @test dimnames(cat(nda, a; dims=d)) == dimnames(nda)
+            @test dimnames(cat(a, nda; dims=d)) == dimnames(nda)
+
+            @test dimnames(cat(nda, nda, nda; dims=d)) == dimnames(nda)
+            @test dimnames(cat(a, nda, nda; dims=d)) == dimnames(nda)
+            @test dimnames(cat(nda, a, nda; dims=d)) == dimnames(nda)
+        end
+
+        # and the same thing for dims=3
+        @test dimnames(cat(nda; dims=3)) == (dimnames(nda)..., :_)
+
+        @test dimnames(cat(nda, nda; dims=3)) == (dimnames(nda)..., :_)
+        @test dimnames(cat(nda, a; dims=3)) == (dimnames(nda)..., :_)
+        @test dimnames(cat(a, nda; dims=3)) == (dimnames(nda)..., :_)
+
+        @test dimnames(cat(nda, nda, nda; dims=3)) == (dimnames(nda)..., :_)
+        @test dimnames(cat(a, nda, nda; dims=3)) == (dimnames(nda)..., :_)
+        @test dimnames(cat(nda, a, nda; dims=3)) == (dimnames(nda)..., :_)
+    end
+
+    # check dims::Symbol cases
+    @testset "dims argument is named" begin
+        @test cat(nda, nda; dims=1) == cat(nda, nda; dims=:x)
+        @test cat(nda, nda; dims=2) == cat(nda, nda; dims=:y)
+
+        @test dimnames(cat(nda, nda; dims=:z)) == (:x, :y, :z)
+    end
+end
+
+for (f, d) in zip((vcat, hcat), (1, 2))
+    a = [10 20; 31 40]
+    nda = NamedDimsArray(a, (:x, :y))
+    v = [1, 2, 3, 4]
+    ndv = NamedDimsArray(v, (:x, ))
+
+    @testset "$f" begin
+        @testset "basic functionality" begin
+            @test f(nda) == cat(nda; dims=d)
+            @test f(ndv) == cat(ndv; dims=d)
+
+            @test f(nda, nda) == cat(nda, nda; dims=d)
+            @test f(a, nda) == cat(a, nda; dims=d)
+            @test f(nda, a) == cat(nda, a; dims=d)
+
+            @test f(nda, nda, nda) == cat(nda, nda, nda; dims=d)
+            @test f(a, nda, nda) == cat(a, nda, nda; dims=d)
+            @test f(nda, a, nda) == cat(nda, a, nda; dims=d)
+            @test f(nda, nda, a) == cat(nda, nda, a; dims=d)
+        end
+
+        @testset "dimension requirements" begin
+            @test_throws DimensionMismatch f(nda, nda')
+            @test_throws DimensionMismatch f(nda, nda, nda')
+
+            @test dimnames(f(nda)) == dimnames(cat(nda; dims=d))
+            @test dimnames(f(ndv)) == dimnames(cat(ndv; dims=d))
+
+            @test dimnames(f(nda, nda)) == dimnames(cat(nda, nda; dims=d))
+            @test dimnames(f(nda, a)) == dimnames(cat(nda, a; dims=d))
+            @test dimnames(f(a, nda)) == dimnames(cat(a, nda; dims=d))
+        end
+    end
+end
diff --git a/test/functions.jl b/test/functions.jl
index 2554a0c..e9552fd 100644
--- a/test/functions.jl
+++ b/test/functions.jl
@@ -39,7 +39,7 @@ using Statistics
 
         # Higher-dim case: `dims` keyword in `sort!` requires Julia v1.1+
         if VERSION > v"1.1-"
-            sort!(nda, dims=:y)
+            sort!(nda; dims=:y)
             @test issorted(a[2, :])
             @test_throws UndefKeywordError sort!(nda)
 
@@ -52,11 +52,11 @@ using Statistics
         a = [10 20; 31 40]
         nda = NamedDimsArray(a, (:x, :y)) # size (2,2)
 
-        nda1 = sum(nda, dims=1)           # size (1,2)
-        nda2 = sum(nda, dims=2)           # size (2,1)
+        nda1 = sum(nda; dims=1)           # size (1,2)
+        nda2 = sum(nda; dims=2)           # size (2,1)
         @testset "ndims==2" begin
-            @test f!(nda1, nda) == f!(nda1, a) == f(a, dims=1)
-            @test f!(nda2, nda) == f!(nda2, a) == f(a, dims=2)
+            @test f!(nda1, nda) == f!(nda1, a) == f(a; dims=1)
+            @test f!(nda2, nda) == f!(nda2, a) == f(a; dims=2)
 
             @test dimnames(f!(nda1, nda)) == (:x, :y) == dimnames(f!(nda1, a))
             @test dimnames(f!(nda2, nda)) == (:x, :y) == dimnames(f!(nda2, a))
@@ -68,10 +68,10 @@ using Statistics
             ndy = NamedDimsArray([5,6], :y)
             nd_ = NamedDimsArray([7,8], :_)
 
-            @test f!(ndx, nda) == f!([0,0], nda) == dropdims(f(a, dims=2), dims=2)
+            @test f!(ndx, nda) == f!([0,0], nda) == dropdims(f(a; dims=2); dims=2)
             @test f!(nd_, nda) == f!(ndx, a)
 
-            @test f!(ndy', nda) == f!([0 0], nda) == f(a, dims=1)
+            @test f!(ndy', nda) == f!([0 0], nda) == f(a; dims=1)
 
             @test_throws DimensionMismatch f!(ndy, nda) # name y on wrong dimension
         end
diff --git a/test/name_core.jl b/test/name_core.jl
index b9647b2..6fd7be9 100644
--- a/test/name_core.jl
+++ b/test/name_core.jl
@@ -5,6 +5,7 @@ using NamedDims:
     unify_names_longest,
     unify_names_shortest,
     dim_noerror,
+    expand_dimnames,
     tuple_issubset,
     tuple_cat,
     names_are_unifiable,
@@ -45,12 +46,16 @@ end
     @test unify_names_longest((:a,), (:a, :b,)) == (:a, :b)
     @test unify_names_longest((:a,), (:a, :_)) == (:a, :_)
     @test unify_names_longest((:a, :b), (:a, :_, :c)) == (:a, :b, :c)
+    @test unify_names_longest((:a,), (:a, :b), (:a, :_, :c)) == (:a, :b, :c)
+
+    @test_throws DimensionMismatch unify_names_longest((:a, :b, :c), (:b, :a))
 
     @test unify_names_shortest((:a,), (:a, :b,)) == (:a,)
     @test unify_names_shortest((:a,), (:a, :_)) == (:a,)
     @test unify_names_shortest((:a, :b), (:a, :_, :c)) == (:a, :b)
+    @test unify_names_shortest((:a,), (:a, :b), (:a, :_, :c)) == (:a,)
 
-    @test_throws DimensionMismatch unify_names_longest((:a, :b, :c), (:b, :a))
+    @test_throws DimensionMismatch unify_names_shortest((:a, :b, :c), (:b, :a))
 
     for unify in (unify_names, unify_names_longest, unify_names_shortest)
         @test unify((:a,), (:a,)) == (:a,)
@@ -83,17 +88,60 @@ end
     if VERSION >= v"1.1"
         @test 0 == @ballocated (()->unify_names_longest((:a, :b), (:a, :_, :c)))()
         @test 0 == @ballocated (()->unify_names_shortest((:a, :b), (:a, :_, :c)))()
+        @test 0 == @ballocated (()->unify_names_longest((:a,), (:a, :b), (:a, :_, :c)))()
+        @test 0 == @ballocated (()->unify_names_shortest((:a,), (:a, :b), (:a, :_, :c)))()
         @test 0 == @ballocated (()->names_are_unifiable((:a, :b), (:a, :_)))()
         @test 0 == @ballocated (()->names_are_unifiable((:a, :b), (:a, :c)))()
     else
         @test_broken 0 == @ballocated (()->unify_names_longest((:a, :b), (:a, :_, :c)))()
         @test_broken 0 == @ballocated (()->unify_names_shortest((:a, :b), (:a, :_, :c)))()
+        @test_broken 0 == @ballocated (()->unify_names_longest((:a,), (:a, :b), (:a, :_, :c)))()
+        @test_broken 0 == @ballocated (()->unify_names_shortest((:a,), (:a, :b), (:a, :_, :c)))()
         @test_broken 0 == @ballocated (()->names_are_unifiable((:a, :b), (:a, :_)))()
         @test_broken 0 == @ballocated (()->names_are_unifiable((:a, :b), (:a, :c)))()
     end
     @test 0 == @ballocated (()->names_are_unifiable((:a, :b), (:a, :b)))()
 end
 
+@testset "expand_dimnames" begin
+    @testset "single symbol" begin
+        @test expand_dimnames((), :x) == (:x,)
+        @test expand_dimnames((:x,), ()) == (:x,)
+
+        @test expand_dimnames((:x, :y), :x) == (:x, :y)
+        @test expand_dimnames((:x, :y), :z) == (:x, :y, :z)
+    end
+
+    @testset "multiple symbols" begin
+        @test expand_dimnames((), (:x, :y)) == (:x, :y)
+        @test expand_dimnames((:x, :y), (:z, :w)) == (:x, :y, :z, :w)
+        @test expand_dimnames((:x, :y), (:x, :w)) == (:x, :y, :w)
+    end
+
+    @testset "numbers et al" begin
+        @test expand_dimnames((:x, :y), 1) == (:x, :y)
+        @test expand_dimnames((:x, :y), 3) == (:x, :y, :_)
+        @test expand_dimnames((:x, :y), (1, 3)) == (:x, :y, :_)
+        @test expand_dimnames((:x, :y), :) == (:x, :y)
+        @test expand_dimnames((:x, :y), ()) == (:x, :y)
+    end
+end
+
+@testset "allocations: expand_dims" begin
+    @testset "names" begin
+        @test 0 == @ballocated (()->NamedDims.expand_dimnames((:x, :y), :x))()
+        @test 0 == @ballocated (()->NamedDims.expand_dimnames((:x, :y), :z))()
+        @test 0 == @ballocated (()->NamedDims.expand_dimnames((:x, :y), (:x, :z)))()
+    end
+
+    @testset "numbers et al" begin
+        @test 0 == @ballocated (()->NamedDims.expand_dimnames((:x, :y), 1))()
+        @test 0 == @ballocated (()->NamedDims.expand_dimnames((:x, :y), 5))()
+        @test 0 == @ballocated (()->NamedDims.expand_dimnames((:x, :y), (1, 5)))()
+        @test 0 == @ballocated (()->NamedDims.expand_dimnames((:x, :y), :))()
+        @test 0 == @ballocated (()->NamedDims.expand_dimnames((:x, :y), ()))()
+    end
+end
 
 @testset "order_named_inds" begin
     @test order_named_inds(Val((:x,))) == (:,)
diff --git a/test/runtests.jl b/test/runtests.jl
index f60ee6d..e2038b4 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -10,6 +10,7 @@ const testfiles = (
     "functions.jl",
     "functions_dims.jl",
     "functions_math.jl",
+    "cat.jl",
     "broadcasting.jl",
     "tracker_compat.jl",
 )