Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ The core API of this library is:

public class TypeClasses {
public static <T> T witness(Ty<T> ty);
public static <T> T witness(); // Compile-time only
}
```

Expand All @@ -33,8 +34,30 @@ Where:
found.
- Witness summoning is the result of recursively invoking witness constructors
up their respective dependency trees.
- `T witness(Ty<T>)` summons a witness of type `T` or fails with a runtime
exception of type `TypeClasses.WitnessResolutionException`.
- `T witness(Ty<T>)` 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<T> (always works)
Show<Integer> show1 = witness(new Ty<>() {});

// Compile-time resolution (requires plugin, more efficient)
Show<Integer> 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
Expand Down
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
<configuration>
<compilerArgs>
<arg>-Xplugin:WitnessResolutionPlugin</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/com/garciat/typeclasses/TypeClasses.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,21 @@ public static <T> T witness(Ty<T> 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 <T> 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> T witness() {
throw new UnsupportedOperationException(
"witness() without parameters should only be used with the compiler plugin enabled. "
+ "Use witness(Ty<T>) for runtime resolution.");
}

public static class WitnessResolutionException extends RuntimeException {
private WitnessResolutionException(SummonError error) {
super(error.format());
Expand Down
85 changes: 85 additions & 0 deletions src/main/java/com/garciat/typeclasses/processor/AstRewriter.java
Original file line number Diff line number Diff line change
@@ -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<JCTree.JCExpression> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<T>) - 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);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.garciat.typeclasses.processor.WitnessResolutionPlugin
Original file line number Diff line number Diff line change
@@ -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<Integer> showInteger = witness();

// If the plugin worked, this should execute without throwing UnsupportedOperationException
String result = showInteger.show(42);
System.out.println("Show<Integer>: " + result);
}

@Test
void parameterlessWitnessForComplexType() {
// This should be rewritten by the plugin to: Show.listShow(Show.integerShow())
Show<java.util.List<Integer>> showList = witness();

String result = showList.show(java.util.List.of(1, 2, 3));
System.out.println("Show<List<Integer>>: " + result);
}
}