Skip to content

Commit a9eb3bb

Browse files
authored
feat: type checking (#723)
Closes #666 ### Summary of Changes Show validation errors if types don't match.
1 parent daad5c4 commit a9eb3bb

File tree

30 files changed

+485
-721
lines changed

30 files changed

+485
-721
lines changed

packages/safe-ds-lang/src/language/typing/safe-ds-type-checker.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,7 @@ export class SafeDsTypeChecker {
105105
const typeEntry = type.outputType.entries[i];
106106
const otherEntry = other.outputType.entries[i];
107107

108-
// Names must match
109-
if (typeEntry.name !== otherEntry.name) {
110-
return false;
111-
}
108+
// Names must not match since we always fetch results by index
112109

113110
// Types must be covariant
114111
if (!this.isAssignableTo(typeEntry.type, otherEntry.type)) {

packages/safe-ds-lang/src/language/validation/safe-ds-validator.ts

+21-3
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,18 @@ import {
4444
yieldMustNotBeUsedInPipeline,
4545
} from './other/statements/assignments.js';
4646
import {
47+
argumentTypeMustMatchParameterType,
4748
attributeMustHaveTypeHint,
4849
callReceiverMustBeCallable,
50+
indexedAccessIndexMustHaveCorrectType,
51+
indexedAccessReceiverMustBeListOrMap,
52+
infixOperationOperandsMustHaveCorrectType,
4953
namedTypeMustSetAllTypeParameters,
54+
parameterDefaultValueTypeMustMatchParameterType,
5055
parameterMustHaveTypeHint,
56+
prefixOperationOperandMustHaveCorrectType,
5157
resultMustHaveTypeHint,
58+
yieldTypeMustMatchResultType,
5259
} from './types.js';
5360
import {
5461
moduleDeclarationsMustMatchFileKind,
@@ -182,6 +189,7 @@ export const registerValidationChecks = function (services: SafeDsServices) {
182189
SdsArgument: [
183190
argumentCorrespondingParameterShouldNotBeDeprecated(services),
184191
argumentCorrespondingParameterShouldNotBeExperimental(services),
192+
argumentTypeMustMatchParameterType(services),
185193
],
186194
SdsArgumentList: [
187195
argumentListMustNotHavePositionalArgumentsAfterNamedArguments,
@@ -226,8 +234,16 @@ export const registerValidationChecks = function (services: SafeDsServices) {
226234
],
227235
SdsImport: [importPackageMustExist(services), importPackageShouldNotBeEmpty(services)],
228236
SdsImportedDeclaration: [importedDeclarationAliasShouldDifferFromDeclarationName],
229-
SdsIndexedAccess: [indexedAccessesShouldBeUsedWithCaution],
230-
SdsInfixOperation: [divisionDivisorMustNotBeZero(services), elvisOperatorShouldBeNeeded(services)],
237+
SdsIndexedAccess: [
238+
indexedAccessIndexMustHaveCorrectType(services),
239+
indexedAccessReceiverMustBeListOrMap(services),
240+
indexedAccessesShouldBeUsedWithCaution,
241+
],
242+
SdsInfixOperation: [
243+
divisionDivisorMustNotBeZero(services),
244+
elvisOperatorShouldBeNeeded(services),
245+
infixOperationOperandsMustHaveCorrectType(services),
246+
],
231247
SdsLambda: [
232248
lambdaMustBeAssignedToTypedParameter(services),
233249
lambdaParametersMustNotBeAnnotated,
@@ -266,12 +282,14 @@ export const registerValidationChecks = function (services: SafeDsServices) {
266282
SdsParameter: [
267283
constantParameterMustHaveConstantDefaultValue(services),
268284
parameterMustHaveTypeHint,
285+
parameterDefaultValueTypeMustMatchParameterType(services),
269286
requiredParameterMustNotBeDeprecated(services),
270287
requiredParameterMustNotBeExpert(services),
271288
],
272289
SdsParameterList: [parameterListMustNotHaveRequiredParametersAfterOptionalParameters],
273290
SdsPipeline: [pipelineMustContainUniqueNames],
274291
SdsPlaceholder: [placeholdersMustNotBeAnAlias, placeholderShouldBeUsed(services)],
292+
SdsPrefixOperation: [prefixOperationOperandMustHaveCorrectType(services)],
275293
SdsReference: [
276294
referenceMustNotBeFunctionPointer,
277295
referenceMustNotBeStaticClassOrEnumReference,
@@ -299,7 +317,7 @@ export const registerValidationChecks = function (services: SafeDsServices) {
299317
unionTypeShouldNotHaveDuplicateTypes(services),
300318
unionTypeShouldNotHaveASingularTypeArgument,
301319
],
302-
SdsYield: [yieldMustNotBeUsedInPipeline],
320+
SdsYield: [yieldMustNotBeUsedInPipeline, yieldTypeMustMatchResultType(services)],
303321
};
304322
registry.register(checks);
305323
};

packages/safe-ds-lang/src/language/validation/types.ts

+211
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,55 @@ import {
88
isSdsPipeline,
99
isSdsReference,
1010
isSdsSchema,
11+
SdsArgument,
1112
SdsAttribute,
1213
SdsCall,
14+
SdsIndexedAccess,
15+
SdsInfixOperation,
1316
SdsNamedType,
1417
SdsParameter,
18+
SdsPrefixOperation,
1519
SdsResult,
20+
SdsYield,
1621
} from '../generated/ast.js';
1722
import { getTypeArguments, getTypeParameters } from '../helpers/nodeProperties.js';
1823
import { SafeDsServices } from '../safe-ds-module.js';
1924
import { pluralize } from '../../helpers/stringUtils.js';
2025
import { isEmpty } from '../../helpers/collectionUtils.js';
2126

2227
export const CODE_TYPE_CALLABLE_RECEIVER = 'type/callable-receiver';
28+
export const CODE_TYPE_MISMATCH = 'type/mismatch';
2329
export const CODE_TYPE_MISSING_TYPE_ARGUMENTS = 'type/missing-type-arguments';
2430
export const CODE_TYPE_MISSING_TYPE_HINT = 'type/missing-type-hint';
2531

2632
// -----------------------------------------------------------------------------
2733
// Type checking
2834
// -----------------------------------------------------------------------------
2935

36+
export const argumentTypeMustMatchParameterType = (services: SafeDsServices) => {
37+
const nodeMapper = services.helpers.NodeMapper;
38+
const typeChecker = services.types.TypeChecker;
39+
const typeComputer = services.types.TypeComputer;
40+
41+
return (node: SdsArgument, accept: ValidationAcceptor) => {
42+
const parameter = nodeMapper.argumentToParameter(node);
43+
if (!parameter) {
44+
return;
45+
}
46+
47+
const argumentType = typeComputer.computeType(node);
48+
const parameterType = typeComputer.computeType(parameter);
49+
50+
if (!typeChecker.isAssignableTo(argumentType, parameterType)) {
51+
accept('error', `Expected type '${parameterType}' but got '${argumentType}'.`, {
52+
node,
53+
property: 'value',
54+
code: CODE_TYPE_MISMATCH,
55+
});
56+
}
57+
};
58+
};
59+
3060
export const callReceiverMustBeCallable = (services: SafeDsServices) => {
3161
const nodeMapper = services.helpers.NodeMapper;
3262

@@ -60,6 +90,187 @@ export const callReceiverMustBeCallable = (services: SafeDsServices) => {
6090
};
6191
};
6292

93+
export const indexedAccessReceiverMustBeListOrMap = (services: SafeDsServices) => {
94+
const coreTypes = services.types.CoreTypes;
95+
const typeChecker = services.types.TypeChecker;
96+
const typeComputer = services.types.TypeComputer;
97+
98+
return (node: SdsIndexedAccess, accept: ValidationAcceptor): void => {
99+
const receiverType = typeComputer.computeType(node.receiver);
100+
if (
101+
!typeChecker.isAssignableTo(receiverType, coreTypes.List) &&
102+
!typeChecker.isAssignableTo(receiverType, coreTypes.Map)
103+
) {
104+
accept('error', `Expected type '${coreTypes.List}' or '${coreTypes.Map}' but got '${receiverType}'.`, {
105+
node: node.receiver,
106+
code: CODE_TYPE_MISMATCH,
107+
});
108+
}
109+
};
110+
};
111+
112+
export const indexedAccessIndexMustHaveCorrectType = (services: SafeDsServices) => {
113+
const coreTypes = services.types.CoreTypes;
114+
const typeChecker = services.types.TypeChecker;
115+
const typeComputer = services.types.TypeComputer;
116+
117+
return (node: SdsIndexedAccess, accept: ValidationAcceptor): void => {
118+
const receiverType = typeComputer.computeType(node.receiver);
119+
if (typeChecker.isAssignableTo(receiverType, coreTypes.List)) {
120+
const indexType = typeComputer.computeType(node.index);
121+
if (!typeChecker.isAssignableTo(indexType, coreTypes.Int)) {
122+
accept('error', `Expected type '${coreTypes.Int}' but got '${indexType}'.`, {
123+
node,
124+
property: 'index',
125+
code: CODE_TYPE_MISMATCH,
126+
});
127+
}
128+
}
129+
};
130+
};
131+
132+
export const infixOperationOperandsMustHaveCorrectType = (services: SafeDsServices) => {
133+
const coreTypes = services.types.CoreTypes;
134+
const typeChecker = services.types.TypeChecker;
135+
const typeComputer = services.types.TypeComputer;
136+
137+
return (node: SdsInfixOperation, accept: ValidationAcceptor): void => {
138+
const leftType = typeComputer.computeType(node.leftOperand);
139+
const rightType = typeComputer.computeType(node.rightOperand);
140+
switch (node.operator) {
141+
case 'or':
142+
case 'and':
143+
if (!typeChecker.isAssignableTo(leftType, coreTypes.Boolean)) {
144+
accept('error', `Expected type '${coreTypes.Boolean}' but got '${leftType}'.`, {
145+
node: node.leftOperand,
146+
code: CODE_TYPE_MISMATCH,
147+
});
148+
}
149+
if (!typeChecker.isAssignableTo(rightType, coreTypes.Boolean)) {
150+
accept('error', `Expected type '${coreTypes.Boolean}' but got '${rightType}'.`, {
151+
node: node.rightOperand,
152+
code: CODE_TYPE_MISMATCH,
153+
});
154+
}
155+
return;
156+
case '<':
157+
case '<=':
158+
case '>=':
159+
case '>':
160+
case '+':
161+
case '-':
162+
case '*':
163+
case '/':
164+
if (
165+
!typeChecker.isAssignableTo(leftType, coreTypes.Float) &&
166+
!typeChecker.isAssignableTo(leftType, coreTypes.Int)
167+
) {
168+
accept('error', `Expected type '${coreTypes.Float}' or '${coreTypes.Int}' but got '${leftType}'.`, {
169+
node: node.leftOperand,
170+
code: CODE_TYPE_MISMATCH,
171+
});
172+
}
173+
if (
174+
!typeChecker.isAssignableTo(rightType, coreTypes.Float) &&
175+
!typeChecker.isAssignableTo(rightType, coreTypes.Int)
176+
) {
177+
accept(
178+
'error',
179+
`Expected type '${coreTypes.Float}' or '${coreTypes.Int}' but got '${rightType}'.`,
180+
{
181+
node: node.rightOperand,
182+
code: CODE_TYPE_MISMATCH,
183+
},
184+
);
185+
}
186+
return;
187+
}
188+
};
189+
};
190+
191+
export const parameterDefaultValueTypeMustMatchParameterType = (services: SafeDsServices) => {
192+
const typeChecker = services.types.TypeChecker;
193+
const typeComputer = services.types.TypeComputer;
194+
195+
return (node: SdsParameter, accept: ValidationAcceptor) => {
196+
const defaultValue = node.defaultValue;
197+
if (!defaultValue) {
198+
return;
199+
}
200+
201+
const defaultValueType = typeComputer.computeType(defaultValue);
202+
const parameterType = typeComputer.computeType(node);
203+
204+
if (!typeChecker.isAssignableTo(defaultValueType, parameterType)) {
205+
accept('error', `Expected type '${parameterType}' but got '${defaultValueType}'.`, {
206+
node,
207+
property: 'defaultValue',
208+
code: CODE_TYPE_MISMATCH,
209+
});
210+
}
211+
};
212+
};
213+
214+
export const prefixOperationOperandMustHaveCorrectType = (services: SafeDsServices) => {
215+
const coreTypes = services.types.CoreTypes;
216+
const typeChecker = services.types.TypeChecker;
217+
const typeComputer = services.types.TypeComputer;
218+
219+
return (node: SdsPrefixOperation, accept: ValidationAcceptor): void => {
220+
const operandType = typeComputer.computeType(node.operand);
221+
switch (node.operator) {
222+
case 'not':
223+
if (!typeChecker.isAssignableTo(operandType, coreTypes.Boolean)) {
224+
accept('error', `Expected type '${coreTypes.Boolean}' but got '${operandType}'.`, {
225+
node,
226+
property: 'operand',
227+
code: CODE_TYPE_MISMATCH,
228+
});
229+
}
230+
return;
231+
case '-':
232+
if (
233+
!typeChecker.isAssignableTo(operandType, coreTypes.Float) &&
234+
!typeChecker.isAssignableTo(operandType, coreTypes.Int)
235+
) {
236+
accept(
237+
'error',
238+
`Expected type '${coreTypes.Float}' or '${coreTypes.Int}' but got '${operandType}'.`,
239+
{
240+
node,
241+
property: 'operand',
242+
code: CODE_TYPE_MISMATCH,
243+
},
244+
);
245+
}
246+
return;
247+
}
248+
};
249+
};
250+
251+
export const yieldTypeMustMatchResultType = (services: SafeDsServices) => {
252+
const typeChecker = services.types.TypeChecker;
253+
const typeComputer = services.types.TypeComputer;
254+
255+
return (node: SdsYield, accept: ValidationAcceptor) => {
256+
const result = node.result?.ref;
257+
if (!result) {
258+
return;
259+
}
260+
261+
const yieldType = typeComputer.computeType(node);
262+
const resultType = typeComputer.computeType(result);
263+
264+
if (!typeChecker.isAssignableTo(yieldType, resultType)) {
265+
accept('error', `Expected type '${resultType}' but got '${yieldType}'.`, {
266+
node,
267+
property: 'result',
268+
code: CODE_TYPE_MISMATCH,
269+
});
270+
}
271+
};
272+
};
273+
63274
// -----------------------------------------------------------------------------
64275
// Missing type arguments
65276
// -----------------------------------------------------------------------------

packages/safe-ds-lang/tests/language/typing/safe-ds-type-checker.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ describe('SafeDsTypeChecker', async () => {
144144
{
145145
type1: callableType8,
146146
type2: callableType7,
147-
expected: false,
147+
expected: true,
148148
},
149149
{
150150
type1: callableType9,

packages/safe-ds-lang/tests/language/validation/creator.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import fs from 'fs';
66
import { findTestChecks } from '../../helpers/testChecks.js';
77
import { getSyntaxErrors, SyntaxErrorsInCodeError } from '../../helpers/diagnostics.js';
88
import { EmptyFileSystem, URI } from 'langium';
9-
import { createSafeDsServices } from '../../../src/language/safe-ds-module.js';
9+
import { createSafeDsServices } from '../../../src/language/index.js';
1010
import { Range } from 'vscode-languageserver';
1111
import { TestDescription, TestDescriptionError } from '../../helpers/testDescription.js';
1212

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

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

4646
// Overall comment is invalid

packages/safe-ds-lang/tests/resources/generation/declarations/parameter with python name/input.sdstest

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ fun f1(param: (a: Int, b: Int, c: Int) -> r: Int)
44
fun f2(param: (a: Int, b: Int, c: Int) -> ())
55

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

33
def test(param1, param_2, param_3=0):
4-
f1(lambda param1, param2, param3=0: 1)
5-
def __gen_block_lambda_0(param1, param2, param3=0):
4+
f1(lambda a, b, c=0: 1)
5+
def __gen_block_lambda_0(a, b, c=0):
66
pass
77
f2(__gen_block_lambda_0)

packages/safe-ds-lang/tests/resources/generation/declarations/parameter with python name/output/tests/generator/parameterWithPythonName/gen_input.py.map

+1-1
Original file line numberDiff line numberDiff line change

packages/safe-ds-lang/tests/resources/generation/expressions/block lambda/input.sdstest

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ pipeline test {
99
f1((a: Int, b: Int = 2) {
1010
yield d = g();
1111
});
12-
f1((a: Int, c: Int) {
12+
f1((a: Int, b: Int) {
1313
yield d = g();
1414
});
1515
f2(() {});

packages/safe-ds-lang/tests/resources/generation/expressions/block lambda/output/tests/generator/blockLambda/gen_input.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ def __gen_block_lambda_0(a, b=2):
55
__gen_block_lambda_result_d = g()
66
return __gen_block_lambda_result_d
77
f1(__gen_block_lambda_0)
8-
def __gen_block_lambda_1(a, c):
8+
def __gen_block_lambda_1(a, b):
99
__gen_block_lambda_result_d = g()
1010
return __gen_block_lambda_result_d
1111
f1(__gen_block_lambda_1)

packages/safe-ds-lang/tests/resources/generation/expressions/block lambda/output/tests/generator/blockLambda/gen_input.py.map

+1-1
Original file line numberDiff line numberDiff line change

0 commit comments

Comments
 (0)