diff --git a/README.md b/README.md index fc50fe7..25349bd 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ The core API of this library is: public class TypeClasses { public static T witness(Ty ty); + public static T witness(); // Compile-time only } ``` @@ -33,8 +34,30 @@ Where: found. - Witness summoning is the result of recursively invoking witness constructors up their respective dependency trees. -- `T witness(Ty)` summons a witness of type `T` or fails with a runtime - exception of type `TypeClasses.WitnessResolutionException`. +- `T witness(Ty)` summons a witness of type `T` at runtime or fails with a + runtime exception of type `TypeClasses.WitnessResolutionException`. +- `T witness()` is a compile-time only method that gets rewritten by the compiler + plugin into direct witness constructor calls. This avoids the runtime overhead + and the `new Ty<>() {}` trick required for type erasure. + +## Compiler Plugin + +The parameterless `witness()` method requires the Java compiler plugin to be +enabled. The plugin is automatically enabled when compiling with this library. + +Example usage: + +```java +// Runtime resolution with Ty (always works) +Show show1 = witness(new Ty<>() {}); + +// Compile-time resolution (requires plugin, more efficient) +Show show2 = witness(); +``` + +The compiler plugin rewrites the parameterless `witness()` call at compile time +into the appropriate witness constructor invocations, eliminating the need for +runtime resolution and improving performance. Now, there are multiple built-in type classes and types in the `classes` and `types` packages, respectively. Their usage is **completely optional**. If your diff --git a/pom.xml b/pom.xml index 4539f6f..be94742 100644 --- a/pom.xml +++ b/pom.xml @@ -52,6 +52,11 @@ maven-compiler-plugin 3.14.1 + + + -Xplugin:WitnessResolutionPlugin + + maven-surefire-plugin diff --git a/src/main/java/com/garciat/typeclasses/TypeClasses.java b/src/main/java/com/garciat/typeclasses/TypeClasses.java index 39592f6..86178a1 100644 --- a/src/main/java/com/garciat/typeclasses/TypeClasses.java +++ b/src/main/java/com/garciat/typeclasses/TypeClasses.java @@ -20,6 +20,21 @@ public static T witness(Ty ty) { }; } + /** + * Parameterless witness method that gets rewritten by the compiler plugin. This method should + * never be called at runtime - the compiler plugin will replace calls to this method with direct + * witness constructor invocations. + * + * @param The witness type to resolve + * @return The witness instance (at compile time, replaced with constructor calls) + * @throws UnsupportedOperationException if called at runtime without compiler plugin + */ + public static T witness() { + throw new UnsupportedOperationException( + "witness() without parameters should only be used with the compiler plugin enabled. " + + "Use witness(Ty) for runtime resolution."); + } + public static class WitnessResolutionException extends RuntimeException { private WitnessResolutionException(SummonError error) { super(error.format()); diff --git a/src/main/java/com/garciat/typeclasses/processor/AstRewriter.java b/src/main/java/com/garciat/typeclasses/processor/AstRewriter.java new file mode 100644 index 0000000..3c9c909 --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/processor/AstRewriter.java @@ -0,0 +1,85 @@ +package com.garciat.typeclasses.processor; + +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.tree.TreeMaker; +import com.sun.tools.javac.util.Context; +import com.sun.tools.javac.util.List; +import com.sun.tools.javac.util.Names; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; + +/** + * Handles AST rewriting to replace witness() calls with direct witness constructor invocations. + */ +public final class AstRewriter { + private final TreeMaker treeMaker; + private final Names names; + + private AstRewriter(TreeMaker treeMaker, Names names) { + this.treeMaker = treeMaker; + this.names = names; + } + + /** + * Creates an AstRewriter instance from a Context. + * + * @param context The javac Context + * @return A new AstRewriter instance + */ + public static AstRewriter create(Context context) { + TreeMaker treeMaker = TreeMaker.instance(context); + Names names = Names.instance(context); + return new AstRewriter(treeMaker, names); + } + + /** + * Translates an InstantiationPlan tree into a JCTree of method call invocations. + * + * @param plan The InstantiationPlan to translate + * @return A JCTree.JCExpression representing the method calls + */ + public JCTree.JCExpression buildWitnessInvocation( + WitnessResolution.InstantiationPlan plan) { + return buildWitnessInvocationRecursive(plan); + } + + private JCTree.JCExpression buildWitnessInvocationRecursive( + WitnessResolution.InstantiationPlan plan) { + if (plan instanceof WitnessResolution.InstantiationPlan.PlanStep step) { + WitnessConstructor constructor = step.target(); + ExecutableElement method = constructor.method(); + + // Get the enclosing class + TypeElement enclosingClass = (TypeElement) method.getEnclosingElement(); + + // Build the class reference (e.g., MyClass) + JCTree.JCExpression classRef = buildClassReference(enclosingClass); + + // Build the method select (e.g., MyClass.witnessMethod) + JCTree.JCFieldAccess methodSelect = + treeMaker.Select(classRef, names.fromString(method.getSimpleName().toString())); + + // Recursively build arguments from dependencies + com.sun.tools.javac.util.List args = + com.sun.tools.javac.util.List.from( + step.dependencies().stream() + .map(this::buildWitnessInvocationRecursive) + .collect(java.util.stream.Collectors.toList())); + + // Build the method invocation + return treeMaker.Apply(List.nil(), methodSelect, args); + } + throw new IllegalArgumentException("Unexpected InstantiationPlan type: " + plan); + } + + private JCTree.JCExpression buildClassReference(TypeElement typeElement) { + String qualifiedName = typeElement.getQualifiedName().toString(); + String[] parts = qualifiedName.split("\\."); + + JCTree.JCExpression expr = treeMaker.Ident(names.fromString(parts[0])); + for (int i = 1; i < parts.length; i++) { + expr = treeMaker.Select(expr, names.fromString(parts[i])); + } + return expr; + } +} diff --git a/src/main/java/com/garciat/typeclasses/processor/WitnessResolutionPlugin.java b/src/main/java/com/garciat/typeclasses/processor/WitnessResolutionPlugin.java new file mode 100644 index 0000000..097908d --- /dev/null +++ b/src/main/java/com/garciat/typeclasses/processor/WitnessResolutionPlugin.java @@ -0,0 +1,136 @@ +package com.garciat.typeclasses.processor; + +import com.sun.source.util.*; +import com.sun.tools.javac.api.BasicJavacTask; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.tree.TreeTranslator; +import com.sun.tools.javac.util.Context; + +/** + * A javac plugin that rewrites witness() calls into direct witness constructor invocations. + */ +public class WitnessResolutionPlugin implements Plugin { + + @Override + public String getName() { + return "WitnessResolutionPlugin"; + } + + @Override + public void init(JavacTask task, String... args) { + Context context = ((BasicJavacTask) task).getContext(); + Trees trees = Trees.instance(task); + AstRewriter astRewriter = AstRewriter.create(context); + + task.addTaskListener( + new TaskListener() { + @Override + public void finished(TaskEvent event) { + if (event.getKind() == TaskEvent.Kind.ANALYZE) { + JCTree.JCCompilationUnit compilationUnit = + (JCTree.JCCompilationUnit) event.getCompilationUnit(); + + // Perform AST rewriting in a single pass + new WitnessRewriter(trees, astRewriter).translate(compilationUnit); + } + } + }); + } + + /** TreeTranslator that detects and rewrites witness() calls. */ + private static class WitnessRewriter extends TreeTranslator { + private final Trees trees; + private final StaticWitnessSystem system; + private final AstRewriter astRewriter; + private JCTree.JCCompilationUnit currentCompilationUnit; + + private WitnessRewriter(Trees trees, AstRewriter astRewriter) { + this.trees = trees; + this.system = new StaticWitnessSystem(); + this.astRewriter = astRewriter; + } + + @Override + public void visitTopLevel(JCTree.JCCompilationUnit tree) { + this.currentCompilationUnit = tree; + super.visitTopLevel(tree); + } + + @Override + public void visitApply(JCTree.JCMethodInvocation tree) { + super.visitApply(tree); + + // Check if this is a call to witness() + if (tree.meth instanceof JCTree.JCFieldAccess methodSelect) { + if (isWitnessMethod(methodSelect)) { + tryRewriteWitnessCall(tree, methodSelect); + } + } else if (tree.meth instanceof JCTree.JCIdent ident) { + if (isWitnessMethod(ident)) { + tryRewriteWitnessCall(tree, null); + } + } + } + + private boolean isWitnessMethod(JCTree.JCFieldAccess methodSelect) { + String methodName = methodSelect.name.toString(); + return "witness".equals(methodName) + && methodSelect.selected.toString().endsWith("TypeClasses"); + } + + private boolean isWitnessMethod(JCTree.JCIdent ident) { + return "witness".equals(ident.name.toString()); + } + + private void tryRewriteWitnessCall( + JCTree.JCMethodInvocation tree, JCTree.JCFieldAccess methodSelect) { + try { + // Determine if this is parameterless or parameterful witness() + if (tree.args.isEmpty()) { + // Parameterless witness() - use type from context + if (tree.type != null) { + rewriteWithType(tree, tree.type); + } + } else if (tree.args.size() == 1) { + // Could be parameterful witness(Ty) - we keep the existing behavior + // The annotation processor will validate it + } + } catch (Exception e) { + // Report as a note, not an error, since this is best-effort rewriting + if (currentCompilationUnit != null) { + trees.printMessage( + javax.tools.Diagnostic.Kind.NOTE, + "Note: Could not rewrite witness call: " + e.getMessage(), + tree, + currentCompilationUnit); + } + } + } + + private void rewriteWithType(JCTree.JCMethodInvocation tree, com.sun.tools.javac.code.Type type) { + try { + ParsedType parsedType = system.parse(type); + WitnessResolution.resolve(system, parsedType) + .fold( + error -> { + // Don't rewrite on error - let the annotation processor report it + return null; + }, + plan -> { + JCTree.JCExpression replacement = astRewriter.buildWitnessInvocation(plan); + result = replacement; + return null; + }); + } catch (Exception e) { + // Report as a note for debugging + if (currentCompilationUnit != null) { + trees.printMessage( + javax.tools.Diagnostic.Kind.NOTE, + "Note: Could not parse type for witness rewriting: " + e.getMessage(), + tree, + currentCompilationUnit); + } + } + } + } +} diff --git a/src/main/resources/META-INF/services/com.sun.source.util.Plugin b/src/main/resources/META-INF/services/com.sun.source.util.Plugin new file mode 100644 index 0000000..2c9aa22 --- /dev/null +++ b/src/main/resources/META-INF/services/com.sun.source.util.Plugin @@ -0,0 +1 @@ +com.garciat.typeclasses.processor.WitnessResolutionPlugin diff --git a/src/test/java/com/garciat/typeclasses/ParameterlessWitnessTest.java b/src/test/java/com/garciat/typeclasses/ParameterlessWitnessTest.java new file mode 100644 index 0000000..0612626 --- /dev/null +++ b/src/test/java/com/garciat/typeclasses/ParameterlessWitnessTest.java @@ -0,0 +1,34 @@ +package com.garciat.typeclasses; + +import static com.garciat.typeclasses.TypeClasses.witness; + +import com.garciat.typeclasses.classes.Show; +import org.junit.jupiter.api.Test; + +/** + * Test the parameterless witness() method with the compiler plugin. + * + * This test verifies that the compiler plugin correctly rewrites calls to the parameterless + * witness() method into direct witness constructor invocations. + */ +final class ParameterlessWitnessTest { + + @Test + void parameterlessWitnessForSimpleType() { + // This should be rewritten by the plugin to: Show.integerShow() + Show showInteger = witness(); + + // If the plugin worked, this should execute without throwing UnsupportedOperationException + String result = showInteger.show(42); + System.out.println("Show: " + result); + } + + @Test + void parameterlessWitnessForComplexType() { + // This should be rewritten by the plugin to: Show.listShow(Show.integerShow()) + Show> showList = witness(); + + String result = showList.show(java.util.List.of(1, 2, 3)); + System.out.println("Show>: " + result); + } +}