diff --git a/pnc/src/ExprBinder.pn b/pnc/src/ExprBinder.pn index bae37d8..e861f0d 100644 --- a/pnc/src/ExprBinder.pn +++ b/pnc/src/ExprBinder.pn @@ -253,7 +253,7 @@ class ExprBinder( expectedType: Type, scope: Scope ): BoundExpression = { - val lhs = bindLHS(expr.left, scope) + val lhs = inferLHS(expr.left, scope) val rhs = infer(expr.right, scope) Tuple2(lhs, rhs) match { case Tuple2(Result.Error(error), _) => @@ -308,15 +308,76 @@ class ExprBinder( } def checkCall( - expr: Expression.Call, + node: Expression.Call, expectedType: Type, scope: Scope ): BoundExpression = { - inferCall(expr, scope) match { - case Result.Error(value) => value - case Result.Success(value) => - val inferred = convertLHSToExpression(value) - subsume(inferred, expectedType) + // For call expressions, we can use the expected type to improve type inference + // for generic functions + inferLHS(node.name, scope) match { + case Result.Error(error) => error + case Result.Success(function) => + val exprs = fromExpressionList(node.arguments.expressions) + val args = bindExpressions(exprs, scope) + + // Try to use expected type for better generic inference + checkCallLHS( + function, + args, + expectedType, + scope + ) match { + case Result.Error(value) => value + case Result.Success(value) => + val inferred = convertLHSToExpression(value) + subsume(inferred, expectedType) + } + } + } + + def checkCallLHS( + function: BoundLeftHandSide, + args: List[BoundExpression], + expectedType: Type, + scope: Scope + ): Result[BoundExpression.Error, BoundLeftHandSide] = { + val functionType = getLHSType(function) + val argTypes = binder.getTypes(args) + + if (typesWithError(List.Cons(functionType, argTypes))) { + Result.Error( + BoundExpression.Error( + "Cannot bind call expression because of type errors" + ) + ) + } else { + functionType match { + case func: Type.Function => + // Non-generic function: use normal binding + bindFunctionCall(function, func, args, scope) match { + case Result.Error(value) => Result.Error(value) + case Result.Success(value) => + Result.Success(BoundLeftHandSide.Call(value)) + } + + case gf: Type.GenericFunction => + // Generic function: use expected type to help infer type arguments! + checkGenericFunctionCall( + function, + gf, + args, + expectedType, + scope + ) match { + case Result.Error(value) => Result.Error(value) + case Result.Success(value) => + Result.Success(BoundLeftHandSide.Call(value)) + } + + case _ => + // Fall back to regular inference + inferCall(function, args, scope) + } } } @@ -528,7 +589,7 @@ class ExprBinder( } } - def bindLHS( + def inferLHS( expr: Expression, scope: Scope ): Result[BoundExpression.Error, BoundLeftHandSide] = { @@ -690,7 +751,7 @@ class ExprBinder( node: Expression.Assignment, scope: Scope ): BoundExpression = { - bindLHS(node.left, scope) match { + inferLHS(node.left, scope) match { case Result.Error(value) => value case Result.Success(lhs) => getLHSType(lhs) match { @@ -800,17 +861,17 @@ class ExprBinder( BoundExpression.Error, BoundLeftHandSide ] = { - bindLHS(node.name, scope) match { + inferLHS(node.name, scope) match { case Result.Error(error) => Result.Error(error) case Result.Success(function) => val exprs = fromExpressionList(node.arguments.expressions) val args = bindExpressions(exprs, scope) - bindCall(function, args, scope) + inferCall(function, args, scope) } } - def bindCall( + def inferCall( function: BoundLeftHandSide, args: List[BoundExpression], scope: Scope @@ -1184,6 +1245,97 @@ class ExprBinder( } } + /** Bind a generic function call using BOTH argument types AND expected return + * type for improved type inference (bidirectional typing) + */ + def checkGenericFunctionCall( + function: BoundLeftHandSide, + genericFunctionType: Type.GenericFunction, + args: List[BoundExpression], + expectedType: Type, + scope: Scope + ): Result[BoundExpression.Error, BoundExpression.Call] = { + val location = AstUtils.locationOfBoundLeftHandSide(function) + + // Infer type arguments using BOTH arguments AND expected return type! + val argTypes = binder.getTypes(args) + val parameterTypes = getParameterTypes(genericFunctionType.parameters) + + val inferredTypeArgs = + typeInference.checkTypeArgumentsFromCall( + genericFunctionType.generics, + parameterTypes, + argTypes, + genericFunctionType.returnType, // The declared return type (may contain type vars) + expectedType // The expected type from context + ) + + // Instantiate the generic function with inferred type arguments + val instantiatedParameterTypes = + typeInference.substituteGenericTypesInParameters( + parameterTypes, + genericFunctionType.generics, + inferredTypeArgs + ) + + val instantiatedReturnType = typeInference.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, + boundArgs, + instantiatedReturnType + ) + ) + case BoundLeftHandSide.Variable(location, symbol) => + Result.Success( + BoundExpression.Call( + location, + Option.None, + symbol, + inferredTypeArgs, + boundArgs, + instantiatedReturnType + ) + ) + case _ => ??? + } + } + } + def bindArgumentsToTypes( parameterTypes: List[Type], args: List[BoundExpression], @@ -1504,7 +1656,7 @@ class ExprBinder( ): Result[BoundExpression.Error, BoundExpression.MemberAccess] = { // For member access, the left side doesn't need to be a left-hand side // It just needs to be a valid expression that produces a value - val leftExpr = bindLHS(node.left, scope) + val leftExpr = inferLHS(node.left, scope) leftExpr match { case Result.Error(error) => Result.Error(error) diff --git a/pnc/src/TypeInference.pn b/pnc/src/TypeInference.pn index f4de9c0..2c64933 100644 --- a/pnc/src/TypeInference.pn +++ b/pnc/src/TypeInference.pn @@ -10,8 +10,6 @@ class TypeInference(binder: Binder) { 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 @@ -21,6 +19,30 @@ class TypeInference(binder: Binder) { mapGenericParamsToTypes(genericParams, typeMap) } + /** Infers type arguments for a generic function call using both argument + * types AND the expected return type (bidirectional inference) + */ + def checkTypeArgumentsFromCall( + genericParams: List[GenericTypeParameter], + parameterTypes: List[Type], + argumentTypes: List[Type], + returnType: Type, + expectedType: Type + ): List[Type] = { + val typeMap = new scala.collection.mutable.HashMap[String, Type]() + + // First, infer from parameter/argument pairs (same as before) + inferFromParameterArgumentPairs(parameterTypes, argumentTypes, typeMap) + + // NEW: Also infer from return type vs expected type + // This helps when the return type contains type variables + // Example: identity(x: T): T called with expected type string + inferTypeFromPair(returnType, expectedType, 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] @@ -167,41 +189,6 @@ class TypeInference(binder: Binder) { } } - 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 substituteParameterTypes( params: List[BoundParameter], typeArgs: List[Type] diff --git a/pncs/src/main/scala/ExprBinder.scala b/pncs/src/main/scala/ExprBinder.scala index f2a9c34..3d49886 100644 --- a/pncs/src/main/scala/ExprBinder.scala +++ b/pncs/src/main/scala/ExprBinder.scala @@ -253,7 +253,7 @@ case class ExprBinder( expectedType: Type, scope: Scope ): BoundExpression = { - val lhs = bindLHS(expr.left, scope) + val lhs = inferLHS(expr.left, scope) val rhs = infer(expr.right, scope) Tuple2(lhs, rhs) match { case Tuple2(Result.Error(error), _) => @@ -308,15 +308,76 @@ case class ExprBinder( } def checkCall( - expr: Expression.Call, + node: Expression.Call, expectedType: Type, scope: Scope ): BoundExpression = { - inferCall(expr, scope) match { - case Result.Error(value) => value - case Result.Success(value) => - val inferred = convertLHSToExpression(value) - subsume(inferred, expectedType) + // For call expressions, we can use the expected type to improve type inference + // for generic functions + inferLHS(node.name, scope) match { + case Result.Error(error) => error + case Result.Success(function) => + val exprs = fromExpressionList(node.arguments.expressions) + val args = bindExpressions(exprs, scope) + + // Try to use expected type for better generic inference + checkCallLHS( + function, + args, + expectedType, + scope + ) match { + case Result.Error(value) => value + case Result.Success(value) => + val inferred = convertLHSToExpression(value) + subsume(inferred, expectedType) + } + } + } + + def checkCallLHS( + function: BoundLeftHandSide, + args: List[BoundExpression], + expectedType: Type, + scope: Scope + ): Result[BoundExpression.Error, BoundLeftHandSide] = { + val functionType = getLHSType(function) + val argTypes = binder.getTypes(args) + + if (typesWithError(List.Cons(functionType, argTypes))) { + Result.Error( + BoundExpression.Error( + "Cannot bind call expression because of type errors" + ) + ) + } else { + functionType match { + case func: Type.Function => + // Non-generic function: use normal binding + bindFunctionCall(function, func, args, scope) match { + case Result.Error(value) => Result.Error(value) + case Result.Success(value) => + Result.Success(BoundLeftHandSide.Call(value)) + } + + case gf: Type.GenericFunction => + // Generic function: use expected type to help infer type arguments! + checkGenericFunctionCall( + function, + gf, + args, + expectedType, + scope + ) match { + case Result.Error(value) => Result.Error(value) + case Result.Success(value) => + Result.Success(BoundLeftHandSide.Call(value)) + } + + case _ => + // Fall back to regular inference + inferCall(function, args, scope) + } } } @@ -528,7 +589,7 @@ case class ExprBinder( } } - def bindLHS( + def inferLHS( expr: Expression, scope: Scope ): Result[BoundExpression.Error, BoundLeftHandSide] = { @@ -690,7 +751,7 @@ case class ExprBinder( node: Expression.Assignment, scope: Scope ): BoundExpression = { - bindLHS(node.left, scope) match { + inferLHS(node.left, scope) match { case Result.Error(value) => value case Result.Success(lhs) => getLHSType(lhs) match { @@ -800,17 +861,17 @@ case class ExprBinder( BoundExpression.Error, BoundLeftHandSide ] = { - bindLHS(node.name, scope) match { + inferLHS(node.name, scope) match { case Result.Error(error) => Result.Error(error) case Result.Success(function) => val exprs = fromExpressionList(node.arguments.expressions) val args = bindExpressions(exprs, scope) - bindCall(function, args, scope) + inferCall(function, args, scope) } } - def bindCall( + def inferCall( function: BoundLeftHandSide, args: List[BoundExpression], scope: Scope @@ -1184,6 +1245,97 @@ case class ExprBinder( } } + /** Bind a generic function call using BOTH argument types AND expected return + * type for improved type inference (bidirectional typing) + */ + def checkGenericFunctionCall( + function: BoundLeftHandSide, + genericFunctionType: Type.GenericFunction, + args: List[BoundExpression], + expectedType: Type, + scope: Scope + ): Result[BoundExpression.Error, BoundExpression.Call] = { + val location = AstUtils.locationOfBoundLeftHandSide(function) + + // Infer type arguments using BOTH arguments AND expected return type! + val argTypes = binder.getTypes(args) + val parameterTypes = getParameterTypes(genericFunctionType.parameters) + + val inferredTypeArgs = + typeInference.checkTypeArgumentsFromCall( + genericFunctionType.generics, + parameterTypes, + argTypes, + genericFunctionType.returnType, // The declared return type (may contain type vars) + expectedType // The expected type from context + ) + + // Instantiate the generic function with inferred type arguments + val instantiatedParameterTypes = + typeInference.substituteGenericTypesInParameters( + parameterTypes, + genericFunctionType.generics, + inferredTypeArgs + ) + + val instantiatedReturnType = typeInference.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, + boundArgs, + instantiatedReturnType + ) + ) + case BoundLeftHandSide.Variable(location, symbol) => + Result.Success( + BoundExpression.Call( + location, + Option.None, + symbol, + inferredTypeArgs, + boundArgs, + instantiatedReturnType + ) + ) + case _ => ??? + } + } + } + def bindArgumentsToTypes( parameterTypes: List[Type], args: List[BoundExpression], @@ -1504,7 +1656,7 @@ case class ExprBinder( ): Result[BoundExpression.Error, BoundExpression.MemberAccess] = { // For member access, the left side doesn't need to be a left-hand side // It just needs to be a valid expression that produces a value - val leftExpr = bindLHS(node.left, scope) + val leftExpr = inferLHS(node.left, scope) leftExpr match { case Result.Error(error) => Result.Error(error) diff --git a/pncs/src/main/scala/TypeInference.scala b/pncs/src/main/scala/TypeInference.scala index 73e457f..690545f 100644 --- a/pncs/src/main/scala/TypeInference.scala +++ b/pncs/src/main/scala/TypeInference.scala @@ -10,8 +10,6 @@ case class TypeInference(binder: Binder) { 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 @@ -21,6 +19,30 @@ case class TypeInference(binder: Binder) { mapGenericParamsToTypes(genericParams, typeMap) } + /** Infers type arguments for a generic function call using both argument + * types AND the expected return type (bidirectional inference) + */ + def checkTypeArgumentsFromCall( + genericParams: List[GenericTypeParameter], + parameterTypes: List[Type], + argumentTypes: List[Type], + returnType: Type, + expectedType: Type + ): List[Type] = { + val typeMap = new scala.collection.mutable.HashMap[String, Type]() + + // First, infer from parameter/argument pairs (same as before) + inferFromParameterArgumentPairs(parameterTypes, argumentTypes, typeMap) + + // NEW: Also infer from return type vs expected type + // This helps when the return type contains type variables + // Example: identity(x: T): T called with expected type string + inferTypeFromPair(returnType, expectedType, 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] @@ -167,41 +189,6 @@ case class TypeInference(binder: Binder) { } } - 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 substituteParameterTypes( params: List[BoundParameter], typeArgs: List[Type] diff --git a/test/src/test/scala/TypeTests.scala b/test/src/test/scala/TypeTests.scala index f592e4b..8642d12 100644 --- a/test/src/test/scala/TypeTests.scala +++ b/test/src/test/scala/TypeTests.scala @@ -643,6 +643,94 @@ class TypeTests extends AnyFunSpec with Matchers { // ) // } + it("should infer generic type from expected return type - identity") { + val setup = "def identity[T](x: T): T = x" + + // When checking against an expected type, the compiler should use that + // information to help infer the generic type parameter + assertCheckExprTypeWithSetup(setup, "identity(42)", "int") + assertCheckExprTypeWithSetup(setup, "identity(\"hello\")", "string") + assertCheckExprTypeWithSetup(setup, "identity(true)", "bool") + assertCheckExprTypeWithSetup(setup, "identity('x')", "char") + } + + it("should infer generic type from expected return type - cast scenarios") { + val setup = "def identity[T](x: T): T = x" + + // Test that expected type flows through to help inference + // Even when the argument requires conversion + assertCheckExprTypeWithSetup(setup, "identity(42 as any)", "any") + assertCheckExprTypeWithSetup(setup, "identity(\"test\" as any)", "any") + } + + it( + "should infer generic type from expected return type - container creation" + ) { + val containerSetup = "class Box[T](value: T)\n" + + "def createBox[T](x: T): Box[T] = new Box(x)" + + // Test that the generic function works with type inference + assertInferExprTypeWithSetup(containerSetup, "createBox(42)", "Box") + assertInferExprTypeWithSetup( + containerSetup, + "createBox(\"test\")", + "Box" + ) + assertInferExprTypeWithSetup( + containerSetup, + "createBox(true)", + "Box" + ) + } + + it( + "should infer generic type from expected return type - multiple type parameters" + ) { + val setup = "def pair[A, B](a: A, b: B): A = a" + + // First type parameter comes from both argument and expected type + assertCheckExprTypeWithSetup(setup, "pair(1, true)", "int") + assertCheckExprTypeWithSetup(setup, "pair(\"a\", 2)", "string") + assertCheckExprTypeWithSetup(setup, "pair(true, 'c')", "bool") + } + + it( + "should infer generic type from expected return type - nested generics" + ) { + val setup = "class Container[T](value: T)\n" + + "def wrap[T](x: T): Container[T] = new Container(x)\n" + + "def wrapTwice[T](x: T): Container[Container[T]] = new Container(wrap(x))" + + // Nested generic types should still work with type inference from arguments + assertInferExprTypeWithSetup( + setup, + "wrapTwice(42)", + "Container>" + ) + assertInferExprTypeWithSetup( + setup, + "wrapTwice(\"test\")", + "Container>" + ) + } + + it("should prefer argument type when both sources agree") { + val setup = "def identity[T](x: T): T = x" + + // When both argument and expected type provide the same information, + // inference should succeed smoothly + assertCheckExprTypeWithSetup(setup, "identity(42)", "int") + assertCheckExprTypeWithSetup(setup, "identity(true)", "bool") + } + + it("should infer from return type when argument is polymorphic") { + val setup = "def selectFirst[T](x: T, y: T): T = x" + + // When both arguments must have the same type T, expected type helps + assertCheckExprTypeWithSetup(setup, "selectFirst(1, 2)", "int") + assertCheckExprTypeWithSetup(setup, "selectFirst(true, false)", "bool") + } + it("should infer enums without args") { val setup = "enum Foo {\n" + " case Bar\n" +