Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: type checking #723

Merged
merged 9 commits into from
Nov 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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