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
22 changes: 21 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>25</maven.compiler.release>
<!-- Note: Using source/target instead of release because add-exports is incompatible with release.
This is necessary for accessing compiler internals (TreeMaker, JCTree) for AST rewriting. -->
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
</properties>

Expand Down Expand Up @@ -52,10 +54,28 @@
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
<configuration>
<compilerArgs>
<arg>--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
<arg>--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
<arg>--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
<arg>--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
<arg>--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
<configuration>
<argLine>
--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
Expand Down
11 changes: 11 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,17 @@ public static <T> T witness(Ty<T> ty) {
};
}

/**
* Parameterless witness method that should be rewritten by the compiler. This method should never
* be called at runtime; the compiler will replace it with the appropriate witness constructor
* calls.
*/
public static <T> T witness() {
throw new AssertionError(
"witness() should have been rewritten by the compiler. "
+ "Make sure the WitnessResolutionChecker annotation processor is enabled.");
}

public static class WitnessResolutionException extends RuntimeException {
private WitnessResolutionException(SummonError error) {
super(error.format());
Expand Down
140 changes: 140 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,140 @@
package com.garciat.typeclasses.processor;

import com.garciat.typeclasses.processor.WitnessResolution.InstantiationPlan;
import com.sun.source.tree.Tree;
import com.sun.source.util.TreePath;
import com.sun.source.util.Trees;
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;

/** Handles AST rewriting for parameterless witness() calls. */
final class AstRewriter {
private final TreeMaker treeMaker;
private final Names names;
private final Trees trees;

AstRewriter(Context context, Trees trees) {
this.treeMaker = TreeMaker.instance(context);
this.names = Names.instance(context);
this.trees = trees;
}

/**
* Translates an InstantiationPlan into a JCTree representing the witness constructor call chain.
*/
JCTree.JCExpression buildWitnessExpression(InstantiationPlan plan) {
return switch (plan) {
case InstantiationPlan.PlanStep(var constructor, var dependencies) -> {
// Get the ExecutableElement for the witness constructor
ExecutableElement method = constructor.method();

// Build the method invocation expression
// Format: ClassName.methodName(dep1, dep2, ...)
JCTree.JCExpression methodSelect = buildMethodSelect(method);

// Recursively build expressions for dependencies
List<JCTree.JCExpression> args =
List.from(
dependencies.stream()
.map(this::buildWitnessExpression)
.toArray(JCTree.JCExpression[]::new));

// Create the method invocation
yield treeMaker.Apply(List.nil(), methodSelect, args);
}
};
}

/**
* Builds a method select expression for a given executable element. For a static method
* "ClassName.methodName", this creates the appropriate JCTree.JCFieldAccess.
*/
private JCTree.JCExpression buildMethodSelect(ExecutableElement method) {
// Get the enclosing class
var enclosingElement = method.getEnclosingElement();

// Build the class reference expression
JCTree.JCExpression classExpr = buildClassReference(enclosingElement.toString());

// Create field access: ClassName.methodName
return treeMaker.Select(classExpr, names.fromString(method.getSimpleName().toString()));
}

/**
* Builds a class reference expression from a fully qualified class name. For example,
* "com.example.MyClass" becomes a chain of field accesses.
*/
private JCTree.JCExpression buildClassReference(String qualifiedName) {
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;
}

/**
* Replaces a tree node in the AST by modifying the parent node.
*
* <p>NOTE: This method is currently not used. AST rewriting is performed via TreeTranslator in
* WitnessCallTranslator.visitApply() which sets the 'result' field. This method is kept as an
* alternative approach for future reference or if a different rewriting strategy is needed.
*
* @param path the tree path to the node to replace
* @param replacement the new tree to use
*/
@SuppressWarnings("unused")
void replaceTree(TreePath path, JCTree.JCExpression replacement) {
Tree leaf = path.getLeaf();
if (!(leaf instanceof JCTree.JCMethodInvocation originalInvocation)) {
return;
}

// Get parent context
TreePath parentPath = path.getParentPath();
if (parentPath == null) {
return;
}

Tree parent = parentPath.getLeaf();

// We need to replace the method invocation in its parent
// This is complex and depends on the parent type, so we'll use a simpler approach:
// Modify the tree in place by replacing fields

if (parent instanceof JCTree.JCVariableDecl varDecl) {
// Case: variable declaration
varDecl.init = replacement;
} else if (parent instanceof JCTree.JCExpressionStatement exprStmt) {
// Case: expression statement
exprStmt.expr = replacement;
} else if (parent instanceof JCTree.JCReturn returnStmt) {
// Case: return statement
returnStmt.expr = replacement;
} else if (parent instanceof JCTree.JCAssign assign) {
// Case: assignment on the right side
if (assign.rhs == originalInvocation) {
assign.rhs = replacement;
}
} else if (parent instanceof JCTree.JCMethodInvocation parentInvocation) {
// Case: method argument
List<JCTree.JCExpression> args = parentInvocation.args;
List<JCTree.JCExpression> newArgs = List.nil();
for (JCTree.JCExpression arg : args) {
if (arg == originalInvocation) {
newArgs = newArgs.append(replacement);
} else {
newArgs = newArgs.append(arg);
}
}
parentInvocation.args = newArgs;
}
// Add more cases as needed for other parent types
}
}
Loading