Skip to content

Commit 9af3b81

Browse files
feat: error if divisor is zero (#644)
Closes partially #543 ### Summary of Changes Show an error if we know that the divisor of a division is zero. Once the partial evaluator gets fully implemented, this will work in more cases. --------- Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com>
1 parent 8b076e7 commit 9af3b81

File tree

5 files changed

+149
-58
lines changed

5 files changed

+149
-58
lines changed

src/language/partialEvaluation/toConstantExpressionOrUndefined.ts

+30-24
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
SdsConstantBoolean,
44
SdsConstantExpression,
55
SdsConstantFloat,
6+
SdsConstantInt,
67
SdsConstantNull,
78
SdsConstantString,
89
SdsIntermediateBlockLambda,
@@ -51,10 +52,10 @@ export const toConstantExpressionOrUndefined = (node: AstNode | undefined): SdsC
5152
return undefined;
5253
}
5354

54-
return toConstantExpressionOrNullImpl(node, new Map());
55+
return toConstantExpressionOrUndefinedImpl(node, new Map());
5556
};
5657

57-
const toConstantExpressionOrNullImpl = (
58+
const toConstantExpressionOrUndefinedImpl = (
5859
node: AstNode,
5960
substitutions: ParameterSubstitutions,
6061
): SdsConstantExpression | undefined => {
@@ -155,13 +156,16 @@ const simplifyExpressionLambda = (
155156
};
156157

157158
const simplifyInfixOperation = (
158-
_node: SdsInfixOperation,
159-
_substitutions: ParameterSubstitutions,
159+
node: SdsInfixOperation,
160+
substitutions: ParameterSubstitutions,
160161
): SdsConstantExpression | undefined => {
161-
// // By design none of the operators are short-circuited
162-
// val constantLeft = leftOperand.toConstantExpressionOrUndefined(substitutions) ?: return undefined
163-
// val constantRight = rightOperand.toConstantExpressionOrUndefined(substitutions) ?: return undefined
164-
//
162+
// By design none of the operators are short-circuited
163+
const constantLeft = toConstantExpressionOrUndefinedImpl(node.leftOperand, substitutions);
164+
if (!constantLeft) return;
165+
166+
const constantRight = toConstantExpressionOrUndefinedImpl(node.rightOperand, substitutions);
167+
if (!constantRight) return;
168+
165169
// return when (operator()) {
166170
// Or -> simplifyLogicalOp(constantLeft, Boolean::or, constantRight)
167171
// And -> simplifyLogicalOp(constantLeft, Boolean::and, constantRight)
@@ -282,30 +286,32 @@ const simplifyInfixOperation = (
282286
// }
283287

284288
const simplifyPrefixOperation = (
285-
_node: SdsPrefixOperation,
286-
_substitutions: ParameterSubstitutions,
289+
node: SdsPrefixOperation,
290+
substitutions: ParameterSubstitutions,
287291
): SdsConstantExpression | undefined => {
288-
// val constantOperand = operand.toConstantExpressionOrUndefined(substitutions) ?: return undefined
289-
//
290-
// return when (operator()) {
291-
// Not -> when (constantOperand) {
292-
// is SdsConstantBoolean -> SdsConstantBoolean(!constantOperand.value)
293-
// else -> undefined
294-
// }
295-
// PrefixMinus -> when (constantOperand) {
296-
// is SdsConstantFloat -> SdsConstantFloat(-constantOperand.value)
297-
// is SdsConstantInt -> SdsConstantInt(-constantOperand.value)
298-
// else -> undefined
299-
// }
300-
// }
292+
const constantOperand = toConstantExpressionOrUndefinedImpl(node.operand, substitutions);
293+
if (!constantOperand) return;
294+
295+
if (node.operator === 'not') {
296+
if (constantOperand instanceof SdsConstantBoolean) {
297+
return new SdsConstantBoolean(!constantOperand.value);
298+
}
299+
} else if (node.operator === '-') {
300+
if (constantOperand instanceof SdsConstantFloat) {
301+
return new SdsConstantFloat(-constantOperand.value);
302+
} else if (constantOperand instanceof SdsConstantInt) {
303+
return new SdsConstantInt(-constantOperand.value);
304+
}
305+
}
306+
301307
return undefined;
302308
};
303309

304310
const simplifyTemplateString = (
305311
node: SdsTemplateString,
306312
substitutions: ParameterSubstitutions,
307313
): SdsConstantExpression | undefined => {
308-
const constantExpressions = node.expressions.map((it) => toConstantExpressionOrNullImpl(it, substitutions));
314+
const constantExpressions = node.expressions.map((it) => toConstantExpressionOrUndefinedImpl(it, substitutions));
309315
if (constantExpressions.some((it) => it === undefined)) {
310316
return undefined;
311317
}

src/language/typing/safe-ds-type-computer.ts

+39-34
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,16 @@ export class SafeDsTypeComputer {
9191
private readonly builtinClasses: SafeDsClasses;
9292
private readonly nodeMapper: SafeDsNodeMapper;
9393

94-
private readonly typeCache: WorkspaceCache<string, Type>;
94+
private readonly coreTypeCache: WorkspaceCache<string, Type>;
95+
private readonly nodeTypeCache: WorkspaceCache<string, Type>;
9596

9697
constructor(services: SafeDsServices) {
9798
this.astNodeLocator = services.workspace.AstNodeLocator;
9899
this.builtinClasses = services.builtins.Classes;
99100
this.nodeMapper = services.helpers.NodeMapper;
100101

101-
this.typeCache = new WorkspaceCache(services.shared);
102+
this.coreTypeCache = new WorkspaceCache(services.shared);
103+
this.nodeTypeCache = new WorkspaceCache(services.shared);
102104
}
103105

104106
/**
@@ -112,7 +114,7 @@ export class SafeDsTypeComputer {
112114
const documentUri = getDocument(node).uri.toString();
113115
const nodePath = this.astNodeLocator.getAstNodePath(node);
114116
const key = `${documentUri}~${nodePath}`;
115-
return this.typeCache.get(key, () => this.doComputeType(node).unwrap());
117+
return this.nodeTypeCache.get(key, () => this.doComputeType(node).unwrap());
116118
}
117119

118120
// fun SdsAbstractObject.hasPrimitiveType(): Boolean {
@@ -257,21 +259,21 @@ export class SafeDsTypeComputer {
257259
private computeTypeOfExpression(node: SdsExpression): Type {
258260
// Terminal cases
259261
if (isSdsBoolean(node)) {
260-
return this.Boolean();
262+
return this.Boolean;
261263
} else if (isSdsFloat(node)) {
262-
return this.Float();
264+
return this.Float;
263265
} else if (isSdsInt(node)) {
264-
return this.Int();
266+
return this.Int;
265267
} else if (isSdsList(node)) {
266-
return this.List();
268+
return this.List;
267269
} else if (isSdsMap(node)) {
268-
return this.Map();
270+
return this.Map;
269271
} else if (isSdsNull(node)) {
270-
return this.NothingOrNull();
272+
return this.NothingOrNull;
271273
} else if (isSdsString(node)) {
272-
return this.String();
274+
return this.String;
273275
} else if (isSdsTemplateString(node)) {
274-
return this.String();
276+
return this.String;
275277
}
276278

277279
// Recursive cases
@@ -304,21 +306,21 @@ export class SafeDsTypeComputer {
304306
// Boolean operators
305307
case 'or':
306308
case 'and':
307-
return this.Boolean();
309+
return this.Boolean;
308310

309311
// Equality operators
310312
case '==':
311313
case '!=':
312314
case '===':
313315
case '!==':
314-
return this.Boolean();
316+
return this.Boolean;
315317

316318
// Comparison operators
317319
case '<':
318320
case '<=':
319321
case '>=':
320322
case '>':
321-
return this.Boolean();
323+
return this.Boolean;
322324

323325
// Arithmetic operators
324326
case '+':
@@ -343,7 +345,7 @@ export class SafeDsTypeComputer {
343345
} else if (isSdsPrefixOperation(node)) {
344346
switch (node.operator) {
345347
case 'not':
346-
return this.Boolean();
348+
return this.Boolean;
347349
case '-':
348350
return this.computeTypeOfArithmeticPrefixOperation(node);
349351

@@ -378,7 +380,7 @@ export class SafeDsTypeComputer {
378380

379381
private computeTypeOfIndexedAccess(node: SdsIndexedAccess): Type {
380382
const receiverType = this.computeType(node.receiver);
381-
if (receiverType.equals(this.List()) || receiverType.equals(this.Map())) {
383+
if (receiverType.equals(this.List) || receiverType.equals(this.Map)) {
382384
return this.AnyOrNull();
383385
} else {
384386
return UnknownType;
@@ -389,10 +391,10 @@ export class SafeDsTypeComputer {
389391
const leftOperandType = this.computeType(node.leftOperand);
390392
const rightOperandType = this.computeType(node.rightOperand);
391393

392-
if (leftOperandType.equals(this.Int()) && rightOperandType.equals(this.Int())) {
393-
return this.Int();
394+
if (leftOperandType.equals(this.Int) && rightOperandType.equals(this.Int)) {
395+
return this.Int;
394396
} else {
395-
return this.Float();
397+
return this.Float;
396398
}
397399
}
398400

@@ -426,10 +428,10 @@ export class SafeDsTypeComputer {
426428
private computeTypeOfArithmeticPrefixOperation(node: SdsPrefixOperation): Type {
427429
const leftOperandType = this.computeType(node.operand);
428430

429-
if (leftOperandType.equals(this.Int())) {
430-
return this.Int();
431+
if (leftOperandType.equals(this.Int)) {
432+
return this.Int;
431433
} else {
432-
return this.Float();
434+
return this.Float;
433435
}
434436
}
435437

@@ -529,43 +531,46 @@ export class SafeDsTypeComputer {
529531
// Builtin types
530532
// -----------------------------------------------------------------------------------------------------------------
531533

532-
private AnyOrNull(): Type {
534+
AnyOrNull(): Type {
533535
return this.createCoreType(this.builtinClasses.Any, true);
534536
}
535537

536-
private Boolean(): Type {
538+
get Boolean(): Type {
537539
return this.createCoreType(this.builtinClasses.Boolean);
538540
}
539541

540-
private Float(): Type {
542+
get Float(): Type {
541543
return this.createCoreType(this.builtinClasses.Float);
542544
}
543545

544-
private Int(): Type {
546+
get Int(): Type {
545547
return this.createCoreType(this.builtinClasses.Int);
546548
}
547549

548-
private List(): Type {
550+
get List(): Type {
549551
return this.createCoreType(this.builtinClasses.List);
550552
}
551553

552-
private Map(): Type {
554+
get Map(): Type {
553555
return this.createCoreType(this.builtinClasses.Map);
554556
}
555557

556-
private NothingOrNull(): Type {
558+
get NothingOrNull(): Type {
557559
return this.createCoreType(this.builtinClasses.Nothing, true);
558560
}
559561

560-
private String(): Type {
562+
get String(): Type {
561563
return this.createCoreType(this.builtinClasses.String);
562564
}
563565

564566
private createCoreType(coreClass: SdsClass | undefined, isNullable: boolean = false): Type {
565-
if (coreClass) {
566-
return new ClassType(coreClass, isNullable);
567-
} /* c8 ignore start */ else {
567+
/* c8 ignore start */
568+
if (!coreClass) {
568569
return UnknownType;
569-
} /* c8 ignore stop */
570+
}
571+
/* c8 ignore stop */
572+
573+
const key = `${coreClass.name}~${isNullable}`;
574+
return this.coreTypeCache.get(key, () => new ClassType(coreClass, isNullable));
570575
}
571576
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { SdsInfixOperation } from '../../../generated/ast.js';
2+
import { ValidationAcceptor } from 'langium';
3+
import { SafeDsServices } from '../../../safe-ds-module.js';
4+
import { toConstantExpressionOrUndefined } from '../../../partialEvaluation/toConstantExpressionOrUndefined.js';
5+
import { SdsConstantFloat, SdsConstantInt } from '../../../partialEvaluation/model.js';
6+
import { UnknownType } from '../../../typing/model.js';
7+
8+
export const CODE_INFIX_OPERATION_DIVISION_BY_ZERO = 'infix-operation/division-by-zero';
9+
10+
export const divisionDivisorMustNotBeZero = (services: SafeDsServices) => {
11+
const typeComputer = services.types.TypeComputer;
12+
const zeroInt = new SdsConstantInt(BigInt(0));
13+
const zeroFloat = new SdsConstantFloat(0.0);
14+
const minusZeroFloat = new SdsConstantFloat(-0.0);
15+
16+
return (node: SdsInfixOperation, accept: ValidationAcceptor): void => {
17+
if (node.operator !== '/') {
18+
return;
19+
}
20+
21+
const dividendType = typeComputer.computeType(node.leftOperand);
22+
if (
23+
dividendType === UnknownType ||
24+
(!dividendType.equals(typeComputer.Int) && !dividendType.equals(typeComputer.Float))
25+
) {
26+
return;
27+
}
28+
29+
const divisorValue = toConstantExpressionOrUndefined(node.rightOperand);
30+
if (
31+
divisorValue &&
32+
(divisorValue.equals(zeroInt) || divisorValue.equals(zeroFloat) || divisorValue.equals(minusZeroFloat))
33+
) {
34+
accept('error', 'Division by zero.', {
35+
node,
36+
code: CODE_INFIX_OPERATION_DIVISION_BY_ZERO,
37+
});
38+
}
39+
};
40+
};

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

+2
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ import {
103103
import { classMustNotInheritItself, classMustOnlyInheritASingleClass } from './inheritance.js';
104104
import { pythonNameShouldDifferFromSafeDsName } from './builtins/pythonName.js';
105105
import { pythonModuleShouldDifferFromSafeDsPackage } from './builtins/pythonModule.js';
106+
import { divisionDivisorMustNotBeZero } from './other/expressions/infixOperations.js';
106107

107108
/**
108109
* Register custom validation checks.
@@ -166,6 +167,7 @@ export const registerValidationChecks = function (services: SafeDsServices) {
166167
SdsImport: [importPackageMustExist(services), importPackageShouldNotBeEmpty(services)],
167168
SdsImportedDeclaration: [importedDeclarationAliasShouldDifferFromDeclarationName],
168169
SdsIndexedAccess: [indexedAccessesShouldBeUsedWithCaution],
170+
SdsInfixOperation: [divisionDivisorMustNotBeZero(services)],
169171
SdsLambda: [lambdaParametersMustNotBeAnnotated, lambdaParameterMustNotHaveConstModifier],
170172
SdsMemberAccess: [
171173
memberAccessMustBeNullSafeIfReceiverIsNullable(services),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package tests.validation.other.expressions.infixOperations.divisionByZero
2+
3+
pipeline test {
4+
// $TEST$ error "Division by zero."
5+
»1.0 / 0.0«;
6+
// $TEST$ error "Division by zero."
7+
»1.0 / -0.0«;
8+
// $TEST$ no error "Division by zero."
9+
»1.0 / 1.0«;
10+
11+
// $TEST$ error "Division by zero."
12+
»1.0 / 0«;
13+
// $TEST$ no error "Division by zero."
14+
»1.0 / 1«;
15+
16+
// $TEST$ error "Division by zero."
17+
»1 / 0.0«;
18+
// $TEST$ error "Division by zero."
19+
»1 / -0.0«;
20+
// $TEST$ no error "Division by zero."
21+
»1 / 1.0«;
22+
23+
// $TEST$ error "Division by zero."
24+
»1 / 0«;
25+
// $TEST$ no error "Division by zero."
26+
»1 / 1«;
27+
28+
// $TEST$ no error "Division by zero."
29+
»null / 0.0«;
30+
// $TEST$ no error "Division by zero."
31+
»null / -0.0«;
32+
// $TEST$ no error "Division by zero."
33+
»null / 1.0«;
34+
// $TEST$ no error "Division by zero."
35+
»null / 0«;
36+
// $TEST$ no error "Division by zero."
37+
»null / 1«;
38+
}

0 commit comments

Comments
 (0)