From dc406d567d84290c9c84e1c72fc7ddebbea84fef Mon Sep 17 00:00:00 2001 From: Kevin Thompson Date: Thu, 2 Oct 2025 22:47:00 -0700 Subject: [PATCH] feat: add improved support for generic methods --- pnc/src/ExprBinder.pn | 297 +++++++++++++++++++++++++++ pncs/src/main/scala/ExprBinder.scala | 297 +++++++++++++++++++++++++++ test/src/test/scala/TypeTests.scala | 99 +++++++++ 3 files changed, 693 insertions(+) diff --git a/pnc/src/ExprBinder.pn b/pnc/src/ExprBinder.pn index 9bbea0e..b029884 100644 --- a/pnc/src/ExprBinder.pn +++ b/pnc/src/ExprBinder.pn @@ -503,6 +503,14 @@ class ExprBinder( case Result.Success(value) => Result.Success(BoundLeftHandSide.Call(value)) } + + case gf: Type.GenericFunction => + bindGenericFunctionCall(function, gf, args, scope) match { + case Result.Error(value) => Result.Error(value) + case Result.Success(value) => + Result.Success(BoundLeftHandSide.Call(value)) + } + case Type.Class(_, ns, name, typeArgs, symbol) => val location = AstUtils.locationOfBoundLeftHandSide(function) // Handle array indexing @@ -747,6 +755,295 @@ class ExprBinder( } } + def bindGenericFunctionCall( + function: BoundLeftHandSide, + genericFunctionType: Type.GenericFunction, + args: List[BoundExpression], + scope: Scope + ): Result[BoundExpression.Error, BoundExpression.Call] = { + val location = AstUtils.locationOfBoundLeftHandSide(function) + + // First, infer type arguments from the call arguments + val argTypes = binder.getTypes(args) + val parameterTypes = getParameterTypes(genericFunctionType.parameters) + val inferredTypeArgs = inferTypeArgumentsFromCall( + genericFunctionType.generics, + parameterTypes, + argTypes + ) + + // Instantiate the generic function with inferred type arguments + val instantiatedParameterTypes = substituteGenericTypesInParameters( + parameterTypes, + genericFunctionType.generics, + inferredTypeArgs + ) + + val instantiatedReturnType = substituteGenericTypesInType( + genericFunctionType.returnType, + genericFunctionType.generics, + inferredTypeArgs + ) + + // Check argument count matches + if (instantiatedParameterTypes.length != args.length) { + diagnosticBag.reportArgumentCountMismatch( + location, + instantiatedParameterTypes.length, + args.length + ) + Result.Error( + BoundExpression.Error( + "Argument count mismatch: expected " + instantiatedParameterTypes.length + + " but got " + args.length + ) + ) + } else { + // Bind arguments with instantiated parameter types + val boundArgs = + bindArgumentsToTypes(instantiatedParameterTypes, args, scope) + + function match { + case BoundLeftHandSide.Index(expression) => ??? + case BoundLeftHandSide.MemberAccess(access) => + val receiver = if (access.member.isStatic()) { + Option.None + } else { + Option.Some(access.receiver) + } + Result.Success( + BoundExpression.Call( + location, + receiver, + access.member, + inferredTypeArgs, // Pass the inferred type arguments + boundArgs, + instantiatedReturnType + ) + ) + case BoundLeftHandSide.Variable(location, symbol) => + Result.Success( + BoundExpression.Call( + location, + Option.None, + symbol, + inferredTypeArgs, // Pass the inferred type arguments + boundArgs, + instantiatedReturnType + ) + ) + case _ => ??? + } + } + } + + def inferTypeArgumentsFromCall( + genericParams: List[GenericTypeParameter], + parameterTypes: List[Type], + argumentTypes: List[Type] + ): List[Type] = { + // Simple type inference: try to match parameter types with argument types + // This is a basic implementation - a full implementation would be more sophisticated + val typeMap = new scala.collection.mutable.HashMap[String, Type]() + + // Try to infer from parameter/argument pairs + inferFromParameterArgumentPairs(parameterTypes, argumentTypes, typeMap) + + // Convert to list of types in the order of generic parameters + mapGenericParamsToTypes(genericParams, typeMap) + } + + def mapGenericParamsToTypes( + genericParams: List[GenericTypeParameter], + typeMap: scala.collection.mutable.HashMap[String, Type] + ): List[Type] = { + mapGenericParamsToTypesWithIndex(genericParams, typeMap, 0) + } + + def mapGenericParamsToTypesWithIndex( + genericParams: List[GenericTypeParameter], + typeMap: scala.collection.mutable.HashMap[String, Type], + index: int + ): List[Type] = { + genericParams match { + case List.Nil => List.Nil + case List.Cons(param, tail) => + val key = "var_" + string(index) + val inferredType = typeMap.getOrElse(key, binder.anyType) + List.Cons( + inferredType, + mapGenericParamsToTypesWithIndex(tail, typeMap, index + 1) + ) + } + } + + def inferFromParameterArgumentPairs( + paramTypes: List[Type], + argTypes: List[Type], + typeMap: scala.collection.mutable.HashMap[String, Type] + ): Unit = { + Tuple2(paramTypes, argTypes) match { + case Tuple2( + List.Cons(paramType, paramTail), + List.Cons(argType, argTail) + ) => + inferTypeFromPair(paramType, argType, typeMap) + inferFromParameterArgumentPairs(paramTail, argTail, typeMap) + case _ => // Different lengths or empty lists + } + } + + def inferTypeFromPair( + paramType: Type, + argType: Type, + typeMap: scala.collection.mutable.HashMap[String, Type] + ): unit = { + paramType match { + case Type.Variable(_, id) => + // This handles both inference variables and generic type parameters + // For generic type parameters, we want to store the inferred type + val key = "var_" + string(id) + if (!typeMap.contains(key)) { + typeMap.put(key, argType) + } + case Type.Class(_, _, _, paramTypeArgs, _) => + argType match { + case Type.Class(_, _, _, argTypeArgs, _) => + inferFromParameterArgumentPairs(paramTypeArgs, argTypeArgs, typeMap) + case _ => + } + case _ => // Other cases don't provide inference information for this simple implementation + } + } + + def substituteGenericTypesInParameters( + parameters: List[Type], + genericParams: List[GenericTypeParameter], + typeArgs: List[Type] + ): List[Type] = { + mapParameterTypes(parameters, genericParams, typeArgs) + } + + def mapParameterTypes( + parameters: List[Type], + genericParams: List[GenericTypeParameter], + typeArgs: List[Type] + ): List[Type] = { + parameters match { + case List.Nil => List.Nil + case List.Cons(param, tail) => + val substituted = + substituteGenericTypesInType(param, genericParams, typeArgs) + List.Cons(substituted, mapParameterTypes(tail, genericParams, typeArgs)) + } + } + + def substituteGenericTypesInType( + typ: Type, + genericParams: List[GenericTypeParameter], + typeArgs: List[Type] + ): Type = { + typ match { + case Type.Variable(loc, id) => + // Look up the type argument at this index + getTypeArgAtIndex(typeArgs, id) match { + case Option.Some(typeArg) => typeArg + case Option.None => typ + } + case Type.Class(loc, ns, name, classTypeArgs, symbol) => + val substitutedTypeArgs = + mapClassTypeArgs(classTypeArgs, genericParams, typeArgs) + Type.Class(loc, ns, name, substitutedTypeArgs, symbol) + case _ => + typ // Other types don't need substitution for this simple implementation + } + } + + def getTypeArgAtIndex(typeArgs: List[Type], index: int): Option[Type] = { + if (index < 0) { + Option.None + } else { + getTypeArgAtIndexHelper(typeArgs, index) + } + } + + def getTypeArgAtIndexHelper( + typeArgs: List[Type], + index: int + ): Option[Type] = { + typeArgs match { + case List.Nil => Option.None + case List.Cons(head, tail) => + if (index == 0) Option.Some(head) + else getTypeArgAtIndexHelper(tail, index - 1) + } + } + + def mapClassTypeArgs( + typeArgs: List[Type], + genericParams: List[GenericTypeParameter], + substitutionArgs: List[Type] + ): List[Type] = { + typeArgs match { + case List.Nil => List.Nil + case List.Cons(arg, tail) => + val substituted = + substituteGenericTypesInType(arg, genericParams, substitutionArgs) + List.Cons( + substituted, + mapClassTypeArgs(tail, genericParams, substitutionArgs) + ) + } + } + + def findTypeArgument( + name: String, + genericParams: List[GenericTypeParameter], + typeArgs: List[Type] + ): Option[Type] = { + Tuple2(genericParams, typeArgs) match { + case Tuple2(List.Cons(param, paramTail), List.Cons(typeArg, argTail)) => + if (param.name == name) Option.Some(typeArg) + else findTypeArgument(name, paramTail, argTail) + case _ => Option.None + } + } + + def createSubstitutionMap( + genericParams: List[GenericTypeParameter], + typeArgs: List[Type] + ): scala.collection.mutable.HashMap[String, Type] = { + val map = new scala.collection.mutable.HashMap[String, Type]() + fillSubstitutionMap(genericParams, typeArgs, map) + map + } + + def fillSubstitutionMap( + genericParams: List[GenericTypeParameter], + typeArgs: List[Type], + map: scala.collection.mutable.HashMap[String, Type] + ): Unit = { + Tuple2(genericParams, typeArgs) match { + case Tuple2(List.Cons(param, paramTail), List.Cons(typeArg, argTail)) => + map.put(param.name, typeArg) + fillSubstitutionMap(paramTail, argTail, map) + case _ => // Lists are different lengths or empty + } + } + + def bindArgumentsToTypes( + parameterTypes: List[Type], + args: List[BoundExpression], + scope: Scope + ): List[BoundExpression] = { + Tuple2(parameterTypes, args) match { + case Tuple2(List.Cons(paramType, paramTail), List.Cons(arg, argTail)) => + val boundArg = bindConversion(arg, paramType, false) + List.Cons(boundArg, bindArgumentsToTypes(paramTail, argTail, scope)) + case _ => List.Nil + } + } + def bindNewExpressionForSymbol( location: TextLocation, ctor: Symbol, diff --git a/pncs/src/main/scala/ExprBinder.scala b/pncs/src/main/scala/ExprBinder.scala index 9722d96..89043dc 100644 --- a/pncs/src/main/scala/ExprBinder.scala +++ b/pncs/src/main/scala/ExprBinder.scala @@ -503,6 +503,14 @@ case class ExprBinder( case Result.Success(value) => Result.Success(BoundLeftHandSide.Call(value)) } + + case gf: Type.GenericFunction => + bindGenericFunctionCall(function, gf, args, scope) match { + case Result.Error(value) => Result.Error(value) + case Result.Success(value) => + Result.Success(BoundLeftHandSide.Call(value)) + } + case Type.Class(_, ns, name, typeArgs, symbol) => val location = AstUtils.locationOfBoundLeftHandSide(function) // Handle array indexing @@ -747,6 +755,295 @@ case class ExprBinder( } } + def bindGenericFunctionCall( + function: BoundLeftHandSide, + genericFunctionType: Type.GenericFunction, + args: List[BoundExpression], + scope: Scope + ): Result[BoundExpression.Error, BoundExpression.Call] = { + val location = AstUtils.locationOfBoundLeftHandSide(function) + + // First, infer type arguments from the call arguments + val argTypes = binder.getTypes(args) + val parameterTypes = getParameterTypes(genericFunctionType.parameters) + val inferredTypeArgs = inferTypeArgumentsFromCall( + genericFunctionType.generics, + parameterTypes, + argTypes + ) + + // Instantiate the generic function with inferred type arguments + val instantiatedParameterTypes = substituteGenericTypesInParameters( + parameterTypes, + genericFunctionType.generics, + inferredTypeArgs + ) + + val instantiatedReturnType = substituteGenericTypesInType( + genericFunctionType.returnType, + genericFunctionType.generics, + inferredTypeArgs + ) + + // Check argument count matches + if (instantiatedParameterTypes.length != args.length) { + diagnosticBag.reportArgumentCountMismatch( + location, + instantiatedParameterTypes.length, + args.length + ) + Result.Error( + BoundExpression.Error( + "Argument count mismatch: expected " + instantiatedParameterTypes.length + + " but got " + args.length + ) + ) + } else { + // Bind arguments with instantiated parameter types + val boundArgs = + bindArgumentsToTypes(instantiatedParameterTypes, args, scope) + + function match { + case BoundLeftHandSide.Index(expression) => ??? + case BoundLeftHandSide.MemberAccess(access) => + val receiver = if (access.member.isStatic()) { + Option.None + } else { + Option.Some(access.receiver) + } + Result.Success( + BoundExpression.Call( + location, + receiver, + access.member, + inferredTypeArgs, // Pass the inferred type arguments + boundArgs, + instantiatedReturnType + ) + ) + case BoundLeftHandSide.Variable(location, symbol) => + Result.Success( + BoundExpression.Call( + location, + Option.None, + symbol, + inferredTypeArgs, // Pass the inferred type arguments + boundArgs, + instantiatedReturnType + ) + ) + case _ => ??? + } + } + } + + def inferTypeArgumentsFromCall( + genericParams: List[GenericTypeParameter], + parameterTypes: List[Type], + argumentTypes: List[Type] + ): List[Type] = { + // Simple type inference: try to match parameter types with argument types + // This is a basic implementation - a full implementation would be more sophisticated + val typeMap = new scala.collection.mutable.HashMap[String, Type]() + + // Try to infer from parameter/argument pairs + inferFromParameterArgumentPairs(parameterTypes, argumentTypes, typeMap) + + // Convert to list of types in the order of generic parameters + mapGenericParamsToTypes(genericParams, typeMap) + } + + def mapGenericParamsToTypes( + genericParams: List[GenericTypeParameter], + typeMap: scala.collection.mutable.HashMap[String, Type] + ): List[Type] = { + mapGenericParamsToTypesWithIndex(genericParams, typeMap, 0) + } + + def mapGenericParamsToTypesWithIndex( + genericParams: List[GenericTypeParameter], + typeMap: scala.collection.mutable.HashMap[String, Type], + index: int + ): List[Type] = { + genericParams match { + case List.Nil => List.Nil + case List.Cons(param, tail) => + val key = "var_" + string(index) + val inferredType = typeMap.getOrElse(key, binder.anyType) + List.Cons( + inferredType, + mapGenericParamsToTypesWithIndex(tail, typeMap, index + 1) + ) + } + } + + def inferFromParameterArgumentPairs( + paramTypes: List[Type], + argTypes: List[Type], + typeMap: scala.collection.mutable.HashMap[String, Type] + ): Unit = { + Tuple2(paramTypes, argTypes) match { + case Tuple2( + List.Cons(paramType, paramTail), + List.Cons(argType, argTail) + ) => + inferTypeFromPair(paramType, argType, typeMap) + inferFromParameterArgumentPairs(paramTail, argTail, typeMap) + case _ => // Different lengths or empty lists + } + } + + def inferTypeFromPair( + paramType: Type, + argType: Type, + typeMap: scala.collection.mutable.HashMap[String, Type] + ): unit = { + paramType match { + case Type.Variable(_, id) => + // This handles both inference variables and generic type parameters + // For generic type parameters, we want to store the inferred type + val key = "var_" + string(id) + if (!typeMap.contains(key)) { + typeMap.put(key, argType) + } + case Type.Class(_, _, _, paramTypeArgs, _) => + argType match { + case Type.Class(_, _, _, argTypeArgs, _) => + inferFromParameterArgumentPairs(paramTypeArgs, argTypeArgs, typeMap) + case _ => + } + case _ => // Other cases don't provide inference information for this simple implementation + } + } + + def substituteGenericTypesInParameters( + parameters: List[Type], + genericParams: List[GenericTypeParameter], + typeArgs: List[Type] + ): List[Type] = { + mapParameterTypes(parameters, genericParams, typeArgs) + } + + def mapParameterTypes( + parameters: List[Type], + genericParams: List[GenericTypeParameter], + typeArgs: List[Type] + ): List[Type] = { + parameters match { + case List.Nil => List.Nil + case List.Cons(param, tail) => + val substituted = + substituteGenericTypesInType(param, genericParams, typeArgs) + List.Cons(substituted, mapParameterTypes(tail, genericParams, typeArgs)) + } + } + + def substituteGenericTypesInType( + typ: Type, + genericParams: List[GenericTypeParameter], + typeArgs: List[Type] + ): Type = { + typ match { + case Type.Variable(loc, id) => + // Look up the type argument at this index + getTypeArgAtIndex(typeArgs, id) match { + case Option.Some(typeArg) => typeArg + case Option.None => typ + } + case Type.Class(loc, ns, name, classTypeArgs, symbol) => + val substitutedTypeArgs = + mapClassTypeArgs(classTypeArgs, genericParams, typeArgs) + Type.Class(loc, ns, name, substitutedTypeArgs, symbol) + case _ => + typ // Other types don't need substitution for this simple implementation + } + } + + def getTypeArgAtIndex(typeArgs: List[Type], index: int): Option[Type] = { + if (index < 0) { + Option.None + } else { + getTypeArgAtIndexHelper(typeArgs, index) + } + } + + def getTypeArgAtIndexHelper( + typeArgs: List[Type], + index: int + ): Option[Type] = { + typeArgs match { + case List.Nil => Option.None + case List.Cons(head, tail) => + if (index == 0) Option.Some(head) + else getTypeArgAtIndexHelper(tail, index - 1) + } + } + + def mapClassTypeArgs( + typeArgs: List[Type], + genericParams: List[GenericTypeParameter], + substitutionArgs: List[Type] + ): List[Type] = { + typeArgs match { + case List.Nil => List.Nil + case List.Cons(arg, tail) => + val substituted = + substituteGenericTypesInType(arg, genericParams, substitutionArgs) + List.Cons( + substituted, + mapClassTypeArgs(tail, genericParams, substitutionArgs) + ) + } + } + + def findTypeArgument( + name: String, + genericParams: List[GenericTypeParameter], + typeArgs: List[Type] + ): Option[Type] = { + Tuple2(genericParams, typeArgs) match { + case Tuple2(List.Cons(param, paramTail), List.Cons(typeArg, argTail)) => + if (param.name == name) Option.Some(typeArg) + else findTypeArgument(name, paramTail, argTail) + case _ => Option.None + } + } + + def createSubstitutionMap( + genericParams: List[GenericTypeParameter], + typeArgs: List[Type] + ): scala.collection.mutable.HashMap[String, Type] = { + val map = new scala.collection.mutable.HashMap[String, Type]() + fillSubstitutionMap(genericParams, typeArgs, map) + map + } + + def fillSubstitutionMap( + genericParams: List[GenericTypeParameter], + typeArgs: List[Type], + map: scala.collection.mutable.HashMap[String, Type] + ): Unit = { + Tuple2(genericParams, typeArgs) match { + case Tuple2(List.Cons(param, paramTail), List.Cons(typeArg, argTail)) => + map.put(param.name, typeArg) + fillSubstitutionMap(paramTail, argTail, map) + case _ => // Lists are different lengths or empty + } + } + + def bindArgumentsToTypes( + parameterTypes: List[Type], + args: List[BoundExpression], + scope: Scope + ): List[BoundExpression] = { + Tuple2(parameterTypes, args) match { + case Tuple2(List.Cons(paramType, paramTail), List.Cons(arg, argTail)) => + val boundArg = bindConversion(arg, paramType, false) + List.Cons(boundArg, bindArgumentsToTypes(paramTail, argTail, scope)) + case _ => List.Nil + } + } + def bindNewExpressionForSymbol( location: TextLocation, ctor: Symbol, diff --git a/test/src/test/scala/TypeTests.scala b/test/src/test/scala/TypeTests.scala index 9e6f159..7141c5d 100644 --- a/test/src/test/scala/TypeTests.scala +++ b/test/src/test/scala/TypeTests.scala @@ -277,6 +277,105 @@ object TypeTests extends TestSuite { // } } + test("generic methods") { + test("simple identity method (testing basic inference)") { + val setup = "def identity[T](x: T): T = x" + + assertExprTypeWithSetup(setup, "identity(42)", "int") + + assertExprTypeWithSetup(setup, "identity(true)", "bool") + + assertExprTypeWithSetup(setup, "identity(\"hello\")", "string") + + assertExprTypeWithSetup(setup, "identity('a')", "char") + } + + test("method returning concrete type") { + val setup = "def getValue[T](x: T): int = 42" + + // These should work because the return type is concrete + assertExprTypeWithSetup(setup, "getValue(42)", "int") + assertExprTypeWithSetup(setup, "getValue(\"hello\")", "int") + assertExprTypeWithSetup(setup, "getValue(true)", "int") + } + + test("generic parameter with concrete return") { + // Test methods that accept generic parameters but return concrete types + val setup = "def stringify[T](x: T): string = string(x)" + + assertExprTypeWithSetup(setup, "stringify(42)", "string") + assertExprTypeWithSetup(setup, "stringify(true)", "string") + assertExprTypeWithSetup(setup, "stringify('a')", "string") + } + +// test("array element access") { +// val setup = "def first[T](arr: Array[T]): T = arr[0]" +// +// assertExprTypeWithSetup(setup, "first(new Array[int](5))", "int") +// assertExprTypeWithSetup(setup, "first(new Array[bool](3))", "bool") +// assertExprTypeWithSetup(setup, "first(new Array[string](2))", "string") +// assertExprTypeWithSetup(setup, "first(new Array[char](1))", "char") +// } +// +// test("generic container creation (commented until generics work)") { +// val containerSetup = "class Container[T](value: T)\n" + +// "def wrap[T](x: T): Container[T] = new Container(x)" +// +// assertExprTypeWithSetup(containerSetup, "wrap(42)", "Container[int]") +// assertExprTypeWithSetup(containerSetup, "wrap(true)", "Container[bool]") +// assertExprTypeWithSetup(containerSetup, "wrap(\"test\")", "Container[string]") +// } +// +// test("option-like generic methods (commented until generics work)") { +// val optionSetup = "enum Option[T] {\n" + +// " case Some(value: T)\n" + +// " case None\n" + +// "}\n" + +// "def maybe[T](condition: bool, value: T): Option[T] = {\n" + +// " if (condition) Option.Some(value) else Option.None\n" + +// "}" +// +// assertExprTypeWithSetup(optionSetup, "maybe(true, 42)", "Option[int]") +// assertExprTypeWithSetup(optionSetup, "maybe(false, \"hello\")", "Option[string]") +// assertExprTypeWithSetup(optionSetup, "maybe(true, 'x')", "Option[char]") +// } + +// test("multiple type parameters (commented until generics work)") { +// val pairSetup = "class Pair[A, B](first: A, second: B)\n" + +// "def makePair[A, B](a: A, b: B): Pair[A, B] = new Pair(a, b)" +// +// assertExprTypeWithSetup(pairSetup, "makePair(42, \"hello\")", "Pair[int, string]") +// assertExprTypeWithSetup(pairSetup, "makePair(true, 'x')", "Pair[bool, char]") +// assertExprTypeWithSetup(pairSetup, "makePair(1, 2)", "Pair[int, int]") +// } +// +// test("list operations (commented until generics work)") { +// val listSetup = "enum List[T] {\n" + +// " case Cons(head: T, tail: List[T])\n" + +// " case Nil\n" + +// "}\n" + +// "def singleton[T](value: T): List[T] = List.Cons(value, List.Nil)\n" + +// "def prepend[T](value: T, list: List[T]): List[T] = List.Cons(value, list)" +// +// assertExprTypeWithSetup(listSetup, "singleton(42)", "List[int]") +// assertExprTypeWithSetup(listSetup, "singleton(\"test\")", "List[string]") +// assertExprTypeWithSetup(listSetup, "prepend(1, singleton(2))", "List[int]") +// assertExprTypeWithSetup(listSetup, "prepend(true, List.Nil)", "List[bool]") +// } +// +// test("chaining generic calls (commented until generics work)") { +// val setupChain = "def identity[T](x: T): T = x\n" + +// "class Box[T](value: T)\n" + +// "def box[T](x: T): Box[T] = new Box(x)\n" + +// "def unbox[T](b: Box[T]): T = b.value" +// +// assertExprTypeWithSetup(setupChain, "identity(identity(42))", "int") +// assertExprTypeWithSetup(setupChain, "unbox(box(42))", "int") +// assertExprTypeWithSetup(setupChain, "identity(unbox(box(\"hello\")))", "string") +// assertExprTypeWithSetup(setupChain, "box(identity(true))", "Box[bool]") +// } + } + test("enums") { test("without args") { val setup = "enum Foo {\n" +