Skip to content

Commit

Permalink
feat: short-circuit and, or, and ?: if RHS has no side effects (#…
Browse files Browse the repository at this point in the history
…789)

Closes #15

### Summary of Changes

Evaluate `and`, `or`, and `?:` if the RHS has no side effects and the
LHS determines the result.
  • Loading branch information
lars-reimann authored Nov 21, 2023
1 parent 98acdde commit 9d9f4b7
Show file tree
Hide file tree
Showing 10 changed files with 208 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { SafeDsServices } from '../safe-ds-module.js';
import {
BlockLambdaClosure,
BooleanConstant,
Constant,
EvaluatedEnumVariant,
EvaluatedList,
EvaluatedMap,
Expand All @@ -58,16 +59,19 @@ import {
StringConstant,
UnknownEvaluatedNode,
} from './model.js';
import { SafeDsPurityComputer } from '../purity/safe-ds-purity-computer.js';

export class SafeDsPartialEvaluator {
private readonly astNodeLocator: AstNodeLocator;
private readonly nodeMapper: SafeDsNodeMapper;
private readonly purityComputer: () => SafeDsPurityComputer;

private readonly cache: WorkspaceCache<string, EvaluatedNode>;

constructor(services: SafeDsServices) {
this.astNodeLocator = services.workspace.AstNodeLocator;
this.nodeMapper = services.helpers.NodeMapper;
this.purityComputer = () => services.purity.PurityComputer;

this.cache = new WorkspaceCache(services.shared);
}
Expand Down Expand Up @@ -169,30 +173,28 @@ export class SafeDsPartialEvaluator {
}

private evaluateInfixOperation(node: SdsInfixOperation, substitutions: ParameterSubstitutions): EvaluatedNode {
// By design none of the operators are short-circuited
// Handle operators that can short-circuit
const evaluatedLeft = this.evaluateWithSubstitutions(node.leftOperand, substitutions);
if (evaluatedLeft === UnknownEvaluatedNode) {
return UnknownEvaluatedNode;
}

switch (node.operator) {
case 'or':
return this.evaluateOr(evaluatedLeft, node.rightOperand, substitutions);
case 'and':
return this.evaluateAnd(evaluatedLeft, node.rightOperand, substitutions);
case '?:':
return this.evaluateElvisOperator(evaluatedLeft, node.rightOperand, substitutions);
}

// Handle other operators
const evaluatedRight = this.evaluateWithSubstitutions(node.rightOperand, substitutions);
if (evaluatedRight === UnknownEvaluatedNode) {
return UnknownEvaluatedNode;
}

switch (node.operator) {
case 'or':
return this.evaluateLogicalOp(
evaluatedLeft,
(leftOperand, rightOperand) => leftOperand || rightOperand,
evaluatedRight,
);
case 'and':
return this.evaluateLogicalOp(
evaluatedLeft,
(leftOperand, rightOperand) => leftOperand && rightOperand,
evaluatedRight,
);
case '==':
case '===':
return new BooleanConstant(evaluatedLeft.equals(evaluatedRight));
Expand Down Expand Up @@ -246,7 +248,7 @@ export class SafeDsPartialEvaluator {
);
case '/':
// Division by zero
if (zeroes.some((it) => it.equals(evaluatedRight))) {
if (zeroConstants.some((it) => it.equals(evaluatedRight))) {
return UnknownEvaluatedNode;
}

Expand All @@ -256,31 +258,82 @@ export class SafeDsPartialEvaluator {
(leftOperand, rightOperand) => leftOperand / rightOperand,
evaluatedRight,
);
case '?:':
if (evaluatedLeft === NullConstant) {
return evaluatedRight;
} else {
return evaluatedLeft;
}

/* c8 ignore next 2 */
default:
throw new Error(`Unexpected operator: ${node.operator}`);
}
}

private evaluateLogicalOp(
leftOperand: EvaluatedNode,
operation: (leftOperand: boolean, rightOperand: boolean) => boolean,
rightOperand: EvaluatedNode,
private evaluateOr(
evaluatedLeft: EvaluatedNode,
rightOperand: SdsExpression,
substitutions: ParameterSubstitutions,
): EvaluatedNode {
if (leftOperand instanceof BooleanConstant && rightOperand instanceof BooleanConstant) {
return new BooleanConstant(operation(leftOperand.value, rightOperand.value));
// Short-circuit if the left operand is true and the right operand has no side effects
if (
evaluatedLeft.equals(trueConstant) &&
!this.purityComputer().expressionHasSideEffects(rightOperand, substitutions)
) {
return trueConstant;
}

// Compute the result if both operands are constant booleans
const evaluatedRight = this.evaluateWithSubstitutions(rightOperand, substitutions);
if (evaluatedLeft instanceof BooleanConstant && evaluatedRight instanceof BooleanConstant) {
return new BooleanConstant(evaluatedLeft.value || evaluatedRight.value);
}

return UnknownEvaluatedNode;
}

private evaluateAnd(
evaluatedLeft: EvaluatedNode,
rightOperand: SdsExpression,
substitutions: ParameterSubstitutions,
): EvaluatedNode {
// Short-circuit if the left operand is true and the right operand has no side effects
if (
evaluatedLeft.equals(falseConstant) &&
!this.purityComputer().expressionHasSideEffects(rightOperand, substitutions)
) {
return falseConstant;
}

// Compute the result if both operands are constant booleans
const evaluatedRight = this.evaluateWithSubstitutions(rightOperand, substitutions);
if (evaluatedLeft instanceof BooleanConstant && evaluatedRight instanceof BooleanConstant) {
return new BooleanConstant(evaluatedLeft.value && evaluatedRight.value);
}

return UnknownEvaluatedNode;
}

private evaluateElvisOperator(
evaluatedLeft: EvaluatedNode,
rightOperand: SdsExpression,
substitutions: ParameterSubstitutions,
): EvaluatedNode {
// Short-circuit if the left operand is a non-null constant and the right operand has no side effects
if (
evaluatedLeft instanceof Constant &&
!evaluatedLeft.equals(NullConstant) &&
!this.purityComputer().expressionHasSideEffects(rightOperand, substitutions)
) {
return evaluatedLeft;
}

// Compute the result from both operands
const evaluatedRight = this.evaluateWithSubstitutions(rightOperand, substitutions);
if (evaluatedLeft.equals(NullConstant)) {
return evaluatedRight;
} else if (evaluatedRight === UnknownEvaluatedNode) {
return UnknownEvaluatedNode;
} else {
return evaluatedLeft;
}
}

private evaluateComparisonOp(
leftOperand: EvaluatedNode,
operation: (leftOperand: number | bigint, rightOperand: number | bigint) => boolean,
Expand Down Expand Up @@ -548,4 +601,6 @@ export class SafeDsPartialEvaluator {
}

const NO_SUBSTITUTIONS: ParameterSubstitutions = new Map();
const zeroes = [new IntConstant(0n), new FloatConstant(0.0), new FloatConstant(-0.0)];
const falseConstant = new BooleanConstant(false);
const trueConstant = new BooleanConstant(true);
const zeroConstants = [new IntConstant(0n), new FloatConstant(0.0), new FloatConstant(-0.0)];
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { AssertionError } from 'assert';
import { NodeFileSystem } from 'langium/node';
import { describe, it } from 'vitest';
import { createSafeDsServices } from '../../../src/language/index.js';
import { createSafeDsServicesWithBuiltins } from '../../../src/language/index.js';
import { locationToString } from '../../helpers/location.js';
import { getNodeByLocation } from '../../helpers/nodeFinder.js';
import { loadDocuments } from '../../helpers/testResources.js';
import { createPartialEvaluationTests } from './creator.js';

const services = createSafeDsServices(NodeFileSystem).SafeDs;
const services = (await createSafeDsServicesWithBuiltins(NodeFileSystem)).SafeDs;
const partialEvaluator = services.evaluation.PartialEvaluator;

describe('partial evaluation', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ pipeline test {
»1 and true«;

// $TEST$ serialization ?
»false and 0«;
»true and 0«;

// $TEST$ serialization ?
»unresolved and false«;
»unresolved and true«;

// $TEST$ serialization ?
»true and unresolved«;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package tests.partialValidation.recursiveCases.infixOperations.`and`

@Pure
fun pureFunction() -> result: Boolean

@Impure([ImpurityReason.FileReadFromConstantPath("test.txt")])
fun functionWithoutSideEffects() -> result: Boolean

@Impure([ImpurityReason.FileWriteToConstantPath("test.txt")])
fun functionWithSideEffects() -> result: Boolean

pipeline test {
// $TEST$ serialization false
»false and pureFunction()«;

// $TEST$ serialization false
»false and functionWithoutSideEffects()«;

// $TEST$ serialization ?
»false and functionWithSideEffects()«;

// $TEST$ serialization ?
»true and pureFunction()«;

// $TEST$ serialization ?
»true and functionWithoutSideEffects()«;

// $TEST$ serialization ?
»true and functionWithSideEffects()«;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ pipeline test {
// $TEST$ serialization false
»null ?: false«;

// $TEST$ serialization $ExpressionLambdaClosure
»(() -> 1) ?: false«;

// $TEST$ serialization ?
»unresolved ?: true«;
»unresolved ?: null«;

// $TEST$ serialization ?
»true ?: unresolved«;
»null ?: unresolved«;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package tests.partialValidation.recursiveCases.infixOperations.elvis

@Pure
fun pureFunction() -> result: Boolean

@Impure([ImpurityReason.FileReadFromConstantPath("test.txt")])
fun functionWithoutSideEffects() -> result: Boolean

@Impure([ImpurityReason.FileWriteToConstantPath("test.txt")])
fun functionWithSideEffects() -> result: Boolean

pipeline test {
// $TEST$ serialization 1
»1 ?: pureFunction()«;

// $TEST$ serialization 1
»1 ?: functionWithoutSideEffects()«;

// $TEST$ serialization ?
»1 ?: functionWithSideEffects()«;

// $TEST$ serialization ?
»null ?: pureFunction()«;

// $TEST$ serialization ?
»null ?: functionWithoutSideEffects()«;

// $TEST$ serialization ?
»null ?: functionWithSideEffects()«;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pipeline test {
»true or true«;

// $TEST$ serialization ?
»1 or true«;
»1 or false«;

// $TEST$ serialization ?
»false or 0«;
Expand All @@ -23,5 +23,5 @@ pipeline test {
»unresolved or false«;

// $TEST$ serialization ?
»true or unresolved«;
»false or unresolved«;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package tests.partialValidation.recursiveCases.infixOperations.`or`

@Pure
fun pureFunction() -> result: Boolean

@Impure([ImpurityReason.FileReadFromConstantPath("test.txt")])
fun functionWithoutSideEffects() -> result: Boolean

@Impure([ImpurityReason.FileWriteToConstantPath("test.txt")])
fun functionWithSideEffects() -> result: Boolean

pipeline test {
// $TEST$ serialization true
»true or pureFunction()«;

// $TEST$ serialization true
»true or functionWithoutSideEffects()«;

// $TEST$ serialization ?
»true or functionWithSideEffects()«;

// $TEST$ serialization ?
»false or pureFunction()«;

// $TEST$ serialization ?
»false or functionWithoutSideEffects()«;

// $TEST$ serialization ?
»false or functionWithSideEffects()«;
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
package tests.typing.operations.elvis
package tests.typing.operations.elvis.nonNullableLeftOperand

fun intOrNull() -> a: Int?
@Pure fun int() -> a: Int
@Pure fun intOrNull() -> a: Int?

pipeline elvisWithNonNullableLeftOperand {
// $TEST$ equivalence_class leftOperand
// $TEST$ equivalence_class leftOperand1
»1«;
// $TEST$ equivalence_class leftOperand
// $TEST$ equivalence_class leftOperand1
»1 ?: intOrNull()«;
// $TEST$ equivalence_class leftOperand
// $TEST$ equivalence_class leftOperand1
»1 ?: 1«;
// $TEST$ equivalence_class leftOperand
// $TEST$ equivalence_class leftOperand1
»1 ?: 1.0«;
// $TEST$ equivalence_class leftOperand
// $TEST$ equivalence_class leftOperand1
»1 ?: ""«;
// $TEST$ equivalence_class leftOperand
// $TEST$ equivalence_class leftOperand1
»1 ?: null«;

// $TEST$ equivalence_class leftOperand2
»int()«;
// $TEST$ equivalence_class leftOperand2
»int() ?: intOrNull()«;
// $TEST$ equivalence_class leftOperand2
»int() ?: 1«;
// $TEST$ equivalence_class leftOperand2
»int() ?: 1.0«;
// $TEST$ equivalence_class leftOperand2
»int() ?: ""«;
// $TEST$ equivalence_class leftOperand2
»int() ?: null«;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package tests.typing.operations.elvis
package tests.typing.operations.elvis.nullableLeftOperand

fun intOrNull() -> a: Int?
fun stringOrNull() -> s: String?
@Pure fun intOrNull() -> a: Int?
@Pure fun stringOrNull() -> s: String?

pipeline elvisWithNullableLeftOperand {
// $TEST$ serialization Int?
Expand Down

0 comments on commit 9d9f4b7

Please sign in to comment.