Skip to content

Commit

Permalink
feat: adjust type cast syntax and restore old precedence (#1158)
Browse files Browse the repository at this point in the history
Closes #1150

### Summary of Changes

* Type casts must now have parentheses around their type. This resolves
the ambiguity between the less than operator and the start of type
argument lists.
* Restore their old, more intuitive precedence.
  • Loading branch information
lars-reimann authored May 5, 2024
1 parent 7549fa1 commit 07623fc
Show file tree
Hide file tree
Showing 18 changed files with 60 additions and 51 deletions.
3 changes: 2 additions & 1 deletion docs/pipeline-language/expressions/precedence.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,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 ?()` ([null-safe calls][null-safe-calls]), `#!sds .` ([member accesses][member-accesses]), `#!sds ?.` ([null-safe member accesses][null-safe-member-accesses]), `#!sds []` ([indexed accesses][indexed-accesses]), `#!sds ?[]` ([null-safe indexed accesses][null-safe-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 All @@ -15,7 +16,7 @@ We all know that `#!sds 2 + 3 * 7` is `#!sds 23` and not `#!sds 35`. The reason
- `#!sds not` ([logical negations][logical-operations])
- `#!sds and` ([conjunctions][logical-operations])
- `#!sds or` ([disjunctions][logical-operations])
- `#!sds () -> 1` ([expression lambdas][expression-lambdas]), `#!sds () {}` ([block lambdas][block-lambdas]), `#!sds as` ([type casts][type-casts])
- `#!sds () -> 1` ([expression lambdas][expression-lambdas]), `#!sds () {}` ([block lambdas][block-lambdas])
- **LOWER PRECEDENCE**

If the default precedence of operators is not sufficient, parentheses can be used to force a part of an expression to be evaluated first.
Expand Down
19 changes: 8 additions & 11 deletions docs/pipeline-language/expressions/type-casts.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,25 @@ The compiler can _infer_ the [type][types] of an expression in almost all cases.
specified explicitly. This is called a _type cast_. Here is an example:

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

A type cast is written as follows:

- The expression to cast.
- The keyword `#!sds as`.
- The type to cast to.
- The type to cast to **enclosed in parentheses**.

Afterward, the compiler will treat the expression as if it had the specified type. If the expression's actual type is
not compatible with the specified type, the compiler will raise an error.

!!! warning "Precedence"
Type casts have the lowest precedence of all operators. If you want to use a type cast in an expression, you must
enclose it in parentheses:
??? info "Rationale for parentheses around type"

```sds
(row.getValue("age") as Int) < 18
```
The parentheses around the type are necessary to avoid ambiguity with other language constructs. In particular, the
less than operator (`<`) looks the same as the opening angle bracket of a type argument list (`#!sds Column<Int>`).

This is necessary, because the less than operator (`<`) looks the same as the opening angle bracket of a type
argument list (`Column<Int>`). We could remove this ambiguity by using different syntax for the less than operator
or for type argument lists, but both are established conventions in other languages.
We could remove this ambiguity by using different syntax for the less than operator or for type argument lists, but
both are established conventions in other languages. Instead, we require parentheses around types in an expression
context to clearly indicate where the type ends.

[types]: ../types.md
33 changes: 16 additions & 17 deletions packages/safe-ds-lang/src/language/grammar/safe-ds.langium
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@ SdsExpressionStatement returns SdsExpressionStatement:
interface SdsExpression extends SdsObject {}

SdsExpression returns SdsExpression:
SdsLambda | SdsTypeCast
SdsLambda | SdsOrExpression
;

interface SdsLambda extends SdsCallable, SdsExpression {}
Expand Down Expand Up @@ -581,20 +581,6 @@ SdsBlockLambdaAssignee returns SdsAssignee:
| {SdsBlockLambdaResult} 'yield' name=ID
;

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

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

interface SdsInfixOperation extends SdsExpression {
leftOperand: SdsExpression
operator: string
Expand Down Expand Up @@ -682,11 +668,24 @@ 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
2 changes: 2 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 @@ -808,6 +808,8 @@ export class SafeDsFormatter extends AbstractFormatter {
const formatter = this.getNodeFormatter(node);

formatter.keyword('as').surround(oneSpace());
formatter.keyword('(').append(noSpace());
formatter.keyword(')').prepend(noSpace());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ class Row(
* pipeline example {
* val row = Row({"b": 2, "a": 1});
* val sortedRow = row.sortColumns((name1, value1, name2, value2) ->
* (value1 as Int) - (value2 as Int)
* value1 as (Int) - value2 as (Int)
* );
* }
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ class Table(
* pipeline example {
* val table = Table({"a": [1, 2], "b": [3, 4]});
* val filteredTable = table.filterRows((row) ->
* (row.getValue("a") as Int) > 1
* row.getValue("a") as (Int) > 1
* );
* // Table({"a": [2], "b": [4]})
* }
Expand All @@ -432,7 +432,7 @@ class Table(
* pipeline example {
* val table = Table({"a": [1, 2, 3], "b": [4, 5, 6]});
* val tablesByKey = table.groupRows((row) ->
* (row.getValue("a") as Int) <= 2
* row.getValue("a") as (Int) <= 2
* );
* // {
* // true: Table({"a": [1, 2], "b": [4, 5]}),
Expand Down Expand Up @@ -830,7 +830,7 @@ class Table(
* "price": [ 100, 2, 4],
* });
* val discountedPrices = prices.transformColumn("price", (row) ->
* (row.getValue("price") as Int) * 0.5
* row.getValue("price") as (Int) * 0.5
* );
* // Table({
* // "product": ["apple", "banana", "cherry"],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
pipeline myPipeline {
1 as Int;
1 as ( Int );
}

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

pipeline myPipeline {
1 as Int;
1 as (Int);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package tests.generation.expressions.typeCasts

segment mySegment(p: Int) {
val a = p as Int;
val a = p as (Int);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// $TEST$ syntax_error

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

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

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

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

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

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

pipeline myPipeline {
(1 as Int) < 2;
1 as (Int) < 2;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package tests.partialValidation.recursiveCases.typeCasts

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

// $TEST$ serialization 1
»1 as Boolean«;
»1 as (Boolean)«;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ package tests.typing.expressions.typeCasts

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

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

// $TEST$ serialization Boolean
»unresolved as Boolean«; // Expression has unknown type
»unresolved as (Boolean)«; // Expression has unknown type
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ class E() sub C

pipeline test {
// $TEST$ error "This type cast can never succeed."
»D() as E«;
»D() as (E)«;

// $TEST$ no error "This type cast can never succeed."
»C() as C«;
»C() as (C)«;

// $TEST$ no error "This type cast can never succeed."
»C() as D«;
»C() as (D)«;

// $TEST$ no error "This type cast can never succeed."
»D() as C«;
»D() as (C)«;

// $TEST$ no error "This type cast can never succeed."
»unresolved as Int«;
»unresolved as (Int)«;
}

0 comments on commit 07623fc

Please sign in to comment.