diff --git a/acceptance-tests/acceptance-tests-manifold-systems/pom.xml b/acceptance-tests/acceptance-tests-manifold-systems/pom.xml
new file mode 100644
index 000000000..35ca6c88d
--- /dev/null
+++ b/acceptance-tests/acceptance-tests-manifold-systems/pom.xml
@@ -0,0 +1,88 @@
+
+
+  4.0.0
+
+  
+    io.github.ascopes.jct
+    acceptance-tests
+    0.0.1-SNAPSHOT
+    ../pom.xml
+  
+
+  acceptance-tests-manifold-systems
+
+  
+    2022.1.26
+  
+
+  
+    
+      ${project.groupId}
+      java-compiler-testing
+      test
+    
+
+    
+      org.apache.groovy
+      groovy
+      test
+    
+
+    
+      org.junit.jupiter
+      junit-jupiter
+      test
+    
+
+    
+      org.slf4j
+      slf4j-simple
+      test
+    
+
+    
+      systems.manifold
+      manifold-preprocessor
+      ${manifold.version}
+      test
+    
+
+    
+      systems.manifold
+      manifold-tuple
+      ${manifold.version}
+      test
+    
+  
+
+  
+    
+      
+        org.apache.maven.plugins
+        maven-checkstyle-plugin
+        ${maven-checkstyle-plugin.version}
+
+        
+          
+          **/resources/**
+        
+      
+
+      
+        org.codehaus.gmavenplus
+        gmavenplus-plugin
+      
+
+      
+        org.apache.maven.plugins
+        maven-surefire-plugin
+
+        
+          true
+        
+      
+    
+  
+
diff --git a/acceptance-tests/acceptance-tests-manifold-systems/src/test/groovy/io/github/ascopes/acceptancetests/manifold/ManifoldPluginConfigurer.groovy b/acceptance-tests/acceptance-tests-manifold-systems/src/test/groovy/io/github/ascopes/acceptancetests/manifold/ManifoldPluginConfigurer.groovy
new file mode 100644
index 000000000..a3d1f9cbd
--- /dev/null
+++ b/acceptance-tests/acceptance-tests-manifold-systems/src/test/groovy/io/github/ascopes/acceptancetests/manifold/ManifoldPluginConfigurer.groovy
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2022 - 2022 Ashley Scopes
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.github.ascopes.acceptancetests.manifold
+
+import io.github.ascopes.jct.compilers.JctCompiler
+import org.junit.jupiter.api.condition.JRE
+
+import static io.github.ascopes.jct.compilers.JctCompilerConfigurer.JctSimpleCompilerConfigurer
+import static org.assertj.core.api.Assumptions.assumeThat
+
+/**
+ * Configurer that sets up Javac to invoke Manifold processors.
+ */
+final class ManifoldPluginConfigurer implements JctSimpleCompilerConfigurer {
+
+  @Override
+  void configure(JctCompiler compiler) {
+    assumeThat(JRE.currentVersion())
+        .as("Manifold accesses internal JRE components at runtime which breaks after JDK 15")
+        .isLessThanOrEqualTo(JRE.JAVA_15)
+
+    // TODO(ascopes): look into what is breaking this. Guess there is incompatibility somewhere.
+    assumeThat(JRE.currentVersion())
+        .as("Manifold triggers exceptions after JDK 11")
+        .isLessThanOrEqualTo(JRE.JAVA_11)
+
+    compiler.addCompilerOptions("-Xplugin:Manifold")
+  }
+}
diff --git a/acceptance-tests/acceptance-tests-manifold-systems/src/test/groovy/io/github/ascopes/acceptancetests/manifold/ManifoldPreprocessorTest.groovy b/acceptance-tests/acceptance-tests-manifold-systems/src/test/groovy/io/github/ascopes/acceptancetests/manifold/ManifoldPreprocessorTest.groovy
new file mode 100644
index 000000000..4645bb8ef
--- /dev/null
+++ b/acceptance-tests/acceptance-tests-manifold-systems/src/test/groovy/io/github/ascopes/acceptancetests/manifold/ManifoldPreprocessorTest.groovy
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2022 - 2022 Ashley Scopes
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.github.ascopes.acceptancetests.manifold
+
+import io.github.ascopes.jct.compilers.JctCompiler
+import io.github.ascopes.jct.junit.JavacCompilerTest
+import org.junit.jupiter.api.Disabled
+import org.junit.jupiter.api.DisplayName
+
+import static io.github.ascopes.jct.assertions.JctAssertions.assertThatCompilation
+import static io.github.ascopes.jct.pathwrappers.TempDirectory.newTempDirectory
+import static org.assertj.core.api.Assertions.assertThat
+
+@DisplayName("Manifold Preprocessor acceptance tests")
+@SuppressWarnings('GrUnresolvedAccess')
+class ManifoldPreprocessorTest {
+  @DisplayName("Preprocessor produces the expected code when a preprocessor symbol is defined")
+  @JavacCompilerTest(configurers = [ManifoldPluginConfigurer])
+  void preprocessorProducesTheExpectedCodeWhenPreprocessorSymbolIsDefined(JctCompiler compiler) {
+    // Given
+    def sources = newTempDirectory("sources")
+        .createDirectory("org", "example")
+        .copyContentsFrom("src", "test", "resources", "code", "preprocessor", "if")
+        .createFile("build.properties").withContents("SOME_SYMBOL=1")
+
+    // When
+    def compilation = compiler
+        .addSourcePath(sources)
+        .compile()
+
+    // Then
+    assertThatCompilation(compilation)
+        .isSuccessfulWithoutWarnings()
+
+    String greeting = compilation
+        .classOutputs
+        .classLoader
+        .loadClass("org.example.HelloWorld")
+        .getDeclaredConstructor()
+        .newInstance()
+        .getGreeting()
+
+    assertThat(greeting).isEqualTo("Hello, World! (symbol was defined)")
+  }
+
+  @DisplayName("Preprocessor produces the expected code when a preprocessor symbol is undefined")
+  @JavacCompilerTest(configurers = [ManifoldPluginConfigurer])
+  void preprocessorProducesTheExpectedCodeWhenPreprocessorSymbolIsUndefined(JctCompiler compiler) {
+    // Given
+    def sources = newTempDirectory("sources")
+        .createDirectory("org", "example")
+        .copyContentsFrom("src", "test", "resources", "code", "preprocessor", "if")
+
+    // When
+    def compilation = compiler
+        .addSourcePath(sources)
+        .compile()
+
+    // Then
+    assertThatCompilation(compilation)
+        .isSuccessfulWithoutWarnings()
+
+    String greeting = compilation
+        .classOutputs
+        .classLoader
+        .loadClass("org.example.HelloWorld")
+        .getDeclaredConstructor()
+        .newInstance()
+        .getGreeting()
+
+    assertThat(greeting).isEqualTo("Hello, World! (symbol was not defined)")
+  }
+
+  @Disabled("See https://github.com/manifold-systems/manifold/issues/399")
+  @DisplayName("Warning directives produce compiler warnings in JCT")
+  @JavacCompilerTest(configurers = [ManifoldPluginConfigurer])
+  void warningDirectivesProduceCompilerWarningsInJct(JctCompiler compiler) {
+    // Given
+    def sources = newTempDirectory("sources")
+        .createDirectory("org", "example")
+        .copyContentsFrom("src", "test", "resources", "code", "preprocessor", "warning")
+
+    // When
+    def compilation = compiler
+        .addSourcePath(sources)
+        .compile()
+
+    // Then
+    assertThatCompilation(compilation)
+        .isSuccessful()
+        .diagnostics().warnings().singleElement()
+        .message().isEqualTo("Hello, this is a friendly warning!")
+  }
+
+  @DisplayName("Error directives produce compiler errors in JCT")
+  @JavacCompilerTest(configurers = [ManifoldPluginConfigurer])
+  void warningDirectivesProduceCompilerErrorsInJct(JctCompiler compiler) {
+    // Given
+    def sources = newTempDirectory("sources")
+        .createDirectory("org", "example")
+        .copyContentsFrom("src", "test", "resources", "code", "preprocessor", "error")
+
+    // When
+    def compilation = compiler
+        .addSourcePath(sources)
+        .compile()
+
+    // Then
+    assertThatCompilation(compilation)
+        .isFailure()
+        .diagnostics().errors().singleElement()
+        .message().isEqualTo("Hello, this is an error!")
+  }
+}
diff --git a/acceptance-tests/acceptance-tests-manifold-systems/src/test/groovy/io/github/ascopes/acceptancetests/manifold/ManifoldTupleTest.groovy b/acceptance-tests/acceptance-tests-manifold-systems/src/test/groovy/io/github/ascopes/acceptancetests/manifold/ManifoldTupleTest.groovy
new file mode 100644
index 000000000..df405dc2a
--- /dev/null
+++ b/acceptance-tests/acceptance-tests-manifold-systems/src/test/groovy/io/github/ascopes/acceptancetests/manifold/ManifoldTupleTest.groovy
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 - 2022 Ashley Scopes
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.github.ascopes.acceptancetests.manifold
+
+import io.github.ascopes.jct.compilers.JctCompiler
+import io.github.ascopes.jct.junit.JavacCompilerTest
+import org.junit.jupiter.api.DisplayName
+
+import static io.github.ascopes.jct.assertions.JctAssertions.assertThatCompilation
+import static io.github.ascopes.jct.pathwrappers.TempDirectory.newTempDirectory
+import static org.assertj.core.api.Assertions.assertThat
+import static org.assertj.core.api.SoftAssertions.assertSoftly
+
+@DisplayName("Manifold Tuple acceptance tests")
+@SuppressWarnings(["GroovyAssignabilityCheck", "GrUnresolvedAccess"])
+class ManifoldTupleTest {
+  @DisplayName("Tuple expressions compile as expected")
+  @JavacCompilerTest(configurers = [ManifoldPluginConfigurer])
+  void tupleExpressionsCompileAsExpected(JctCompiler compiler) {
+    // Given
+    def sources = newTempDirectory("sources")
+        .createDirectory("org", "example")
+        .copyContentsFrom("src", "test", "resources", "code", "tuple")
+
+    // When
+    def compilation = compiler
+        .addSourcePath(sources)
+        .compile()
+
+    // Then
+    assertThatCompilation(compilation)
+        .isSuccessfulWithoutWarnings()
+
+    def userType = compilation
+        .classOutputs
+        .classLoader
+        .loadClass("org.example.User")
+        .getDeclaredConstructor(long, String, int)
+
+    def users = [
+        userType.newInstance(123, "Roy Rodgers McFreely", 25),
+        userType.newInstance(456, "Steve-O", 30),
+        userType.newInstance(789, "Dave Davison", 23)
+    ]
+
+    def oldestUsers = compilation
+        .classOutputs
+        .classLoader
+        .loadClass("org.example.UserRecords")
+        .oldestUsers(users)
+
+    assertThat(oldestUsers).hasSize(3)
+
+    assertSoftly { softly ->
+      softly.assertThat(oldestUsers[0].name).isEqualTo("Steve-O")
+      softly.assertThat(oldestUsers[0].age).isEqualTo(30)
+      softly.assertThat(oldestUsers[1].name).isEqualTo("Roy Rodgers McFreely")
+      softly.assertThat(oldestUsers[1].age).isEqualTo(25)
+      softly.assertThat(oldestUsers[2].name).isEqualTo("Dave Davison")
+      softly.assertThat(oldestUsers[2].age).isEqualTo(23)
+    }
+  }
+}
diff --git a/acceptance-tests/acceptance-tests-manifold-systems/src/test/resources/code/preprocessor/error/ClassWithErrors.java b/acceptance-tests/acceptance-tests-manifold-systems/src/test/resources/code/preprocessor/error/ClassWithErrors.java
new file mode 100644
index 000000000..ad54b48a7
--- /dev/null
+++ b/acceptance-tests/acceptance-tests-manifold-systems/src/test/resources/code/preprocessor/error/ClassWithErrors.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2022 - 2022 Ashley Scopes
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.example;
+
+/**
+ * Preprocessor test snippet.
+ *
+ * @author Ashley Scopes
+ */
+public class ClassWithErrors {
+#error "Hello, this is an error!"
+}
diff --git a/acceptance-tests/acceptance-tests-manifold-systems/src/test/resources/code/preprocessor/if/HelloWorld.java b/acceptance-tests/acceptance-tests-manifold-systems/src/test/resources/code/preprocessor/if/HelloWorld.java
new file mode 100644
index 000000000..fb5178ce4
--- /dev/null
+++ b/acceptance-tests/acceptance-tests-manifold-systems/src/test/resources/code/preprocessor/if/HelloWorld.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2022 - 2022 Ashley Scopes
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.example;
+
+/**
+ * Preprocessor test snippet.
+ *
+ * @author Ashley Scopes
+ */
+public class HelloWorld {
+
+  /**
+   * Say some greeting that depends on the compile flags.
+   *
+   * @return the greeting.
+   */
+  public String getGreeting() {
+#if SOME_SYMBOL
+    return "Hello, World! (symbol was defined)";
+#else
+    return "Hello, World! (symbol was not defined)";
+#endif
+  }
+}
diff --git a/acceptance-tests/acceptance-tests-manifold-systems/src/test/resources/code/preprocessor/warning/ClassWithWarnings.java b/acceptance-tests/acceptance-tests-manifold-systems/src/test/resources/code/preprocessor/warning/ClassWithWarnings.java
new file mode 100644
index 000000000..2b42eb1f8
--- /dev/null
+++ b/acceptance-tests/acceptance-tests-manifold-systems/src/test/resources/code/preprocessor/warning/ClassWithWarnings.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2022 - 2022 Ashley Scopes
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.example;
+
+/**
+ * Preprocessor test snippet.
+ *
+ * @author Ashley Scopes
+ */
+public class ClassWithWarnings {
+#warning "Hello, this is a friendly warning!"
+}
diff --git a/acceptance-tests/acceptance-tests-manifold-systems/src/test/resources/code/tuple/User.java b/acceptance-tests/acceptance-tests-manifold-systems/src/test/resources/code/tuple/User.java
new file mode 100644
index 000000000..3f47d32d2
--- /dev/null
+++ b/acceptance-tests/acceptance-tests-manifold-systems/src/test/resources/code/tuple/User.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2022 - 2022 Ashley Scopes
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.example;
+
+/**
+ * A dataclass for a user.
+ *
+ * @author Ashley Scopes
+ */
+public final class User {
+
+  public final long id;
+  public final String name;
+  public final int age;
+
+  /**
+   * Initialise the user.
+   *
+   * @param id the user ID.
+   * @param name the user name.
+   * @param age the user age.
+   */
+  public User(long id, String name, int age) {
+    this.id = id;
+    this.name = name;
+    this.age = age;
+  }
+}
diff --git a/acceptance-tests/acceptance-tests-manifold-systems/src/test/resources/code/tuple/UserRecords.java b/acceptance-tests/acceptance-tests-manifold-systems/src/test/resources/code/tuple/UserRecords.java
new file mode 100644
index 000000000..15fb0665d
--- /dev/null
+++ b/acceptance-tests/acceptance-tests-manifold-systems/src/test/resources/code/tuple/UserRecords.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2022 - 2022 Ashley Scopes
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.example;
+
+import static java.util.Comparator.comparingInt;
+import static java.util.stream.Collectors.toList;
+
+import java.util.List;
+import manifold.ext.rt.api.auto;
+
+/**
+ * Operations to perform on users using Manifold tuple expressions.
+ *
+ * @author Ashley Scopes
+ */
+public class UserRecords {
+
+  /**
+   * Get the oldest users in the given list.
+   *
+   * @param users the users to check.
+   * @return the list of user ages and user names, sorted with the oldest age first.
+   */
+  public static auto oldestUsers(List users) {
+    return users
+        .stream()
+        .sorted(comparingInt(user -> -user.age))
+        .map(user -> (user.age, user.name))
+        .collect(toList());
+  }
+}
diff --git a/acceptance-tests/acceptance-tests-manifold-systems/src/test/resources/junit-platform.properties b/acceptance-tests/acceptance-tests-manifold-systems/src/test/resources/junit-platform.properties
new file mode 100644
index 000000000..1d27b78fb
--- /dev/null
+++ b/acceptance-tests/acceptance-tests-manifold-systems/src/test/resources/junit-platform.properties
@@ -0,0 +1 @@
+junit.jupiter.execution.parallel.enabled=true
diff --git a/acceptance-tests/pom.xml b/acceptance-tests/pom.xml
index 1f5e3940a..f98db9b09 100644
--- a/acceptance-tests/pom.xml
+++ b/acceptance-tests/pom.xml
@@ -22,6 +22,7 @@
     acceptance-tests-google-auto-value
     acceptance-tests-immutables
     acceptance-tests-lombok
+    acceptance-tests-manifold-systems
     acceptance-tests-mapstruct
     acceptance-tests-serviceloader
     acceptance-tests-serviceloader-jpms