Skip to content

Commit

Permalink
feat: allow use of operators for cells (#1247)
Browse files Browse the repository at this point in the history
Closes #1165

### Summary of Changes

Operators can now also be used if the operands are cells, leading to
considerably more readable code when working with cells.

Before:

```
val bothN = tableShortN.countRowIf(
    (row) -> (row.getValue("survived").eq("Yes")).^and
             (row.getValue("age").gt(30))
)
```

After:

```
val bothN = tableShortN.countRowIf(
    (row) -> row.getValue("survived") == "Yes" and
             row.getValue("age") > 30
)
```

The old named methods will remain part of the API, so they show up in
auto-completion and the API reference and guide users towards the
corresponding operators.
  • Loading branch information
lars-reimann authored Oct 31, 2024
1 parent 0d27e33 commit 2930357
Show file tree
Hide file tree
Showing 20 changed files with 742 additions and 218 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SafeDsModuleMembers } from './safe-ds-module-members.js';
import { resourceNameToUri } from '../../helpers/resources.js';
import { URI } from 'langium';

const CELL_URI = resourceNameToUri('builtins/safeds/data/tabular/containers/Cell.sdsstub');
const CORE_CLASSES_URI = resourceNameToUri('builtins/safeds/lang/coreClasses.sdsstub');
const IMAGE_URI = resourceNameToUri('builtins/safeds/data/image/containers/Image.sdsstub');
const TABLE_URI = resourceNameToUri('builtins/safeds/data/tabular/containers/Table.sdsstub');
Expand All @@ -16,6 +17,10 @@ export class SafeDsClasses extends SafeDsModuleMembers<SdsClass> {
return this.getClass('Boolean');
}

get Cell(): SdsClass | undefined {
return this.getClass('Cell', CELL_URI);
}

get Float(): SdsClass | undefined {
return this.getClass('Float');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ import {
} from './utilityFunctions.js';
import { CODEGEN_PREFIX } from './constants.js';
import { SafeDsSlicer } from '../../flow/safe-ds-slicer.js';
import { SafeDsTypeChecker } from '../../typing/safe-ds-type-checker.js';
import { SafeDsCoreTypes } from '../../typing/safe-ds-core-types.js';

const LAMBDA_PREFIX = `${CODEGEN_PREFIX}lambda_`;
const BLOCK_LAMBDA_RESULT_PREFIX = `${CODEGEN_PREFIX}block_lambda_result_`;
Expand All @@ -130,18 +132,22 @@ const SPACING = new CompositeGeneratorNode(NL, NL);

export class SafeDsPythonGenerator {
private readonly builtinAnnotations: SafeDsAnnotations;
private readonly coreTypes: SafeDsCoreTypes;
private readonly nodeMapper: SafeDsNodeMapper;
private readonly partialEvaluator: SafeDsPartialEvaluator;
private readonly purityComputer: SafeDsPurityComputer;
private readonly slicer: SafeDsSlicer;
private readonly typeChecker: SafeDsTypeChecker;
private readonly typeComputer: SafeDsTypeComputer;

constructor(services: SafeDsServices) {
this.builtinAnnotations = services.builtins.Annotations;
this.coreTypes = services.typing.CoreTypes;
this.nodeMapper = services.helpers.NodeMapper;
this.partialEvaluator = services.evaluation.PartialEvaluator;
this.purityComputer = services.purity.PurityComputer;
this.slicer = services.flow.Slicer;
this.typeChecker = services.typing.TypeChecker;
this.typeComputer = services.typing.TypeComputer;
}

Expand Down Expand Up @@ -722,19 +728,43 @@ export class SafeDsPythonGenerator {
} else if (isSdsInfixOperation(expression)) {
const leftOperand = this.generateExpression(expression.leftOperand, frame);
const rightOperand = this.generateExpression(expression.rightOperand, frame);

const leftOperandType = this.typeComputer.computeType(expression.leftOperand);
const rightOperandType = this.typeComputer.computeType(expression.rightOperand);

switch (expression.operator) {
case 'or':
frame.addUtility(eagerOr);
return expandTracedToNode(expression)`${traceToNode(
expression,
'operator',
)(eagerOr.name)}(${leftOperand}, ${rightOperand})`;
if (
this.typeChecker.isSubtypeOf(leftOperandType, this.coreTypes.Boolean) &&
this.typeChecker.isSubtypeOf(rightOperandType, this.coreTypes.Boolean)
) {
frame.addUtility(eagerOr);
return expandTracedToNode(expression)`${traceToNode(
expression,
'operator',
)(eagerOr.name)}(${leftOperand}, ${rightOperand})`;
} else {
return expandTracedToNode(expression)`(${leftOperand}) ${traceToNode(
expression,
'operator',
)('|')} (${rightOperand})`;
}
case 'and':
frame.addUtility(eagerAnd);
return expandTracedToNode(expression)`${traceToNode(
expression,
'operator',
)(eagerAnd.name)}(${leftOperand}, ${rightOperand})`;
if (
this.typeChecker.isSubtypeOf(leftOperandType, this.coreTypes.Boolean) &&
this.typeChecker.isSubtypeOf(rightOperandType, this.coreTypes.Boolean)
) {
frame.addUtility(eagerAnd);
return expandTracedToNode(expression)`${traceToNode(
expression,
'operator',
)(eagerAnd.name)}(${leftOperand}, ${rightOperand})`;
} else {
return expandTracedToNode(expression)`(${leftOperand}) ${traceToNode(
expression,
'operator',
)('&')} (${rightOperand})`;
}
case '?:':
frame.addUtility(eagerElvis);
return expandTracedToNode(expression)`${traceToNode(
Expand Down Expand Up @@ -807,7 +837,14 @@ export class SafeDsPythonGenerator {
const operand = this.generateExpression(expression.operand, frame);
switch (expression.operator) {
case 'not':
return expandTracedToNode(expression)`${traceToNode(expression, 'operator')('not')} (${operand})`;
const operandType = this.typeComputer.computeType(expression.operand);
if (this.typeChecker.isSubtypeOf(operandType, this.coreTypes.Boolean)) {
return expandTracedToNode(
expression,
)`${traceToNode(expression, 'operator')('not')} (${operand})`;
} else {
return expandTracedToNode(expression)`${traceToNode(expression, 'operator')('~')}(${operand})`;
}
case '-':
return expandTracedToNode(expression)`${traceToNode(expression, 'operator')('-')}(${operand})`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,22 @@ import {
substitutionsAreEqual,
UnknownEvaluatedNode,
} from './model.js';
import type { SafeDsTypeComputer } from '../typing/safe-ds-type-computer.js';
import { SafeDsCoreTypes } from '../typing/safe-ds-core-types.js';

export class SafeDsPartialEvaluator {
private readonly astNodeLocator: AstNodeLocator;
private readonly coreTypes: SafeDsCoreTypes;
private readonly nodeMapper: SafeDsNodeMapper;
private readonly typeComputer: () => SafeDsTypeComputer;

private readonly cache: WorkspaceCache<string, EvaluatedNode>;

constructor(services: SafeDsServices) {
this.astNodeLocator = services.workspace.AstNodeLocator;
this.coreTypes = services.typing.CoreTypes;
this.nodeMapper = services.helpers.NodeMapper;
this.typeComputer = () => services.typing.TypeComputer;

this.cache = new WorkspaceCache(services.shared);
}
Expand Down Expand Up @@ -357,7 +363,10 @@ export class SafeDsPartialEvaluator {
): EvaluatedNode {
// Short-circuit
if (evaluatedLeft.equals(trueConstant)) {
return trueConstant;
const rightType = this.typeComputer().computeType(rightOperand);
if (rightType.equals(this.coreTypes.Boolean)) {
return trueConstant;
}
}

// Compute the result if both operands are constant booleans
Expand All @@ -377,7 +386,10 @@ export class SafeDsPartialEvaluator {
): EvaluatedNode {
// Short-circuit
if (evaluatedLeft.equals(falseConstant)) {
return falseConstant;
const rightType = this.typeComputer().computeType(rightOperand);
if (rightType.equals(this.coreTypes.Boolean)) {
return falseConstant;
}
}

// Compute the result if both operands are constant booleans
Expand Down
13 changes: 13 additions & 0 deletions packages/safe-ds-lang/src/language/typing/safe-ds-core-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ export class SafeDsCoreTypes {
return this.createCoreType(this.builtinClasses.Boolean);
}

Cell(wrappedType: Type = this.AnyOrNull): Type {
const cell = this.builtinClasses.Cell;
const wrappedTypeParameter = getTypeParameters(cell)[0];

if (!cell || !wrappedTypeParameter) {
/* c8 ignore next 2 */
return UnknownType;
}

let substitutions = new Map([[wrappedTypeParameter, wrappedType]]);
return new ClassType(cell, substitutions, false);
}

get Float(): Type {
return this.createCoreType(this.builtinClasses.Float);
}
Expand Down
54 changes: 44 additions & 10 deletions packages/safe-ds-lang/src/language/typing/safe-ds-type-computer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,24 +371,23 @@ export class SafeDsTypeComputer {
return this.computeTypeOfIndexedAccess(node);
} else if (isSdsInfixOperation(node)) {
switch (node.operator) {
// Boolean operators
case 'or':
case 'and':
return this.coreTypes.Boolean;

// Equality operators
case '==':
case '!=':
case '===':
case '!==':
return this.coreTypes.Boolean;
case '==':
case '!=':

// Logical operators
case 'or':
case 'and':

// Comparison operators
case '<':
case '<=':
case '>=':
case '>':
return this.coreTypes.Boolean;
return this.computeTypeOfBooleanOperation(node);

// Arithmetic operators
case '+':
Expand Down Expand Up @@ -429,7 +428,7 @@ export class SafeDsTypeComputer {
} else if (isSdsPrefixOperation(node)) {
switch (node.operator) {
case 'not':
return this.coreTypes.Boolean;
return this.computeTypeOfBooleanPrefixOperation(node);
case '-':
return this.computeTypeOfArithmeticPrefixOperation(node);

Expand Down Expand Up @@ -509,11 +508,32 @@ export class SafeDsTypeComputer {
return UnknownType;
}

private computeTypeOfBooleanOperation(node: SdsInfixOperation): Type {
const leftOperandType = this.computeType(node.leftOperand);
const rightOperandType = this.computeType(node.rightOperand);
const cellType = this.coreTypes.Cell();

if (
this.typeChecker.isSubtypeOf(leftOperandType, cellType) ||
this.typeChecker.isSubtypeOf(rightOperandType, cellType)
) {
return this.coreTypes.Cell(this.coreTypes.Boolean);
} else {
return this.coreTypes.Boolean;
}
}

private computeTypeOfArithmeticInfixOperation(node: SdsInfixOperation): Type {
const leftOperandType = this.computeType(node.leftOperand);
const rightOperandType = this.computeType(node.rightOperand);
const cellType = this.coreTypes.Cell();

if (
this.typeChecker.isSubtypeOf(leftOperandType, cellType) ||
this.typeChecker.isSubtypeOf(rightOperandType, cellType)
) {
return this.coreTypes.Cell(this.coreTypes.Number);
} else if (
this.typeChecker.isSubtypeOf(leftOperandType, this.coreTypes.Int) &&
this.typeChecker.isSubtypeOf(rightOperandType, this.coreTypes.Int)
) {
Expand Down Expand Up @@ -596,10 +616,24 @@ export class SafeDsTypeComputer {
);
}

private computeTypeOfBooleanPrefixOperation(node: SdsPrefixOperation): Type {
const operandType = this.computeType(node.operand);
const cellType = this.coreTypes.Cell();

if (this.typeChecker.isSubtypeOf(operandType, cellType)) {
return this.coreTypes.Cell(this.coreTypes.Boolean);
} else {
return this.coreTypes.Boolean;
}
}

private computeTypeOfArithmeticPrefixOperation(node: SdsPrefixOperation): Type {
const operandType = this.computeType(node.operand);
const cellType = this.coreTypes.Cell();

if (this.typeChecker.isSubtypeOf(operandType, this.coreTypes.Int)) {
if (this.typeChecker.isSubtypeOf(operandType, cellType)) {
return this.coreTypes.Cell(this.coreTypes.Number);
} else if (this.typeChecker.isSubtypeOf(operandType, this.coreTypes.Int)) {
return this.coreTypes.Int;
} else {
return this.coreTypes.Float;
Expand Down
64 changes: 37 additions & 27 deletions packages/safe-ds-lang/src/language/validation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,17 +161,27 @@ export const infixOperationOperandsMustHaveCorrectType = (services: SafeDsServic
return (node: SdsInfixOperation, accept: ValidationAcceptor): void => {
const leftType = typeComputer.computeType(node.leftOperand);
const rightType = typeComputer.computeType(node.rightOperand);
const cellType = coreTypes.Cell();

switch (node.operator) {
case 'or':
case 'and':
if (node.leftOperand && !typeChecker.isSubtypeOf(leftType, coreTypes.Boolean)) {
accept('error', `Expected type '${coreTypes.Boolean}' but got '${leftType}'.`, {
if (
node.leftOperand &&
!typeChecker.isSubtypeOf(leftType, coreTypes.Boolean) &&
!typeChecker.isSubtypeOf(leftType, cellType)
) {
accept('error', `This operator is not defined for type '${leftType}'.`, {
node: node.leftOperand,
code: CODE_TYPE_MISMATCH,
});
}
if (node.rightOperand && !typeChecker.isSubtypeOf(rightType, coreTypes.Boolean)) {
accept('error', `Expected type '${coreTypes.Boolean}' but got '${rightType}'.`, {
if (
node.rightOperand &&
!typeChecker.isSubtypeOf(rightType, coreTypes.Boolean) &&
!typeChecker.isSubtypeOf(rightType, cellType)
) {
accept('error', `This operator is not defined for type '${rightType}'.`, {
node: node.rightOperand,
code: CODE_TYPE_MISMATCH,
});
Expand Down Expand Up @@ -202,26 +212,24 @@ export const infixOperationOperandsMustHaveCorrectType = (services: SafeDsServic
if (
node.leftOperand &&
!typeChecker.isSubtypeOf(leftType, coreTypes.Float) &&
!typeChecker.isSubtypeOf(leftType, coreTypes.Int)
!typeChecker.isSubtypeOf(leftType, coreTypes.Int) &&
!typeChecker.isSubtypeOf(leftType, cellType)
) {
accept('error', `Expected type '${coreTypes.Float}' or '${coreTypes.Int}' but got '${leftType}'.`, {
accept('error', `This operator is not defined for type '${leftType}'.`, {
node: node.leftOperand,
code: CODE_TYPE_MISMATCH,
});
}
if (
node.rightOperand &&
!typeChecker.isSubtypeOf(rightType, coreTypes.Float) &&
!typeChecker.isSubtypeOf(rightType, coreTypes.Int)
!typeChecker.isSubtypeOf(rightType, coreTypes.Int) &&
!typeChecker.isSubtypeOf(rightType, cellType)
) {
accept(
'error',
`Expected type '${coreTypes.Float}' or '${coreTypes.Int}' but got '${rightType}'.`,
{
node: node.rightOperand,
code: CODE_TYPE_MISMATCH,
},
);
accept('error', `This operator is not defined for type '${rightType}'.`, {
node: node.rightOperand,
code: CODE_TYPE_MISMATCH,
});
}
return;
}
Expand Down Expand Up @@ -338,10 +346,15 @@ export const prefixOperationOperandMustHaveCorrectType = (services: SafeDsServic

return (node: SdsPrefixOperation, accept: ValidationAcceptor): void => {
const operandType = typeComputer.computeType(node.operand);
const cellType = coreTypes.Cell(coreTypes.AnyOrNull);

switch (node.operator) {
case 'not':
if (!typeChecker.isSubtypeOf(operandType, coreTypes.Boolean)) {
accept('error', `Expected type '${coreTypes.Boolean}' but got '${operandType}'.`, {
if (
!typeChecker.isSubtypeOf(operandType, coreTypes.Boolean) &&
!typeChecker.isSubtypeOf(operandType, cellType)
) {
accept('error', `This operator is not defined for type '${operandType}'.`, {
node,
property: 'operand',
code: CODE_TYPE_MISMATCH,
Expand All @@ -351,17 +364,14 @@ export const prefixOperationOperandMustHaveCorrectType = (services: SafeDsServic
case '-':
if (
!typeChecker.isSubtypeOf(operandType, coreTypes.Float) &&
!typeChecker.isSubtypeOf(operandType, coreTypes.Int)
!typeChecker.isSubtypeOf(operandType, coreTypes.Int) &&
!typeChecker.isSubtypeOf(operandType, cellType)
) {
accept(
'error',
`Expected type '${coreTypes.Float}' or '${coreTypes.Int}' but got '${operandType}'.`,
{
node,
property: 'operand',
code: CODE_TYPE_MISMATCH,
},
);
accept('error', `This operator is not defined for type '${operandType}'.`, {
node,
property: 'operand',
code: CODE_TYPE_MISMATCH,
});
}
return;
}
Expand Down
Loading

0 comments on commit 2930357

Please sign in to comment.