From 56464ef53729494f729364d8d606f428a5eb99f5 Mon Sep 17 00:00:00 2001 From: Dave Holoway Date: Mon, 22 Jun 2020 19:05:26 +0100 Subject: [PATCH] add basic type checking of lambda expressions --- langserver/java/anys.js | 10 ++- langserver/java/expression-resolver.js | 84 ++++++++++++++++++- .../java/expressiontypes/LambdaExpression.js | 21 ++++- .../expressiontypes/MethodCallExpression.js | 28 ++++--- 4 files changed, 127 insertions(+), 16 deletions(-) diff --git a/langserver/java/anys.js b/langserver/java/anys.js index 322b1d3..dc556a3 100644 --- a/langserver/java/anys.js +++ b/langserver/java/anys.js @@ -87,7 +87,15 @@ class MethodType { * eg. `() => null` */ class LambdaType { - + /** + * + * @param {JavaType[]} param_types + * @param {ResolvedValue} return_type + */ + constructor(param_types, return_type) { + this.param_types = param_types; + this.return_type = return_type; + } } /** diff --git a/langserver/java/expression-resolver.js b/langserver/java/expression-resolver.js index b7b9061..e857fba 100644 --- a/langserver/java/expression-resolver.js +++ b/langserver/java/expression-resolver.js @@ -5,7 +5,7 @@ */ const ParseProblem = require('./parsetypes/parse-problem'); const { TypeVariable, JavaType, PrimitiveType, NullType, ArrayType, CEIType, WildcardType, TypeVariableType, InferredTypeArgument } = require('java-mti'); -const { AnyType, ArrayValueType, MultiValueType } = require('./anys'); +const { AnyType, ArrayValueType, LambdaType, MultiValueType } = require('./anys'); const { ResolveInfo } = require('./body-types'); const { NumberLiteral } = require('./expressiontypes/literals/Number'); @@ -41,6 +41,10 @@ function checkTypeAssignable(variable_type, value, tokens, problems) { checkArrayLiteral(variable_type, value, tokens, problems); return; } + if (value instanceof LambdaType) { + checkLambdaAssignable(variable_type, value, tokens, problems); + return; + } if (value instanceof JavaType) { if (!isTypeAssignable(variable_type, value)) { incompatibleTypesError(variable_type, value, tokens, problems); @@ -61,6 +65,78 @@ function incompatibleTypesError(variable_type, value_type, tokens, problems) { problems.push(ParseProblem.Error(tokens(), `Incompatible types: Expression of type '${value_type.fullyDottedTypeName}' cannot be assigned to a variable of type '${variable_type.fullyDottedTypeName}'`)); } +/** + * + * @param {JavaType} variable_type + * @param {LambdaType} value + * @param {() => Token|Token[]} tokens + * @param {ParseProblem[]} problems + */ +function checkLambdaAssignable(variable_type, value, tokens, problems) { + const res = isLambdaAssignable(variable_type, value); + if (res === true) { + return; + } + switch (res[0]) { + case 'non-interface': + problems.push(ParseProblem.Error(tokens(), `Incompatible types: Cannot assign lambda expression to type '${variable_type.fullyDottedTypeName}'`)); + return; + case 'no-methods': + problems.push(ParseProblem.Error(tokens(), `Incompatible types: Interface '${variable_type.fullyDottedTypeName}' contains no abstract methods compatible with the specified lambda expression`)); + return; + case 'param-count': + problems.push(ParseProblem.Error(tokens(), `Incompatible types: Interface method '${variable_type.methods[0].label}' and lambda expression have different parameter counts`)); + return; + case 'bad-param': + problems.push(ParseProblem.Error(tokens(), `Incompatible types: Interface method '${variable_type.methods[0].label}' and lambda expression have different parameter types`)); + return; + } +} + +/** + * + * @param {JavaType} variable_type + * @param {LambdaType} value + */ +function isLambdaAssignable(variable_type, value) { + if (!(variable_type instanceof CEIType) || variable_type.typeKind !== 'interface') { + return ['non-interface']; + } + // the functional interface must only contain one abstract method excluding public Object methods + // and ignoring type-compatible methods from superinterfaces. + // this is quite complicated to calculate, so for now, just check against the most common case: a simple interface type with + // a single abstract method + if (variable_type.supers.length > 1) { + return true; + } + if (variable_type.methods.length === 0) { + return ['no-methods'] + } + if (variable_type.methods.length > 1) { + return true; + } + const intf_method = variable_type.methods[0]; + const intf_params = intf_method.parameters; + if (intf_params.length !== value.param_types.length) { + return ['param-count']; + } + + for (let i = 0; i < intf_params.length; i++) { + // explicit parameter types must match exactly + if (value.param_types[i] instanceof AnyType) { + continue; + } + if (intf_params[i].type instanceof AnyType) { + continue; + } + if (intf_params[i].type.typeSignature !== value.param_types[i].typeSignature) { + return ['bad-param'] + } + } + + return true; +} + /** * * @param {JavaType} variable_type @@ -168,7 +244,7 @@ const valid_primitive_types = { /** * Returns true if a value of value_type is assignable to a variable of dest_type * @param {JavaType} dest_type - * @param {JavaType|NumberLiteral} value_type + * @param {JavaType|NumberLiteral|LambdaType} value_type */ function isTypeAssignable(dest_type, value_type) { @@ -176,6 +252,10 @@ function isTypeAssignable(dest_type, value_type) { return value_type.isCompatibleWith(dest_type); } + if (value_type instanceof LambdaType) { + return isLambdaAssignable(dest_type, value_type) === true; + } + let is_assignable = false; if (dest_type.typeSignature === value_type.typeSignature) { // exact signature match diff --git a/langserver/java/expressiontypes/LambdaExpression.js b/langserver/java/expressiontypes/LambdaExpression.js index 08fad3b..37e449a 100644 --- a/langserver/java/expressiontypes/LambdaExpression.js +++ b/langserver/java/expressiontypes/LambdaExpression.js @@ -4,12 +4,13 @@ */ const { Expression } = require("./Expression"); const { Block } = require('../statementtypes/Block'); -const { LambdaType } = require('../anys'); +const { AnyType, LambdaType } = require('../anys'); +const { Local } = require('../body-types'); class LambdaExpression extends Expression { /** * - * @param {*[]} params + * @param {(Local|ResolvedIdent)[]} params * @param {ResolvedIdent|Block} body */ constructor(params, body) { @@ -22,7 +23,21 @@ class LambdaExpression extends Expression { * @param {ResolveInfo} ri */ resolveExpression(ri) { - return new LambdaType(); + let return_type; + if (this.body instanceof Block) { + // todo - search for return statements to work out what return value the lambda has + return_type = AnyType.Instance; + } else { + return_type = this.body.resolveExpression(ri); + } + const param_types = this.params.map(p => { + if (p instanceof Local) { + return p.type; + } + return AnyType.Instance; + }) + return new LambdaType(param_types, return_type); + } tokens() { diff --git a/langserver/java/expressiontypes/MethodCallExpression.js b/langserver/java/expressiontypes/MethodCallExpression.js index 9d824b6..d1b214f 100644 --- a/langserver/java/expressiontypes/MethodCallExpression.js +++ b/langserver/java/expressiontypes/MethodCallExpression.js @@ -4,7 +4,7 @@ * @typedef {import('../tokenizer').Token} Token */ const { Expression } = require("./Expression"); -const { AnyType, AnyMethod, MethodType } = require('../anys'); +const { AnyType, AnyMethod, LambdaType, MethodType } = require('../anys'); const { ArrayType, JavaType, Method,PrimitiveType, ReifiedConstructor, ReifiedMethod, Constructor } = require('java-mti'); const { NumberLiteral } = require('./literals/Number'); const { InstanceLiteral } = require('./literals/Instance') @@ -68,11 +68,11 @@ class MethodCallExpression extends Expression { function resolveMethodCall(ri, methods, args, tokens) { const resolved_args = args.map(arg => arg.resolveExpression(ri)); - // all the arguments must be typed expressions or number literals - /** @type {(JavaType|NumberLiteral)[]} */ + // all the arguments must be typed expressions, number literals or lambdas + /** @type {(JavaType|NumberLiteral|LambdaType)[]} */ const arg_types = []; resolved_args.forEach((a, idx) => { - if (a instanceof JavaType || a instanceof NumberLiteral) { + if (a instanceof JavaType || a instanceof NumberLiteral || a instanceof LambdaType) { arg_types.push(a); return; } @@ -82,7 +82,11 @@ function resolveMethodCall(ri, methods, args, tokens) { }); // reify any methods with type-variables - const arg_java_types = arg_types.map(a => a instanceof NumberLiteral ? a.type : a); + // - lambda expressions can't be used as type arguments so just pass them as void + const arg_java_types = arg_types.map(a => + a instanceof NumberLiteral ? a.type + : a instanceof LambdaType ? PrimitiveType.map.V + : a); const reified_methods = methods.map(m => { if (m.typeVariables.length) { m = ReifiedMethod.build(m, arg_java_types); @@ -142,11 +146,11 @@ function resolveMethodCall(ri, methods, args, tokens) { function resolveConstructorCall(ri, constructors, args, tokens) { const resolved_args = args.map(arg => arg.resolveExpression(ri)); - // all the arguments must be typed expressions or number literals - /** @type {(JavaType|NumberLiteral)[]} */ + // all the arguments must be typed expressions, number literals or lambdas + /** @type {(JavaType|NumberLiteral|LambdaType)[]} */ const arg_types = []; resolved_args.forEach((a, idx) => { - if (a instanceof JavaType || a instanceof NumberLiteral) { + if (a instanceof JavaType || a instanceof NumberLiteral || a instanceof LambdaType) { arg_types.push(a); return; } @@ -156,7 +160,11 @@ function resolveConstructorCall(ri, constructors, args, tokens) { }); // reify any methods with type-variables - const arg_java_types = arg_types.map(a => a instanceof NumberLiteral ? a.type : a); + // - lambda expressions can't be used as type arguments so just pass them as void + const arg_java_types = arg_types.map(a => + a instanceof NumberLiteral ? a.type + : a instanceof LambdaType ? PrimitiveType.map.V + : a); const reifed_ctrs = constructors.map(c => { if (c.typeVariables.length) { c = ReifiedConstructor.build(c, arg_java_types); @@ -206,7 +214,7 @@ function resolveConstructorCall(ri, constructors, args, tokens) { /** * * @param {Method|Constructor} m - * @param {(JavaType | NumberLiteral)[]} arg_types + * @param {(JavaType | NumberLiteral | LambdaType)[]} arg_types */ function isCallCompatible(m, arg_types) { if (m instanceof AnyMethod) {