From f739f193b0fe5eea45aff9a36098cf512c3901b2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Dec 2025 20:14:50 +0000 Subject: [PATCH 1/7] Initial plan From 00e691dff3eab57448b395682ec87c6eecf1e622 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Dec 2025 20:22:19 +0000 Subject: [PATCH 2/7] Implement staged resolution system with InstantiationPlan, Expr, and staged methods Co-authored-by: Garciat <118277+Garciat@users.noreply.github.com> --- .../com/garciat/typeclasses/TypeClasses.java | 88 +++++++++++++++++-- 1 file changed, 83 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/garciat/typeclasses/TypeClasses.java b/src/main/java/com/garciat/typeclasses/TypeClasses.java index 3a712fd..8181b13 100644 --- a/src/main/java/com/garciat/typeclasses/TypeClasses.java +++ b/src/main/java/com/garciat/typeclasses/TypeClasses.java @@ -73,12 +73,48 @@ case Nested(ParsedType target, SummonError cause) -> } } + /** + * Summons a witness for the given target type using the staged resolution approach: parseType >> + * resolveWitness >> compile >> interpret + */ private static Either summon( ParsedType target, List context) { + return resolveWitness(target, context) + .map(TypeClasses::compile) + .map(expr -> interpret(new InterpretContext(context), expr)); + } + + private record Candidate(WitnessRule rule, List requirements) {} + + // ========== Staged Resolution Components ========== + + /** + * 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. + */ + private sealed interface InstantiationPlan { + record PlanStep(WitnessRule target, List dependencies) + implements InstantiationPlan {} + } + + /** + * Represents a reduced AST of interpretable Java operations. This is the compiled form of an + * InstantiationPlan. + */ + private sealed interface Expr { + record InvokeConstructor(Method method, List> arguments) implements Expr {} + + record Lookup(K key) implements Expr {} + } + + /** Resolves a ParsedType into an InstantiationPlan. */ + private static Either resolveWitness( + ParsedType target, List context) { return switch (ZeroOneMore.of(findCandidates(target, context))) { case ZeroOneMore.One(Candidate(var rule, var requirements)) -> - summonAll(requirements, context) - .map(rule::instantiate) + resolveWitnessAll(requirements, context) + .map( + dependencies -> new InstantiationPlan.PlanStep(rule, dependencies)) .mapLeft(error -> new SummonError.Nested(target, error)); case ZeroOneMore.Zero() -> Either.left(new SummonError.NotFound(target)); case ZeroOneMore.More(var candidates) -> @@ -86,12 +122,54 @@ private static Either summon( }; } - private static Either> summonAll( + /** Resolves multiple ParsedTypes into a list of InstantiationPlans. */ + private static Either> resolveWitnessAll( List targets, List context) { - return Either.traverse(targets, target -> summon(target, context)); + return Either.traverse(targets, target -> resolveWitness(target, context)); } - private record Candidate(WitnessRule rule, List requirements) {} + /** Compiles an InstantiationPlan into an Expr. */ + private static Expr compile(InstantiationPlan plan) { + return switch (plan) { + case InstantiationPlan.PlanStep(var rule, var dependencies) -> + switch (rule) { + case ContextInstance(var instance, var type) -> new Expr.Lookup<>(type); + case InstanceConstructor(var func) -> + new Expr.InvokeConstructor<>( + func.java(), dependencies.stream().map(TypeClasses::compile).toList()); + }; + }; + } + + /** + * Context for interpretation - maps keys to resolved instances. For our use case, keys are + * ParsedTypes. + */ + private record InterpretContext(List instances) { + Object lookup(ParsedType type) { + return instances.stream() + .filter(ci -> ci.type().equals(type)) + .findFirst() + .map(ContextInstance::instance) + .orElseThrow( + () -> new RuntimeException("Context lookup failed for type: " + type.format())); + } + } + + /** Interprets an Expr with a given context. */ + private static Object interpret(InterpretContext context, Expr expr) { + return switch (expr) { + case Expr.Lookup(var type) -> context.lookup(type); + case Expr.InvokeConstructor(var method, var args) -> { + Object[] evaluatedArgs = args.stream().map(arg -> interpret(context, arg)).toArray(); + try { + yield method.invoke(null, evaluatedArgs); + } catch (Exception e) { + throw new RuntimeException("Failed to invoke constructor: " + method, e); + } + } + }; + } private static List findCandidates(ParsedType target, List context) { return Stream.concat( From c264eb15e02dc0a00d4a253b2d58ffdd23c77b59 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Dec 2025 20:44:29 +0000 Subject: [PATCH 3/7] Split error types into ResolutionError, InstantiationError, and SummonError Co-authored-by: Garciat <118277+Garciat@users.noreply.github.com> --- .../com/garciat/typeclasses/TypeClasses.java | 98 +++++++++++++++---- 1 file changed, 77 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/garciat/typeclasses/TypeClasses.java b/src/main/java/com/garciat/typeclasses/TypeClasses.java index 8181b13..b02bc4f 100644 --- a/src/main/java/com/garciat/typeclasses/TypeClasses.java +++ b/src/main/java/com/garciat/typeclasses/TypeClasses.java @@ -46,12 +46,12 @@ private WitnessResolutionException(SummonError error) { } } - private sealed interface SummonError { - record NotFound(ParsedType target) implements SummonError {} + private sealed interface ResolutionError { + record NotFound(ParsedType target) implements ResolutionError {} - record Ambiguous(ParsedType target, List candidates) implements SummonError {} + record Ambiguous(ParsedType target, List candidates) implements ResolutionError {} - record Nested(ParsedType target, SummonError cause) implements SummonError {} + record Nested(ParsedType target, ResolutionError cause) implements ResolutionError {} default String format() { return switch (this) { @@ -64,8 +64,8 @@ case Ambiguous(ParsedType target, List candidates) -> .map(c -> c.rule().toString()) .collect(Collectors.joining("\n")) .indent(2); - case Nested(ParsedType target, SummonError cause) -> - "While summoning witness for type: " + case Nested(ParsedType target, ResolutionError cause) -> + "While resolving witness for type: " + target.format() + "\nCaused by: " + cause.format().indent(2); @@ -73,15 +73,48 @@ case Nested(ParsedType target, SummonError cause) -> } } + private sealed interface InstantiationError { + record LookupMiss(ParsedType type) implements InstantiationError {} + + record InvocationException(Method method, Exception cause) implements InstantiationError {} + + default String format() { + return switch (this) { + case LookupMiss(ParsedType type) -> "Context lookup failed for type: " + type.format(); + case InvocationException(Method method, Exception cause) -> + "Failed to invoke constructor: " + method + "\nCause: " + cause.getMessage(); + }; + } + } + + private sealed interface SummonError { + record Resolution(ResolutionError error) implements SummonError {} + + record Instantiation(InstantiationError error) implements SummonError {} + + default String format() { + return switch (this) { + case Resolution(ResolutionError error) -> error.format(); + case Instantiation(InstantiationError error) -> error.format(); + }; + } + } + /** * Summons a witness for the given target type using the staged resolution approach: parseType >> * resolveWitness >> compile >> interpret */ private static Either summon( ParsedType target, List context) { - return resolveWitness(target, context) - .map(TypeClasses::compile) - .map(expr -> interpret(new InterpretContext(context), expr)); + Either resolutionResult = + resolveWitness(target, context).mapLeft(SummonError.Resolution::new); + + Either> compilationResult = + resolutionResult.map(TypeClasses::compile); + + return compilationResult.flatMap( + expr -> + interpret(new InterpretContext(context), expr).mapLeft(SummonError.Instantiation::new)); } private record Candidate(WitnessRule rule, List requirements) {} @@ -108,22 +141,22 @@ record Lookup(K key) implements Expr {} } /** Resolves a ParsedType into an InstantiationPlan. */ - private static Either resolveWitness( + private static Either resolveWitness( ParsedType target, List context) { return switch (ZeroOneMore.of(findCandidates(target, context))) { case ZeroOneMore.One(Candidate(var rule, var requirements)) -> resolveWitnessAll(requirements, context) .map( dependencies -> new InstantiationPlan.PlanStep(rule, dependencies)) - .mapLeft(error -> new SummonError.Nested(target, error)); - case ZeroOneMore.Zero() -> Either.left(new SummonError.NotFound(target)); + .mapLeft(error -> new ResolutionError.Nested(target, error)); + case ZeroOneMore.Zero() -> Either.left(new ResolutionError.NotFound(target)); case ZeroOneMore.More(var candidates) -> - Either.left(new SummonError.Ambiguous(target, candidates)); + Either.left(new ResolutionError.Ambiguous(target, candidates)); }; } /** Resolves multiple ParsedTypes into a list of InstantiationPlans. */ - private static Either> resolveWitnessAll( + private static Either> resolveWitnessAll( List targets, List context) { return Either.traverse(targets, target -> resolveWitness(target, context)); } @@ -146,26 +179,49 @@ case InstanceConstructor(var func) -> * ParsedTypes. */ private record InterpretContext(List instances) { - Object lookup(ParsedType type) { + Either lookup(ParsedType type) { return instances.stream() .filter(ci -> ci.type().equals(type)) .findFirst() .map(ContextInstance::instance) - .orElseThrow( - () -> new RuntimeException("Context lookup failed for type: " + type.format())); + .>map(Either::right) + .orElseGet(() -> Either.left(new InstantiationError.LookupMiss(type))); } } /** Interprets an Expr with a given context. */ - private static Object interpret(InterpretContext context, Expr expr) { + private static Either interpret( + InterpretContext context, Expr expr) { return switch (expr) { case Expr.Lookup(var type) -> context.lookup(type); case Expr.InvokeConstructor(var method, var args) -> { - Object[] evaluatedArgs = args.stream().map(arg -> interpret(context, arg)).toArray(); + // Evaluate all arguments first + List> evaluatedArgs = + args.stream().map(arg -> interpret(context, arg)).toList(); + + // Check if any argument evaluation failed + for (Either argResult : evaluatedArgs) { + if (argResult instanceof Either.Left(var error)) { + yield Either.left(error); + } + } + + // Extract successful values + Object[] argValues = + evaluatedArgs.stream() + .map( + e -> + switch (e) { + case Either.Right(var value) -> value; + case Either.Left(var error) -> + throw new IllegalStateException("Unreachable"); + }) + .toArray(); + try { - yield method.invoke(null, evaluatedArgs); + yield Either.right(method.invoke(null, argValues)); } catch (Exception e) { - throw new RuntimeException("Failed to invoke constructor: " + method, e); + yield Either.left(new InstantiationError.InvocationException(method, e)); } } }; From e4dff04cbc7cea2fd976c4155d2de7cf8077ed3c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Dec 2025 21:22:43 +0000 Subject: [PATCH 4/7] Use Either.traverse in interpret and remove Candidate type Co-authored-by: Garciat <118277+Garciat@users.noreply.github.com> --- .../com/garciat/typeclasses/TypeClasses.java | 83 +++++++------------ 1 file changed, 31 insertions(+), 52 deletions(-) diff --git a/src/main/java/com/garciat/typeclasses/TypeClasses.java b/src/main/java/com/garciat/typeclasses/TypeClasses.java index b02bc4f..65b3911 100644 --- a/src/main/java/com/garciat/typeclasses/TypeClasses.java +++ b/src/main/java/com/garciat/typeclasses/TypeClasses.java @@ -49,19 +49,19 @@ private WitnessResolutionException(SummonError error) { private sealed interface ResolutionError { record NotFound(ParsedType target) implements ResolutionError {} - record Ambiguous(ParsedType target, List candidates) implements ResolutionError {} + record Ambiguous(ParsedType target, List 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 candidates) -> + case Ambiguous(ParsedType target, List candidates) -> "Ambiguous witnesses found for type: " + target.format() + "\nCandidates:\n" + candidates.stream() - .map(c -> c.rule().toString()) + .map(WitnessRule::toString) .collect(Collectors.joining("\n")) .indent(2); case Nested(ParsedType target, ResolutionError cause) -> @@ -117,8 +117,6 @@ private static Either summon( interpret(new InterpretContext(context), expr).mapLeft(SummonError.Instantiation::new)); } - private record Candidate(WitnessRule rule, List requirements) {} - // ========== Staged Resolution Components ========== /** @@ -143,15 +141,28 @@ record Lookup(K key) implements Expr {} /** Resolves a ParsedType into an InstantiationPlan. */ private static Either resolveWitness( ParsedType target, List context) { - return switch (ZeroOneMore.of(findCandidates(target, context))) { - case ZeroOneMore.One(Candidate(var rule, var requirements)) -> + record Match(WitnessRule rule, List requirements) {} + + List matches = + Stream.concat(context.stream(), reduceOverlapping(findRules(target)).stream()) + .flatMap( + rule -> + rule + .tryMatch(target) + .map(requirements -> new Match(rule, requirements)) + .stream()) + .toList(); + + return switch (ZeroOneMore.of(matches)) { + case ZeroOneMore.One(Match(var rule, var requirements)) -> resolveWitnessAll(requirements, context) .map( dependencies -> new InstantiationPlan.PlanStep(rule, dependencies)) .mapLeft(error -> new ResolutionError.Nested(target, error)); - case ZeroOneMore.Zero() -> Either.left(new ResolutionError.NotFound(target)); - case ZeroOneMore.More(var candidates) -> - Either.left(new ResolutionError.Ambiguous(target, candidates)); + case ZeroOneMore.Zero() -> Either.left(new ResolutionError.NotFound(target)); + case ZeroOneMore.More(var matches2) -> + Either.left( + new ResolutionError.Ambiguous(target, matches2.stream().map(Match::rule).toList())); }; } @@ -194,51 +205,19 @@ private static Either interpret( InterpretContext context, Expr expr) { return switch (expr) { case Expr.Lookup(var type) -> context.lookup(type); - case Expr.InvokeConstructor(var method, var args) -> { - // Evaluate all arguments first - List> evaluatedArgs = - args.stream().map(arg -> interpret(context, arg)).toList(); - - // Check if any argument evaluation failed - for (Either argResult : evaluatedArgs) { - if (argResult instanceof Either.Left(var error)) { - yield Either.left(error); - } - } - - // Extract successful values - Object[] argValues = - evaluatedArgs.stream() - .map( - e -> - switch (e) { - case Either.Right(var value) -> value; - case Either.Left(var error) -> - throw new IllegalStateException("Unreachable"); - }) - .toArray(); - - try { - yield Either.right(method.invoke(null, argValues)); - } catch (Exception e) { - yield Either.left(new InstantiationError.InvocationException(method, e)); - } - } + case Expr.InvokeConstructor(var method, var args) -> + Either.traverse(args, arg -> interpret(context, arg)) + .flatMap( + argValues -> { + try { + return Either.right(method.invoke(null, argValues.toArray())); + } catch (Exception e) { + return Either.left(new InstantiationError.InvocationException(method, e)); + } + }); }; } - private static List findCandidates(ParsedType target, List context) { - return Stream.concat( - context.stream(), reduceOverlapping(findRules(target)).stream()) - .flatMap( - rule -> - rule - .tryMatch(target) - .map(requirements -> new Candidate(rule, requirements)) - .stream()) - .toList(); - } - /** * @implSpec 6.8.8.5. From fdc90e4236797509bd0b37749744c6badeb89f44 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Dec 2025 21:47:53 +0000 Subject: [PATCH 5/7] Remove instantiate method from WitnessRule and subtypes Co-authored-by: Garciat <118277+Garciat@users.noreply.github.com> --- .../com/garciat/typeclasses/TypeClasses.java | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/main/java/com/garciat/typeclasses/TypeClasses.java b/src/main/java/com/garciat/typeclasses/TypeClasses.java index 65b3911..e87888a 100644 --- a/src/main/java/com/garciat/typeclasses/TypeClasses.java +++ b/src/main/java/com/garciat/typeclasses/TypeClasses.java @@ -269,8 +269,6 @@ private static boolean isWitnessMethod(Method m) { private sealed interface WitnessRule { Maybe> tryMatch(ParsedType target); - - Object instantiate(List dependencies); } private record ContextInstance(Object instance, ParsedType type) implements WitnessRule { @@ -279,11 +277,6 @@ public Maybe> tryMatch(ParsedType target) { return target.equals(type) ? Maybe.just(List.of()) : Maybe.nothing(); } - @Override - public Object instantiate(List dependencies) { - return instance; - } - @Override public String toString() { return "context instance: " + type.format(); @@ -301,15 +294,6 @@ public Maybe> tryMatch(ParsedType target) { .map(map -> Unification.substituteAll(map, func.paramTypes())); } - @Override - public Object instantiate(List dependencies) { - try { - return func.java().invoke(null, dependencies.toArray()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - @Override public String toString() { return func.format(); From d469d2ab42d3d02e2fcf67597441361d09b4adb5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:06:30 +0000 Subject: [PATCH 6/7] Add Either.call helper method for wrapping Callable execution Co-authored-by: Garciat <118277+Garciat@users.noreply.github.com> --- src/main/java/com/garciat/typeclasses/types/Either.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/com/garciat/typeclasses/types/Either.java b/src/main/java/com/garciat/typeclasses/types/Either.java index 2cba8a5..f470aa8 100644 --- a/src/main/java/com/garciat/typeclasses/types/Either.java +++ b/src/main/java/com/garciat/typeclasses/types/Either.java @@ -10,6 +10,7 @@ import com.garciat.typeclasses.classes.Functor; import com.garciat.typeclasses.classes.Monad; import java.util.List; +import java.util.concurrent.Callable; import java.util.function.Function; public sealed interface Either extends TApp, R> { @@ -25,6 +26,14 @@ static Either right(R value) { return new Right<>(value); } + static Either call(Callable callable) { + try { + return right(callable.call()); + } catch (Exception e) { + return left(e); + } + } + default Either map(Function f) { return fold(Either::left, f.andThen(Either::right)); } From 7c59357b8ad69982da56135b1fcd7f9fc84223c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:09:37 +0000 Subject: [PATCH 7/7] Use Either.call helper in interpret method Co-authored-by: Garciat <118277+Garciat@users.noreply.github.com> --- src/main/java/com/garciat/typeclasses/TypeClasses.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/garciat/typeclasses/TypeClasses.java b/src/main/java/com/garciat/typeclasses/TypeClasses.java index e87888a..a7ebec6 100644 --- a/src/main/java/com/garciat/typeclasses/TypeClasses.java +++ b/src/main/java/com/garciat/typeclasses/TypeClasses.java @@ -208,13 +208,9 @@ private static Either interpret( case Expr.InvokeConstructor(var method, var args) -> Either.traverse(args, arg -> interpret(context, arg)) .flatMap( - argValues -> { - try { - return Either.right(method.invoke(null, argValues.toArray())); - } catch (Exception e) { - return Either.left(new InstantiationError.InvocationException(method, e)); - } - }); + argValues -> + Either.call(() -> method.invoke(null, argValues.toArray())) + .mapLeft(e -> new InstantiationError.InvocationException(method, e))); }; }