Skip to content

Commit

Permalink
feat: type checking (#723)
Browse files Browse the repository at this point in the history
Closes #666

### Summary of Changes

Show validation errors if types don't match.
  • Loading branch information
lars-reimann authored Nov 4, 2023
1 parent daad5c4 commit a9eb3bb
Show file tree
Hide file tree
Showing 30 changed files with 485 additions and 721 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,7 @@ export class SafeDsTypeChecker {
const typeEntry = type.outputType.entries[i];
const otherEntry = other.outputType.entries[i];

// Names must match
if (typeEntry.name !== otherEntry.name) {
return false;
}
// Names must not match since we always fetch results by index

// Types must be covariant
if (!this.isAssignableTo(typeEntry.type, otherEntry.type)) {
Expand Down
24 changes: 21 additions & 3 deletions packages/safe-ds-lang/src/language/validation/safe-ds-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,18 @@ import {
yieldMustNotBeUsedInPipeline,
} from './other/statements/assignments.js';
import {
argumentTypeMustMatchParameterType,
attributeMustHaveTypeHint,
callReceiverMustBeCallable,
indexedAccessIndexMustHaveCorrectType,
indexedAccessReceiverMustBeListOrMap,
infixOperationOperandsMustHaveCorrectType,
namedTypeMustSetAllTypeParameters,
parameterDefaultValueTypeMustMatchParameterType,
parameterMustHaveTypeHint,
prefixOperationOperandMustHaveCorrectType,
resultMustHaveTypeHint,
yieldTypeMustMatchResultType,
} from './types.js';
import {
moduleDeclarationsMustMatchFileKind,
Expand Down Expand Up @@ -182,6 +189,7 @@ export const registerValidationChecks = function (services: SafeDsServices) {
SdsArgument: [
argumentCorrespondingParameterShouldNotBeDeprecated(services),
argumentCorrespondingParameterShouldNotBeExperimental(services),
argumentTypeMustMatchParameterType(services),
],
SdsArgumentList: [
argumentListMustNotHavePositionalArgumentsAfterNamedArguments,
Expand Down Expand Up @@ -226,8 +234,16 @@ export const registerValidationChecks = function (services: SafeDsServices) {
],
SdsImport: [importPackageMustExist(services), importPackageShouldNotBeEmpty(services)],
SdsImportedDeclaration: [importedDeclarationAliasShouldDifferFromDeclarationName],
SdsIndexedAccess: [indexedAccessesShouldBeUsedWithCaution],
SdsInfixOperation: [divisionDivisorMustNotBeZero(services), elvisOperatorShouldBeNeeded(services)],
SdsIndexedAccess: [
indexedAccessIndexMustHaveCorrectType(services),
indexedAccessReceiverMustBeListOrMap(services),
indexedAccessesShouldBeUsedWithCaution,
],
SdsInfixOperation: [
divisionDivisorMustNotBeZero(services),
elvisOperatorShouldBeNeeded(services),
infixOperationOperandsMustHaveCorrectType(services),
],
SdsLambda: [
lambdaMustBeAssignedToTypedParameter(services),
lambdaParametersMustNotBeAnnotated,
Expand Down Expand Up @@ -266,12 +282,14 @@ export const registerValidationChecks = function (services: SafeDsServices) {
SdsParameter: [
constantParameterMustHaveConstantDefaultValue(services),
parameterMustHaveTypeHint,
parameterDefaultValueTypeMustMatchParameterType(services),
requiredParameterMustNotBeDeprecated(services),
requiredParameterMustNotBeExpert(services),
],
SdsParameterList: [parameterListMustNotHaveRequiredParametersAfterOptionalParameters],
SdsPipeline: [pipelineMustContainUniqueNames],
SdsPlaceholder: [placeholdersMustNotBeAnAlias, placeholderShouldBeUsed(services)],
SdsPrefixOperation: [prefixOperationOperandMustHaveCorrectType(services)],
SdsReference: [
referenceMustNotBeFunctionPointer,
referenceMustNotBeStaticClassOrEnumReference,
Expand Down Expand Up @@ -299,7 +317,7 @@ export const registerValidationChecks = function (services: SafeDsServices) {
unionTypeShouldNotHaveDuplicateTypes(services),
unionTypeShouldNotHaveASingularTypeArgument,
],
SdsYield: [yieldMustNotBeUsedInPipeline],
SdsYield: [yieldMustNotBeUsedInPipeline, yieldTypeMustMatchResultType(services)],
};
registry.register(checks);
};
211 changes: 211 additions & 0 deletions packages/safe-ds-lang/src/language/validation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,55 @@ import {
isSdsPipeline,
isSdsReference,
isSdsSchema,
SdsArgument,
SdsAttribute,
SdsCall,
SdsIndexedAccess,
SdsInfixOperation,
SdsNamedType,
SdsParameter,
SdsPrefixOperation,
SdsResult,
SdsYield,
} from '../generated/ast.js';
import { getTypeArguments, getTypeParameters } from '../helpers/nodeProperties.js';
import { SafeDsServices } from '../safe-ds-module.js';
import { pluralize } from '../../helpers/stringUtils.js';
import { isEmpty } from '../../helpers/collectionUtils.js';

export const CODE_TYPE_CALLABLE_RECEIVER = 'type/callable-receiver';
export const CODE_TYPE_MISMATCH = 'type/mismatch';
export const CODE_TYPE_MISSING_TYPE_ARGUMENTS = 'type/missing-type-arguments';
export const CODE_TYPE_MISSING_TYPE_HINT = 'type/missing-type-hint';

// -----------------------------------------------------------------------------
// Type checking
// -----------------------------------------------------------------------------

export const argumentTypeMustMatchParameterType = (services: SafeDsServices) => {
const nodeMapper = services.helpers.NodeMapper;
const typeChecker = services.types.TypeChecker;
const typeComputer = services.types.TypeComputer;

return (node: SdsArgument, accept: ValidationAcceptor) => {
const parameter = nodeMapper.argumentToParameter(node);
if (!parameter) {
return;
}

const argumentType = typeComputer.computeType(node);
const parameterType = typeComputer.computeType(parameter);

if (!typeChecker.isAssignableTo(argumentType, parameterType)) {
accept('error', `Expected type '${parameterType}' but got '${argumentType}'.`, {
node,
property: 'value',
code: CODE_TYPE_MISMATCH,
});
}
};
};

export const callReceiverMustBeCallable = (services: SafeDsServices) => {
const nodeMapper = services.helpers.NodeMapper;

Expand Down Expand Up @@ -60,6 +90,187 @@ export const callReceiverMustBeCallable = (services: SafeDsServices) => {
};
};

export const indexedAccessReceiverMustBeListOrMap = (services: SafeDsServices) => {
const coreTypes = services.types.CoreTypes;
const typeChecker = services.types.TypeChecker;
const typeComputer = services.types.TypeComputer;

return (node: SdsIndexedAccess, accept: ValidationAcceptor): void => {
const receiverType = typeComputer.computeType(node.receiver);
if (
!typeChecker.isAssignableTo(receiverType, coreTypes.List) &&
!typeChecker.isAssignableTo(receiverType, coreTypes.Map)
) {
accept('error', `Expected type '${coreTypes.List}' or '${coreTypes.Map}' but got '${receiverType}'.`, {
node: node.receiver,
code: CODE_TYPE_MISMATCH,
});
}
};
};

export const indexedAccessIndexMustHaveCorrectType = (services: SafeDsServices) => {
const coreTypes = services.types.CoreTypes;
const typeChecker = services.types.TypeChecker;
const typeComputer = services.types.TypeComputer;

return (node: SdsIndexedAccess, accept: ValidationAcceptor): void => {
const receiverType = typeComputer.computeType(node.receiver);
if (typeChecker.isAssignableTo(receiverType, coreTypes.List)) {
const indexType = typeComputer.computeType(node.index);
if (!typeChecker.isAssignableTo(indexType, coreTypes.Int)) {
accept('error', `Expected type '${coreTypes.Int}' but got '${indexType}'.`, {
node,
property: 'index',
code: CODE_TYPE_MISMATCH,
});
}
}
};
};

export const infixOperationOperandsMustHaveCorrectType = (services: SafeDsServices) => {
const coreTypes = services.types.CoreTypes;
const typeChecker = services.types.TypeChecker;
const typeComputer = services.types.TypeComputer;

return (node: SdsInfixOperation, accept: ValidationAcceptor): void => {
const leftType = typeComputer.computeType(node.leftOperand);
const rightType = typeComputer.computeType(node.rightOperand);
switch (node.operator) {
case 'or':
case 'and':
if (!typeChecker.isAssignableTo(leftType, coreTypes.Boolean)) {
accept('error', `Expected type '${coreTypes.Boolean}' but got '${leftType}'.`, {
node: node.leftOperand,
code: CODE_TYPE_MISMATCH,
});
}
if (!typeChecker.isAssignableTo(rightType, coreTypes.Boolean)) {
accept('error', `Expected type '${coreTypes.Boolean}' but got '${rightType}'.`, {
node: node.rightOperand,
code: CODE_TYPE_MISMATCH,
});
}
return;
case '<':
case '<=':
case '>=':
case '>':
case '+':
case '-':
case '*':
case '/':
if (
!typeChecker.isAssignableTo(leftType, coreTypes.Float) &&
!typeChecker.isAssignableTo(leftType, coreTypes.Int)
) {
accept('error', `Expected type '${coreTypes.Float}' or '${coreTypes.Int}' but got '${leftType}'.`, {
node: node.leftOperand,
code: CODE_TYPE_MISMATCH,
});
}
if (
!typeChecker.isAssignableTo(rightType, coreTypes.Float) &&
!typeChecker.isAssignableTo(rightType, coreTypes.Int)
) {
accept(
'error',
`Expected type '${coreTypes.Float}' or '${coreTypes.Int}' but got '${rightType}'.`,
{
node: node.rightOperand,
code: CODE_TYPE_MISMATCH,
},
);
}
return;
}
};
};

export const parameterDefaultValueTypeMustMatchParameterType = (services: SafeDsServices) => {
const typeChecker = services.types.TypeChecker;
const typeComputer = services.types.TypeComputer;

return (node: SdsParameter, accept: ValidationAcceptor) => {
const defaultValue = node.defaultValue;
if (!defaultValue) {
return;
}

const defaultValueType = typeComputer.computeType(defaultValue);
const parameterType = typeComputer.computeType(node);

if (!typeChecker.isAssignableTo(defaultValueType, parameterType)) {
accept('error', `Expected type '${parameterType}' but got '${defaultValueType}'.`, {
node,
property: 'defaultValue',
code: CODE_TYPE_MISMATCH,
});
}
};
};

export const prefixOperationOperandMustHaveCorrectType = (services: SafeDsServices) => {
const coreTypes = services.types.CoreTypes;
const typeChecker = services.types.TypeChecker;
const typeComputer = services.types.TypeComputer;

return (node: SdsPrefixOperation, accept: ValidationAcceptor): void => {
const operandType = typeComputer.computeType(node.operand);
switch (node.operator) {
case 'not':
if (!typeChecker.isAssignableTo(operandType, coreTypes.Boolean)) {
accept('error', `Expected type '${coreTypes.Boolean}' but got '${operandType}'.`, {
node,
property: 'operand',
code: CODE_TYPE_MISMATCH,
});
}
return;
case '-':
if (
!typeChecker.isAssignableTo(operandType, coreTypes.Float) &&
!typeChecker.isAssignableTo(operandType, coreTypes.Int)
) {
accept(
'error',
`Expected type '${coreTypes.Float}' or '${coreTypes.Int}' but got '${operandType}'.`,
{
node,
property: 'operand',
code: CODE_TYPE_MISMATCH,
},
);
}
return;
}
};
};

export const yieldTypeMustMatchResultType = (services: SafeDsServices) => {
const typeChecker = services.types.TypeChecker;
const typeComputer = services.types.TypeComputer;

return (node: SdsYield, accept: ValidationAcceptor) => {
const result = node.result?.ref;
if (!result) {
return;
}

const yieldType = typeComputer.computeType(node);
const resultType = typeComputer.computeType(result);

if (!typeChecker.isAssignableTo(yieldType, resultType)) {
accept('error', `Expected type '${resultType}' but got '${yieldType}'.`, {
node,
property: 'result',
code: CODE_TYPE_MISMATCH,
});
}
};
};

// -----------------------------------------------------------------------------
// Missing type arguments
// -----------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ describe('SafeDsTypeChecker', async () => {
{
type1: callableType8,
type2: callableType7,
expected: false,
expected: true,
},
{
type1: callableType9,
Expand Down
4 changes: 2 additions & 2 deletions packages/safe-ds-lang/tests/language/validation/creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import fs from 'fs';
import { findTestChecks } from '../../helpers/testChecks.js';
import { getSyntaxErrors, SyntaxErrorsInCodeError } from '../../helpers/diagnostics.js';
import { EmptyFileSystem, URI } from 'langium';
import { createSafeDsServices } from '../../../src/language/safe-ds-module.js';
import { createSafeDsServices } from '../../../src/language/index.js';
import { Range } from 'vscode-languageserver';
import { TestDescription, TestDescriptionError } from '../../helpers/testDescription.js';

Expand Down Expand Up @@ -40,7 +40,7 @@ const createValidationTest = async (parentDirectory: URI, uris: URI[]): Promise<
}

for (const check of checksResult.value) {
const regex = /\s*(?<isAbsent>no\s+)?(?<severity>\S+)\s*(?:(?<messageIsRegex>r)?"(?<message>[^"]*)")?/gu;
const regex = /\s*(?<isAbsent>no\s+)?(?<severity>\S+)\s*(?:(?<messageIsRegex>r)?"(?<message>.*)")?/gu;
const match = regex.exec(check.comment);

// Overall comment is invalid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ fun f1(param: (a: Int, b: Int, c: Int) -> r: Int)
fun f2(param: (a: Int, b: Int, c: Int) -> ())

segment test(param1: Int, @PythonName("param_2") param2: Int, @PythonName("param_3") param3: Int = 0) {
f1((param1: Int, param2: Int, param3: Int = 0) -> 1);
f2((param1: Int, param2: Int, param3: Int = 0) {});
f1((a: Int, b: Int, c: Int = 0) -> 1);
f2((a: Int, b: Int, c: Int = 0) {});
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Segments ---------------------------------------------------------------------

def test(param1, param_2, param_3=0):
f1(lambda param1, param2, param3=0: 1)
def __gen_block_lambda_0(param1, param2, param3=0):
f1(lambda a, b, c=0: 1)
def __gen_block_lambda_0(a, b, c=0):
pass
f2(__gen_block_lambda_0)

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pipeline test {
f1((a: Int, b: Int = 2) {
yield d = g();
});
f1((a: Int, c: Int) {
f1((a: Int, b: Int) {
yield d = g();
});
f2(() {});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ def __gen_block_lambda_0(a, b=2):
__gen_block_lambda_result_d = g()
return __gen_block_lambda_result_d
f1(__gen_block_lambda_0)
def __gen_block_lambda_1(a, c):
def __gen_block_lambda_1(a, b):
__gen_block_lambda_result_d = g()
return __gen_block_lambda_result_d
f1(__gen_block_lambda_1)
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit a9eb3bb

Please sign in to comment.