Skip to content

Commit

Permalink
feat: type casts (#838)
Browse files Browse the repository at this point in the history
Closes #835

### Summary of Changes

In some cases, e.g. when no runner is installed to infer the initial
schema of a dataset, we may not be able to infer the type of an
expression. This PR adds type casts to specify the type manually in this
case.
  • Loading branch information
lars-reimann authored Feb 1, 2024
1 parent b35566d commit 66c3666
Show file tree
Hide file tree
Showing 20 changed files with 161 additions and 3 deletions.
19 changes: 19 additions & 0 deletions docs/language/pipeline-language/expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,24 @@ At the moment, lambdas can only be used if the context determines the type of it
- As the value that is [assigned to a result of a segment][assignments-to-segment-results].
In other cases, declare a segment instead and use a [reference](#references) to this segment where you would write the lambda.

## Type Casts

The compiler can _infer_ the [type][types] of an expression in almost all cases. However, sometimes its [type][types]
has to be specified explicitly. This is called a _type cast_. Here is an example:

```sds
dataset.getColumn("age") as Column<Int>
```

A type cast is written as follows:

- The expression to cast.
- The keyword `#!sds as`.
- The [type][types] to cast to.

Type casts are only allowed if the type of the expression is unknown. They cannot be used to override the inferred type
of an expression.

## Precedence

We all know that `#!sds 2 + 3 * 7` is `#!sds 23` and not `#!sds 35`. The reason is that the `#!sds *` operator has a higher precedence than the `#!sds +` operator and is, therefore, evaluated first. These precedence rules are necessary for all types of expressions listed above and shown in the following list. The higher up an expression is in the list, the higher its precedence and the earlier it is evaluated. Expressions listed beside each other have the same precedence and are evaluated from left to right:
Expand All @@ -550,6 +568,7 @@ We all know that `#!sds 2 + 3 * 7` is `#!sds 23` and not `#!sds 35`. The reason
- `#!sds 1` ([integer literals](#int-literals)), `#!sds 1.0` ([float literals](#float-literals)), `#!sds "a"` ([string literals](#string-literals)), `#!sds true`/`false` ([boolean literals](#boolean-literals)), `#!sds null` ([null literal](#null-literal)), `#!sds someName` ([references](#references)), `#!sds "age: {{ age }}"` ([template strings](#template-strings))
- `#!sds ()` ([calls](#calls)), `#!sds .` ([member accesses](#member-accesses)), `#!sds ?.` ([null-safe member accesses](#null-safe-member-access)), `#!sds []` ([indexed accesses](#indexed-accesses))
- `#!sds -` (unary, [arithmetic negations](#operations-on-numbers))
- `#!sds as` ([type casts](#type-casts))
- `#!sds ?:` ([Elvis operators](#elvis-operator))
- `#!sds *`, `#!sds /` ([multiplicative operators](#operations-on-numbers))
- `#!sds +`, `#!sds -` (binary, [additive operators](#operations-on-numbers))
Expand Down
18 changes: 16 additions & 2 deletions packages/safe-ds-lang/src/language/grammar/safe-ds.langium
Original file line number Diff line number Diff line change
Expand Up @@ -657,11 +657,25 @@ SdsMultiplicativeOperator returns string:
;

SdsElvisExpression returns SdsExpression:
SdsUnaryOperation
SdsTypeCast
(
{SdsInfixOperation.leftOperand=current}
operator='?:'
rightOperand=SdsUnaryOperation
rightOperand=SdsTypeCast
)*
;

interface SdsTypeCast extends SdsExpression {
expression: SdsExpression
^type: SdsType
}

SdsTypeCast returns SdsExpression:
SdsUnaryOperation
(
{SdsTypeCast.expression=current}
'as'
^type=SdsType
)*
;

Expand Down
8 changes: 8 additions & 0 deletions packages/safe-ds-lang/src/language/lsp/safe-ds-formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ export class SafeDsFormatter extends AbstractFormatter {
this.formatSdsTemplateStringInner(node);
} else if (ast.isSdsTemplateStringEnd(node)) {
this.formatSdsTemplateStringEnd(node);
} else if (ast.isSdsTypeCast(node)) {
this.formatSdsTypeCast(node);
}

// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -814,6 +816,12 @@ export class SafeDsFormatter extends AbstractFormatter {
formatter.node(node).prepend(oneSpace());
}

private formatSdsTypeCast(node: ast.SdsTypeCast) {
const formatter = this.getNodeFormatter(node);

formatter.keyword('as').surround(oneSpace());
}

/**
* Returns whether the expression is considered complex and requires special formatting like placing the associated
* expression on its own line.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
isSdsTemplateStringEnd,
isSdsTemplateStringInner,
isSdsTemplateStringStart,
isSdsTypeCast,
type SdsArgument,
type SdsAssignee,
type SdsCall,
Expand Down Expand Up @@ -240,6 +241,8 @@ export class SafeDsPartialEvaluator {
return this.evaluateWithRecursionCheck(node.target.ref, substitutions, visited);
} else if (isSdsTemplateString(node)) {
return this.evaluateTemplateString(node, substitutions, visited);
} else if (isSdsTypeCast(node)) {
return this.evaluateWithRecursionCheck(node.expression, substitutions, visited);
} /* c8 ignore start */ else {
throw new Error(`Unexpected expression type: ${node.$type}`);
} /* c8 ignore stop */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
isSdsTemplateString,
isSdsType,
isSdsTypeArgument,
isSdsTypeCast,
isSdsTypeParameter,
isSdsTypeProjection,
isSdsUnionType,
Expand Down Expand Up @@ -287,6 +288,11 @@ export class SafeDsTypeComputer {
}

private computeTypeOfExpression(node: SdsExpression): Type {
// Type cast
if (isSdsTypeCast(node)) {
return this.computeType(node.type);
}

// Partial evaluation (definitely handles SdsBoolean, SdsFloat, SdsInt, SdsNull, and SdsString)
const evaluatedNode = this.partialEvaluator.evaluate(node);
if (evaluatedNode instanceof Constant) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ import {
parameterMustHaveTypeHint,
prefixOperationOperandMustHaveCorrectType,
resultMustHaveTypeHint,
typeCastExpressionMustHaveUnknownType,
yieldTypeMustMatchResultType,
} from './types.js';
import { statementMustDoSomething } from './other/statements/statements.js';
Expand Down Expand Up @@ -350,6 +351,7 @@ export const registerValidationChecks = function (services: SafeDsServices) {
SdsStatement: [statementMustDoSomething(services)],
SdsTemplateString: [templateStringMustHaveExpressionBetweenTwoStringParts],
SdsTypeArgumentList: [typeArgumentListsShouldBeUsedWithCaution(services)],
SdsTypeCast: [typeCastExpressionMustHaveUnknownType(services)],
SdsTypeParameter: [typeParameterMustHaveSufficientContext, typeParameterMustBeUsedInCorrectContext],
SdsTypeParameterConstraint: [typeParameterConstraintLeftOperandMustBeOwnTypeParameter],
SdsTypeParameterList: [
Expand Down
18 changes: 17 additions & 1 deletion packages/safe-ds-lang/src/language/validation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ import {
SdsParameter,
SdsPrefixOperation,
SdsResult,
SdsTypeCast,
SdsYield,
} from '../generated/ast.js';
import { getTypeArguments, getTypeParameters, TypeParameter } from '../helpers/nodeProperties.js';
import { SafeDsServices } from '../safe-ds-module.js';
import { NamedTupleType } from '../typing/model.js';
import { NamedTupleType, UnknownType } from '../typing/model.js';

export const CODE_TYPE_CALLABLE_RECEIVER = 'type/callable-receiver';
export const CODE_TYPE_MISMATCH = 'type/mismatch';
Expand Down Expand Up @@ -296,6 +297,21 @@ export const prefixOperationOperandMustHaveCorrectType = (services: SafeDsServic
};
};

export const typeCastExpressionMustHaveUnknownType = (services: SafeDsServices) => {
const typeComputer = services.types.TypeComputer;

return (node: SdsTypeCast, accept: ValidationAcceptor): void => {
const expressionType = typeComputer.computeType(node.expression);
if (node.expression && expressionType !== UnknownType) {
accept('error', 'Type casts can only be applied to expressions of unknown type.', {
// Using property: "expression" does not work here, probably due to eclipse-langium/langium#1218
node: node.expression,
code: CODE_TYPE_MISMATCH,
});
}
};
};

export const yieldTypeMustMatchResultType = (services: SafeDsServices) => {
const typeChecker = services.types.TypeChecker;
const typeComputer = services.types.TypeComputer;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
pipeline myPipeline {
1 ?: 2;
}

// -----------------------------------------------------------------------------

pipeline myPipeline {
1 ?: 2;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
pipeline myPipeline {
1 as Int;
}

// -----------------------------------------------------------------------------

pipeline myPipeline {
1 as Int;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// $TEST$ syntax_error

pipeline myPipeline {
?: Int;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// $TEST$ syntax_error

pipeline myPipeline {
1 ?:;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// $TEST$ no_syntax_error

pipeline myPipeline {
null ?: 1;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// $TEST$ no_syntax_error

pipeline myPipeline {
null ?: null ?: 1;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// $TEST$ syntax_error

pipeline myPipeline {
as Int;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// $TEST$ syntax_error

pipeline myPipeline {
1 as;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// $TEST$ no_syntax_error

pipeline myPipeline {
1 as Int;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// $TEST$ no_syntax_error

pipeline myPipeline {
1 as Int as String;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package tests.partialValidation.recursiveCases.typeCasts

pipeline test {
// $TEST$ serialization true
»true as Boolean«;

// $TEST$ serialization 1
»1 as Boolean«;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package tests.typing.expressions.typeCasts

@Pure fun f() -> r: Int

pipeline myPipeline {
// $TEST$ serialization Boolean
»1 as Boolean«; // Partial evaluator can handle expression

// $TEST$ serialization Boolean
»r as Boolean«; // Partial evaluator cannot handle expression

// $TEST$ serialization Boolean
»unresolved as Boolean«; // Expression has unknown type
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package tests.validation.types.checking.typeCasts

pipeline test {
// $TEST$ no error "Type casts can only be applied to expressions of unknown type."
»unresolved« as Int;

// $TEST$ error "Type casts can only be applied to expressions of unknown type."
»1« as Int;
}

0 comments on commit 66c3666

Please sign in to comment.