diff --git a/Spigot-Server-Patches/Fix-concurrency-and-performance-issues-in-DataFixers.patch b/Spigot-Server-Patches/Fix-concurrency-and-performance-issues-in-DataFixers.patch new file mode 100644 index 0000000000..336e77e351 --- /dev/null +++ b/Spigot-Server-Patches/Fix-concurrency-and-performance-issues-in-DataFixers.patch @@ -0,0 +1,339 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar +Date: Mon, 3 Sep 2018 22:18:38 -0400 +Subject: [PATCH] Fix concurrency and performance issues in DataFixers + +We are seeing issues with DataFixers being not thread safe in async chunks +and even in some spigot packet sending code. + +There are a few more global objects that are mutated that need to +be synchronized to be safe for use over multiple threads. + +There may be more cases, but these are extremely obvious ones. + +Also replaced quite a few bad uses of Map.containsKey + +containsKey is a common newbie mistake that "reads" cleaner, but +results in double the performance cost of all map operations as +containsKey in MOST cases where null values are not used is identical to get() == null + +Considering how deep datafixers go in call stacks, with tons of map lookups, +this micro optimization could provide some gains. + +Additionally, many of the containsKey/get/put style operations were +also a concurrency risk, resulting in multiple computation/insertions. + +diff --git a/src/main/java/com/mojang/datafixers/DataFixerUpper.java b/src/main/java/com/mojang/datafixers/DataFixerUpper.java +index fb2c380f8a..a4922a35a2 100644 +--- a/src/main/java/com/mojang/datafixers/DataFixerUpper.java ++++ b/src/main/java/com/mojang/datafixers/DataFixerUpper.java +@@ -0,0 +0,0 @@ public class DataFixerUpper implements DataFixer { + private final Int2ObjectSortedMap schemas; + private final List globalList; + private final IntSortedSet fixerVersions; +- private final Long2ObjectMap rules = new Long2ObjectOpenHashMap<>(); ++ private final Long2ObjectMap rules = it.unimi.dsi.fastutil.longs.Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>()); // Paper + + protected DataFixerUpper(final Int2ObjectSortedMap schemas, final List globalList, final IntSortedSet fixerVersions) { + this.schemas = schemas; +@@ -0,0 +0,0 @@ public class DataFixerUpper implements DataFixer { + final int expandedDataVersion = DataFixUtils.makeKey(dataVersion); + + final long key = (long) expandedVersion << 32 | expandedDataVersion; +- if (!rules.containsKey(key)) { ++ return rules.computeIfAbsent(key, k -> { // Paper + final List rules = Lists.newArrayList(); + for (final DataFix fix : globalList) { + final int fixVersion = fix.getVersionKey(); +@@ -0,0 +0,0 @@ public class DataFixerUpper implements DataFixer { + rules.add(fixRule); + } + } +- this.rules.put(key, TypeRewriteRule.seq(rules)); +- } +- return rules.get(key); ++ return TypeRewriteRule.seq(rules); // Paper ++ }); // Paper + } + + protected IntSortedSet fixerVersions() { +diff --git a/src/main/java/com/mojang/datafixers/NamedChoiceFinder.java b/src/main/java/com/mojang/datafixers/NamedChoiceFinder.java +index 2c259d74e9..17481fb6e6 100644 +--- a/src/main/java/com/mojang/datafixers/NamedChoiceFinder.java ++++ b/src/main/java/com/mojang/datafixers/NamedChoiceFinder.java +@@ -0,0 +0,0 @@ final class NamedChoiceFinder implements OpticFinder { + }*/ + if (targetType instanceof TaggedChoice.TaggedChoiceType) { + final TaggedChoice.TaggedChoiceType choiceType = (TaggedChoice.TaggedChoiceType) targetType; +- if (choiceType.types().containsKey(name)) { +- final Type elementType = choiceType.types().get(name); ++ // Paper start - performance - don't use containsKey ++ final Type elementType = choiceType.types().get(name); ++ if (elementType != null) { ++ // Paper end + if (!Objects.equals(type, elementType)) { + return Either.right(new Type.FieldNotFoundException(String.format("Type error for choice type \"%s\": expected type: %s, actual type: %s)", name, targetType, elementType))); + } +diff --git a/src/main/java/com/mojang/datafixers/functions/PointFree.java b/src/main/java/com/mojang/datafixers/functions/PointFree.java +index 0d88490f77..028942b8ea 100644 +--- a/src/main/java/com/mojang/datafixers/functions/PointFree.java ++++ b/src/main/java/com/mojang/datafixers/functions/PointFree.java +@@ -0,0 +0,0 @@ public abstract class PointFree { + private Function, T> value; + + @SuppressWarnings("ConstantConditions") +- public Function, T> evalCached() { ++ public synchronized Function, T> evalCached() { // Paper + if (!initialized) { + initialized = true; + value = eval(); +diff --git a/src/main/java/com/mojang/datafixers/schemas/Schema.java b/src/main/java/com/mojang/datafixers/schemas/Schema.java +index 7c67d989e0..f1fd694b06 100644 +--- a/src/main/java/com/mojang/datafixers/schemas/Schema.java ++++ b/src/main/java/com/mojang/datafixers/schemas/Schema.java +@@ -0,0 +0,0 @@ import java.util.function.Function; + import java.util.function.Supplier; + + public class Schema { +- protected final Object2IntMap RECURSIVE_TYPES = new Object2IntOpenHashMap<>(); +- private final Map> TYPE_TEMPLATES = Maps.newHashMap(); ++ protected final Object2IntMap RECURSIVE_TYPES = it.unimi.dsi.fastutil.objects.Object2IntMaps.synchronize(new Object2IntOpenHashMap<>()); // Paper ++ private final Map> TYPE_TEMPLATES = Maps.newConcurrentMap(); // Paper + private final Map> TYPES; + private final int versionKey; + private final String name; +@@ -0,0 +0,0 @@ public class Schema { + } + + protected Map> buildTypes() { +- final Map> types = Maps.newHashMap(); ++ final Map> types = Maps.newConcurrentMap(); // Paper + + final List templates = Lists.newArrayList(); + ++ synchronized (RECURSIVE_TYPES) { // Paper + for (final Object2IntMap.Entry entry : RECURSIVE_TYPES.object2IntEntrySet()) { + templates.add(DSL.check(entry.getKey(), entry.getIntValue(), getTemplate(entry.getKey()))); +- } ++ } } // Paper + + final TypeTemplate choice = templates.stream().reduce(DSL::or).get(); + final TypeFamily family = new RecursiveTypeFamily(name, choice); + ++ synchronized (TYPE_TEMPLATES) { // Paper + for (final String name : TYPE_TEMPLATES.keySet()) { + final Type type; +- if (RECURSIVE_TYPES.containsKey(name)) { +- type = family.apply(RECURSIVE_TYPES.getInt(name)); ++ // Paper start - concurrency and performance improvement, don't use containsKey ++ int recurseId = RECURSIVE_TYPES.getOrDefault(name, -1); ++ if (recurseId != -1) { ++ type = family.apply(recurseId); ++ // Paper end + } else { + type = getTemplate(name).apply(family).apply(-1); + } + types.put(name, type); +- } ++ } } // Paper + return types; + } + +@@ -0,0 +0,0 @@ public class Schema { + } + + public TypeTemplate id(final String name) { +- if (RECURSIVE_TYPES.containsKey(name)) { +- return DSL.id(RECURSIVE_TYPES.get(name)); ++ // Paper start - improve concurrency and performance ++ int id = RECURSIVE_TYPES.getOrDefault(name, -1); ++ if (id != -1) { ++ return DSL.id(id); ++ // Paper end + } + return getTemplate(name); + } +@@ -0,0 +0,0 @@ public class Schema { + + public Type getChoiceType(final DSL.TypeReference type, final String choiceName) { + final TaggedChoice.TaggedChoiceType choiceType = findChoiceType(type); +- if (!choiceType.types().containsKey(choiceName)) { ++ // Paper start - performance and concurrency - don't use containsKey ++ Type result = choiceType.types().get(choiceName); ++ if (result == null) { + throw new IllegalArgumentException("Data fixer not registered for: " + choiceName + " in " + type.typeName()); + } +- return choiceType.types().get(choiceName); ++ return result; ++ // Paper end + } + + public TaggedChoice.TaggedChoiceType findChoiceType(final DSL.TypeReference type) { +@@ -0,0 +0,0 @@ public class Schema { + public void registerType(final boolean recursive, final DSL.TypeReference type, final Supplier template) { + TYPE_TEMPLATES.put(type.typeName(), template); + // TODO: calculate recursiveness instead of hardcoding ++ synchronized (RECURSIVE_TYPES) { // Paper + if (recursive && !RECURSIVE_TYPES.containsKey(type.typeName())) { + RECURSIVE_TYPES.put(type.typeName(), RECURSIVE_TYPES.size()); +- } ++ } } // Paper + } + + public int getVersionKey() { +diff --git a/src/main/java/com/mojang/datafixers/types/DynamicOps.java b/src/main/java/com/mojang/datafixers/types/DynamicOps.java +index 3c76929f24..f21531b3cd 100644 +--- a/src/main/java/com/mojang/datafixers/types/DynamicOps.java ++++ b/src/main/java/com/mojang/datafixers/types/DynamicOps.java +@@ -0,0 +0,0 @@ public interface DynamicOps { + + default Optional getGeneric(final T input, final T key) { + return getMapValues(input).flatMap(map -> { +- if (map.containsKey(key)) { +- return Optional.of(map.get(key)); +- } +- return Optional.empty(); ++ // Paper start - performance - don't use containsKey ++ T value = map.get(key); ++ return value != null ? Optional.of(value) : Optional.empty(); ++ // Paper end + }); + } + +diff --git a/src/main/java/com/mojang/datafixers/types/Type.java b/src/main/java/com/mojang/datafixers/types/Type.java +index a80e3fee91..2d5bae7a37 100644 +--- a/src/main/java/com/mojang/datafixers/types/Type.java ++++ b/src/main/java/com/mojang/datafixers/types/Type.java +@@ -0,0 +0,0 @@ import javax.annotation.Nullable; + import java.util.Map; + import java.util.Objects; + import java.util.Optional; ++import java.util.concurrent.CompletableFuture; + + public abstract class Type implements App { ++ private static final Map, TypeRewriteRule, PointFreeRule>, CompletableFuture>>> PENDING_REWRITE_CACHE = Maps.newConcurrentMap(); + private static final Map, TypeRewriteRule, PointFreeRule>, Optional>> REWRITE_CACHE = Maps.newConcurrentMap(); + + public static class Mu implements K1 {} +@@ -0,0 +0,0 @@ public abstract class Type implements App { + @SuppressWarnings("unchecked") + public Optional> rewrite(final TypeRewriteRule rule, final PointFreeRule fRule) { + final Triple, TypeRewriteRule, PointFreeRule> key = Triple.of(this, rule, fRule); +- if (!REWRITE_CACHE.containsKey(key)) { +- final Optional> result = rule.rewrite(this).flatMap(r -> r.view().rewrite(fRule).map(view -> RewriteResult.create(view, r.recData()))); ++ // Paper start - concurrency and performance boost - this code under contention would generate multiple rewrites ++ // rewrite this to use a CompletableFuture for pending rewrites. We can not use computeIfAbsent because this is ++ // a recursive call that will block server startup during the Bootstrap phrase thats trying to precache these rewrites ++ Optional> rewrite = REWRITE_CACHE.get(key); ++ //noinspection OptionalAssignedToNull ++ if (rewrite != null) { ++ return (Optional>) rewrite; ++ } ++ CompletableFuture>> pending; ++ boolean needsCreate; ++ synchronized (PENDING_REWRITE_CACHE) { ++ pending = PENDING_REWRITE_CACHE.get(key); ++ needsCreate = pending == null; ++ if (pending == null) { ++ pending = new CompletableFuture<>(); ++ PENDING_REWRITE_CACHE.put(key, pending); ++ } ++ } ++ if (needsCreate) { ++ Optional> result = rule.rewrite(this).flatMap(r -> r.view().rewrite(fRule).map(view -> RewriteResult.create(view, r.recData()))); + REWRITE_CACHE.put(key, result); ++ pending.complete(result); ++ PENDING_REWRITE_CACHE.remove(key); ++ return result; ++ } else { ++ return (Optional>) pending.join(); + } +- return (Optional>) REWRITE_CACHE.get(key); ++ // Paper end + } + + public Type getSetType(final OpticFinder optic, final Type newType) { +diff --git a/src/main/java/com/mojang/datafixers/types/families/RecursiveTypeFamily.java b/src/main/java/com/mojang/datafixers/types/families/RecursiveTypeFamily.java +index 4a1f906837..93c2f565fd 100644 +--- a/src/main/java/com/mojang/datafixers/types/families/RecursiveTypeFamily.java ++++ b/src/main/java/com/mojang/datafixers/types/families/RecursiveTypeFamily.java +@@ -0,0 +0,0 @@ public final class RecursiveTypeFamily implements TypeFamily { + private final TypeTemplate template; + private final int size; + +- private final Int2ObjectMap> types = new Int2ObjectOpenHashMap<>(); ++ private final Int2ObjectMap> types = it.unimi.dsi.fastutil.ints.Int2ObjectMaps.synchronize(new Int2ObjectOpenHashMap<>()); // Paper + private final int hashCode; + + public RecursiveTypeFamily(final String name, final TypeTemplate template) { +diff --git a/src/main/java/com/mojang/datafixers/types/templates/Tag.java b/src/main/java/com/mojang/datafixers/types/templates/Tag.java +index ece3fb5f88..396637bbd1 100644 +--- a/src/main/java/com/mojang/datafixers/types/templates/Tag.java ++++ b/src/main/java/com/mojang/datafixers/types/templates/Tag.java +@@ -0,0 +0,0 @@ public final class Tag implements TypeTemplate { + public Pair> read(final DynamicOps ops, final T input) { + final Optional> map = ops.getMapValues(input); + final T nameObject = ops.createString(name); +- if (map.isPresent() && map.get().containsKey(nameObject)) { +- final T elementValue = map.get().get(nameObject); ++ // Paper start - performance - don't use containsKey ++ final T elementValue; ++ if (map.isPresent() && (elementValue = map.get().get(nameObject)) != null) { ++ // Paper end + final Optional value = element.read(ops, elementValue).getSecond(); + if (value.isPresent()) { + return Pair.of(ops.createMap(map.get().entrySet().stream().filter(e -> !Objects.equals(e.getKey(), nameObject)).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))), value); +diff --git a/src/main/java/com/mojang/datafixers/types/templates/TaggedChoice.java b/src/main/java/com/mojang/datafixers/types/templates/TaggedChoice.java +index e6dd31233a..77d64362b1 100644 +--- a/src/main/java/com/mojang/datafixers/types/templates/TaggedChoice.java ++++ b/src/main/java/com/mojang/datafixers/types/templates/TaggedChoice.java +@@ -0,0 +0,0 @@ public final class TaggedChoice implements TypeTemplate { + if (values.isPresent()) { + final Map map = values.get(); + final T nameObject = ops.createString(name); +- if (map.containsKey(nameObject)) { +- final Optional key = keyType.read(ops, map.get(nameObject)).getSecond(); +- if (!key.isPresent() || !types.containsKey(key.get())) { ++ // Paper start - performance - don't use containsKey ++ T mapValue = map.get(nameObject); ++ if (mapValue != null) { ++ final Optional key = keyType.read(ops, mapValue).getSecond(); ++ // also skip containsKey here ++ //noinspection OptionalIsPresent ++ K keyValue = key.isPresent() ? key.get() : null; ++ Type type = keyValue != null ? types.get(keyValue) : null; ++ if (type == null) { + if (DataFixerUpper.ERRORS_ARE_FATAL) { +- throw new IllegalArgumentException("Unsupported key: " + key.get() + " in " + this); ++ throw new IllegalArgumentException("Unsupported key: " + keyValue + " in " + this); + } else { +- LOGGER.warn("Unsupported key: {} in {}", key.get(), this); ++ LOGGER.warn("Unsupported key: {} in {}", keyValue, this); + return Pair.of(input, Optional.empty()); + } + } +- return types.get(key.get()).read(ops, input).mapSecond(vo -> vo.map(v -> Pair.of(key.get(), v))); ++ ++ return type.read(ops, input).mapSecond(vo -> vo.map(v -> Pair.of(keyValue, v))); ++ // Paper end + } + } + return Pair.of(input, Optional.empty()); +@@ -0,0 +0,0 @@ public final class TaggedChoice implements TypeTemplate { + + @Override + public T write(final DynamicOps ops, final T rest, final Pair value) { +- if (!types.containsKey(value.getFirst())) { ++ // Paper start - performance - don't use containsKey ++ final Type type = types.get(value.getFirst()); ++ if (type == null) { + // TODO: better error handling? + // TODO: See todo in read method + throw new IllegalArgumentException("Unsupported key: " + value.getFirst() + " in " + this); + } +- final Type type = types.get(value.getFirst()); ++ // Paper end + return capWrite(ops, type, value.getFirst(), value.getSecond(), rest); + } + +-- \ No newline at end of file diff --git a/Spigot-Server-Patches/Fix-concurrency-issues-in-DataFixers.patch b/Spigot-Server-Patches/Fix-concurrency-issues-in-DataFixers.patch deleted file mode 100644 index 054de2a80a..0000000000 --- a/Spigot-Server-Patches/Fix-concurrency-issues-in-DataFixers.patch +++ /dev/null @@ -1,144 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Mon, 3 Sep 2018 22:18:38 -0400 -Subject: [PATCH] Fix concurrency issues in DataFixers - -We are seeing issues with DataFixers being not thread safe in async chunks -and even in some spigot packet sending code. - -There are a few more global objects that are mutated that need to -be synchronized to be safe for use over multiple threads. - -There may be more cases, but these are extremely obvious ones. - -diff --git a/src/main/java/com/mojang/datafixers/DataFixerUpper.java b/src/main/java/com/mojang/datafixers/DataFixerUpper.java -index fb2c380f8a..c8e7a8aa10 100644 ---- a/src/main/java/com/mojang/datafixers/DataFixerUpper.java -+++ b/src/main/java/com/mojang/datafixers/DataFixerUpper.java -@@ -0,0 +0,0 @@ public class DataFixerUpper implements DataFixer { - private final Int2ObjectSortedMap schemas; - private final List globalList; - private final IntSortedSet fixerVersions; -- private final Long2ObjectMap rules = new Long2ObjectOpenHashMap<>(); -+ private final Long2ObjectMap rules = it.unimi.dsi.fastutil.longs.Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>()); // Paper - - protected DataFixerUpper(final Int2ObjectSortedMap schemas, final List globalList, final IntSortedSet fixerVersions) { - this.schemas = schemas; -@@ -0,0 +0,0 @@ public class DataFixerUpper implements DataFixer { - final int expandedDataVersion = DataFixUtils.makeKey(dataVersion); - - final long key = (long) expandedVersion << 32 | expandedDataVersion; -- if (!rules.containsKey(key)) { -+ rules.computeIfAbsent(key, k -> { // Paper - final List rules = Lists.newArrayList(); - for (final DataFix fix : globalList) { - final int fixVersion = fix.getVersionKey(); -@@ -0,0 +0,0 @@ public class DataFixerUpper implements DataFixer { - rules.add(fixRule); - } - } -- this.rules.put(key, TypeRewriteRule.seq(rules)); -- } -+ return TypeRewriteRule.seq(rules); // Paper -+ }); // Paper - return rules.get(key); - } - -diff --git a/src/main/java/com/mojang/datafixers/functions/PointFree.java b/src/main/java/com/mojang/datafixers/functions/PointFree.java -index 0d88490f77..028942b8ea 100644 ---- a/src/main/java/com/mojang/datafixers/functions/PointFree.java -+++ b/src/main/java/com/mojang/datafixers/functions/PointFree.java -@@ -0,0 +0,0 @@ public abstract class PointFree { - private Function, T> value; - - @SuppressWarnings("ConstantConditions") -- public Function, T> evalCached() { -+ public synchronized Function, T> evalCached() { // Paper - if (!initialized) { - initialized = true; - value = eval(); -diff --git a/src/main/java/com/mojang/datafixers/schemas/Schema.java b/src/main/java/com/mojang/datafixers/schemas/Schema.java -index 7c67d989e0..45f3ef5957 100644 ---- a/src/main/java/com/mojang/datafixers/schemas/Schema.java -+++ b/src/main/java/com/mojang/datafixers/schemas/Schema.java -@@ -0,0 +0,0 @@ import java.util.function.Function; - import java.util.function.Supplier; - - public class Schema { -- protected final Object2IntMap RECURSIVE_TYPES = new Object2IntOpenHashMap<>(); -- private final Map> TYPE_TEMPLATES = Maps.newHashMap(); -+ protected final Object2IntMap RECURSIVE_TYPES = it.unimi.dsi.fastutil.objects.Object2IntMaps.synchronize(new Object2IntOpenHashMap<>()); // Paper -+ private final Map> TYPE_TEMPLATES = Maps.newConcurrentMap(); // Paper - private final Map> TYPES; - private final int versionKey; - private final String name; -@@ -0,0 +0,0 @@ public class Schema { - } - - protected Map> buildTypes() { -- final Map> types = Maps.newHashMap(); -+ final Map> types = Maps.newConcurrentMap(); // Paper - - final List templates = Lists.newArrayList(); - -+ synchronized (RECURSIVE_TYPES) { // Paper - for (final Object2IntMap.Entry entry : RECURSIVE_TYPES.object2IntEntrySet()) { - templates.add(DSL.check(entry.getKey(), entry.getIntValue(), getTemplate(entry.getKey()))); -- } -+ } } // Paper - - final TypeTemplate choice = templates.stream().reduce(DSL::or).get(); - final TypeFamily family = new RecursiveTypeFamily(name, choice); - -+ synchronized (TYPE_TEMPLATES) { // Paper - for (final String name : TYPE_TEMPLATES.keySet()) { - final Type type; - if (RECURSIVE_TYPES.containsKey(name)) { -@@ -0,0 +0,0 @@ public class Schema { - type = getTemplate(name).apply(family).apply(-1); - } - types.put(name, type); -- } -+ } } // Paper - return types; - } - -@@ -0,0 +0,0 @@ public class Schema { - } - - public TypeTemplate id(final String name) { -+ synchronized (RECURSIVE_TYPES) { // Paper - if (RECURSIVE_TYPES.containsKey(name)) { -- return DSL.id(RECURSIVE_TYPES.get(name)); -- } -+ return DSL.id(RECURSIVE_TYPES.getInt(name)); // Paper -+ } } // Paper - return getTemplate(name); - } - -@@ -0,0 +0,0 @@ public class Schema { - public void registerType(final boolean recursive, final DSL.TypeReference type, final Supplier template) { - TYPE_TEMPLATES.put(type.typeName(), template); - // TODO: calculate recursiveness instead of hardcoding -+ synchronized (RECURSIVE_TYPES) { // Paper - if (recursive && !RECURSIVE_TYPES.containsKey(type.typeName())) { - RECURSIVE_TYPES.put(type.typeName(), RECURSIVE_TYPES.size()); -- } -+ } } // Paper - } - - public int getVersionKey() { -diff --git a/src/main/java/com/mojang/datafixers/types/families/RecursiveTypeFamily.java b/src/main/java/com/mojang/datafixers/types/families/RecursiveTypeFamily.java -index 4a1f906837..93c2f565fd 100644 ---- a/src/main/java/com/mojang/datafixers/types/families/RecursiveTypeFamily.java -+++ b/src/main/java/com/mojang/datafixers/types/families/RecursiveTypeFamily.java -@@ -0,0 +0,0 @@ public final class RecursiveTypeFamily implements TypeFamily { - private final TypeTemplate template; - private final int size; - -- private final Int2ObjectMap> types = new Int2ObjectOpenHashMap<>(); -+ private final Int2ObjectMap> types = it.unimi.dsi.fastutil.ints.Int2ObjectMaps.synchronize(new Int2ObjectOpenHashMap<>()); // Paper - private final int hashCode; - - public RecursiveTypeFamily(final String name, final TypeTemplate template) { --- \ No newline at end of file diff --git a/scripts/importmcdev.sh b/scripts/importmcdev.sh index 156bf3893b..dbfda717fe 100755 --- a/scripts/importmcdev.sh +++ b/scripts/importmcdev.sh @@ -105,7 +105,12 @@ done importLibrary com.mojang datafixerupper com/mojang/datafixers \ schemas/Schema.java \ DataFixerUpper.java \ + NamedChoiceFinder.java \ functions/PointFree.java \ + types/Type.java \ + types/DynamicOps.java \ + types/templates/Tag.java \ + types/templates/TaggedChoice.java \ types/families/RecursiveTypeFamily.java # dont forget \ at end of each line but last