Repository: AlgebraicJulia/Catlab.jl
Pull request #665 (2022-08-22 16:36:02)

Refactor ACSets to use CompTime.jl
This is a major change to how acsets are implemented.

First of all, the code for acsets is generated using CompTime.jl. This brings along the benefits of being able to have "dynamic acsets" and "static acsets" share the same code, where with dynamic acsets all of the code is executed at runtime but with static acsets some of the code gets executed ahead of time.

One other change is that indexing is now handled at a lower level than acsets, in a new file called "ACSetColumns.jl". Index management is now not code-generated; it is handled with normal Julia method dispatch. Hopefully this will ease the maintenance burden, and make innovation easier.

This won't pass checks until CompTime is a registered package, which should happen by the afternoon of August 22nd.

github-actions[bot]: ### Review Checklist

Does this PR follow the [development guidelines](https://github.com/AlgebraicJulia/Catlab.jl/wiki/Guidelines-for-PR-writers-and-reviewers)? Following is a *partial* checklist:

**Tests**

- [ ] New features and bug fixes have unit tests
- [ ] New modules have tests that are ultimately called by the test runner (`test/runtests.jl`)
- [ ] Existing tests have not been deleted
- [ ] Code coverage &gt;= 90% or reduction justified in PR

**Documentation**

- [ ] All exported functions, types, and constants have docstrings, written in complete sentences
- [ ] Citations are given for any constructions, algorithms, or code drawn from external sources

**Other**

- [ ] Style guidelines are followed, including indent width 2
- [ ] Changes breaking backwards compatibility have been approved

Patch:
diff --git a/Project.toml b/Project.toml
index 51d2bdd4f..388b7d2e5 100644
--- a/Project.toml
+++ b/Project.toml
@@ -6,6 +6,7 @@ version = "0.14.3"
 
 [deps]
 Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
+CompTime = "0fb5dd42-039a-4ca4-a1d7-89a96eae6d39"
 Compose = "a81c6b42-2e10-5240-aca2-a61377ecd94b"
 DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
 GeneralizedGenerated = "6b9d7cbe-bcb9-11e9-073f-15a7a543e2eb"
@@ -27,6 +28,7 @@ Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
 
 [compat]
 Colors = "0.12"
+CompTime = "0.1"
 Compose = "0.7, 0.8, 0.9"
 DataStructures = "0.17, 0.18"
 GeneralizedGenerated = "0.2, 0.3"
diff --git a/docs/src/apis/categorical_algebra.md b/docs/src/apis/categorical_algebra.md
index 0d19989e5..c4cfb79a4 100644
--- a/docs/src/apis/categorical_algebra.md
+++ b/docs/src/apis/categorical_algebra.md
@@ -58,7 +58,7 @@ An acset $$F$$ on a schema consists of...
 
 For those with a categorical background, an acset on a schema $$S$$ consists of a functor from $$S$$ to $$\mathsf{Set}$$, such that objects in $$S^{-1}(0)$$ map to finite sets, and objects in $$S^{-1}(1)$$ map to sets that represent types. For any particular functor $$K \colon S^{-1}(1) \to \mathsf{Set}$$, we can also take the category of acsets that restrict to this map on $$S^{-1}$$.
 
-We can also add relations to this presentation, but we currently do nothing with those relations in the implementation; they mostly serve as documentation.
+We can also add equations to this presentation, but we currently do nothing with those equations in the implementation; they mostly serve as documentation.
 
 We will now give an example of how this all works in practice.
 
@@ -92,22 +92,47 @@ end
 
 ### API
 
-We first give an overview of the data types used in the acset machinery.
+The mathematical abstraction of an acset can of course be implemented in many different ways. Currently, there are three implementations of acsets in Catlab, which share a great deal of code.
 
-`FreeSchema` A finite presentation of a category that will be used as the schema of a database in the *algebraic databases* conception of categorical database theory. Functors out of a schema into FinSet are combinatorial structures over the schema. Attributes in a schema allow you to encode numerical (any julia type) into the database. You can find several examples of schemas in `Catlab.Graphs` where they define categorical versions of graph theory.
+These implementations can be split into two categories.
 
-`CSet/AttributedCSet` is a struct/constructors whose values (tables, indices) are parameterized by a CatDesc/AttrDesc. These are in memory databases over the schema equiped with `ACSetTranformations` as natural transformations that encode relationships between database instances.
+The first category is **static acset types**. In this implementation, different schemas correspond to different Julia types. Methods on these Julia types are then custom-generated for the schema, using [CompTime.jl](https://github.com/AlgebraicJulia/CompTime.jl).
 
-`CSetType/AttributedCSetType`provides a function to construct a julia type for ACSet instances, parameterized by CatDesc/AttrDesc. This function constructs the new type at runtime. In order to have the interactive nature of Julia, and to dynamically construct schemas based on runtime values, we need to define new Julia types at runtime. This function converts the schema spec to the corresponding Julia type.
+Under this category, there are two classes of static acset types. The first class is acset types that are generated using the `@acset_type` macro. These acset types are custom-derived structs. The advantage of this is that the structs have names like `Graph` or `WiringDiagram` that are printed out in error messages. The disadvantage is that if you are taking in schemas at runtime, you have to `eval` code in order to use them.
 
-`CatDesc/AttrDesc` the encoding of a schema into a Julia type. These exist because Julia only allows certain kinds of data in the parameter of a dependent type. Thus, we have to serialize a schema into those primitive data types so that we can use them to parameterize the ACSet type over the schema. This is an implementation detail subject to complete overhaul.
+Here is an example of using `@acset_type`
 
+```julia
+@acset_type WeightedGraph(SchWeightedGraph, index=[:src,:tgt])
+g = WeightedGraph()
+```
+
+The second class is `AnonACSet`s. Like acset types derived from `@acset_type`, these contain the schema in their type. However, they also contain the type of their fields in their types, so the types printed out in error messages are long and ugly. The advantage of these is that they can be used in situations where the schema is passed in at runtime, and they don't require using `eval` to create a new acset type.
+
+Here is an example of using `AnonACSet`
+
+```julia
+const WeightedGraph = AnonACSetType(SchWeightedGraph, index=[:src,:tgt])
+g = WeightedGraph()
+```
+
+The second category is **dynamic acset types**. Currently, there is just one type that falls under this category: `DynamicACSet`. This type has a **field** for the schema, and no code-generation is done for operations on acsets of this type. This means that if the schema is large compared to the data, this type will often be faster than the static acsets.
+
+However, dynamics acsets are a new addition to Catlab, and much of the machinery of limits, colimits, and other high-level acset constructions assumes that the schema of an acset can be derived from the type. Thus, more work will have to be done before dynamic acsets become a drop-in replacement for static acsets.
+
+Here is an example of using a dynamic acset
+
+```julia
+g = DynamicACSet("WeightedGraph", SchWeightedGraph; index=[:src,:tgt])
+```
 
 ```@autodocs
 Modules = [
   CategoricalAlgebra.CSets,
   CategoricalAlgebra.StructuredCospans,
   CategoricalAlgebra.ACSetInterface,
+  CategoricalAlgebra.ACSetColumns,
+  CategoricalAlgebra.CSetDataStructures
 ]
 Private = false
 ```
diff --git a/src/Catlab.jl b/src/Catlab.jl
index 798510288..f1dabf10c 100644
--- a/src/Catlab.jl
+++ b/src/Catlab.jl
@@ -5,6 +5,7 @@ include("theories/Theories.jl")
 
 include("categorical_algebra/IndexUtils.jl")
 include("categorical_algebra/ACSetInterface.jl")
+include("categorical_algebra/ACSetColumns.jl")
 include("categorical_algebra/CSetDataStructures.jl")
 include("categorical_algebra/Permutations.jl")
 include("graphs/Graphs.jl")
diff --git a/src/categorical_algebra/ACSetColumns.jl b/src/categorical_algebra/ACSetColumns.jl
new file mode 100644
index 000000000..705b3fc73
--- /dev/null
+++ b/src/categorical_algebra/ACSetColumns.jl
@@ -0,0 +1,276 @@
+"""
+An acset column should satisfy the following interface
+
+```julia
+Base.getindex
+Base.setindex!
+Base.values
+clear_index!
+codom_hint!
+preimage
+preimage_multi
+resize_clearing!
+```
+"""
+module ACSetColumns
+export preimage, preimage_multi, clear_index!, clear_indices!, codom_hint!, IndexedVector, resize_clearing!
+
+using ..IndexUtils
+
+
+"""
+This function takes an acset column and an element of the domain and sets the column
+value at that element be "missing"; 0 in the case of an integer, or nothing in the case
+of a type that is a supertype of nothing. It also clears the index of the previous value
+at the element.
+
+The semantics of this function are deeply janky because we don't have proper support
+for partial acsets; this will be fixed soon.
+"""
+function clear_index! end
+
+"""
+This is called to alert the column that there are new values in its codomain; the column
+may then potentially preallocate some space for those new values.
+"""
+function codom_hint! end
+
+"""
+This gets the preimage of a single value in the codomain.
+"""
+function preimage end
+
+"""
+This gets the preimage of several values in the codomain. This is semantically equivalent
+to broadcasting preimage, but a column implementation might instead return a view of
+the index.
+"""
+function preimage_multi end
+
+"""
+This resizes a column, and if the column grows, initializes the new elements to the "missing"
+value: 0 in the case of an integer or nothing in the case of a type that is a supertype of Nothing.
+
+The semantics of this function are deeply janky because we don't have proper support for partial
+acsets; this will be fixed soon. Specifically, if we have values that aren't a supertype of Nothing
+or an integer, we get random uninitialized memory.
+"""
+function resize_clearing! end
+
+# Additionally, the type should be able to be called with no arguments to create an empty column
+
+function clear_indices!(v, idxs)
+  for i in idxs
+    clear_index!(v, i)
+  end
+end
+
+function preimage(v::AbstractVector, x)
+  findall(y -> x == y, v)
+end
+
+function preimage_multi(v::AbstractVector, xs)
+  broadcast(x -> preimage(v,x), xs)
+end
+
+function clear_index!(v::AbstractVector{Int}, i::Int)
+  v[i] = 0
+end
+
+function clear_index!(v::AbstractVector{T}, i::Int) where {T >: Nothing}
+  v[i] = nothing
+end
+
+function clear_index!(v::AbstractVector{T}, i::Int) where {T}
+end
+
+function codom_hint!(v::AbstractVector{T}, n::Int) where {T}
+end
+
+function resize_clearing!(v::AbstractVector{Int}, n::Int)
+  oldn = length(v)
+  resize!(v, n)
+  v[(oldn+1):n] .= 0
+end
+
+function resize_clearing!(v::AbstractVector{T}, n::Int) where {T}
+  resize!(v, n)
+end
+
+struct IndexedVector{T,Index} <: AbstractVector{T}
+  vals::Vector{T}
+  index::Index
+end
+
+function IndexedVector{T,Index}() where {T,Index}
+  IndexedVector{T,Index}(T[],Index())
+end
+
+Base.copy(v::IndexedVector{T,Index}) where {T,Index} =
+  IndexedVector{T,Index}(copy(v.vals), deepcopy(v.index))
+
+Base.size(v::IndexedVector) = size(v.vals)
+
+function Base.getindex(v::IndexedVector{T}, i::Int) where {T}
+  v.vals[i]
+end
+
+function Base.setindex!(v::IndexedVector{T}, x::T, i::Int) where {T}
+  if isassigned(v.vals, i)
+    oldx = v.vals[i]
+    v.vals[i] = x
+    update_index!(v.index, x, oldx, i)
+  else
+    v.vals[i] = x
+    insert_index!(v.index, x, i)
+  end
+end
+
+function insert_index!(index::Vector{Vector{Int}}, x::Int, i::Int)
+  if x != 0
+    insertsorted!(index[x], i)
+  end
+end
+
+function insert_index!(index::Dict{T,Vector{Int}}, x::T, i::Int) where {T}
+  if !isnothing(x)
+    if x ∈ keys(index)
+      insertsorted!(index[x],i)
+    else
+      index[x] = [i]
+    end
+  end
+end
+
+function insert_index!(index::Vector{Int}, x::Int, i::Int)
+  if x != 0
+    @assert index[x] == 0
+    index[x] = i
+  end
+end
+
+function insert_index!(index::Dict{T,Int}, x::T, i::Int) where {T}
+  if !isnothing(x)
+    @assert !(x ∈ keys(index))
+    index[x] = i
+  end
+end
+
+Base.values(v::IndexedVector) = v.vals
+
+function update_index!(index::Vector{Vector{Int}}, x::Int, oldx::Int, i::Int)
+  if 1 ≤ oldx ≤ length(index)
+    deletesorted!(index[oldx], i)
+  end
+  insert_index!(index,x,i)
+end
+
+function update_index!(index::Dict{T,Vector{Int}}, x::T, oldx::T, i::Int) where {T}
+  if oldx ∈ keys(index) # oldx could just be gobbledegook
+    deletesorted!(index[oldx],i)
+  end
+  insert_index!(index,x,i)
+end
+
+function update_index!(index::Vector{Int}, x::Int, oldx::Int, i::Int)
+  if oldx != 0
+    index[oldx] = 0
+  end
+  insert_index!(index,x,i)
+end
+
+function update_index!(index::Dict{T,Int}, x::T, oldx::T, i::Int) where {T}
+  if oldx ∈ keys(index)
+    delete!(index, oldx)
+  end
+  insert_index!(index,x,i)
+end
+
+function resize_clearing!(v::IndexedVector{Int}, n::Int)
+  oldn = length(v.vals)
+  resize!(v.vals, n)
+  v.vals[(oldn+1):n] .= 0
+end
+
+function resize_clearing!(v::IndexedVector{T}, n::Int) where {T}
+  resize!(v.vals, n)
+end
+
+function clear_index!(v::IndexedVector{T}, i::Int) where {T >: Nothing}
+  v[i] = nothing
+end
+
+function clear_index!(v::IndexedVector{Int}, i::Int)
+  v[i] = 0
+end
+
+# There isn't an "empty" variable in this case, but we can still unset the index
+function clear_index!(v::IndexedVector{T,Dict{T,Vector{Int}}}, i::Int) where {T}
+  if isassigned(v.vals, i)
+    oldx = v[i]
+    if oldx ∈ keys(v.index)
+      deletesorted!(v.index[oldx], i)
+    end
+  end
+end
+
+# There isn't an "empty" variable in this case, but we can still unset the index
+function clear_index!(v::IndexedVector{T,Dict{T,Int}}, i::Int) where {T}
+  if isassigned(v.vals, i)
+    oldx = v[i]
+    if oldx ∈ keys(v.index)
+      delete!(v.index, oldx)
+    end
+  end
+end
+
+function preimage(v::IndexedVector{Int, <:Vector{<:Union{Int,Vector{Int}}}}, x::Int)
+  v.index[x]
+end
+
+function preimage(v::IndexedVector{T, Dict{T, Vector{Int}}}, x) where {T}
+  if x ∈ keys(v.index)
+    v.index[x]
+  else
+    []
+  end
+end
+
+function preimage(v::IndexedVector{T, Dict{T, Int}}, x) where {T}
+  if x ∈ keys(v.index)
+    v.index[x]
+  else
+    0
+  end
+end
+
+function preimage_multi(v::IndexedVector{Int, <:Vector{<:Union{Int,Vector{Int}}}},
+                  xs::Union{AbstractVector,UnitRange})
+  @view v.index[xs]
+end
+
+function preimage_multi(v::IndexedVector{T, <:Dict{T, <:Union{Int,Vector{Int}}}},
+                  xs::Union{AbstractVector,UnitRange}) where {T}
+  [preimage(v, x) for x in xs]
+end
+
+
+function codom_hint!(v::IndexedVector{T}, n::Int) where {T}
+  codom_hint_index!(v.index, n)
+end
+
+function codom_hint_index!(index::Vector{Vector{Int}}, n::Int)
+  oldn = length(index)
+  resize!(index, n)
+  for i in (oldn + 1):n
+    index[i] = Vector{Int}[]
+  end
+end
+
+function codom_hint_index!(index::Vector{Int}, n::Int)
+  oldn = length(index)
+  resize!(index, n)
+  index[(oldn + 1):n] .= 0
+end
+
+end
diff --git a/src/categorical_algebra/ACSetInterface.jl b/src/categorical_algebra/ACSetInterface.jl
index 8d09f59cf..c1b2a4dd9 100644
--- a/src/categorical_algebra/ACSetInterface.jl
+++ b/src/categorical_algebra/ACSetInterface.jl
@@ -1,7 +1,7 @@
 module ACSetInterface
-export ACSet, acset_schema,
+export ACSet, acset_schema, acset_name,
   nparts, parts, has_part, has_subpart, subpart, incident,
-  add_part!, add_parts!, set_subpart!, set_subparts!, rem_part!, rem_parts!,
+  add_part!, add_parts!, set_subpart!, set_subparts!, rem_part!, rem_parts!, clear_subpart!,
   copy_parts!, copy_parts_only!, disjoint_union, tables, pretty_tables, @acset
 
 using StaticArrays: StaticArray
@@ -20,6 +20,11 @@ Get the schema of an acset at runtime.
 """
 function acset_schema end
 
+"""
+Get the name of an acset at runtime
+"""
+function acset_name end
+
 """ Number of parts of given type in an acset.
 """
 function nparts end
@@ -84,6 +89,9 @@ subpart(acs, names::AbstractVector{Symbol}) =
   subpart(acs, subpart(acs, names[1]), names[2:end])
 subpart(acs, expr::GATExpr{:compose}) = subpart(acs, subpart_names(expr))
 
+Base.getindex(acs::ACSet, part, name) = subpart(acs, part, name)
+Base.getindex(acs::ACSet, name) = subpart(acs, name)
+
 subpart_names(expr::GATExpr{:generator}) = Symbol[first(expr)]
 subpart_names(expr::GATExpr{:id}) = Symbol[]
 subpart_names(expr::GATExpr{:compose}) =
@@ -175,9 +183,9 @@ function set_subpart! end
 
 # Inlined for the same reason as `subpart`.
 
-@inline function set_subpart!(acs, part::AbstractVector{Int}, name, vals)
-  broadcast(part, vals) do part, vals
-    set_subpart!(acs, part, name, vals)
+@inline function set_subpart!(acs, parts::AbstractVector{Int}, name, vals)
+  broadcast(parts, vals) do part, val
+    set_subpart!(acs, part, name, val)
   end
 end
 
@@ -193,6 +201,26 @@ end
 
 @inline set_subparts!(acs, part; kw...) = set_subparts!(acs, part, (;kw...))
 
+Base.setindex!(acs::ACSet, val, part, name) = set_subpart!(acs, part, name, val)
+Base.setindex!(acs::ACSet, vals, name) = set_subpart!(acs, name, vals)
+
+"""Clear a subpart in a C-set
+
+If the subpart is a hom, this is equivalent to setting it to 0
+If the subpart is an attr, then if the type has nothing as a subtype, it
+sets value to nothing. If the type doesn't have nothing as a subtype, then
+it does not change the value, but still unsets the index, so this can
+potentially cause an inconsistent acset if used without caution.
+"""
+
+function clear_subpart! end
+
+function clear_subpart!(acs, parts::AbstractVector{Int}, name)
+  for part in parts
+    clear_subpart!(acs, part, name)
+  end
+end
+
 """ Remove part from a C-set.
 
 The part is removed using the "pop and swap" strategy familiar from
@@ -287,7 +315,7 @@ end
 pretty_tables(acs::ACSet; kw...) = pretty_tables(stdout, acs; kw...)
 
 function Base.show(io::IO, ::MIME"text/plain", acs::T) where T <: ACSet
-  print(io, T)
+  print(io, acset_name(acs))
   print(io, " with elements ")
   join(io, ["$ob = $(parts(acs,ob))" for ob in keys(tables(acs))], ", ")
   println(io)
@@ -297,7 +325,7 @@ end
 function Base.show(io::IO, ::MIME"text/html", acs::T) where T <: ACSet
   println(io, "<div class=\"c-set\">")
   print(io, "<span class=\"c-set-summary\">")
-  print(io, T)
+  print(io, acset_name(acs))
   print(io, " with elements ")
   join(io, ["$ob = $(parts(acs,ob))" for ob in keys(tables(acs))], ", ")
   println(io, "</span>")
diff --git a/src/categorical_algebra/CSetDataStructures.jl b/src/categorical_algebra/CSetDataStructures.jl
index f9b540f1c..96c044c0e 100644
--- a/src/categorical_algebra/CSetDataStructures.jl
+++ b/src/categorical_algebra/CSetDataStructures.jl
@@ -1,34 +1,87 @@
 module CSetDataStructures
 export @acset_type, @abstract_acset_type, @declare_schema, FreeSchema,
-  StructACSet, StructCSet, ACSetTableType
+  StructACSet, DynamicACSet, SimpleACSet, AnonACSet, StructCSet, ACSetTableType
 
 using MLStyle
-using StaticArrays
 using Reexport
+using CompTime
 import Tables
 
 @reexport using ..ACSetInterface
+using ..ACSetColumns
 using ..IndexUtils
 using ...Theories, ...Present, ...Syntax
 using ...Theories: FreeSchema, SchemaDesc, SchemaDescType, CSetSchemaDescType,
   SchemaDescTypeType, ob_num, codom_num, attr, attrtype
 import ...Present: Presentation
 
+# Homebrew LVector
+#################
+
+struct LVector{T,Labels} <: AbstractVector{T}
+  v::Vector{T}
+  function LVector{T,Labels}(v::Vector{T}) where {T,Labels}
+    @assert length(v) == length(Labels)
+    new{T,Labels}(v)
+  end
+end
+
+Base.copy(v::LVector{T,Labels}) where {T,Labels} = LVector{T,Labels}(copy(v.v))
+
+@inline Base.getindex(v::LVector, i::Int) = v.v[i]
+
+@ct_enable function label_to_index(v::LVector{T,Labels}, @ct l) where {T,Labels}
+  @ct begin
+    i = findfirst(Labels .== l)
+    if i != nothing
+      i
+    else
+      error("label $l not found")
+    end
+  end
+end
+
+@inline Base.getindex(v::LVector, l::Symbol) = v.v[label_to_index(v,Val{l})]
+
+@inline Base.setindex!(v::LVector{T}, x::T, i::Int) where {T} =
+  setindex!(v.v, x, i)
+
+@inline Base.setindex!(v::LVector{T}, x::T, l::Symbol) where {T} =
+  setindex!(v.v, x, label_to_index(v, Val{l}))
+
+@inline Base.size(v::LVector) = size(v.v)
+@inline Base.length(v::LVector) = length(v.v)
+
+
 # StructACSet Struct Generation
 ###############################
 
-abstract type StructACSet{S<:SchemaDescType,Ts<:Tuple,Idxed,UniqueIdxed} <: ACSet end
+""" A `SimpleACSet` is an abstract type for any acset that has a certain layout
 
-const StructCSet = StructACSet{S,Tuple{},Idxed,UniqueIdxed} where
-  {S<:CSetSchemaDescType,Idxed,UniqueIdxed}
+Specifically, subtypes of `SimpleACSet` are expected to have a `parts` field
+which is a mapping from symbols to ints, and a `subparts` field which is a
+mapping from symbols to acset columns, which are any data structure that
+satisfies the interface given in ACSetColumns.jl.
+"""
+abstract type SimpleACSet <: ACSet end
 
-q(s::Symbol) = Expr(:quote,s)
-q(s::GATExpr) = q(nameof(s))
+""" A `StructACSet` is a SimpleACSet where the schema and the types assigned
+to the attrtypes are available in the type.
+"""
+abstract type StructACSet{S<:SchemaDescType,Ts<:Tuple} <: SimpleACSet end
 
-""" Creates a quoted named tuple used for `StructACSet`s
+""" A special case where there are no attributes.
+"""
+const StructCSet = StructACSet{S,Tuple{}} where
+  {S<:CSetSchemaDescType}
+
+q(s::Symbol) = s
+q(s::GATExpr) = nameof(s)
+
+""" Creates a type used for `StructACSet`s
 """
 function pi_type(dom::Vector, F::Function)
-  :(NamedTuple{($(q.(dom)...),),Tuple{$(F.(dom)...)}})
+  NamedTuple{Tuple(q.(dom)),Tuple{F.(dom)...}}
 end
 
 """ Creates a quoted element of a named tuple used for `StructACSet`s
@@ -41,54 +94,108 @@ function pi_type_elt(dom::Vector, f::Function)
   end
 end
 
+function genericize(T::Type, tvars::Vector{TypeVar})
+  occuring_variables = []
+  cur = T
+  for tvar in reverse(tvars)
+    next = UnionAll(tvar, cur)
+    if typeof(next) == UnionAll && next.var == tvar
+      push!(occuring_variables, tvar)
+      cur = next
+    end
+  end
+  if length(occuring_variables) > 0
+    :($cur{$([tvar.name for tvar in reverse(occuring_variables)]...)})
+  else
+    cur
+  end
+end
+
+function struct_acset(name::Symbol, parent, p::Presentation{ThSchema};
+                      index = [], unique_index = [])
+  homs = p.generators[:Hom]
+  attrs = p.generators[:Attr]
+  function column_type(f)
+    if f ∈ homs
+      if nameof(f) ∈ index
+        IndexedVector{Int,Vector{Vector{Int}}}
+      elseif nameof(f) ∈ unique_index
+        IndexedVector{Int,Vector{Int}}
+      else
+        Vector{Int}
+      end
+    elseif f ∈ attrs
+      if nameof(f) ∈ index
+        IndexedVector{T,Dict{T,Vector{Int}}} where {T}
+      elseif nameof(f) ∈ unique_index
+        IndexedVector{T,Dict{T,Int}} where {T}
+      else
+        Vector{T} where {T}
+      end
+    end
+  end
+  struct_acset(name, parent, p,
+               Dict{Symbol,Type}([nameof(f) => column_type(f) for f in vcat(homs,attrs)]))
+end
+
 """ Create the struct declaration for a `StructACSet` from a Presentation
 """
-function struct_acset(name::Symbol, parent, p::Presentation{ThSchema};
-                      index=[], unique_index=[])
+function struct_acset(name::Symbol, parent, p::Presentation{ThSchema},
+                      column_types::Dict{Symbol,Type})
   obs = p.generators[:Ob]
   homs = p.generators[:Hom]
-  attr_types = p.generators[:AttrType]
-  Ts = nameof.(attr_types)
+  attrtypes = p.generators[:AttrType]
+  Ts = nameof.(attrtypes)
+  Tvars = Dict{Symbol,TypeVar}(name => TypeVar(name) for name in Ts)
   attrs = p.generators[:Attr]
-  idxed = (;[x => x ∈ index for x in [nameof.(homs);nameof.(attrs)]]...)
-  unique_idxed = (;[x => x ∈ unique_index for x in [nameof.(homs);nameof.(attrs)]]...)
-  indexed_homs = filter(f -> idxed[nameof(f)], homs)
-  unique_indexed_homs = filter(f -> unique_idxed[nameof(f)], homs)
-  indexed_attrs = filter(a -> idxed[nameof(a)], attrs)
-  unique_indexed_attrs = filter(a -> unique_idxed[nameof(a)], attrs)
-  parameterized_type, new_call = if length(attr_types) > 0
-    (:($name{$(nameof.(attr_types)...)}), :(new{$(nameof.(attr_types)...)}))
+  function subpart_type(f)
+    if f ∈ attrs
+      T = Tvars[nameof(codom(f))]
+      if nameof(f) ∈ keys(column_types)
+        column_types[nameof(f)]{T}
+      else
+        Vector{T}
+      end
+    elseif f ∈ homs
+      if nameof(f) ∈ keys(column_types)
+        column_types[nameof(f)]
+      else
+        Vector{Int}
+      end
+    end
+  end
+  parameterized_type, new_call = if length(attrtypes) > 0
+    (:($name{$(Ts...)}), :(new{$(Ts...)}))
   else
     name, :new
   end
   schema_type = SchemaDescTypeType(p)
-  obs_t = :($(GlobalRef(StaticArrays, :MVector)){$(length(obs)), Int})
+  parts_t = :($(GlobalRef(CSetDataStructures, :LVector)){
+    Int64, $(Tuple([nameof.(obs)...]))
+  })
+  subparts_t = genericize(pi_type(vcat(homs,attrs), subpart_type),TypeVar[values(Tvars)...])
   quote
-    struct $parameterized_type <: $parent{$schema_type, Tuple{$(Ts...)}, $idxed, $unique_idxed}
-      obs::$obs_t
-      homs::$(pi_type(homs, _ -> :(Vector{Int})))
-      attrs::$(pi_type(attrs, a -> :(Vector{$(nameof(codom(a)))})))
-      hom_indices::$(pi_type(indexed_homs, _ -> :(Vector{Vector{Int}})))
-      hom_unique_indices::$(pi_type(unique_indexed_homs, _ -> :(Vector{Int})))
-      attr_indices::$(pi_type(indexed_attrs, a -> :(Dict{$(nameof(codom(a))),Vector{Int}})))
-      attr_unique_indices::$(pi_type(unique_indexed_attrs, a -> :(Dict{$(nameof(codom(a))), Int})))
-      function $parameterized_type() where {$(nameof.(attr_types)...)}
+    struct $parameterized_type <: $parent{$schema_type, Tuple{$(Ts...)}}
+      parts::$parts_t
+      subparts::$subparts_t
+      function $parameterized_type() where {$(nameof.(attrtypes)...)}
         $new_call(
-          $obs_t(zeros(Int, $(length(obs)))),
-          $(pi_type_elt(homs, _ -> :(Int[]))),
-          $(pi_type_elt(attrs, a -> :($(nameof(codom(a)))[]))),
-          $(pi_type_elt(indexed_homs, _ -> :(Vector{Int}[]))),
-          $(pi_type_elt(unique_indexed_homs, _ -> :(Int[]))),
-          $(pi_type_elt(indexed_attrs, a -> :(Dict{$(nameof(codom(a))),Vector{Int}}()))),
-          $(pi_type_elt(unique_indexed_attrs, a -> :(Dict{$(nameof(codom(a))),Int}())))
+          $parts_t(zeros(Int, $(length(obs)))),
+          $(pi_type_elt(vcat(homs,attrs), f -> :($(genericize(subpart_type(f), TypeVar[values(Tvars)...]))())))
         )
       end
+      function $parameterized_type(parts::$parts_t, subparts::$subparts_t) where {$(nameof.(attrtypes)...)}
+        $new_call(parts, subparts)
+      end
     end
   end
 end
 
 unquote(x::QuoteNode) = x.value
 
+""" This macro creates custom structs that subclass `StructACSet{S}` for specific `S`.
+These are used for acsets whose schema is known at compile time.
+"""
 macro acset_type(head)
   head, parent = @match head begin
     Expr(:(<:), h, p) => (h,p)
@@ -106,42 +213,154 @@ macro acset_type(head)
   end
 end
 
+""" We want control over the type class hierarchy of acsets; this allows us
+to create abstract types that subtype StructACSet. For instance, we might have
+an `AbstractGraph` type, and then assume (this is not enforced) that any
+subtype of `AbstractGraph` has `E,V,src,tgt` in its schema.
+"""
 macro abstract_acset_type(head)
   type, parent = @match head begin
     Expr(:(<:), h, p) => (h,p)
     _ => (head, GlobalRef(CSetDataStructures, :StructACSet))
   end
   esc(quote
-    abstract type $type{S,Ts,Idxed,UniqueIdxed} <: $parent{S,Ts,Idxed,UniqueIdxed} end
+    abstract type $type{S,Ts} <: $parent{S,Ts} end
   end)
 end
 
-# StructACSet DataFrames
-########################
-
-struct StructACSetFrame{Ob, S, Ts, Idxed, UniqueIdxed, Attrs} <:
-    StructACSet{S, Ts, Idxed, UniqueIdxed}
-  obs::MVector{1,Int}
-  homs::NamedTuple{(),Tuple{}}
-  attrs::Attrs
-  hom_indices::NamedTuple{(),Tuple{}}
-  hom_unique_indices::NamedTuple{(),Tuple{}}
-  attr_indices::NamedTuple{(),Tuple{}}
-  attr_unique_indices::NamedTuple{(),Tuple{}}
-  function StructACSetFrame{Ob, S, Ts, Idxed, UniqueIdxed, Attrs}() where
-      {Ob,S,Ts,Idxed,UniqueIdxed,Attrs}
-    new{Ob,S,Ts,Idxed,UniqueIdxed,Attrs}(
-      MVector(0),
-      (;),
-      Attrs([[] for a in attr(S)]),
-      (;),
-      (;),
-      (;),
-      (;)
+# FIXME: we have two slightly similar column_type functions
+# which are not exactly the same. Merge these.
+function column_type(s, f, attrtypes, index, unique_index)
+  if f ∈ s.homs
+    if f ∈ index
+      IndexedVector{Int,Vector{Vector{Int}}}
+    elseif f ∈ unique_index
+      IndexedVector{Int,Vector{Int}}
+    else
+      Vector{Int}
+    end
+  elseif f ∈ s.attrs
+    codom = attrtypes[s.codoms[f]]
+    if f ∈ index
+      IndexedVector{codom,Dict{codom, Vector{Int}}}
+    elseif f ∈ unique_index
+      IndexedVector{codom,Dict{codom,Int}}
+    else
+      Vector{codom}
+    end
+  end
+end
+
+""" This is a SimpleACSet which has the schema as a field value rather
+than as a type parameter.
+"""
+struct DynamicACSet <: SimpleACSet
+  name::String
+  schema::SchemaDesc
+  attrtypes::Dict{Symbol,Type}
+  parts::Dict{Symbol,Int}
+  subparts::Dict{Symbol,AbstractVector}
+  function DynamicACSet(
+    name::String,
+    p::Presentation;
+    attrtypes=Dict{Symbol,Type}(),
+    index::Vector{Symbol}=Symbol[],
+    unique_index::Vector{Symbol}=Symbol[]
+  )
+    s = SchemaDesc(p)
+
+    new(
+      name,
+      s,
+      attrtypes,
+      Dict(ob => 0 for ob in s.obs),
+      Dict(f => column_type(s,f,attrtypes,index,unique_index)() for f in [s.homs; s.attrs])
+    )
+  end
+  function DynamicACSet(
+    name::String,
+    schema::SchemaDesc,
+    attrtypes::Dict{Symbol,Type},
+    parts::Dict{Symbol,Int},
+    subparts::Dict{Symbol,AbstractVector}
+  )
+    new(name,schema,attrtypes,parts,subparts)
+  end
+end
+
+""" This works the same as something made with `@acset_type`, only the types of the
+parts and subparts are stored as type parameters. Thus, this can be used with any schema.
+"""
+struct AnonACSet{S,Ts,Parts,Subparts} <: StructACSet{S,Ts}
+  parts::Parts
+  subparts::Subparts
+  function AnonACSet{S,Ts,Parts,Subparts}(
+    parts::Parts,
+    subparts::Subparts
+  ) where {S,Ts,Parts,Subparts}
+    new{S,Ts,Parts,Subparts}(parts,subparts)
+  end
+
+  function AnonACSet{S,Ts,Parts,Subparts}() where {S,Ts,Parts,Subparts}
+    new{S,Ts,Parts,Subparts}(
+      Parts(zeros(Int64, length(S.parameters[1]))),
+      Subparts(T() for T in Subparts.parameters[2].parameters)
     )
   end
+
+  function AnonACSet(
+    p::Presentation;
+    attrtypes=Dict{Symbol,Type}(),
+    index::Vector{Symbol}=Symbol[],
+    unique_index::Vector{Symbol}=Symbol[]
+  )
+    T = AnonACSetType(p; attrtypes=attrtypes, index=index, unique_index=unique_index)
+    T()
+  end
+end
+
+""" This can be used to fill out the type parameters to an AnonACSet ahead of time.
+"""
+function AnonACSetType(
+  s::SchemaDesc;
+  attrtypes::Dict{Symbol, Type}=Dict{Symbol,Type}(),
+  index::Vector{Symbol}=Symbol[],
+  unique_index::Vector{Symbol}=Symbol[],
+  union_all::Bool=false
+)
+  (!union_all || isempty(attrtypes)) || error("If union_all is true, then attrtypes must be empty")
+  S = SchemaDescTypeType(s)
+  if union_all
+    type_vars = [TypeVar(at) for at in s.attrtypes]
+    attrtypes = Dict(zip(s.attrtypes, type_vars))
+  else
+    type_vars = [attrtypes[at] for at in s.attrtypes]
+  end
+  Ts = Tuple{type_vars...}
+  Parts = LVector{Int64, (s.obs...,)}
+  Subparts = NamedTuple{
+    ([s.homs; s.attrs]...,),
+    Tuple{(column_type(s,f,attrtypes,index,unique_index) for f in [s.homs; s.attrs])...}
+  }
+  T = AnonACSet{S,Ts,Parts,Subparts}
+  if union_all
+    foldr(UnionAll, type_vars; init=T)
+  else
+    T
+  end
+end
+
+function AnonACSetType(
+  p::Presentation;
+  attrtypes::Dict{Symbol, Type}=Dict{Symbol,Type}(),
+  index::Vector{Symbol}=Symbol[],
+  unique_index::Vector{Symbol}=Symbol[],
+  union_all::Bool=false
+)
+  AnonACSetType(SchemaDesc(p); attrtypes, index, unique_index, union_all)
 end
 
+
 function ACSetTableDesc(::Type{S}, ob::Symbol) where {S}
   s = SchemaDesc(S)
   attrs = filter(s.attrs) do attr
@@ -153,513 +372,316 @@ function ACSetTableDesc(::Type{S}, ob::Symbol) where {S}
   codoms = filter(s.codoms) do (f,i)
     f ∈ attrs
   end
-  s′ = SchemaDesc([ob], Symbol[], s.attrtypes, attrs, doms, codoms)
-  SchemaDescTypeType(s′)
+  SchemaDesc([ob], Symbol[], s.attrtypes, attrs, doms, codoms)
 end
 
 function ACSetTableDataType(::Type{<:StructACSet{S,Ts}}, ob::Symbol) where {S,Ts}
-  S′ = ACSetTableDesc(S,ob)
-  rowtype = NamedTuple{
-    attr(S′),
-    Tuple{[Vector{Ts.parameters[codom_num(S′,a)]} for a in attr(S′)]...}
-  }
-  StructACSetFrame{(ob,), S′, Ts,
-                   NamedTuple(a => false for a in attr(S′)),
-                   NamedTuple(a => false for a in attr(S′)),
-                   rowtype}
+  s = SchemaDesc(S)
+  s′ = ACSetTableDesc(S,ob)
+  attrtypes = Dict{Symbol, Type}(a => T for (a,T) in zip(s.attrtypes, Ts.parameters))
+  AnonACSetType(s′;attrtypes)
 end
 
 function ACSetTableUnionAll(::Type{<:StructACSet{S}}, ob::Symbol) where {S}
-  S′ = ACSetTableDesc(S,ob)
-  type_vars = [ TypeVar(at) for at in attrtype(S′) ]
-  rowtype = NamedTuple{
-    attr(S′),
-    Tuple{[Vector{type_vars[codom_num(S′,a)]} for a in attr(S′)]...}
-  }
-  T = StructACSetFrame{(ob,), S′, Tuple{type_vars...},
-                       NamedTuple(a => false for a in attr(S′)),
-                       NamedTuple(a => false for a in attr(S′)),
-                       rowtype}
-  foldr(UnionAll, type_vars, init=T)
+  s′ = ACSetTableDesc(S,ob)
+  AnonACSetType(s′;union_all=true)
 end
 
+""" This takes an ACSet type, and produces an AnonACSet which represents an
+acset with just the object passed in, and then all of the attributes of that
+object.
+
+TODO: rename this to be less confusing with ACSetTable. Maybe ASet (attributed
+set)
+"""
 function ACSetTableType(X::Type, ob::Symbol; union_all::Bool=false)
   (union_all ? ACSetTableUnionAll : ACSetTableDataType)(X, ob)
 end
 
-# StructACSet Operations
-########################
+Base.copy(acs::DynamicACSet) =
+  DynamicACSet(acs.name,acs.schema,acs.attrtypes,copy(acs.parts), deepcopy(acs.subparts))
 
-Presentation(::Type{<:StructACSet{S}}) where S = Presentation(S)
-Presentation(::StructACSet{S}) where S = Presentation(S)
+Base.copy(acs::T) where {T <: StructACSet} = T(copy(acs.parts), map(copy, acs.subparts))
 
-# Accessors
-###########
+Base.:(==)(acs1::T, acs2::T) where {T <: SimpleACSet} =
+  acs1.parts == acs2.parts && acs1.subparts == acs2.subparts
 
-function Base.:(==)(x1::T, x2::T) where T <: StructACSet
-  # The indices hold redundant information, so need not be compared.
-  unref(x) = x[]
-  unref.(values(x1.obs)) == unref.(values(x2.obs)) && x1.homs == x2.homs && x1.attrs == x2.attrs
-end
+ACSetInterface.acset_schema(acs::StructACSet{S}) where {S} = SchemaDesc(S)
+ACSetInterface.acset_schema(acs::DynamicACSet) = acs.schema
 
-ACSetInterface.acset_schema(::StructACSet{S}) where {S} = SchemaDesc(S)
+add_parts_with_indices!(acs::SimpleACSet, ob::Symbol, n::Int, index_sizes::NamedTuple) =
+  add_parts!(acs, ob, n)
 
-@inline ACSetInterface.nparts(acs::StructACSet, ob::Symbol) = _nparts(acs, Val{ob})
+@inline ACSetInterface.add_parts!(acs::StructACSet{S}, type::Symbol, n::Int) where {S} =
+  _add_parts!(acs, Val{S}, Val{type}, n)
 
-@generated function _nparts(acs::StructACSet{S}, ::Type{Val{ob}}) where {S,ob}
-  :(acs.obs[$(ob_num(S,ob))])
-end
+ACSetInterface.add_parts!(acs::DynamicACSet, type::Symbol, n::Int) =
+  runtime(_add_parts!, acs, acs.schema, type, n)
 
-@inline ACSetInterface.has_part(acs::StructACSet, ob::Symbol) = _has_part(acs, Val{ob})
+@ct_enable function _add_parts!(acs::SimpleACSet, @ct(S), @ct(ob), n::Int)
+  @ct s = SchemaDesc(S)
+  m = acs.parts[@ct ob]
+  nparts = m + n
+  newparts = (m+1):nparts
+  acs.parts[@ct ob] = nparts
 
-outgoing(acs::StructACSet, ob::Symbol) = _outgoing(acs, Val{ob})
+  @ct_ctrl for f in vcat(s.homs,s.attrs)
+    @ct_ctrl if s.doms[f] == ob
+      resize_clearing!(acs.subparts[@ct f], nparts)
+    end
+    @ct_ctrl if s.codoms[f] == ob
+      codom_hint!(acs.subparts[@ct f], nparts)
+    end
+  end
 
-@generated function _outgoing(::StructACSet{S}, ::Type{Val{ob}}) where {S,ob}
-  s = SchemaDesc(S)
-  Tuple(filter(f -> s.doms[f] == ob, vcat(s.homs,s.attrs)))
+  newparts
 end
 
-@generated function _has_part(acs::StructACSet{S}, ::Type{Val{ob}}) where
-  {obs, S <: SchemaDescType{obs}, ob}
-  ob ∈ obs
-end
+@inline ACSetInterface.nparts(acs::SimpleACSet, type::Symbol) = acs.parts[type]
 
-@inline ACSetInterface.has_subpart(acs::StructACSet, f::Symbol) = _has_subpart(acs, Val{f})
+ACSetInterface.has_part(acs::StructACSet{S}, ob::Symbol) where {S} =
+  _has_part(Val{S}, Val{ob})
 
-@generated function _has_subpart(acs::StructACSet{S}, ::Type{Val{f}}) where
-  {f, obs, homs, attrtypes, attrs, S <: SchemaDescType{obs,homs,attrtypes,attrs}}
-  f ∈ homs || f ∈ attrs
+ACSetInterface.has_part(acs::DynamicACSet, ob::Symbol) =
+  runtime(_has_part, acs.schema, ob)
+
+@ct_enable function _has_part(@ct(S), @ct(ob))
+  @ct s = SchemaDesc(S)
+  @ct ob ∈ s.obs
 end
 
-@inline ACSetInterface.subpart(acs::StructACSet, f::Symbol) = _subpart(acs,Val{f})
+outgoing(acs::StructACSet{S}, ob::Symbol) where {S} = _outgoing(Val{S}, Val{ob})
 
-@generated function _subpart(acs::StructACSet{S}, ::Type{Val{f}}) where {S,f}
-  s = SchemaDesc(S)
-  if f ∈ s.homs
-    :(acs.homs.$f)
-  elseif f ∈ s.attrs
-    :(acs.attrs.$f)
-  else
-    throw(ArgumentError("$(repr(f)) not in $(s.homs) or $(s.attrs)"))
-  end
+outgoing(acs::DynamicACSet, ob::Symbol) = runtime(_outgoing, acs.schema, ob)
+
+@ct_enable function _outgoing(@ct(S), @ct(ob))
+  @ct s = SchemaDesc(S)
+  @ct Tuple(filter(f -> s.doms[f] == ob, vcat(s.homs,s.attrs)))
 end
 
-@inline Base.getindex(acs::StructACSet, args...) = ACSetInterface.subpart(acs, args...)
+@inline ACSetInterface.subpart(acs::SimpleACSet, f::Symbol) = values(acs.subparts[f])
 
-@inline ACSetInterface.incident(acs::StructACSet, part, f::Symbol; copy::Bool=false) =
-  _incident(acs, part, Val{f}, Val{copy})
+@inline ACSetInterface.subpart(acs::SimpleACSet, part::Int, f::Symbol) = acs.subparts[f][part]
 
-broadcast_findall(xs, array::AbstractArray) =
-  broadcast(x -> findall(y -> x == y, array), xs)
+@inline ACSetInterface.has_subpart(acs::StructACSet{S}, f::Symbol) where {S} =
+  _has_subpart(Val{S}, Val{f})
 
-function get_attr_index(idx::AbstractDict, k)
-  get(idx, k, [])
+ACSetInterface.has_subpart(acs::DynamicACSet, f::Symbol) =
+  runtime(_has_subpart, acs.schema, f)
+
+@ct_enable function _has_subpart(@ct(S), @ct(f))
+  @ct s = SchemaDesc(S)
+  @ct f ∈ [s.homs; s.attrs]
 end
-function get_attr_index(idx::AbstractDict, k::AbstractArray)
-  get_attr_index.(Ref(idx), k)
+
+@inline ACSetInterface.incident(acs::SimpleACSet, part, f::Symbol) = preimage(acs.subparts[f], part)
+@inline ACSetInterface.incident(acs::SimpleACSet, parts::Union{AbstractVector,UnitRange}, f::Symbol) =
+  preimage_multi(acs.subparts[f], parts)
+@inline ACSetInterface.incident(acs::StructACSet{S}, ::Colon, f::Symbol) where {S} =
+  _incident(acs, Val{S}, :, Val{f})
+
+ACSetInterface.incident(acs::DynamicACSet, ::Colon, f::Symbol) where {S} =
+  runtime(_incident, acs, acs.schema, :, f)
+
+@ct_enable function _incident(acs::SimpleACSet, @ct(S), ::Colon, @ct(f))
+  @ct s = SchemaDesc(S)
+  incident(acs, parts(acs, @ct(s.codoms[f])), @ct(f))
 end
 
-"""
-We keep the main body of the code generating out of the @generated function
-so that the code-generating function only needs to be compiled once.
-"""
-function incident_body(s::SchemaDesc, idxed::AbstractDict{Symbol,Bool},
-                       unique_idxed::AbstractDict{Symbol,Bool}, f::Symbol,
-                       copy::Bool)
-  if f ∈ s.homs
-    if idxed[f]
-      quote
-        indices = $(GlobalRef(ACSetInterface,:view_or_slice))(acs.hom_indices.$f, part)
-        $(copy ? :(Base.copy.(indices)) : :(indices))
-      end
-    elseif unique_idxed[f]
-      quote
-        indices = $(GlobalRef(ACSetInterface,:view_or_slice))(acs.hom_unique_indices.$f, part)
-        $(copy ? :(Base.copy.(indices)) : :(indices))
-      end
-    else
-      :(broadcast_findall(part, acs.homs.$f))
-    end
-  elseif f ∈ s.attrs
-    if idxed[f]
-      quote
-        indices = get_attr_index(acs.attr_indices.$f, part)
-        $(copy ? :(Base.copy.(indices)) : :(indices))
-      end
-    elseif unique_idxed[f]
-      quote
-        get.(Ref(acs.attr_unique_indices.$f), part, Ref(0))
-      end
-    else
-      :(broadcast_findall(part, acs.attrs.$f))
-    end
-  else
-    throw(ArgumentError("$(repr(f)) not in $(s.homs)"))
+@inline ACSetInterface.set_subpart!(acs::StructACSet{S}, part::Int, f::Symbol, subpart) where {S} =
+  _set_subpart!(acs, Val{S}, part, Val{f}, subpart)
+
+ACSetInterface.set_subpart!(acs::DynamicACSet, part::Int, f::Symbol, subpart) =
+  runtime(_set_subpart!, acs, acs.schema, part, f, subpart)
+
+@ct_enable function _set_subpart!(acs::SimpleACSet, @ct(S), part, @ct(f), subpart)
+  @ct s = SchemaDesc(S)
+  @ct_ctrl if f ∈ s.homs
+    @assert 0 <= subpart <= acs.parts[@ct s.codoms[f]]
   end
+  acs.subparts[@ct f][part] = subpart
 end
 
-@generated function _incident(acs::StructACSet{S,Ts,Idxed,UniqueIdxed},
-                              part, ::Type{Val{f}}, ::Type{Val{copy}}) where
-  {S,Ts,Idxed,UniqueIdxed,f,copy}
-  incident_body(SchemaDesc(S),pairs(Idxed),pairs(UniqueIdxed),f,copy)
-end
+@inline ACSetInterface.clear_subpart!(acs::SimpleACSet, part::Int, f::Symbol) =
+  clear_index!(acs.subparts[f], part)
 
-# Mutators
-##########
+@inline ACSetInterface.rem_part!(acs::StructACSet{S}, type::Symbol, part::Int) where {S} =
+  _rem_part!(acs, Val{S}, Val{type}, part)
 
-"""
-This is a specialized function to add parts to an ACSet and preallocate the indices of
-morphisms leading from those parts. This is useful if you want to reduce your
-total number of allocations when allocating an acset if you already know ahead of
-time a reasonable bound on the size of the preimages of the morphisms that you are using.
-
-For instance, if you are making a cyclic graph, then you know that the preimages of
-src and tgt will all be of size 1, and hence you can avoid allocating a zero-size
-array, and then again allocating a 1-size array and instead just allocate a
-1-size array off the bat.
-
-This function is currently exposed, but is not well-integrated with wrappers
-around add_parts; only use this if you really need it for performance and
-understand what you are doing. Additionally, the only guarantee w.r.t. to this
-is that it works the same semantically as `add_parts!`; it might make your code
-faster, but it also might not. Only use this if you have the benchmarks to back
-it up.
-"""
-@inline add_parts_with_indices!(acs::StructACSet, ob::Symbol, n::Int, index_sizes::NamedTuple) =
-  _add_parts!(acs, Val{ob}, n, index_sizes)
-
-@inline ACSetInterface.add_parts!(acs::StructACSet, ob::Symbol, n::Int) =
-  _add_parts!(acs, Val{ob}, n, (;))
-
-function add_parts_body(s::SchemaDesc, idxed::AbstractDict,
-                        unique_idxed::AbstractDict, ob::Symbol,
-                        index_sized_homs::Vector)
-  code = quote
-    m = acs.obs[$(ob_num(s, ob))]
-    nparts = m + n
-    newparts = (m+1):m+n
-    acs.obs[$(ob_num(s, ob))] = nparts
-  end
-  for f in s.homs
-    if s.doms[f] == ob
-      push!(code.args, quote
-              resize!(acs.homs.$f, nparts)
-              acs.homs.$f[newparts] .= 0
-            end)
+ACSetInterface.rem_part!(acs::DynamicACSet, type::Symbol, part::Int) =
+  runtime(_rem_part!, acs, acs.schema, type, part)
+
+@ct_enable function _rem_part!(acs::SimpleACSet, @ct(S), @ct(ob), part)
+  @ct s = SchemaDesc(S)
+  @ct in_homs = filter(hom -> s.codoms[hom] == ob, s.homs)
+  @ct out_homs = filter(f -> s.doms[f] == ob, s.homs)
+  @ct out_attrs = filter(f -> s.doms[f] == ob, s.attrs)
+
+  last_part = acs.parts[@ct ob]
+
+  @ct_ctrl for hom in in_homs
+    incoming_to_part = copy(incident(acs, part, @ct hom))
+    # Nasty unique_indexing bs
+    if incoming_to_part != 0
+      clear_subpart!(acs, incoming_to_part, @ct hom)
     end
-    if s.codoms[f] == ob && idxed[f]
-      size = if f ∈ index_sized_homs
-        :(index_sizes[$(Expr(:quote, f))])
-      else
-        0
-      end
-      push!(code.args, quote
-            resize!(acs.hom_indices.$f, nparts)
-            for i in newparts
-              acs.hom_indices.$f[i] = Array{Int}(undef, $size)
-              empty!(acs.hom_indices.$f[i])
-            end
-            end)
-    elseif s.codoms[f] == ob && unique_idxed[f]
-      push!(code.args, quote
-            resize!(acs.hom_unique_indices.$f, nparts)
-            for i in newparts
-              acs.hom_unique_indices.$f[i] = 0
-            end
-            end)
+
+    incoming_to_last_part = copy(incident(acs, last_part, @ct hom))
+    if incoming_to_last_part != 0
+      set_subpart!(acs, incoming_to_last_part, (@ct hom), part)
     end
-    code
+
+    codom_hint!(acs.subparts[@ct hom], last_part - 1)
   end
-  for a in s.attrs
-    if s.doms[a] == ob
-      push!(code.args,:(resize!(acs.attrs.$a, nparts)))
+
+  @ct_ctrl for f in [out_homs; out_attrs]
+    if isassigned(acs.subparts[@ct f], last_part)
+      set_subpart!(acs, part, (@ct f), subpart(acs, last_part, @ct f))
     end
+    clear_subpart!(acs, last_part, @ct f)
+    resize_clearing!(acs.subparts[@ct f], last_part - 1)
   end
-  push!(code.args, :(return newparts))
-  code
-end
 
-""" This generates the _add_parts! methods for a specific object of a `StructACSet`.
-"""
-@generated function _add_parts!(acs::StructACSet{S,Ts,Idxed,UniqueIdxed},
-                                ::Type{Val{ob}}, n::Int,
-                                index_sizes::NamedTuple{index_sized_homs}) where
-  {S, Ts, Idxed, UniqueIdxed, ob, index_sized_homs}
-  add_parts_body(SchemaDesc(S),pairs(Idxed),pairs(UniqueIdxed),ob,[index_sized_homs...])
+  acs.parts[@ct ob] -= 1
 end
 
-@inline ACSetInterface.set_subpart!(acs::StructACSet, part::Int, f::Symbol, subpart) =
-  _set_subpart!(acs, part, Val{f}, subpart)
-
+# Copy Parts
+############
 
-function set_subpart_body(s::SchemaDesc, idxed::AbstractDict{Symbol,Bool},
-                          unique_idxed::AbstractDict{Symbol,Bool}, f::Symbol)
-  if f ∈ s.homs
-    if idxed[f]
-      quote
-        @assert 0 <= subpart <= acs.obs[$(ob_num(s, s.codoms[f]))]
-        old = acs.homs.$f[part]
-        acs.homs.$f[part] = subpart
-        if old > 0
-          @assert deletesorted!(acs.hom_indices.$f[old], part)
-        end
-        if subpart > 0
-          insertsorted!(acs.hom_indices.$f[subpart], part)
-        end
-        subpart
-      end
-    elseif unique_idxed[f]
-      quote
-        @assert 0 <= subpart <= acs.obs[$(ob_num(s, s.codoms[f]))]
-        @assert subpart == 0 || acs.hom_unique_indices.$f[subpart] == 0
-        old = acs.homs.$f[part]
-        acs.homs.$f[part] = subpart
-        if old > 0
-          acs.hom_unique_indices.$f[old] = 0
-        end
-        if subpart > 0
-          acs.hom_unique_indices.$f[subpart] = part
-        end
-        subpart
-      end
-    else
-      quote
-        @assert 0 <= subpart <= acs.obs[$(ob_num(s, s.codoms[f]))]
-        acs.homs.$f[part] = subpart
-        subpart
-      end
-    end
-  elseif f ∈ s.attrs
-    if idxed[f]
-      quote
-        if isassigned(acs.attrs.$f, part)
-          old = acs.attrs.$f[part]
-          unset_attr_index!(acs.attr_indices.$f, old, part)
-        end
-        acs.attrs.$f[part] = subpart
-        set_attr_index!(acs.attr_indices.$f, subpart, part)
-        subpart
-      end
-    elseif unique_idxed[f]
-      quote
-        @boundscheck @assert subpart ∉ keys(acs.attr_unique_indices.$f) "subpart not unique"
-        if isassigned(acs.attrs.$f, part)
-          old = acs.attrs.$f[part]
-          delete!(acs.attr_unique_indices.$f, old)
-        end
-        acs.attrs.$f[part] = subpart
-        acs.attr_unique_indices.$f[subpart] = part
-        subpart
-      end
-    else
-      :(acs.attrs.$f[part] = subpart)
-    end
-  else
-    throw(ArgumentError("$(f) not in $(s.homs) or $(s.attrs)"))
-  end
+@ct_enable function common_objects(@ct(S), @ct(S′))
+  @ct s,s′ = SchemaDesc(S), SchemaDesc(S′)
+  @ct Tuple(intersect(s.obs, s′.obs))
 end
 
-""" This generates the `_set_subparts!` method for a specific arrow (hom/attr) of a StructACSet
-"""
-@generated function _set_subpart!(acs::StructACSet{S,Ts,Idxed,UniqueIdxed},
-                                  part, ::Type{Val{f}}, subpart) where {S,Ts,Idxed,UniqueIdxed,f}
-  set_subpart_body(SchemaDesc(S),pairs(Idxed),pairs(UniqueIdxed),f)
-end
+ACSetInterface.copy_parts!(to::StructACSet{S}, from::StructACSet{S′}) where {S,S′} =
+  copy_parts!(to, from, common_objects(Val{S}, Val{S′}))
 
-@inline Base.setindex!(acs::StructACSet, val, ob, part) = set_subpart!(acs, ob, part, val)
+ACSetInterface.copy_parts!(to::DynamicACSet, from::DynamicACSet) =
+  copy_parts!(to, from, runtime(common_objects, to.schema, from.schema))
 
-@inline Base.setindex!(acs::StructACSet, val, ob) = set_subpart!(acs, ob, val)
+ACSetInterface.copy_parts!(to::SimpleACSet, from::SimpleACSet; kw...) =
+  copy_parts!(to, from, (;kw...))
 
-@inline ACSetInterface.rem_part!(acs::StructACSet, type::Symbol, part::Int) =
-  _rem_part!(acs, Val{type}, part)
+ACSetInterface.copy_parts!(to::SimpleACSet, from::SimpleACSet, obs::Tuple) =
+  copy_parts!(to, from, NamedTuple{obs}((:) for ob in obs))
 
-function rem_part_body(s::SchemaDesc, idxed::AbstractDict{Symbol,Bool},
-                       unique_idxed::AbstractDict{Symbol,Bool}, ob::Symbol)
-  in_homs = filter(hom -> s.codoms[hom] == ob, s.homs)
-  out_homs = filter(f -> s.doms[f] == ob, s.homs)
-  out_attrs = filter(f -> s.doms[f] == ob, s.attrs)
-  quote
-    last_part = @inbounds acs.obs[$(ob_num(s, ob))]
-    @assert 1 <= part <= last_part
-    # Unassign superparts of the part to be removed and also reassign superparts
-    # of the last part to this part.
-    for hom in $(Tuple(in_homs))
-      set_subpart!(acs, incidence_vector(acs, part, hom), hom, 0)
-      set_subpart!(acs, incidence_vector(acs, last_part, hom), hom, part)
+ACSetInterface.copy_parts!(to::StructACSet{S}, from::StructACSet{S′}, parts::NamedTuple) where {S,S′} =
+  _copy_parts!(to, Val{S}, from, Val{S′}, replace_colons(from, parts))
+
+ACSetInterface.copy_parts!(to::DynamicACSet, from::DynamicACSet, parts::NamedTuple) =
+  runtime(_copy_parts!, to, to.schema, from, from.schema, replace_colons(from, parts))
+
+@ct_enable function _copy_parts!(
+  to::SimpleACSet, @ct(S),
+  from::SimpleACSet, @ct(S′),
+  parts::NamedTuple{obs}
+) where {obs}
+
+  @ct begin
+    s, s′ = SchemaDesc(S), SchemaDesc(S′)
+    @assert obs ⊆ intersect(s.obs, s′.obs)
+    homs = intersect(s.homs, s′.homs)
+    homs = filter(homs) do hom
+      c, c′, d, d′ = s.doms[hom], s′.doms[hom], s.codoms[hom], s′.codoms[hom]
+      c == c′ && d == d′ && c ∈ obs && d ∈ obs
     end
+    hom_triples = [ (hom, s.doms[hom], s.codoms[hom]) for hom in homs ]
+    in_obs = unique!(map(last, hom_triples))
+  end
 
-    # This is a hack to avoid allocating a named tuple, because these parts
-    # are a union type, so there would be dynamic dispatch
-    $(Expr(:block, (map([out_homs; out_attrs]) do f
-         :($(Symbol("last_row_" * string(f))) =
-           if isassigned(subpart(acs, $(Expr(:quote,f))), last_part)
-             subpart(acs, last_part, $(Expr(:quote,f)))
-           else
-             nothing
-           end)
-       end)...))
-
-    # Clear any morphism and data attribute indices for last part.
-    $(Expr(:block,
-           (map(filter(f -> idxed[f] || unique_idxed[f], out_homs)) do hom
-              :(set_subpart!(acs, last_part, $(Expr(:quote, hom)), 0))
-            end)...))
-
-    for attr in $(Tuple(filter(f -> idxed[f] || unique_idxed[f], out_attrs)))
-      if isassigned(subpart(acs, attr), last_part)
-        unset_attr_index!(acs.attr_indices[attr], subpart(acs, last_part, attr), last_part)
+  newparts = copy_parts_only!(to, from, parts)
+  partmaps = NamedTuple{@ct Tuple(in_obs)}(((@ct_ctrl (
+    Dict{Int,Int}(zip(parts[@ct type], newparts[@ct type]))
+    for type in in_obs
+  )...),))
+
+  @ct_ctrl for (name, dom, codom) in hom_triples
+    for (p, newp) in zip(parts[@ct dom], newparts[@ct dom])
+      q = subpart(from, p, @ct name)
+      newq = get(partmaps[@ct codom], q, nothing)
+      if !isnothing(newq)
+        set_subpart!(to, newp, @ct(name), newq)
       end
     end
-
-    # Finally, delete the last part and update subparts of the removed part.
-    for f in $(Tuple(out_homs))
-      resize!(acs.homs[f], last_part - 1)
-    end
-    for a in $(Tuple(out_attrs))
-      resize!(acs.attrs[a], last_part - 1)
-    end
-    for f in $(Tuple(filter(f -> idxed[f], in_homs)))
-      resize!(acs.hom_indices[f], last_part - 1)
-    end
-    for f in $(Tuple(filter(f -> unique_idxed[f], in_homs)))
-      resize!(acs.hom_unique_indices[f], last_part - 1)
-    end
-    @inbounds acs.obs[$(ob_num(s, ob))] -= 1
-    if part < last_part
-      $(Expr(:block,
-             (map([out_homs; out_attrs]) do f
-                quote
-                  x = $(Symbol("last_row_" * string(f)))
-                  if !isnothing(x)
-                    set_subpart!(acs, part, $(Expr(:quote, f)), x)
-                  end
-                end
-             end)...))
-    end
   end
-end
 
-@generated function _rem_part!(acs::StructACSet{S,Ts,Idxed,UniqueIdxed}, ::Type{Val{ob}},
-                               part::Int) where {S,Ts,Idxed,UniqueIdxed,ob}
-  rem_part_body(SchemaDesc(S),pairs(Idxed),pairs(UniqueIdxed),ob)
+  newparts
 end
 
-# XXX: Hack because `incident` has variable return type (issue #597).
-incidence_vector(args...) = [x for x in incident(args...) if x != 0]
+ACSetInterface.copy_parts_only!(to::SimpleACSet, from::SimpleACSet; kw...) =
+  copy_parts!(to, from, (;kw...))
 
-function Base.copy(acs::StructACSet)
-  new_acs = typeof(acs)()
-  ACSetInterface.copy_parts!(new_acs, acs)
-  new_acs
-end
-
-""" Copy parts from a C-set to a C′-set.
+ACSetInterface.copy_parts_only!(to::StructACSet{S}, from::StructACSet{S′}) where {S,S′} =
+  copy_parts_only!(to, from, common_objects(Val{S}, Val{S′}))
 
-The selected parts must belong to both schemas. All subparts common to the
-selected parts, including data attributes, are preserved. Thus, if the selected
-parts form a sub-C-set, then the whole sub-C-set is preserved. On the other
-hand, if the selected parts do *not* form a sub-C-set, then some copied parts
-will have undefined subparts.
-"""
-@generated function ACSetInterface.copy_parts!(to::StructACSet{S},
-                                from::StructACSet{S′}; kw...) where {S, S′}
-  s, s′ = SchemaDesc(S), SchemaDesc(S′)
-  obs = intersect(s.obs, s′.obs)
-  :(copy_parts!(to, from, isempty(kw) ? $(Tuple(obs)) : (; kw...)))
-end
+ACSetInterface.copy_parts_only!(to::SimpleACSet, from::SimpleACSet, obs::Tuple) =
+  copy_parts_only!(to, from, NamedTuple{obs}((:) for ob in obs))
 
-ACSetInterface.copy_parts!(to::StructACSet, from::StructACSet, obs::Tuple) =
-  copy_parts!(to, from, NamedTuple{obs}((:) for ob in obs))
-ACSetInterface.copy_parts!(to::StructACSet, from::StructACSet, parts::NamedTuple) =
-  _copy_parts!(to, from, replace_colons(from, parts))
-
-@generated function _copy_parts!(to::StructACSet{S}, from::StructACSet{S′},
-                                 parts::NamedTuple{obs}) where {S, S′, obs}
-  s, s′ = SchemaDesc(S), SchemaDesc(S′)
-  @assert obs ⊆ intersect(s.obs, s′.obs)
-  homs = intersect(s.homs, s′.homs)
-  homs = filter(homs) do hom
-    c, c′, d, d′ = s.doms[hom], s′.doms[hom], s.codoms[hom], s′.codoms[hom]
-    c == c′ && d == d′ && c ∈ obs && d ∈ obs
-  end
-  hom_triples = [ (hom, s.doms[hom], s.codoms[hom]) for hom in homs ]
-  in_obs = unique!(map(last, hom_triples))
-  quote
-    newparts = _copy_parts_only!(to, from, parts)
-    partmaps = NamedTuple{$(Tuple(in_obs))}(tuple($(map(in_obs) do type
-      :(Dict{Int,Int}(zip(parts.$type, newparts.$type)))
-    end...)))
-    for (name, dom, codom) in $(Tuple(hom_triples))
-      for (p, newp) in zip(parts[dom], newparts[dom])
-        q = subpart(from, p, name)
-        newq = get(partmaps[codom], q, nothing)
-        if !isnothing(newq)
-          set_subpart!(to, newp, name, newq)
-        end
-      end
+ACSetInterface.copy_parts_only!(to::StructACSet{S}, from::StructACSet{S′}, parts::NamedTuple) where {S,S′}=
+  _copy_parts_only!(to, Val{S}, from, Val{S′}, replace_colons(from, parts))
+
+ACSetInterface.copy_parts_only!(to::DynamicACSet, from::DynamicACSet, parts::NamedTuple) =
+  runtime(_copy_parts_only!, to, to.schema, from, from.schema, replace_colons(from, parts))
+
+@ct_enable function _copy_parts_only!(
+  to::SimpleACSet, @ct(S),
+  from::SimpleACSet, @ct(S′),
+  parts::NamedTuple{obs}
+) where {obs}
+
+  @ct begin
+    s, s′ = SchemaDesc(S), SchemaDesc(S′)
+    @assert obs ⊆ intersect(s.obs, s′.obs)
+    attrs = intersect(s.attrs, s′.attrs)
+    attrs = filter(attrs) do attr
+      ob, ob′ = s.doms[attr], s′.doms[attr]
+      ob == ob′ && ob ∈ obs
     end
-    newparts
   end
-end
 
-""" Copy parts from a C-set to a C′-set, ignoring all non-attribute subparts.
+  newparts = NamedTuple{@ct obs}((@ct_ctrl(
+    (add_parts!(to, @ct(ob), length(parts[@ct ob])) for ob in obs)...
+  ),))
 
-The selected parts must belong to both schemas. Attributes common to both
-schemas are also copied, but no other subparts are copied.
+  @ct_ctrl for a in attrs
+    @ct ob = s.doms[a]
+    set_subpart!(to, newparts[@ct ob], @ct(a), subpart(from, parts[@ct ob], @ct(a)))
+  end
 
-See also: [`copy_parts!`](@ref).
-"""
-@generated function copy_parts_only!(to::StructACSet{S}, from::StructACSet{S′}; kw...) where
-  {S,S′}
-  s, s′ = SchemaDesc(S), SchemaDesc(S′)
-  obs = intersect(s.obs, s′.obs)
-  :(copy_parts_only!(to, from, isempty(kw) ? $(Tuple(obs)) : (; kw...)))
+  newparts
 end
 
-copy_parts_only!(to::StructACSet, from::StructACSet, obs::Tuple) =
-  copy_parts_only!(to, from, NamedTuple{obs}((:) for ob in obs))
-copy_parts_only!(to::StructACSet, from::StructACSet, parts::NamedTuple) =
-  _copy_parts_only!(to, from, replace_colons(from, parts))
-
-@generated function _copy_parts_only!(to::StructACSet{S}, from::StructACSet{S′},
-    parts::NamedTuple{obs}) where {S, S′, obs}
-  s, s′ = SchemaDesc(S), SchemaDesc(S′)
-  @assert obs ⊆ intersect(s.obs, s′.obs)
-  attrs = intersect(s.attrs, s′.attrs)
-  attrs = filter(attrs) do attr
-    ob, ob′ = s.doms[attr], s′.doms[attr]
-    ob == ob′ && ob ∈ obs
-  end
-  Expr(:block,
-    :(newparts = (; $(map(obs) do ob
-        Expr(:kw, ob, :(add_parts!(to, $(QuoteNode(ob)), length(parts.$ob))))
-        end...))),
-    map(attrs) do attr
-      ob = s.doms[attr]
-      :(set_subpart!(to, newparts.$ob, $(QuoteNode(attr)),
-                     subpart(from, parts.$ob, $(QuoteNode(attr)))))
-    end...,
-    :newparts)
-end
-
-function replace_colons(acs::StructACSet, parts::NamedTuple{types}) where {types}
+function replace_colons(acs::ACSet, parts::NamedTuple{types}) where {types}
   NamedTuple{types}(map(types, parts) do type, part
     part == (:) ? (1:nparts(acs, type)) : part
   end)
 end
 
+
 # Printing
 ##########
 
-function Base.show(io::IO, acs::T) where {S,T<:StructACSet{S}}
-  s = SchemaDesc(S)
+ACSetInterface.acset_name(x::StructACSet) = sprint(show, typeof(x))
+
+function ACSetInterface.acset_name(x::DynamicACSet)
+  s = x.schema
+  if length(s.attrtypes) == 0
+    x.name
+  else
+    Ts = join([x.attrtypes[at] for at in s.attrtypes], ",")
+    "$(x.name){$(Ts)}"
+  end
+end
+
+function Base.show(io::IO, acs::SimpleACSet)
+  s = acset_schema(acs)
   if get(io, :compact, false)
-    print(io, nameof(T))
+    print(io, acset_name(acs))
     print(io, " {")
     join(io, ("$ob = $(nparts(acs,ob))" for ob in s.obs), ", ")
     print(io, "}")
   else
-    print(io, T)
+    print(io, acset_name(acs))
     println(io, ":")
     join(io, Iterators.flatten((
       ("  $ob = $(parts(acs,ob))" for ob in s.obs),
@@ -671,64 +693,121 @@ function Base.show(io::IO, acs::T) where {S,T<:StructACSet{S}}
   end
 end
 
-# TODO: implement Tables interface
-# Steps:
-# - Wrapper data structure
+# Tables
+########
 
-struct StructACSetTable{T<:StructACSet, ob} <: Tables.AbstractColumns
+struct ACSetTable{T<:ACSet, ob} <: Tables.AbstractColumns
   parent::T
 end
 
-StructACSetTable(acs::StructACSet, ob::Symbol) = StructACSetTable{typeof(acs), ob}(acs)
+ACSetTable(acs::ACSet, ob::Symbol) = ACSetTable{typeof(acs), ob}(acs)
 
 ACSetInterface.tables(acs::StructACSet{<:SchemaDescType{obs}}) where {obs} =
-  NamedTuple([ob => StructACSetTable(acs, ob) for ob in obs])
+  NamedTuple([ob => ACSetTable(acs, ob) for ob in obs])
+
+ACSetInterface.tables(acs::DynamicACSet) =
+  NamedTuple([ob => ACSetTable(acs, ob) for ob in acs.schema.obs])
 
-parent(sat::StructACSetTable) = getfield(sat, :parent)
+parent(sat::ACSetTable) = getfield(sat, :parent)
 
-struct StructACSetRow{T<:StructACSet, ob} <: Tables.AbstractRow
+struct ACSetRow{T<:ACSet, ob} <: Tables.AbstractRow
   parent::T
   idx::Int
 end
 
-parent(row::StructACSetRow) = getfield(row, :parent)
-idx(row::StructACSetRow) = getfield(row, :idx)
+parent(row::ACSetRow) = getfield(row, :parent)
+idx(row::ACSetRow) = getfield(row, :idx)
 
 # - Tables.jl interface
 
-Tables.istable(sat::StructACSetTable) = true
+Tables.istable(sat::ACSetTable) = true
 
-Tables.columnaccess(sat::StructACSetTable) = true
-Tables.columns(sat::StructACSetTable) = sat
+Tables.columnaccess(sat::ACSetTable) = true
+Tables.columns(sat::ACSetTable) = sat
 
-Tables.getcolumn(sat::StructACSetTable{T,ob}, i::Int) where {T,ob} =
+Tables.getcolumn(sat::ACSetTable{T,ob}, i::Int) where {T,ob} =
   Base.getproperty(sat, outgoing(parent(sat), ob)[i])
 
-Base.getproperty(sat::StructACSetTable, nm::Symbol) = subpart(parent(sat), :, nm)
+Base.getproperty(sat::ACSetTable, nm::Symbol) = subpart(parent(sat), :, nm)
 
-Tables.getcolumn(sat::StructACSetTable, nm::Symbol) = Base.getproperty(sat, nm)
+Tables.getcolumn(sat::ACSetTable, nm::Symbol) = Base.getproperty(sat, nm)
 
-Base.propertynames(sat::StructACSetTable{T,ob}) where {T,ob} = outgoing(parent(sat), ob)
+Base.propertynames(sat::ACSetTable{T,ob}) where {T,ob} = outgoing(parent(sat), ob)
 
-Tables.columnnames(sat::StructACSetTable) = Base.propertynames(sat)
+Tables.columnnames(sat::ACSetTable) = Base.propertynames(sat)
 
-Tables.rowaccess(sat::StructACSetTable) = true
-Tables.rows(sat::StructACSetTable{T,ob}) where {T,ob} =
-  StructACSetRow{T,ob}.(Ref(parent(sat)), parts(parent(sat), ob))
+Tables.rowaccess(sat::ACSetTable) = true
+Tables.rows(sat::ACSetTable{T,ob}) where {T,ob} =
+  ACSetRow{T,ob}.(Ref(parent(sat)), parts(parent(sat), ob))
 
 
-Tables.getcolumn(row::StructACSetRow{T,ob}, i::Int) where {T,ob} =
+Tables.getcolumn(row::ACSetRow{T,ob}, i::Int) where {T,ob} =
   Base.getproperty(row, outgoing(parent(row), ob)[i])
 
-Base.getproperty(row::StructACSetRow, nm::Symbol) = subpart(parent(row), idx(row), nm)
+Base.getproperty(row::ACSetRow, nm::Symbol) = subpart(parent(row), idx(row), nm)
+
+Tables.getcolumn(row::ACSetRow, nm::Symbol) = Base.getproperty(row, nm)
+
+Base.propertynames(row::ACSetRow{T,ob}) where {T,ob} = outgoing(parent(row), ob)
+
+Tables.columnnames(row::ACSetRow) = Base.propertynames(row)
+
+# ACSet Macro
+#############
+
+@ct_enable function _make_acset(@ct(S), T, rows::NamedTuple{names}) where {names}
+  @ct begin
+    s = SchemaDesc(S)
+  end
+  acs = T()
+  @ct_ctrl for ob in intersect(s.obs, names)
+    add_parts!(acs, @ct(ob), rows[@ct ob])
+  end
+  @ct_ctrl for f in intersect([s.homs; s.attrs], names)
+    set_subpart!(acs,:,@ct(f), rows[@ct f])
+  end
+  acs
+end
+
+function make_acset(T::Type{<:StructACSet{S}}, rows::NamedTuple) where {S}
+  _make_acset(Val{S}, T, rows)
+end
 
-Tables.getcolumn(row::StructACSetRow, nm::Symbol) = Base.getproperty(row, nm)
+"""
+This provides a shorthand for constructing an acset by giving its parts and
+subparts
 
-Base.propertynames(row::StructACSetRow{T,ob}) where {T,ob} = outgoing(parent(row), ob)
+Usage:
 
-Tables.columnnames(row::StructACSetRow) = Base.propertynames(row)
+@acset WeightedGraph{String} begin
+  V = 2
+  E = 1
+  src = [1]
+  tgt = [2]
+  weight = ["fig"]
+end
+"""
+macro acset(head, body)
+  tuplized_body = @match body begin
+    Expr(:block, lines...) => begin
+      params = []
+      map(lines) do line
+        @match line begin
+          Expr(:(=), x, y) => push!(params, Expr(:kw, x, y))
+          _ => nothing
+        end
+      end
+      Expr(:tuple, Expr(:parameters, params...))
+    end
+    _ => error("expected block")
+  end
+  esc(quote
+    $(GlobalRef(CSetDataStructures, :make_acset))($head, $tuplized_body)
+  end)
+end
 
 # Mapping
+#########
 
 function Base.map(acs::ACSet; kwargs...)
   _map(acs, (;kwargs...))
@@ -753,11 +832,14 @@ function groupby(f::Function, xs)
   d
 end
 
+# Eventually should translate this to comptime so it will work with dynamic acsets
 @generated function _map(acs::AT, fns::NamedTuple{map_over}) where
   {S, Ts, AT<:StructACSet{S,Ts}, map_over}
   s = SchemaDesc(S)
   map_over = Symbol[map_over...]
 
+  q(s) = Expr(:quote, s)
+
   mapped_attrs = intersect(s.attrs, map_over)
   mapped_attrtypes = intersect(s.attrtypes, map_over)
   mapped_attrs_from_attrtypes = filter(a -> s.codoms[a] ∈ mapped_attrtypes, s.attrs)
@@ -812,4 +894,10 @@ end
   end
 end
 
+# Presentations
+###############
+
+Presentation(::StructACSet{S}) where {S} = Presentation(S)
+Presentation(::Type{<:StructACSet{S}}) where {S} = Presentation(S)
+
 end
diff --git a/src/categorical_algebra/CSets.jl b/src/categorical_algebra/CSets.jl
index 7361f95ce..31172a563 100644
--- a/src/categorical_algebra/CSets.jl
+++ b/src/categorical_algebra/CSets.jl
@@ -17,6 +17,7 @@ using StructEquality
 import JSON
 using Reexport
 using Tables
+using CompTime
 import Pkg
 
 @reexport using ...CSetDataStructures
@@ -39,17 +40,20 @@ import ..FinCats: FinDomFunctor, components, is_natural
 
 For objects, the result is a `FinSet`; for attribute types, a `TypeSet`.
 """
-@inline SetOb(X::StructACSet, type::Symbol)::SetOb = set_ob(X, Val{type})
+@inline SetOb(X::StructACSet{S,Ts}, type::Symbol) where {S,Ts} =
+  set_ob(X, Val{S}, Val{Ts}, Val{type})
 
-@generated function set_ob(X::StructACSet{S,Ts},
-                           ::Type{Val{type}}) where {S,Ts,type}
-  if type ∈ ob(S)
-    :(FinSet(X, $(Meta.quot(type))))
+@inline SetOb(X::DynamicACSet, type::Symbol) =
+  runtime(set_ob, X.schema, types(X), type)
+
+@ct_enable function set_ob(X::ACSet, @ct(S), @ct(Ts), @ct(type))
+  @ct_ctrl if type ∈ ob(S)
+    FinSet(X, @ct type)
   elseif type ∈ attrtype(S)
-    T = Ts.parameters[attrtype_num(S, type)]
-    :(TypeSet{$T}())
+    @ct T = Ts.parameters[attrtype_num(S, type)]
+    TypeSet{@ct T}()
   else
-    throw(ArgumentError("$(repr(type)) not in $(ob(S)) or $(attrtype(S))"))
+    @ct throw(ArgumentError("$(repr(type)) not in $(ob(S)) or $(attrtype(S))"))
   end
 end
 
@@ -59,18 +63,20 @@ end
 
 """ Create `TypeSet` for object or attribute type of attributed C-set.
 """
-@inline TypeSet(X::StructACSet, type::Symbol)::TypeSet = type_set(X, Val{type})
-
-@generated function type_set(X::StructACSet{S,Ts},
-                             ::Type{Val{type}}) where {S,Ts,type}
-  T = if type ∈ ob(S)
-    Int
-  elseif type ∈ attrtype(S)
-    Ts.parameters[attrtype_num(S, type)]
-  else
-    throw(ArgumentError("$(repr(type)) not in $(ob(S)) or $(attrtype(S))"))
+@inline TypeSet(::StructACSet{S,Ts}, type::Symbol) where {S,Ts} =
+  type_set(Val{S}, Val{Ts}, Val{type})
+
+@ct_enable function type_set(@ct(S), @ct(Ts), @ct(type))
+  @ct begin
+    T = if type ∈ ob(S)
+      Int
+    elseif type ∈ attrtype(S)
+      Ts.parameters[attrtype_num(S, type)]
+    else
+      throw(ArgumentError("$(repr(type)) not in $(ob(S)) or $(attrtype(S))"))
+    end
+    TypeSet{T}()
   end
-  :(TypeSet{$T}())
 end
 
 """ Create `SetFunction` for morphism or attribute of attributed C-set.
@@ -78,23 +84,18 @@ end
 For morphisms, the result is a `FinFunction`; for attributes, a
 `FinDomFunction`.
 """
-@inline SetFunction(X::StructACSet, name::Symbol)::SetFunction =
-  set_function(X, Val{name})
+@inline SetFunction(X::StructACSet{S}, name::Symbol) where {S} =
+  set_function(X, Val{S}, Val{name})
 
-@generated function set_function(X::StructACSet{S,Ts,Idxed},
-                                 ::Type{Val{name}}) where {S,Ts,Idxed,name}
-  if name ∈ ob(S) || name ∈ attrtype(S)
-    :(SetFunction(identity, SetOb(X, $(Meta.quot(name)))))
+@ct_enable function set_function(X::SimpleACSet, @ct(S), @ct(name))
+  @ct_ctrl if name ∈ ob(S) || name ∈ attrtype(S)
+    SetFunction(identity, SetOb(X, @ct name))
   elseif name ∈ hom(S)
-    quote
-      FinFunction(subpart(X, $(Meta.quot(name))),
-                  FinSet(X, $(Meta.quot(codom(S, name)))),
-                  index=$(Idxed[name] ? :(X.hom_indices.$name) : false))
-    end
+    FinFunction(X.subparts[@ct name], FinSet(X, @ct(codom(S, name))))
   elseif name ∈ attr(S)
-    :(FinDomFunction(X, $(Meta.quot(name))))
+    FinDomFunction(X, @ct name)
   else
-    throw(ArgumentError("$(repr(name)) does not belong to schema $S"))
+    @ct throw(ArgumentError("$(repr(name)) does not belong to schema $(S)"))
   end
 end
 
@@ -102,33 +103,25 @@ end
 
 Indices are included whenever they exist.
 """
-@inline FinFunction(X::StructACSet, name::Symbol)::FinFunction =
-  set_function(X, Val{name})
+@inline FinFunction(X::StructACSet{S}, name::Symbol) where {S} =
+  set_function(X, Val{S}, Val{name})
 
 """ Create `FinDomFunction` for morphism or attribute of attributed C-set.
 
 Indices are included whenever they exist. Unlike the `FinFunction` constructor,
 the codomain of the result is always of type `TypeSet`.
 """
-@inline FinDomFunction(X::StructACSet, name::Symbol)::FinDomFunction =
-  fin_dom_function(X, Val{name})
-
-@generated function fin_dom_function(X::StructACSet{S,Ts,Idxed},
-    ::Type{Val{name}}) where {S,Ts,Idxed,name}
-  if name ∈ ob(S)
-    quote
-      n = nparts(X, $(Meta.quot(name)))
-      FinDomFunction(1:n, FinSet(n), TypeSet{Int}())
-    end
+@inline FinDomFunction(X::StructACSet{S}, name::Symbol) where {S} =
+  fin_dom_function(X, Val{S}, Val{name})
+
+@ct_enable function fin_dom_function(X::SimpleACSet, @ct(S), @ct(name))
+  @ct_ctrl if name ∈ ob(S)
+    n = nparts(X, @ct name)
+    FinDomFunction(1:n, FinSet(n), TypeSet{Int}())
   elseif name ∈ hom(S) || name ∈ attr(S)
-    index_name = name ∈ hom(S) ? :hom_indices : :attr_indices
-    quote
-      FinDomFunction(subpart(X, $(Meta.quot(name))),
-                     index=$(Idxed[name] ? :(X.$index_name.$name) : false))
-    end
+    FinDomFunction(X.subparts[@ct name])
   else
-    throw(ArgumentError(
-      "$(repr(name)) not in $(ob(S)), $(hom(S)), or $(attr(S))"))
+    @ct throw(ArgumentError("$(repr(name)) not in $(ob(S)), $(hom(S)), or $(attr(S))"))
   end
 end
 
@@ -896,7 +889,7 @@ function unpack_sets(Xs::AbstractVector{<:StructACSet{S}};
   # return type of the more general `SetOb`.
   fin_sets = (c => map(X->FinSet(X,c), Xs) for c in ob(S))
   NamedTuple(all ?
-    flatten((fin_sets, (d => map(X->TypeSet(X,d), Xs) for d in attrtype(S)))) :
+    flatten((fin_sets, (d => Vector{TypeSet}(map(X->TypeSet(X,d), Xs)) for d in attrtype(S)))) :
     fin_sets)
 end
 
diff --git a/src/categorical_algebra/DataMigrations.jl b/src/categorical_algebra/DataMigrations.jl
index 88cda5643..62c8d5937 100644
--- a/src/categorical_algebra/DataMigrations.jl
+++ b/src/categorical_algebra/DataMigrations.jl
@@ -277,7 +277,7 @@ function ob_map(ΣF::SigmaMigration, ::Type{T}, X::FinDomFunctor) where T<:ACSet
   colimX = map(parts(diagramD, :V)) do i
     F∇d = ob(comma_cats, i)
     Xobs = FinSet{Int,Int}[ ob_map(X, c) for (c,_) in ob(F∇d) ]
-    Xhoms = [ hom_map(X, hom(F∇d, g)) for g in parts(F∇d, :E) ]
+    Xhoms = FinFunction[ hom_map(X, hom(F∇d, g)) for g in parts(F∇d, :E) ]
     colimit(FreeDiagram(Xobs, collect(zip(Xhoms, src(F∇d), tgt(F∇d)))))
   end
 
diff --git a/src/categorical_algebra/FinSets.jl b/src/categorical_algebra/FinSets.jl
index df020e398..f51c35168 100644
--- a/src/categorical_algebra/FinSets.jl
+++ b/src/categorical_algebra/FinSets.jl
@@ -14,7 +14,7 @@ using StaticArrays: StaticVector, SVector, SizedVector, similar_type
 import Tables, PrettyTables
 
 @reexport using ..Sets
-using ...GAT, ...Theories, ...CSetDataStructures, ...Graphs
+using ...GAT, ...Theories, ...CSetDataStructures, ...Graphs, ...ACSetColumns
 using ..FinCats, ..FreeDiagrams, ..Limits, ..Subobjects
 import ...Theories: Ob, meet, ∧, join, ∨, top, ⊤, bottom, ⊥
 import ..Categories: ob, hom, dom, codom, compose, id, ob_map, hom_map
@@ -193,6 +193,9 @@ end
 FinFunction(f::AbstractVector{Int}; kw...) =
   FinFunction(f, FinSet(isempty(f) ? 0 : maximum(f)); kw...)
 
+FinFunction(f::IndexedVector{Int, Vector{Vector{Int}}}, args...) =
+  IndexedFinFunctionVector(f.vals, args...; index=f.index)
+
 Sets.show_type_constructor(io::IO, ::Type{<:FinFunction}) =
   print(io, "FinFunction")
 
@@ -218,6 +221,12 @@ function FinDomFunction(f::AbstractVector, args...; index=false)
   end
 end
 
+FinDomFunction(f::IndexedVector{Int, Vector{Vector{Int}}}, args...) =
+  IndexedFinDomFunctionVector(f.vals, args...; index=f.index)
+
+FinDomFunction(f::IndexedVector{T, Dict{T, Vector{Int}}}, args...) where {T} =
+  IndexedFinDomFunctionVector(f.vals, args...; index=f.index)
+
 Sets.show_type_constructor(io::IO, ::Type{<:FinDomFunction}) =
   print(io, "FinDomFunction")
 
diff --git a/src/theories/Schema.jl b/src/theories/Schema.jl
index 3e26d9010..870de710a 100644
--- a/src/theories/Schema.jl
+++ b/src/theories/Schema.jl
@@ -155,6 +155,19 @@ struct SchemaDesc
   end
 end
 
+function SchemaDesc(s::SchemaDesc)
+  s
+end
+
+function SchemaDesc(p::Presentation)
+  obs,homs,attrtypes,attrs = map(t -> p.generators[t],[:Ob,:Hom,:AttrType,:Attr])
+  ob_syms,hom_syms,attrtype_syms,attr_syms = map(xs -> Symbol[nameof.(xs)...],
+                                                 [obs,homs,attrtypes,attrs])
+  doms = Dict(nameof(f) => nameof(dom(f)) for f in [homs; attrs])
+  codoms = Dict(nameof(f) => nameof(codom(f)) for f in [homs; attrs])
+  SchemaDesc(ob_syms, hom_syms, attrtype_syms, attr_syms, doms, codoms)
+end
+
 ob_num(S::SchemaDesc, x::Symbol) where {obs} = findfirst(S.obs .== x)
 
 function SchemaDescTypeType(s::SchemaDesc)
diff --git a/src/wiring_diagrams/Directed.jl b/src/wiring_diagrams/Directed.jl
index 88b5f7435..87f7150e7 100644
--- a/src/wiring_diagrams/Directed.jl
+++ b/src/wiring_diagrams/Directed.jl
@@ -205,6 +205,11 @@ mutable struct WiringDiagram{Theory, PortValue, WireValue, BoxValue} <: Abstract
     # Copy constructor for shallow copy
     new{T, PortValue, WireValue, BoxValue}(copy(f.diagram), f.value)
   end
+  function WiringDiagram{T, PortValue, WireValue, BoxValue}(
+      diagram::WiringDiagramACSet{PortValue, WireValue, Union{BoxValue, AbstractBox}, DataType},
+      value) where {T, PortValue, WireValue, BoxValue}
+    new{T, PortValue, WireValue, BoxValue}(diagram, value)
+  end
 end
 
 function WiringDiagram{T, PortValue, WireValue, BoxValue}(inputs::Vector, outputs::Vector) where {T, PortValue, WireValue, BoxValue}
@@ -232,7 +237,23 @@ function is_isomorphic(d1::WiringDiagram, d2::WiringDiagram)
   d1.value == d2.value && is_isomorphic(d1.diagram, d2.diagram)
 end
 
-Base.copy(diagram::WiringDiagram) = WiringDiagram(diagram)
+Base.copy(d::WiringDiagram) = begin
+  # WARNING WARNING WARNING
+  # IF YOU WANT TO UNDERSTAND WHY THIS HACK WAS NECESSARY, YOU MUST BE PREPARED
+  # TO LOSE YOUR SANITY
+  #
+  #
+  #
+  # OK, READY?
+  #
+  # THE copy METHOD FOR ACSETS BREAKS THE JULIA COMPILER WHEN THE TYPE OF THE
+  # ARGUMENT IS *PARTIALLY* INFERRED; FULL INFERENCE AND FULL DYNAMIC DISPATCH
+  # BOTH WORK FINE. SO HERE WE ARE FORGETTING OUR PARTIAL KNOWLEDGE OF THE TYPE
+  # OF THE ACSET SO THAT FULL DYNAMIC DISPATCH IS INVOKED. THIS IS ONLY
+  # NECESSARY FOR JULIA 1.6; 1.7 AND ABOVE DON'T HAVE THIS PROBLEM
+  diag = (Any[d.diagram])[1]
+  WiringDiagram(copy(diag), d.value)
+end
 
 function Base.show(io::IO, diagram::WiringDiagram{T}) where T
   sshowcompact = x -> sprint(show, x, context=:compact => true)


Test patch:
diff --git a/test/categorical_algebra/CSetDataStructures.jl b/test/categorical_algebra/CSetDataStructures.jl
index e1840df47..960824d29 100644
--- a/test/categorical_algebra/CSetDataStructures.jl
+++ b/test/categorical_algebra/CSetDataStructures.jl
@@ -16,108 +16,115 @@ end
 
 @abstract_acset_type AbstractDDS
 @acset_type DDS(SchDDS, index=[:Φ]) <: AbstractDDS
+@acset_type UnindexedDDS(SchDDS)
 @test DDS <: AbstractDDS
 @test DDS <: StructACSet
 @test DDS <: StructCSet
 @test DDS <: ACSet
 
-dds = DDS()
-@test keys(tables(dds)) == (:X,)
-@test nparts(dds, :X) == 0
-@test add_part!(dds, :X) == 1
-@test nparts(dds, :X) == 1
-@test incident(dds, 1, :Φ) == []
-
-set_subpart!(dds, 1, :Φ, 1)
-@test subpart(dds, 1, :Φ) == 1
-@test incident(dds, 1, :Φ) == [1]
-
-@test add_part!(dds, :X, Φ=1) == 2
-@test add_part!(dds, :X, Φ=1) == 3
-@test subpart(dds, :Φ) == [1,1,1]
-@test subpart(dds, [2,3], :Φ) == [1,1]
-@test incident(dds, 1, :Φ) == [1,2,3]
-
-@test has_part(dds, :X)
-@test !has_part(dds, :nonpart)
-@test has_part(dds, :X, 3)
-@test !has_part(dds, :X, 4)
-@test has_part(dds, :X, 1:5) == [true, true, true, false, false]
-
-@test has_subpart(dds, :Φ)
-@test !has_subpart(dds, :nonsubpart)
-@test_throws ArgumentError subpart(dds, 1, :nonsubpart)
-@test_throws ArgumentError incident(dds, 1, :nonsuppart)
-@test_throws ArgumentError set_subpart!(dds, 1, :nonsubpart, 1)
-
-# Deletion.
-dds = DDS()
-add_parts!(dds, :X, 3, Φ=[2,3,3])
-rem_part!(dds, :X, 2)
-@test nparts(dds, :X) == 2
-@test subpart(dds, :Φ) == [0,2]
-@test incident(dds, 1, :Φ) == []
-@test incident(dds, 2, :Φ) == [2]
-@test_throws BoundsError incident(dds, 3, :Φ)
-rem_part!(dds, :X, 2)
-@test nparts(dds, :X) == 1
-@test subpart(dds, :Φ) == [0]
-rem_part!(dds, :X, 1)
-@test nparts(dds, :X) == 0
-
-dds = DDS()
-add_parts!(dds, :X, 4, Φ=[2,3,3,4])
-@test_throws ErrorException rem_parts!(dds, :X, [4,1])
-rem_parts!(dds, :X, [1,4])
-@test subpart(dds, :Φ) == [1,1]
-@test incident(dds, 1, :Φ) == [1,2]
-@test incident(dds, 2, :Φ) == []
-
-# Pretty printing.
-dds = DDS()
-add_parts!(dds, :X, 3, Φ=[2,3,3])
-s = sprint(show, dds)
-@test contains(s, "DDS:")
-@test contains(s, "X = 1:3")
-@test contains(s, "Φ : X → X = ")
-s = sprint(show, dds, context=:compact=>true)
-@test contains(s, "DDS")
-@test !contains(s, "\n")
-@test contains(s, "X = 3")
-
-s = sprint(show, MIME"text/plain"(), dds)
-@test contains(s, "DDS")
-@test contains(s, "X = 1:3")
-@test contains(s, "│ X │")
-
-s = sprint(show, MIME"text/html"(), dds)
-@test startswith(s, "<div class=\"c-set\">")
-@test contains(s, "<table>")
-@test endswith(rstrip(s), "</div>")
-
-# Special case of pretty print: empty table.
-empty_dds = DDS()
-@test !isempty(sprint(show, empty_dds))
-@test !isempty(sprint(show, MIME"text/plain"(), empty_dds))
-@test !isempty(sprint(show, MIME"text/html"(), empty_dds))
-
-# Error handling when adding parts.
-dds = DDS()
-add_parts!(dds, :X, 3, Φ=[1,1,1])
-@test_throws AssertionError add_part!(dds, :X, Φ=5)
-@test nparts(dds, :X) == 3
-@test subpart(dds, :Φ) == [1,1,1]
-@test_throws AssertionError add_parts!(dds, :X, 2, Φ=[3,6])
-@test nparts(dds, :X) == 3
-@test incident(dds, 3, :Φ) == []
-
-# Incidence without indexing.
-@acset_type UnindexedDDS(SchDDS)
-dds = UnindexedDDS()
-add_parts!(dds, :X, 4, Φ=[3,3,4,4])
-# @test isempty(keys(dds.indices))
-@test incident(dds, 3, :Φ) == [1,2]
-@test incident(dds, 4, :Φ) == [3,4]
+dds_makers = [
+  DDS,
+  UnindexedDDS,
+  () -> DynamicACSet("DDS", SchDDS; index=[:Φ]),
+  () -> AnonACSet(SchDDS; index=[:Φ])
+]
+
+for dds_maker in dds_makers
+  dds = dds_maker()
+  @test keys(tables(dds)) == (:X,)
+  @test nparts(dds, :X) == 0
+  @test add_part!(dds, :X) == 1
+  @test nparts(dds, :X) == 1
+  @test incident(dds, 1, :Φ) == Int[]
+
+  set_subpart!(dds, 1, :Φ, 1)
+  @test subpart(dds, 1, :Φ) == 1
+  @test incident(dds, 1, :Φ) == [1]
+
+  @test add_part!(dds, :X, Φ=1) == 2
+  @test add_part!(dds, :X, Φ=1) == 3
+  @test subpart(dds, :Φ) == [1,1,1]
+  @test subpart(dds, [2,3], :Φ) == [1,1]
+  @test incident(dds, 1, :Φ) == [1,2,3]
+
+  @test has_part(dds, :X)
+  @test !has_part(dds, :nonpart)
+  @test has_part(dds, :X, 3)
+  @test !has_part(dds, :X, 4)
+  @test has_part(dds, :X, 1:5) == [true, true, true, false, false]
+
+  @test has_subpart(dds, :Φ)
+  @test !has_subpart(dds, :nonsubpart)
+  # @test_throws ArgumentError subpart(dds, 1, :nonsubpart)
+  # @test_throws ArgumentError incident(dds, 1, :nonsuppart)
+  # @test_throws ArgumentError set_subpart!(dds, 1, :nonsubpart, 1)
+
+  # Deletion.
+  dds = dds_maker()
+  add_parts!(dds, :X, 3, Φ=[2,3,3])
+  rem_part!(dds, :X, 2)
+  @test nparts(dds, :X) == 2
+  @test subpart(dds, :Φ) == [0,2]
+  @test incident(dds, 1, :Φ) == []
+  @test incident(dds, 2, :Φ) == [2]
+  rem_part!(dds, :X, 2)
+  @test nparts(dds, :X) == 1
+  @test subpart(dds, :Φ) == [0]
+  rem_part!(dds, :X, 1)
+  @test nparts(dds, :X) == 0
+
+  dds = dds_maker()
+  add_parts!(dds, :X, 4, Φ=[2,3,3,4])
+  @test_throws ErrorException rem_parts!(dds, :X, [4,1])
+  rem_parts!(dds, :X, [1,4])
+  @test subpart(dds, :Φ) == [1,1]
+  @test incident(dds, 1, :Φ) == [1,2]
+  @test incident(dds, 2, :Φ) == []
+
+  # Pretty printing.
+  dds = dds_maker()
+  add_parts!(dds, :X, 3, Φ=[2,3,3])
+  s = sprint(show, dds)
+  if !(dds isa AnonACSet)
+    @test contains(s, "DDS:")
+  end
+  @test contains(s, "X = 1:3")
+  @test contains(s, "Φ : X → X = ")
+  s = sprint(show, dds, context=:compact=>true)
+  if !(dds isa AnonACSet)
+    @test contains(s, "DDS")
+  end
+  @test !contains(s, "\n")
+  @test contains(s, "X = 3")
+
+  s = sprint(show, MIME"text/plain"(), dds)
+  if !(dds isa AnonACSet)
+    @test contains(s, "DDS")
+  end
+  @test contains(s, "X = 1:3")
+  @test contains(s, "│ X │")
+
+  s = sprint(show, MIME"text/html"(), dds)
+  @test startswith(s, "<div class=\"c-set\">")
+  @test contains(s, "<table>")
+  @test endswith(rstrip(s), "</div>")
+
+  # Special case of pretty print: empty table.
+  empty_dds = dds_maker()
+  @test !isempty(sprint(show, empty_dds))
+  @test !isempty(sprint(show, MIME"text/plain"(), empty_dds))
+  @test !isempty(sprint(show, MIME"text/html"(), empty_dds))
+
+  # # Error handling when adding parts.
+  dds = dds_maker()
+  add_parts!(dds, :X, 3, Φ=[1,1,1])
+  @test_throws AssertionError add_part!(dds, :X, Φ=5)
+  @test nparts(dds, :X) == 3
+  @test subpart(dds, :Φ) == [1,1,1]
+  @test_throws AssertionError add_parts!(dds, :X, 2, Φ=[3,6])
+  @test nparts(dds, :X) == 3
+  @test incident(dds, 3, :Φ) == []
+end
 
 # Dendrograms
 #############
@@ -142,95 +149,6 @@ end
 @test Dendrogram <: ACSet
 @test Dendrogram{Real} <: AbstractDendrogram{S,Tuple{Real}} where {S}
 
-d = Dendrogram{Int}()
-add_parts!(d, :X, 3, height=0)
-add_parts!(d, :X, 2, height=[10,20])
-set_subpart!(d, 1:3, :parent, 4)
-set_subpart!(d, [4,5], :parent, 5)
-
-@test nparts(d, :X) == 5
-@test subpart(d, 1:3, :parent) isa SubArray{Int,1}
-@test subpart(d, 1:3, :parent) == [4,4,4]
-@test subpart(d, 4, :parent) == 5
-@test subpart(d, :, :parent) == [4,4,4,5,5]
-@test incident(d, 4, :parent) == [1,2,3]
-@test incident(d, 4:5, :parent) isa SubArray{Vector{Int},1}
-@test incident(d, 4:5, :parent) == [[1,2,3], [4,5]]
-@test has_subpart(d, :height)
-@test subpart(d, [1,2,3], :height) == [0,0,0]
-@test subpart(d, 4, :height) == 10
-@test subpart(d, :, :height) == [0,0,0,10,20]
-
-# Chained accessors.
-@test subpart(d, 3, [:parent, :parent]) == 5
-@test subpart(d, 3, [:parent, :height]) == 10
-@test incident(d, 5, [:parent, :parent]) == [1,2,3,4,5]
-@test incident(d, 10, [:parent, :height]) == [1,2,3]
-
-X, parent, height = SchDendrogram[[:X, :parent, :height]]
-@test subpart(d, 3, parent) == 4
-@test subpart(d, 3, compose(parent, height)) == 10
-@test subpart(d, 3, id(X)) == 3
-@test incident(d, 10, compose(parent, height)) == [1,2,3]
-@test subpart(d, parent) == [4,4,4,5,5]
-@test subpart(d, id(X)) == 1:5
-@test subpart(d, compose(parent, height)) == [10,10,10,20,20]
-
-# Indexing syntax.
-@test d[3, :parent] == 4
-@test d[3, [:parent, :height]] == 10
-@test d[3:5, [:parent, :height]] == [10,20,20]
-@test d[:, :parent] == [4,4,4,5,5]
-d2 = copy(d)
-d2[1, :parent] = 1
-d2[2:3, :parent] = 2:3
-@test d2[1:3, :parent] == 1:3
-d2[1:3, :parent] = 4
-@test d2 == d
-
-# Copying parts.
-d2 = Dendrogram{Int}()
-copy_parts!(d2, d, X=[4,5])
-@test nparts(d2, :X) == 2
-@test subpart(d2, [1,2], :parent) == [2,2]
-@test subpart(d2, [1,2], :height) == [10,20]
-
-du = disjoint_union(d, d2)
-@test nparts(du, :X) == 7
-@test subpart(du, :parent) == [4,4,4,5,5,7,7]
-@test subpart(du, :height) == [0,0,0,10,20,10,20]
-
-# Pretty printing of data attributes.
-s = sprint(show, d)
-@test contains(s, "Dendrogram{Int64}:")
-@test contains(s, "height : X → R = ")
-
-s = sprint(show, MIME"text/plain"(), d)
-@test contains(s, "Dendrogram{Int64}")
-
-# Allow type inheritance for data attributes.
-d_abs = Dendrogram{Number}()
-add_parts!(d_abs, :X, 2, height=[10.0, 4])
-@test subpart(d_abs, :height) == [10.0, 4]
-
-# Tables interface
-td = tables(d)
-@test keys(td) == (:X,)
-@test Tables.istable(td.X) == true
-cols = Tables.columns(td.X)
-@test Tables.columnnames(cols) == (:parent, :height)
-@test Tables.getcolumn(cols, :parent) == [4,4,4,5,5]
-@test Tables.getcolumn(cols, 1) == [4,4,4,5,5]
-rows = [Tables.rows(td.X)...]
-@test length(rows) == 5
-@test Tables.columnnames(rows[1]) == (:parent, :height)
-@test Tables.getcolumn(rows[1], :parent) == 4
-@test Tables.getcolumn(rows[1], :height) == 0
-@test Tables.getcolumn(rows[1], 1) == 4
-
-# Dendrograms with leaves
-#------------------------
-
 @present SchLDendrogram <: SchDendrogram begin
   L::Ob
   leafparent::Hom(L,X)
@@ -238,17 +156,113 @@ end
 
 @acset_type LDendrogram(SchLDendrogram, index=[:parent, :leafparent]) <: AbstractDendrogram
 
-# Copying between C-sets and C′-sets with C != C′.
-ld = LDendrogram{Int}()
-copy_parts!(ld, d)
-@test nparts(ld, :L) == 0
-@test subpart(ld, :parent) == subpart(d, :parent)
+dgram_makers = [
+  (T -> Dendrogram{T}(), T -> LDendrogram{T}()),
+  (T -> DynamicACSet("Dendrogram", SchDendrogram; attrtypes=Dict(:R=>T), index=[:parent]),
+   T -> DynamicACSet("LDendrogram", SchLDendrogram; attrtypes=Dict(:R=>T), index=[:parent, :leafparent])
+   )
+]
+
+for (dgram_maker, ldgram_maker) in dgram_makers
+  d = dgram_maker(Int)
+  add_parts!(d, :X, 3, height=0)
+  add_parts!(d, :X, 2, height=[10,20])
+  set_subpart!(d, 1:3, :parent, 4)
+  set_subpart!(d, [4,5], :parent, 5)
+
+  @test nparts(d, :X) == 5
+  @test subpart(d, 1:3, :parent) isa SubArray{Int,1}
+  @test subpart(d, 1:3, :parent) == [4,4,4]
+  @test subpart(d, 4, :parent) == 5
+  @test subpart(d, :, :parent) == [4,4,4,5,5]
+  @test incident(d, 4, :parent) == [1,2,3]
+  @test incident(d, 4:5, :parent) isa SubArray{Vector{Int},1}
+  @test incident(d, 4:5, :parent) == [[1,2,3], [4,5]]
+  @test has_subpart(d, :height)
+  @test subpart(d, [1,2,3], :height) == [0,0,0]
+  @test subpart(d, 4, :height) == 10
+  @test subpart(d, :, :height) == [0,0,0,10,20]
+
+  # Chained accessors.
+  @test subpart(d, 3, [:parent, :parent]) == 5
+  @test subpart(d, 3, [:parent, :height]) == 10
+  @test incident(d, 5, [:parent, :parent]) == [1,2,3,4,5]
+  @test incident(d, 10, [:parent, :height]) == [1,2,3]
+
+  X, parent, height = SchDendrogram[[:X, :parent, :height]]
+  @test subpart(d, 3, parent) == 4
+  @test subpart(d, 3, compose(parent, height)) == 10
+  @test subpart(d, 3, id(X)) == 3
+  @test incident(d, 10, compose(parent, height)) == [1,2,3]
+  @test subpart(d, parent) == [4,4,4,5,5]
+  @test subpart(d, id(X)) == 1:5
+  @test subpart(d, compose(parent, height)) == [10,10,10,20,20]
+
+  # Indexing syntax.
+  @test d[3, :parent] == 4
+  @test d[3, [:parent, :height]] == 10
+  @test d[3:5, [:parent, :height]] == [10,20,20]
+  @test d[:, :parent] == [4,4,4,5,5]
+  d2 = copy(d)
+  d2[1, :parent] = 1
+  d2[2:3, :parent] = 2:3
+  @test d2[1:3, :parent] == 1:3
+  d2[1:3, :parent] = 4
+  @test d2 == d
+
+  # Copying parts.
+  d2 = dgram_maker(Int)
+  copy_parts!(d2, d, X=[4,5])
+  @test nparts(d2, :X) == 2
+  @test subpart(d2, [1,2], :parent) == [2,2]
+  @test subpart(d2, [1,2], :height) == [10,20]
+
+  du = disjoint_union(d, d2)
+  @test nparts(du, :X) == 7
+  @test subpart(du, :parent) == [4,4,4,5,5,7,7]
+  @test subpart(du, :height) == [0,0,0,10,20,10,20]
+
+  # Pretty printing of data attributes.
+  s = sprint(show, d)
+  @test contains(s, "Dendrogram{Int64}:")
+  @test contains(s, "height : X → R = ")
+
+  s = sprint(show, MIME"text/plain"(), d)
+  @test contains(s, "Dendrogram{Int64}")
+
+  # Allow type inheritance for data attributes.
+  d_abs = dgram_maker(Number)
+  add_parts!(d_abs, :X, 2, height=[10.0, 4])
+  @test subpart(d_abs, :height) == [10.0, 4]
+
+  # Tables interface
+  td = tables(d)
+  @test keys(td) == (:X,)
+  @test Tables.istable(td.X) == true
+  cols = Tables.columns(td.X)
+  @test Tables.columnnames(cols) == (:parent, :height)
+  @test Tables.getcolumn(cols, :parent) == [4,4,4,5,5]
+  @test Tables.getcolumn(cols, 1) == [4,4,4,5,5]
+  rows = [Tables.rows(td.X)...]
+  @test length(rows) == 5
+  @test Tables.columnnames(rows[1]) == (:parent, :height)
+  @test Tables.getcolumn(rows[1], :parent) == 4
+  @test Tables.getcolumn(rows[1], :height) == 0
+  @test Tables.getcolumn(rows[1], 1) == 4
+
+  # Copying between C-sets and C′-sets with C != C′.
+  ld = ldgram_maker(Int)
+  copy_parts!(ld, d)
+  @test nparts(ld, :L) == 0
+  @test subpart(ld, :parent) == subpart(d, :parent)
+
+  add_parts!(ld, :L, 3, leafparent=[2,3,4])
+  @test subpart(ld, :leafparent) == [2,3,4]
+  d′ = dgram_maker(Int)
+  copy_parts!(d′, ld)
+  @test d′ == d
+end
 
-add_parts!(ld, :L, 3, leafparent=[2,3,4])
-@test subpart(ld, :leafparent) == [2,3,4]
-d′ = Dendrogram{Int}()
-copy_parts!(d′, ld)
-@test d′ == d
 
 # Subsets
 #########
@@ -294,64 +308,77 @@ end
 
 @acset_type IndexedLabeledSet(SchLabeledSet, index=[:label])
 
-lset = IndexedLabeledSet{Symbol}()
-# @test keys(lset.indices) == (:label,)
-add_parts!(lset, :X, 2, label=[:foo, :bar])
-@test subpart(lset, :, :label) == [:foo, :bar]
-@test incident(lset, :foo, :label) == [1]
-@test isempty(incident(lset, :nonkey, :label))
-
-add_part!(lset, :X, label=:foo)
-@test incident(lset, :foo, :label) == [1,3]
-set_subpart!(lset, 1, :label, :baz)
-@test subpart(lset, 1, :label) == :baz
-@test incident(lset, [:foo,:baz], :label) == [[3],[1]]
-set_subpart!(lset, 3, :label, :biz)
-@test incident(lset, :foo, :label) == []
-
-# Labeled set with compound label (tuple).
-lset = IndexedLabeledSet{Tuple{Int,Int}}()
-add_parts!(lset, :X, 2, label=[(1,1), (1,2)])
-@test incident(lset, (1,2), :label) == [2]
-
-# Deletion with indexed data attribute.
-lset = IndexedLabeledSet{Symbol}()
-add_parts!(lset, :X, 3, label=[:foo, :foo, :bar])
-rem_part!(lset, :X, 1)
-@test subpart(lset, :label) == [:bar, :foo]
-@test incident(lset, [:foo, :bar], :label) == [[2], [1]]
-
-# Deletion with unitialized data attribute.
-lset = IndexedLabeledSet{Symbol}()
-add_part!(lset, :X)
-rem_part!(lset, :X, 1)
-@test nparts(lset, :X) == 0
-
-# Pretty-printing with unitialized data attribute.
-lset = IndexedLabeledSet{Symbol}()
-add_part!(lset, :X)
-@test contains(sprint(show, lset), "#undef")
-@test contains(sprint(show, MIME"text/plain"(), lset), "#undef")
-@test contains(sprint(show, MIME"text/html"(), lset), "#undef")
+lset_makers = [
+  T -> IndexedLabeledSet{T}(),
+  T -> DynamicACSet("IndexedLabeledSet", SchLabeledSet; attrtypes=Dict(:Label=>T), index=[:label])
+]
+
+for lset_maker in lset_makers
+  lset = lset_maker(Symbol)
+  add_parts!(lset, :X, 2, label=[:foo, :bar])
+  @test subpart(lset, :, :label) == [:foo, :bar]
+  @test incident(lset, :foo, :label) == [1]
+  @test isempty(incident(lset, :nonkey, :label))
+
+  add_part!(lset, :X, label=:foo)
+  @test incident(lset, :foo, :label) == [1,3]
+  set_subpart!(lset, 1, :label, :baz)
+  @test subpart(lset, 1, :label) == :baz
+  @test incident(lset, [:foo,:baz], :label) == [[3],[1]]
+  set_subpart!(lset, 3, :label, :biz)
+  @test incident(lset, :foo, :label) == []
+
+  # Labeled set with compound label (tuple).
+  lset = lset_maker(Tuple{Int,Int})
+  add_parts!(lset, :X, 2, label=[(1,1), (1,2)])
+  @test incident(lset, (1,2), :label) == [2]
+
+  # Deletion with indexed data attribute.
+  lset = lset_maker(Symbol)
+  add_parts!(lset, :X, 3, label=[:foo, :foo, :bar])
+  rem_part!(lset, :X, 1)
+  @test subpart(lset, :label) == [:bar, :foo]
+  @test incident(lset, [:foo, :bar], :label) == [[2], [1]]
+
+  # Deletion with unitialized data attribute.
+  lset = lset_maker(Tuple{Int,Int})
+  add_part!(lset, :X)
+  rem_part!(lset, :X, 1)
+  @test nparts(lset, :X) == 0
+
+  # Pretty-printing with unitialized data attribute.
+  lset = lset_maker(Symbol)
+  add_part!(lset, :X)
+  @test contains(sprint(show, lset), "#undef")
+  @test contains(sprint(show, MIME"text/plain"(), lset), "#undef")
+  @test contains(sprint(show, MIME"text/html"(), lset), "#undef")
+end
 
 # Labeled sets with unique index
 #-------------------------------
 
 @acset_type UniqueIndexedLabeledSet(SchLabeledSet, unique_index=[:label])
 
-lset = UniqueIndexedLabeledSet{Symbol}()
-add_parts!(lset, :X, 2, label=[:foo, :bar])
-@test subpart(lset, :, :label) == [:foo, :bar]
-@test incident(lset, :foo, :label) == 1
-@test incident(lset, [:foo,:bar], :label) == [1,2]
-@test incident(lset, :nonkey, :label) == 0
-
-set_subpart!(lset, 1, :label, :baz)
-@test subpart(lset, 1, :label) == :baz
-@test incident(lset, :baz, :label) == 1
-@test incident(lset, :foo, :label) == 0
-
-@test_throws Any set_subpart!(lset, 1, :label, :bar)
+lset_makers = [
+  T -> UniqueIndexedLabeledSet{T}(),
+  T -> DynamicACSet("UniqueIndexedLabeledSet", SchLabeledSet; attrtypes=Dict(:Label=>T), unique_index=[:label])
+]
+
+for lset_maker in lset_makers
+  lset = lset_maker(Symbol)
+  add_parts!(lset, :X, 2, label=[:foo, :bar])
+  @test subpart(lset, :, :label) == [:foo, :bar]
+  @test incident(lset, :foo, :label) == 1
+  @test incident(lset, [:foo,:bar], :label) == [1,2]
+  @test incident(lset, :nonkey, :label) == 0
+
+  set_subpart!(lset, 1, :label, :baz)
+  @test subpart(lset, 1, :label) == :baz
+  @test incident(lset, :baz, :label) == 1
+  @test incident(lset, :foo, :label) == 0
+
+  @test_throws Any set_subpart!(lset, 1, :label, :bar)
+end
 
 # @acset macro
 #-------------
@@ -375,6 +402,7 @@ g = @acset DecGraph{String} begin
   tgt = [2,3,4,1]
   dec = ["a","b","c","d"]
 end
+
 @test nparts(g, :V) == 4
 @test subpart(g, :, :src) == [1,2,3,4]
 @test incident(g, 1, :src) == [1]
diff --git a/test/categorical_algebra/CSets.jl b/test/categorical_algebra/CSets.jl
index 77e578e29..8f54c5d7f 100644
--- a/test/categorical_algebra/CSets.jl
+++ b/test/categorical_algebra/CSets.jl
@@ -264,7 +264,7 @@ end
                            index=[:src,:tgt]) <: AbstractGraph
 
 # Terminal labeled graph.
-@test ob(terminal(VELabeledGraph)) == cycle_graph(VELabeledGraph{Nothing}, 1)
+@test ob(terminal(VELabeledGraph)) == cycle_graph(VELabeledGraph{Tuple{}}, 1)
 
 # Product of labeled graphs.
 g = path_graph(VELabeledGraph{Symbol}, 2, V=(vlabel=[:a,:b],), E=(elabel=:f,))