Skip to content
Merged
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
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<version>0.8.14</version>
<executions>
<execution>
<goals>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.garciat.typeclasses.processor;

import static com.garciat.typeclasses.api.TypeClass.Witness.Overlap.OVERLAPPABLE;
import static com.garciat.typeclasses.api.TypeClass.Witness.Overlap.OVERLAPPING;

import java.util.List;

public final class OverlappingInstances {
private OverlappingInstances() {}

/**
* @implSpec <a href=
* "https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/instances.html#overlapping-instances">6.8.8.5.
* Overlapping instances</a>
*/
public static List<WitnessConstructor> reduce(List<WitnessConstructor> candidates) {
return candidates.stream()
.filter(
iX ->
candidates.stream().filter(iY -> iX != iY).noneMatch(iY -> isOverlappedBy(iX, iY)))
.toList();
}

private static boolean isOverlappedBy(WitnessConstructor iX, WitnessConstructor iY) {
return (iX.overlap() == OVERLAPPABLE || iY.overlap() == OVERLAPPING)
&& isSubstitutionInstance(iX, iY)
&& !isSubstitutionInstance(iY, iX);
Comment thread
Garciat marked this conversation as resolved.
Comment thread
Garciat marked this conversation as resolved.
}

private static boolean isSubstitutionInstance(
WitnessConstructor base, WitnessConstructor reference) {
return Unification.unify(base.returnType(), reference.returnType())
.fold(() -> false, map -> !map.isEmpty());
}
}
34 changes: 34 additions & 0 deletions src/main/java/com/garciat/typeclasses/processor/ParsedType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.garciat.typeclasses.processor;

import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.PrimitiveType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;

public sealed interface ParsedType {
record Var(TypeVariable java) implements ParsedType {}

record App(ParsedType fun, ParsedType arg) implements ParsedType {}

record ArrayOf(ParsedType elementType) implements ParsedType {}

record Const(DeclaredType java) implements ParsedType {}

record Primitive(PrimitiveType java) implements ParsedType {}

default String format() {
return switch (this) {
case Var v -> v.java.toString();
case Const c ->
c.java().asElement().getSimpleName()
+ c.java().getTypeArguments().stream()
.map(TypeMirror::toString)
.reduce((a, b) -> a + ", " + b)
Comment thread
Garciat marked this conversation as resolved.
.map(s -> "[" + s + "]")
.orElse("");
case App a -> a.fun.format() + "(" + a.arg.format() + ")";
case ArrayOf a -> a.elementType.format() + "[]";
case Primitive p -> p.java().toString();
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package com.garciat.typeclasses.processor;

import com.garciat.typeclasses.api.TypeClass;
import com.garciat.typeclasses.api.hkt.TApp;
import com.garciat.typeclasses.api.hkt.TPar;
import com.garciat.typeclasses.api.hkt.TagBase;
import com.garciat.typeclasses.impl.utils.Lists;
import com.garciat.typeclasses.types.Maybe;
import com.garciat.typeclasses.types.Pair;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.*;

Comment thread
Garciat marked this conversation as resolved.
public class StaticWitnessSystem {
private static final Class<?> TAG_BASE_CLASS = TagBase.class;
private static final Class<?> TAPP_CLASS = TApp.class;
private static final Class<?> TPAR_CLASS = TPar.class;

public StaticWitnessSystem() {}

public List<WitnessConstructor> findRules(ParsedType target) {
return switch (target) {
case ParsedType.App(var fun, var arg) -> Lists.concat(findRules(fun), findRules(arg));
case ParsedType.Const(var java) ->
java.asElement().getEnclosedElements().stream()
.flatMap(isInstanceOf(ExecutableElement.class))
.flatMap(method -> parseWitnessConstructor(method).stream())
.toList();
case ParsedType.Var(var ignore) -> List.of();
case ParsedType.ArrayOf(var ignore) -> List.of();
case ParsedType.Primitive(var ignore) -> List.of();
Comment thread
Garciat marked this conversation as resolved.
};
}

private Maybe<WitnessConstructor> parseWitnessConstructor(ExecutableElement method) {
if (method.getModifiers().contains(Modifier.PUBLIC)
&& method.getModifiers().contains(Modifier.STATIC)
&& method.getAnnotation(TypeClass.Witness.class) instanceof TypeClass.Witness witnessAnn) {
return Maybe.just(
new WitnessConstructor(
method,
witnessAnn.overlap(),
method.getParameters().stream()
.map(VariableElement::asType)
.map(this::parse)
.toList(),
parse(method.getReturnType())));

} else {
return Maybe.nothing();
}
}

public ParsedType parse(TypeMirror type) {
return switch (type) {
case TypeVariable tv -> new ParsedType.Var(tv);
case ArrayType at -> new ParsedType.ArrayOf(parse(at.getComponentType()));
// Store primitive as its boxed type representation, just to have a DeclaredType.
case PrimitiveType pt -> new ParsedType.Primitive(pt);
case DeclaredType dt
when parseTagType(dt) instanceof Maybe.Just<DeclaredType>(var realType) ->
new ParsedType.Const(realType);
case DeclaredType dt when dt.getTypeArguments().isEmpty() -> new ParsedType.Const(dt);
case DeclaredType dt
when parseAppType(dt)
instanceof
Maybe.Just<Pair<TypeMirror, TypeMirror>>(
Pair<TypeMirror, TypeMirror>(var fun, var arg)) ->
new ParsedType.App(parse(fun), parse(arg));
case DeclaredType dt ->
dt.getTypeArguments().stream()
.map(this::parse)
.reduce(new ParsedType.Const(erasure(dt)), ParsedType.App::new);
case WildcardType wt ->
throw new IllegalArgumentException("Cannot parse wildcard type: " + wt);
default -> throw new IllegalArgumentException("Unsupported type: " + type);
};
}

private static Maybe<DeclaredType> parseTagType(DeclaredType t) {
if (t.asElement() instanceof TypeElement tag
&& tag.getEnclosingElement() instanceof TypeElement enclosing
&& enclosing.asType() instanceof DeclaredType enclosingType
&& tag.getSuperclass() instanceof DeclaredType tagSuperType
&& tagSuperType.asElement() instanceof TypeElement tagSuper
&& tagSuper.getQualifiedName().contentEquals(TAG_BASE_CLASS.getName())) {
return Maybe.just(enclosingType);
} else {
return Maybe.nothing();
}
}

private Maybe<Pair<TypeMirror, TypeMirror>> parseAppType(DeclaredType t) {
return t.getTypeArguments().size() == 2 && isAppType(erasure(t))
? Maybe.just(new Pair<>(t.getTypeArguments().get(0), t.getTypeArguments().get(1)))
: Maybe.nothing();
}

private boolean isAppType(TypeMirror erasure) {
return erasure instanceof DeclaredType dt
&& dt.asElement() instanceof TypeElement te
&& (te.getQualifiedName().contentEquals(TAPP_CLASS.getName())
|| te.getQualifiedName().contentEquals(TPAR_CLASS.getName()));
}

private DeclaredType erasure(DeclaredType t) {
return t.asElement().asType() instanceof DeclaredType typeCtor ? typeCtor : t;
}

private static <T extends U, U> Function<U, Stream<T>> isInstanceOf(Class<T> cls) {
return u -> cls.isInstance(u) ? Stream.of(cls.cast(u)) : Stream.empty();
}
}
52 changes: 52 additions & 0 deletions src/main/java/com/garciat/typeclasses/processor/Unification.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.garciat.typeclasses.processor;

import com.garciat.typeclasses.impl.utils.Maps;
import com.garciat.typeclasses.types.Maybe;
import com.garciat.typeclasses.types.Pair;
import java.util.List;
import java.util.Map;

public final class Unification {
private Unification() {}

public static Maybe<Map<ParsedType.Var, ParsedType>> unify(ParsedType t1, ParsedType t2) {
return switch (Pair.of(t1, t2)) {
case Pair<ParsedType, ParsedType>(ParsedType.Var var1, ParsedType.Primitive p) ->
Comment thread
Garciat marked this conversation as resolved.
Maybe.nothing(); // no primitives in generics
case Pair<ParsedType, ParsedType>(ParsedType.Var var1, var t) -> Maybe.just(Map.of(var1, t));
Comment thread
Garciat marked this conversation as resolved.
case Pair<ParsedType, ParsedType>(ParsedType.Const const1, ParsedType.Const const2)
when const1.equals(const2) ->
Maybe.just(Map.of());
case Pair<ParsedType, ParsedType>(
ParsedType.App(var fun1, var arg1),
ParsedType.App(var fun2, var arg2)) ->
Maybe.apply(Maps::merge, unify(fun1, fun2), unify(arg1, arg2));
case Pair<ParsedType, ParsedType>(
ParsedType.ArrayOf(var elem1),
ParsedType.ArrayOf(var elem2)) ->
unify(elem1, elem2);
case Pair<ParsedType, ParsedType>(
ParsedType.Primitive(var prim1),
ParsedType.Primitive(var prim2))
when prim1.equals(prim2) ->
Maybe.just(Map.of());
default -> Maybe.nothing();
};
}

public static ParsedType substitute(Map<ParsedType.Var, ParsedType> map, ParsedType type) {
return switch (type) {
case ParsedType.Var var -> map.getOrDefault(var, var);
case ParsedType.App(var fun, var arg) ->
new ParsedType.App(substitute(map, fun), substitute(map, arg));
case ParsedType.ArrayOf var -> new ParsedType.ArrayOf(substitute(map, var.elementType()));
case ParsedType.Primitive p -> p;
case ParsedType.Const c -> c;
};
}

public static List<ParsedType> substituteAll(
Map<ParsedType.Var, ParsedType> map, List<ParsedType> types) {
return types.stream().map(t -> substitute(map, t)).toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.garciat.typeclasses.processor;

import com.garciat.typeclasses.api.TypeClass;
import java.util.List;
import javax.lang.model.element.ExecutableElement;

public record WitnessConstructor(
ExecutableElement method,
TypeClass.Witness.Overlap overlap,
List<ParsedType> paramTypes,
ParsedType returnType) {}
Comment thread
Garciat marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.garciat.typeclasses.processor;

import com.garciat.typeclasses.impl.utils.ZeroOneMore;
import com.garciat.typeclasses.types.Either;
import com.garciat.typeclasses.types.Maybe;
import java.util.List;
import java.util.stream.Collectors;

public final class WitnessResolution {
private WitnessResolution() {}

/** Resolves a ParsedType into an InstantiationPlan. */
public static Either<ResolutionError, InstantiationPlan> resolve(
StaticWitnessSystem system, ParsedType target) {

List<Match> matches =
OverlappingInstances.reduce(system.findRules(target)).stream()
.flatMap(rule -> tryMatch(rule, target).stream())
.toList();

return switch (ZeroOneMore.of(matches)) {
case ZeroOneMore.One<Match>(Match(var rule, var requirements)) ->
Either.traverse(requirements, req -> resolve(system, req))
.<InstantiationPlan>map(
dependencies -> new InstantiationPlan.PlanStep(rule, dependencies))
.mapLeft(error -> new ResolutionError.Nested(target, error));
case ZeroOneMore.Zero<Match>() -> Either.left(new ResolutionError.NotFound(target));
case ZeroOneMore.More<Match>(var matches2) ->
Either.left(
new ResolutionError.Ambiguous(target, matches2.stream().map(Match::rule).toList()));
Comment thread
Garciat marked this conversation as resolved.
};
}

private static Maybe<Match> tryMatch(WitnessConstructor rule, ParsedType target) {
return Unification.unify(rule.returnType(), target)
.map(map -> Unification.substituteAll(map, rule.paramTypes()))
.map(requirements -> new Match(rule, requirements));
}

record Match(WitnessConstructor rule, List<ParsedType> requirements) {}

/**
* Represents the fully resolved instantiation plan. This is a tree structure where each node is a
* step in the instantiation process, with dependencies on other steps.
*/
public sealed interface InstantiationPlan {
record PlanStep(WitnessConstructor target, List<InstantiationPlan> dependencies)
implements InstantiationPlan {}
}

public sealed interface ResolutionError {
record NotFound(ParsedType target) implements ResolutionError {}

record Ambiguous(ParsedType target, List<WitnessConstructor> candidates)
implements ResolutionError {}

record Nested(ParsedType target, ResolutionError cause) implements ResolutionError {}

default String format() {
return switch (this) {
case NotFound(ParsedType target) -> "No witness found for type: " + target.format();
case Ambiguous(ParsedType target, List<WitnessConstructor> candidates) ->
"Ambiguous witnesses found for type: "
+ target.format()
+ "\nCandidates:\n"
+ candidates.stream()
.map(WitnessConstructor::toString)
.collect(Collectors.joining("\n"))
.indent(2);
case Nested(ParsedType target, ResolutionError cause) ->
"While resolving witness for type: "
+ target.format()
+ "\nCaused by: "
+ cause.format().indent(2);
};
}
}
}
Loading