Skip to content

Commit

Permalink
feat: list & map literals (#619)
Browse files Browse the repository at this point in the history
Closes #615
Closes #616

### Summary of Changes

Add list and map literals. The computation of the element & key/value
types will happen in a later pull request, together with type
computation of other type parameters
(#23).

This PR also removes `vararg` parameters in favor of a parameter with
list types. This simplifies the graphical view and gets rid of a bunch
of validation rules.

---------

Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com>
  • Loading branch information
lars-reimann and megalinter-bot authored Oct 9, 2023
1 parent ea4a9ba commit e3b2870
Show file tree
Hide file tree
Showing 280 changed files with 783 additions and 2,642 deletions.
47 changes: 6 additions & 41 deletions docs/language/common/parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

_Parameters_ define the expected inputs of some declaration that can be [called][calls]. We refer to such declarations as _callables_. We distinguish between

- [required parameters](#required-parameters), which must always be passed,
- [optional parameters](#optional-parameters), which use a default value if no value is passed explicitly, and
- [variadic parameters](#variadic-parameters), which can accept zero or more values.
- [required parameters](#required-parameters), which must always be passed, and
- [optional parameters](#optional-parameters), which use a default value if no value is passed explicitly.

## Required Parameters

Expand Down Expand Up @@ -36,21 +35,6 @@ These are the syntactic elements:
- An equals sign.
- The default value of the parameter (here `1`). This must be a constant expression, i.e. something that can be evaluated by the compiler. Particularly [calls][calls] usually do not fulfill this requirement.

## Variadic Parameters

_Variadic parameters_ can consume arbitrarily many [arguments][calls]. Here is an example:

```txt
vararg variadicParameter: Int
```

Let us break down the syntax:

- The keyword `vararg`
- The name of the parameter (here `variadicParameter`). This can be any combination of upper- and lowercase letters, underscores, and numbers, as long as it does not start with a number. However, we suggest to use `lowerCamelCase` for the names of parameters.
- A colon.
- The [type][types] of the parameter (here `Int`).

## Complete Example

Let us now look at a full example of a [segment][segments] called `doSomething` with one [required parameter](#required-parameters) and one [optional parameter](#optional-parameters):
Expand All @@ -72,8 +56,6 @@ The interesting part is the list of parameters, which uses the following syntact
Several restrictions apply to the order of parameters and to combinations of the various categories of parameters:

- After an [optional parameter](#optional-parameters) all parameters must be optional.
- A single [variadic parameter](#variadic-parameters) can be added at the end of the parameter list.
- Implied by this: A callable cannot have both [optional parameters](#optional-parameters) and [variadic parameters](#variadic-parameters).

## Corresponding Python Code

Expand All @@ -83,7 +65,7 @@ Parameters must be ordered the same way in Python as they are in Safe-DS. Moreov

- Name
- Type
- Kind (required vs. optional vs. variadic)
- Optionality (required vs. optional)
- Default value for optional parameters

Let's look at these elements in turn.
Expand Down Expand Up @@ -114,19 +96,17 @@ In this case, the Safe-DS parameters `xPred` and `xTest` refer to the Python par

The Safe-DS type of a parameter should capture the legal values of this parameter accurately. Ideally, the Python parameter should also have a matching [type hint][types-python].

### Matching Kind
### Matching Optionality

Parameters kinds must match on the Safe-DS and Python sides as well. Concretely, this means:

- All required parameters in Safe-DS must be required in Python.
- All optional parameters in Safe-DS must be optional in Python.
- All variadic parameters in Safe-DS must be variadic in Python (`*args`).

Moreover, it must be possible to pass

- required parameters by position,
- optional parameters by name,
- variadic parameters by position.
- required parameters by position, and
- optional parameters by name.

These rules allow us to restrict required parameters to [positional-only][python-positional-only] or optional parameters to [keyword-only][python-keyword-only]. We can also keep both unrestricted.

Expand Down Expand Up @@ -162,21 +142,6 @@ def optional(a: int = 1):
fun optional(a: Int = 1)
```

#### Variadic Parameter

```py
# Python code

def variadic(*a: int):
pass
```

```txt
// Safe-DS code
fun variadic(vararg a: Int)
```

### Matching Default Value

Most commonly, default values in Python are literals, since default values are only evaluated once in Python rather than every time the function is called. The following table shows how Safe-DS literals and Python literals correspond:
Expand Down
2 changes: 1 addition & 1 deletion docs/language/common/variance.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Variance deals with the question, which generic types are compatible with each other. We explain this concept using the following [class][classes]:

```txt
class Stack<T>(vararg initialElements: T) {
class Stack<T>(initialElements: List<T>) {
fun push(element: T)
fun pop() -> element: T
Expand Down
16 changes: 7 additions & 9 deletions docs/language/pipeline-language/expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,6 @@ There are some restriction regarding the choice of positional vs. named argument
- For all [parameters][parameters] of the callee there must be at most one argument.
- For all [required parameters][required-parameters] there must be exactly one argument.
- After a named argument all arguments must be named.
- [Variadic parameters][variadic-parameters] can only be assigned by position.

### Legal Callees

Expand Down Expand Up @@ -402,17 +401,17 @@ createValueWrapper()

## Indexed Accesses

An indexed access is currently only used to access one value assigned to a [variadic parameter][variadic-parameters]. In the following example, we use an index access to retrieve the first value that is assigned to the [variadic parameter][variadic-parameters] `values` of the segment `printFirst`:
An indexed access is currently only used to access elements of a list or values of a map by their key. In the following example, we use an index access to retrieve the first element of the `values` list:

```txt
segment printFirst(vararg values: Int) {
segment printFirst(values: List<Int>) {
print(values[0]);
}
```

These are the elements of the syntax:

- The name of the variadic parameter (here `values`).
- An expression that evaluates to a list or map (here the [reference](#references) `values`).
- An opening square bracket.
- The index, which is an expression that evaluates to an integer. The first element has index 0.
- A closing square bracket.
Expand All @@ -434,16 +433,16 @@ This is a [class][classes] `LinearRegression`, which has a constructor and an in
We can then use those declarations in a [segment][segments]:

```txt
segment mySegment(vararg regressions: LinearRegression) {
segment mySegment(regressions: List<LinearRegression>) {
regressions[0].drawAsGraph();
}
```

This segment is called `mySegment` and has a [variadic parameter][variadic-parameters] `regressions` of type `LinearRegression`. This means we can pass an arbitrary number of instances of `LinearRegression` to the segment when we [call](#calls) it.
This segment is called `mySegment` and has a [parameter][parameters] `regressions` of type `List<LinearRegression>`.

In the body of the segment we then

1. access the first instance that was pass using an [indexed access](#indexed-accesses),
1. access the first instance in the list using an [indexed access](#indexed-accesses),
2. access the instance method `drawAsGraph` of this instance using a [member access](#member-accesses),
3. [call](#calls) this method.

Expand All @@ -456,7 +455,7 @@ class IntList {
fun filter(filterFunction: (element: Int) -> shouldKeep: Boolean) -> filteredList: IntList
}
fun intListOf(vararg elements: Int) -> result: IntList
fun intListOf(elements: List<Int>) -> result: IntList
```

First, we declare a [class][classes] `IntList`, which has a single [method][methods] called `filter`. The `filter` method returns a single result called `filteredList`, which is a new `IntList`. `filteredList` is supposed to only contain the elements of the receiving `IntList` for which the `filterFunction` [parameter][parameters] returns `true`.
Expand Down Expand Up @@ -566,7 +565,6 @@ If the default precedence of operators is not sufficient, parentheses can be use
[packages]: ../common/packages.md
[parameters]: ../common/parameters.md
[required-parameters]: ../common/parameters.md#required-parameters
[variadic-parameters]: ../common/parameters.md#variadic-parameters
[results]: ../common/results.md
[types]: ../common/types.md
[callable-types]: ../common/types.md#callable-types
Expand Down
2 changes: 1 addition & 1 deletion docs/stdlib/safeds_lang.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ The annotation can target these declaration types. If the @Target annotation is

**Parameters:**

* `vararg targets: AnnotationTarget` - The valid targets.
* `targets: List<AnnotationTarget>` - The valid targets.

**Valid targets:**

Expand Down
11 changes: 8 additions & 3 deletions src/language/builtins/safe-ds-classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,10 @@ import { SafeDsModuleMembers } from './safe-ds-module-members.js';
const CORE_CLASSES_URI = resolveRelativePathToBuiltinFile('safeds/lang/coreClasses.sdsstub');

export class SafeDsClasses extends SafeDsModuleMembers<SdsClass> {
/* c8 ignore start */
get Any(): SdsClass | undefined {
return this.getClass('Any');
}

/* c8 ignore stop */

get Boolean(): SdsClass | undefined {
return this.getClass('Boolean');
}
Expand All @@ -24,6 +21,14 @@ export class SafeDsClasses extends SafeDsModuleMembers<SdsClass> {
return this.getClass('Int');
}

get List(): SdsClass | undefined {
return this.getClass('List');
}

get Map(): SdsClass | undefined {
return this.getClass('Map');
}

get Nothing(): SdsClass | undefined {
return this.getClass('Nothing');
}
Expand Down
59 changes: 57 additions & 2 deletions src/language/formatting/safe-ds-formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ export class SafeDsFormatter extends AbstractFormatter {
this.formatSdsIndexedAccess(node);
} else if (ast.isSdsMemberAccess(node)) {
this.formatSdsMemberAccess(node);
} else if (ast.isSdsList(node)) {
this.formatSdsList(node);
} else if (ast.isSdsMap(node)) {
this.formatSdsMap(node);
} else if (ast.isSdsMapEntry(node)) {
this.formatSdsMapEntry(node);
} else if (ast.isSdsParenthesizedExpression(node)) {
this.formatSdsParenthesizedExpression(node);
} else if (ast.isSdsTemplateStringStart(node)) {
Expand Down Expand Up @@ -727,6 +733,55 @@ export class SafeDsFormatter extends AbstractFormatter {
formatter.keyword('.').surround(noSpace());
}

private formatSdsList(node: ast.SdsList) {
const formatter = this.getNodeFormatter(node);

const openingSquareBracket = formatter.keyword('[');
const closingSquareBracket = formatter.keyword(']');

const elements = node.elements;

if (elements.some((it) => this.isComplexExpression(it))) {
formatter.nodes(...elements).prepend(indent());
formatter.keywords(',').prepend(noSpace());
closingSquareBracket.prepend(newLine());
} else {
openingSquareBracket.append(noSpace());
formatter.nodes(...elements.slice(1)).prepend(oneSpace());
formatter.keywords(',').prepend(noSpace());
closingSquareBracket.prepend(noSpace());
}
}

private formatSdsMap(node: ast.SdsMap) {
const formatter = this.getNodeFormatter(node);

const openingCurlyBrace = formatter.keyword('{');
const closingCurlyBrace = formatter.keyword('}');

const entries = node.entries;

if (
entries.length >= 2 ||
entries.some((it) => this.isComplexExpression(it.key) || this.isComplexExpression(it.value))
) {
formatter.nodes(...entries).prepend(indent());
formatter.keywords(',').prepend(noSpace());
closingCurlyBrace.prepend(newLine());
} else {
openingCurlyBrace.append(noSpace());
formatter.nodes(...entries.slice(1)).prepend(oneSpace());
formatter.keywords(',').prepend(noSpace());
closingCurlyBrace.prepend(noSpace());
}
}

private formatSdsMapEntry(node: ast.SdsMapEntry) {
const formatter = this.getNodeFormatter(node);

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

private formatSdsParenthesizedExpression(node: ast.SdsParenthesizedExpression): void {
const formatter = this.getNodeFormatter(node);

Expand Down Expand Up @@ -754,12 +809,12 @@ export class SafeDsFormatter extends AbstractFormatter {

/**
* Returns whether the expression is considered complex and requires special formatting like placing the associated
* argument on its own line.
* expression on its own line.
*
* @param node The expression to check.
*/
private isComplexExpression(node: ast.SdsExpression | undefined): boolean {
return ast.isSdsChainedExpression(node);
return ast.isSdsChainedExpression(node) || ast.isSdsList(node) || ast.isSdsMap(node);
}

// -----------------------------------------------------------------------------
Expand Down
Loading

0 comments on commit e3b2870

Please sign in to comment.