add basic type checking of lambda expressions

This commit is contained in:
Dave Holoway
2020-06-22 19:05:26 +01:00
parent 4f0e55a909
commit 56464ef537
4 changed files with 127 additions and 16 deletions

View File

@@ -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;
}
}
/**

View File

@@ -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

View File

@@ -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() {

View File

@@ -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) {