diff --git a/docs/api/safeds/data/tabular/containers/Column.md b/docs/api/safeds/data/tabular/containers/Column.md
index 9d336f57b..46ba5c304 100644
--- a/docs/api/safeds/data/tabular/containers/Column.md
+++ b/docs/api/safeds/data/tabular/containers/Column.md
@@ -57,7 +57,7 @@ pipeline example {
*/
attr type: DataType
- /*
+ /**
* Return the distinct values in the column.
*
* @param ignoreMissingValues Whether to ignore missing values.
@@ -915,17 +915,29 @@ pipeline example {
## `getDistinctValues` {#safeds.data.tabular.containers.Column.getDistinctValues data-toc-label='[function] getDistinctValues'}
+Return the distinct values in the column.
+
**Parameters:**
| Name | Type | Description | Default |
|------|------|-------------|---------|
-| `ignoreMissingValues` | [`Boolean`][safeds.lang.Boolean] | - | `#!sds true` |
+| `ignoreMissingValues` | [`Boolean`][safeds.lang.Boolean] | Whether to ignore missing values. | `#!sds true` |
**Results:**
| Name | Type | Description |
|------|------|-------------|
-| `distinctValues` | [`List`][safeds.lang.List] | - |
+| `distinctValues` | [`List`][safeds.lang.List] | The distinct values in the column. |
+
+**Examples:**
+
+```sds hl_lines="3"
+pipeline example {
+ val column = Column("test", [1, 2, 3, 2]);
+ val result = column.getDistinctValues();
+ // [1, 2, 3]
+}
+```
??? quote "Stub code in `Column.sdsstub`"
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 054211c55..de789bf1d 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -25,6 +25,7 @@ nav:
- pipeline-language/statements/README.md
- Expression Statements: pipeline-language/statements/expression-statements.md
- Assignments: pipeline-language/statements/assignments.md
+ - Output Statements: pipeline-language/statements/output-statements.md
- Expressions:
- pipeline-language/expressions/README.md
- Literals: pipeline-language/expressions/literals.md
diff --git a/docs/pipeline-language/statements/README.md b/docs/pipeline-language/statements/README.md
index fde4b1fa7..e6f046952 100644
--- a/docs/pipeline-language/statements/README.md
+++ b/docs/pipeline-language/statements/README.md
@@ -1,14 +1,16 @@
# Statements
-Statements are used to run some action. Safe-DS has only two type of statements:
+Statements are used to run some action. Safe-DS only has three type of statements:
- [Expression statements][expression-statements] evaluate an [expression][expressions] and discard any results. They are
only useful if the expression has side effects, such as writing to a file.
- [Assignments][assignments] also evaluate an [expression][expressions], but then store results in
[placeholders][placeholders]. This allows reusing the results multiple times without having to recompute them.
+- [Output statements][output-statements] evaluate an [expression][expressions] as well, and provide options to inspect
+ its results. Unlike when using assignments, the result cannot be reused.
-
-[assignments]: ./assignments.md
-[expression-statements]: ./expression-statements.md
+[assignments]: assignments.md
+[expression-statements]: expression-statements.md
+[output-statements]: output-statements.md
[expressions]: ../expressions/README.md
-[placeholders]: ./assignments.md#declaring-placeholders
+[placeholders]: assignments.md#declaring-placeholders
diff --git a/docs/pipeline-language/statements/assignments.md b/docs/pipeline-language/statements/assignments.md
index e4112e395..6081be4f1 100644
--- a/docs/pipeline-language/statements/assignments.md
+++ b/docs/pipeline-language/statements/assignments.md
@@ -27,14 +27,15 @@ This assignment to a placeholder has the following syntactic elements:
- The name of the placeholder, here `titanic`. It can be any combination of lower- and uppercase letters, underscores,
and numbers, as long as it does not start with a number.
- An `#!sds =` sign.
-- The expression to evaluate (right-hand side).
+- The [expression][expressions] to evaluate (right-hand side).
- A semicolon at the end.
??? info "Name convention"
Use `#!sds lowerCamelCase` for the name of the placeholder. You may prefix the name of an unused placeholder with an
underscore (`_`) to indicate that it is intentionally unused, e.g. to
- [inspect its value](#inspecting-placeholder-values-in-vs-code). This disables the "unused" warning.
+ [inspect its value](#inspecting-placeholder-values-in-vs-code). This disables the "unused" warning. For value
+ inspection, also consider using an [output statement][output-statements] instead.
### References to Placeholder
@@ -104,6 +105,7 @@ such cases.
[expressions]: ../expressions/README.md
[expression-statements]: expression-statements.md
[installation]: ../../getting-started/installation.md
+[output-statements]: output-statements.md
[references]: ../expressions/references.md#references
[runner]: https://github.com/Safe-DS/Runner
[results]: ../segments.md#results
diff --git a/docs/pipeline-language/statements/output-statements.md b/docs/pipeline-language/statements/output-statements.md
new file mode 100644
index 000000000..dc7d7465e
--- /dev/null
+++ b/docs/pipeline-language/statements/output-statements.md
@@ -0,0 +1,26 @@
+# Output Statements
+
+Output statements are used to evaluate an expression and inspect its results. Unlike when using assignments, the results
+cannot be reused. However, it is also not necessary to think of unique names for placeholders, which saves time and
+keeps the namespace clean.
+
+The next snippet shows how the singular result of an expression (the loaded
+[`Table`][safeds.data.tabular.containers.Table]) can be inspected:
+
+```sds
+out Table.fromCsvFile("titanic.csv");
+```
+
+This output statement has the following syntactic elements:
+
+- The keyword `#!sds out`, which indicates that we want to inspect the results of an expression.
+- The expression to evaluate.
+- A semicolon at the end.
+
+Inspecting values requires a working installation of the [Safe-DS Runner][runner]. Follow the instructions in the
+[installation guide][installation] to install it. Afterward, you can inspect values of various types via
+_code lenses_ in the editor, as explained for [assignments][value-inspection].
+
+[installation]: ../../getting-started/installation.md
+[runner]: https://github.com/Safe-DS/Runner
+[value-inspection]: assignments.md#inspecting-placeholder-values-in-vs-code
diff --git a/packages/safe-ds-cli/src/cli/generate.ts b/packages/safe-ds-cli/src/cli/generate.ts
index 2a90f245a..390355c61 100644
--- a/packages/safe-ds-cli/src/cli/generate.ts
+++ b/packages/safe-ds-cli/src/cli/generate.ts
@@ -22,7 +22,7 @@ export const generate = async (fsPaths: string[], options: GenerateOptions): Pro
const generatedFiles = services.generation.PythonGenerator.generate(document, {
destination: URI.file(path.resolve(options.out)),
createSourceMaps: options.sourcemaps,
- targetPlaceholders: undefined,
+ targetStatements: undefined,
disableRunnerIntegration: false,
});
diff --git a/packages/safe-ds-lang/src/language/communication/commands.ts b/packages/safe-ds-lang/src/language/communication/commands.ts
index 615bd67cd..df32ebd3d 100644
--- a/packages/safe-ds-lang/src/language/communication/commands.ts
+++ b/packages/safe-ds-lang/src/language/communication/commands.ts
@@ -1,3 +1,4 @@
+export const COMMAND_EXPLORE_TABLE = 'safe-ds.exploreTable';
export const COMMAND_PRINT_VALUE = 'safe-ds.printValue';
export const COMMAND_RUN_PIPELINE = 'safe-ds.runPipeline';
export const COMMAND_SHOW_IMAGE = 'safe-ds.showImage';
diff --git a/packages/safe-ds-lang/src/language/communication/rpc.ts b/packages/safe-ds-lang/src/language/communication/rpc.ts
index 22e4e59af..d05dd3239 100644
--- a/packages/safe-ds-lang/src/language/communication/rpc.ts
+++ b/packages/safe-ds-lang/src/language/communication/rpc.ts
@@ -1,5 +1,6 @@
import { MessageDirection, NotificationType0, RequestType0 } from 'vscode-languageserver';
import { NotificationType } from 'vscode-languageserver-protocol';
+import { UUID } from 'node:crypto';
export namespace InstallRunnerNotification {
export const method = 'runner/install' as const;
@@ -32,6 +33,39 @@ export namespace UpdateRunnerNotification {
export const type = new NotificationType0(method);
}
+export namespace ExploreTableNotification {
+ export const method = 'runner/exploreTable' as const;
+ export const messageDirection = MessageDirection.serverToClient;
+ export const type = new NotificationType(method);
+}
+
+export interface ExploreTableNotification {
+ /**
+ * The ID of the pipeline execution.
+ */
+ pipelineExecutionId: UUID;
+
+ /**
+ * The URI of the pipeline document.
+ */
+ uri: string;
+
+ /**
+ * The name of the pipeline.
+ */
+ pipelineName: string;
+
+ /**
+ * The end offset of the pipeline node. This is used to add more code to the pipeline by the EDA tool.
+ */
+ pipelineNodeEndOffset: number;
+
+ /**
+ * The name of the placeholder containing the table.
+ */
+ placeholderName: string;
+}
+
export namespace ShowImageNotification {
export const method = 'runner/showImage' as const;
export const messageDirection = MessageDirection.serverToClient;
diff --git a/packages/safe-ds-lang/src/language/flow/safe-ds-slicer.ts b/packages/safe-ds-lang/src/language/flow/safe-ds-slicer.ts
index 9e6265745..f09530c11 100644
--- a/packages/safe-ds-lang/src/language/flow/safe-ds-slicer.ts
+++ b/packages/safe-ds-lang/src/language/flow/safe-ds-slicer.ts
@@ -15,14 +15,19 @@ export class SafeDsSlicer {
/**
* Computes the subset of the given statements that are needed to calculate the target placeholders.
*/
- computeBackwardSlice(statements: SdsStatement[], targets: SdsPlaceholder[]): SdsStatement[] {
- const aggregator = new BackwardSliceAggregator(this.purityComputer, targets);
+ computeBackwardSliceToTargets(statements: SdsStatement[], targets: SdsStatement[]): SdsStatement[] {
+ const aggregator = new BackwardSliceAggregator(this.purityComputer);
for (const statement of statements.reverse()) {
- // Keep if it declares a target
- if (
+ // Keep if it is a target
+ if (targets.includes(statement)) {
+ aggregator.addStatement(statement);
+ }
+
+ // Keep if it declares a referenced placeholder
+ else if (
isSdsAssignment(statement) &&
- getAssignees(statement).some((it) => isSdsPlaceholder(it) && aggregator.targets.has(it))
+ getAssignees(statement).some((it) => isSdsPlaceholder(it) && aggregator.referencedPlaceholders.has(it))
) {
aggregator.addStatement(statement);
}
@@ -49,24 +54,24 @@ class BackwardSliceAggregator {
private readonly purityComputer: SafeDsPurityComputer;
/**
- * The statements that are needed to calculate the target placeholders.
+ * The statements that are needed to calculate the target statements.
*/
readonly statements: SdsStatement[] = [];
/**
- * The target placeholders that should be calculated.
+ * The placeholders that are needed to calculate the target statements.
*/
- readonly targets: Set;
+ readonly referencedPlaceholders: Set;
/**
* The impurity reasons of the collected statements.
*/
readonly impurityReasons: ImpurityReason[] = [];
- constructor(purityComputer: SafeDsPurityComputer, initialTargets: SdsPlaceholder[]) {
+ constructor(purityComputer: SafeDsPurityComputer) {
this.purityComputer = purityComputer;
- this.targets = new Set(initialTargets);
+ this.referencedPlaceholders = new Set();
}
addStatement(statement: SdsStatement): void {
@@ -74,7 +79,7 @@ class BackwardSliceAggregator {
// Remember all referenced placeholders
this.getReferencedPlaceholders(statement).forEach((it) => {
- this.targets.add(it);
+ this.referencedPlaceholders.add(it);
});
// Remember all impurity reasons
diff --git a/packages/safe-ds-lang/src/language/generation/python/safe-ds-python-generator.ts b/packages/safe-ds-lang/src/language/generation/python/safe-ds-python-generator.ts
index 7b76fb0f1..ea59a3aff 100644
--- a/packages/safe-ds-lang/src/language/generation/python/safe-ds-python-generator.ts
+++ b/packages/safe-ds-lang/src/language/generation/python/safe-ds-python-generator.ts
@@ -37,6 +37,7 @@ import {
isSdsMap,
isSdsMemberAccess,
isSdsModule,
+ isSdsOutputStatement,
isSdsParameter,
isSdsParenthesizedExpression,
isSdsPipeline,
@@ -67,6 +68,7 @@ import {
SdsFunction,
SdsLambda,
SdsModule,
+ SdsOutputStatement,
SdsParameter,
SdsParameterList,
SdsPipeline,
@@ -82,7 +84,6 @@ import {
getAssignees,
getModuleMembers,
getParameters,
- getPlaceholderByName,
getStatements,
isStatic,
Parameter,
@@ -116,9 +117,11 @@ 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';
+import { SafeDsSyntheticProperties } from '../../helpers/safe-ds-synthetic-properties.js';
const LAMBDA_PREFIX = `${CODEGEN_PREFIX}lambda_`;
const BLOCK_LAMBDA_RESULT_PREFIX = `${CODEGEN_PREFIX}block_lambda_result_`;
+const OUTPUT_PREFIX = `${CODEGEN_PREFIX}output_`;
const PLACEHOLDER_PREFIX = `${CODEGEN_PREFIX}placeholder_`;
const RECEIVER_PREFIX = `${CODEGEN_PREFIX}receiver_`;
const YIELD_PREFIX = `${CODEGEN_PREFIX}yield_`;
@@ -137,6 +140,7 @@ export class SafeDsPythonGenerator {
private readonly partialEvaluator: SafeDsPartialEvaluator;
private readonly purityComputer: SafeDsPurityComputer;
private readonly slicer: SafeDsSlicer;
+ private readonly syntheticProperties: SafeDsSyntheticProperties;
private readonly typeChecker: SafeDsTypeChecker;
private readonly typeComputer: SafeDsTypeComputer;
@@ -147,6 +151,7 @@ export class SafeDsPythonGenerator {
this.partialEvaluator = services.evaluation.PartialEvaluator;
this.purityComputer = services.purity.PurityComputer;
this.slicer = services.flow.Slicer;
+ this.syntheticProperties = services.helpers.SyntheticProperties;
this.typeChecker = services.typing.TypeChecker;
this.typeComputer = services.typing.TypeComputer;
}
@@ -416,12 +421,17 @@ export class SafeDsPythonGenerator {
typeVariableSet: Set,
generateOptions: GenerateOptions,
): Generated {
+ const targetStatements =
+ typeof generateOptions.targetStatements === 'number'
+ ? [generateOptions.targetStatements]
+ : generateOptions.targetStatements;
+
const infoFrame = new GenerationInfoFrame(
importSet,
utilitySet,
typeVariableSet,
true,
- generateOptions.targetPlaceholders,
+ targetStatements,
generateOptions.disableRunnerIntegration,
);
return expandTracedToNode(pipeline)`def ${traceToNode(
@@ -473,11 +483,14 @@ export class SafeDsPythonGenerator {
frame: GenerationInfoFrame,
generateLambda: boolean = false,
): CompositeGeneratorNode {
- let statements = getStatements(block).filter((stmt) => this.purityComputer.statementDoesSomething(stmt));
- if (frame.targetPlaceholders) {
- const targetPlaceholders = frame.targetPlaceholders.flatMap((it) => getPlaceholderByName(block, it) ?? []);
- if (!isEmpty(targetPlaceholders)) {
- statements = this.slicer.computeBackwardSlice(statements, targetPlaceholders);
+ // TODO: if there are no target statements, only generate code that causes side-effects
+ let statements = getStatements(block).filter((stmt) => this.statementDoesSomething(stmt));
+ if (frame.targetStatements) {
+ const targetStatements = frame.targetStatements.flatMap((it) => {
+ return getStatements(block)[it] ?? [];
+ });
+ if (!isEmpty(targetStatements)) {
+ statements = this.slicer.computeBackwardSliceToTargets(statements, targetStatements);
}
}
if (statements.length === 0) {
@@ -491,6 +504,29 @@ export class SafeDsPythonGenerator {
},
)!;
}
+
+ /**
+ * Returns whether the given statement does something. It must either
+ * - create a placeholder,
+ * - assign to a result, or
+ * - call a function that has side effects.
+ *
+ * @param node
+ * The statement to check.
+ */
+ private statementDoesSomething(node: SdsStatement): boolean {
+ if (isSdsAssignment(node)) {
+ return (
+ !getAssignees(node).every(isSdsWildcard) ||
+ this.purityComputer.expressionHasSideEffects(node.expression)
+ );
+ } else if (isSdsExpressionStatement(node)) {
+ return this.purityComputer.expressionHasSideEffects(node.expression);
+ } else {
+ return isSdsOutputStatement(node);
+ }
+ }
+
private generateStatement(statement: SdsStatement, frame: GenerationInfoFrame, generateLambda: boolean): Generated {
const result: Generated[] = [];
@@ -500,6 +536,14 @@ export class SafeDsPythonGenerator {
} else if (isSdsExpressionStatement(statement)) {
const expressionStatement = this.generateExpression(statement.expression, frame);
result.push(...frame.getExtraStatements(), expressionStatement);
+ } else if (isSdsOutputStatement(statement)) {
+ if (frame.disableRunnerIntegration || !frame.targetStatements?.includes(statement.$containerIndex ?? -1)) {
+ const expressionStatement = this.generateExpression(statement.expression, frame);
+ result.push(...frame.getExtraStatements(), expressionStatement);
+ } else {
+ const outputStatement = this.generateOutputStatement(statement, frame);
+ result.push(...frame.getExtraStatements(), outputStatement);
+ }
} /* c8 ignore start */ else {
throw new Error(`Unknown statement: ${statement}`);
} /* c8 ignore stop */
@@ -557,6 +601,33 @@ export class SafeDsPythonGenerator {
}
}
+ private generateOutputStatement(node: SdsOutputStatement, frame: GenerationInfoFrame): Generated {
+ const valueNames = this.syntheticProperties.getValueNamesForExpression(node.expression);
+ const assignmentStatements: Generated[] = [];
+
+ assignmentStatements.push(
+ expandTracedToNode(node)`${joinToNode(
+ valueNames,
+ (valueName) => `${OUTPUT_PREFIX}${node.$containerIndex}_${valueName}`,
+ {
+ separator: ', ',
+ },
+ )} = ${this.generateExpression(node.expression!, frame)}`,
+ );
+
+ for (const valueName of valueNames) {
+ frame.addImport({ importPath: RUNNER_PACKAGE });
+
+ assignmentStatements.push(
+ expandToNode`${RUNNER_PACKAGE}.save_placeholder('${CODEGEN_PREFIX}${node.$containerIndex}_${valueName}', ${OUTPUT_PREFIX}${node.$containerIndex}_${valueName})`,
+ );
+ }
+
+ return joinTracedToNode(node)(assignmentStatements, (stmt) => stmt, {
+ separator: NL,
+ })!;
+ }
+
private generateAssignee(assignee: SdsAssignee): Generated {
if (isSdsBlockLambdaResult(assignee)) {
return expandTracedToNode(assignee)`${BLOCK_LAMBDA_RESULT_PREFIX}${traceToNode(
@@ -1234,7 +1305,7 @@ class GenerationInfoFrame {
private readonly utilitySet: Set;
private readonly typeVariableSet: Set;
public readonly isInsidePipeline: boolean;
- public readonly targetPlaceholders: string[] | undefined;
+ public readonly targetStatements: number[] | undefined;
public readonly disableRunnerIntegration: boolean;
private extraStatements = new Map();
@@ -1243,7 +1314,7 @@ class GenerationInfoFrame {
utilitySet: Set = new Set(),
typeVariableSet: Set = new Set(),
insidePipeline: boolean = false,
- targetPlaceholders: string[] | undefined = undefined,
+ targetStatements: number[] | undefined = undefined,
disableRunnerIntegration: boolean = false,
idManager: IdManager = new IdManager(),
) {
@@ -1252,7 +1323,7 @@ class GenerationInfoFrame {
this.utilitySet = utilitySet;
this.typeVariableSet = typeVariableSet;
this.isInsidePipeline = insidePipeline;
- this.targetPlaceholders = targetPlaceholders;
+ this.targetStatements = targetStatements;
this.disableRunnerIntegration = disableRunnerIntegration;
}
@@ -1311,7 +1382,7 @@ class GenerationInfoFrame {
this.utilitySet,
this.typeVariableSet,
this.isInsidePipeline,
- this.targetPlaceholders,
+ this.targetStatements,
this.disableRunnerIntegration,
this.idManager,
);
@@ -1319,8 +1390,26 @@ class GenerationInfoFrame {
}
export interface GenerateOptions {
+ /**
+ * Where the generated code should be written to.
+ */
destination: URI;
+
+ /**
+ * Whether to create source maps for the generated code.
+ */
createSourceMaps: boolean;
- targetPlaceholders: string[] | undefined;
+
+ /**
+ * The indices of the statements to generate code for. Code will also be generated for any statements that affect
+ * the target statements.
+ *
+ * If undefined, only code for statements with side effects and those that affect them will be generated.
+ */
+ targetStatements: number[] | number | undefined;
+
+ /**
+ * Whether to disable the integration with the `safe-ds-runner` package and instead generate plain Python code.
+ */
disableRunnerIntegration: boolean;
}
diff --git a/packages/safe-ds-lang/src/language/grammar/safe-ds.langium b/packages/safe-ds-lang/src/language/grammar/safe-ds.langium
index 9566341a8..a38a1b076 100644
--- a/packages/safe-ds-lang/src/language/grammar/safe-ds.langium
+++ b/packages/safe-ds-lang/src/language/grammar/safe-ds.langium
@@ -485,6 +485,7 @@ SdsBlock returns SdsBlock:
SdsStatement returns SdsStatement:
SdsAssignment
| SdsExpressionStatement
+ | SdsOutputStatement
;
interface SdsAssignment extends SdsStatement {
@@ -528,6 +529,14 @@ SdsExpressionStatement returns SdsExpressionStatement:
expression=SdsExpression ';'
;
+interface SdsOutputStatement extends SdsStatement {
+ expression: SdsExpression
+}
+
+SdsOutputStatement returns SdsOutputStatement:
+ 'out' expression=SdsExpression ';'
+;
+
// -----------------------------------------------------------------------------
// Expressions
@@ -562,7 +571,9 @@ SdsBlockLambdaBlock returns SdsBlock:
;
SdsBlockLambdaStatement returns SdsStatement:
- SdsBlockLambdaAssignment | SdsExpressionStatement
+ SdsBlockLambdaAssignment
+ | SdsExpressionStatement
+ | SdsOutputStatement
;
SdsBlockLambdaAssignment returns SdsAssignment:
diff --git a/packages/safe-ds-lang/src/language/helpers/safe-ds-synthetic-properties.ts b/packages/safe-ds-lang/src/language/helpers/safe-ds-synthetic-properties.ts
new file mode 100644
index 000000000..04b32a998
--- /dev/null
+++ b/packages/safe-ds-lang/src/language/helpers/safe-ds-synthetic-properties.ts
@@ -0,0 +1,50 @@
+import { SafeDsServices } from '../safe-ds-module.js';
+import { SafeDsNodeMapper } from './safe-ds-node-mapper.js';
+import {
+ isSdsCall,
+ isSdsClass,
+ isSdsEnumVariant,
+ isSdsExpressionLambda,
+ isSdsMemberAccess,
+ isSdsReference,
+ SdsExpression,
+} from '../generated/ast.js';
+import { getAbstractResults } from './nodeProperties.js';
+
+export class SafeDsSyntheticProperties {
+ private readonly nodeMapper: SafeDsNodeMapper;
+
+ constructor(services: SafeDsServices) {
+ this.nodeMapper = services.helpers.NodeMapper;
+ }
+
+ /**
+ * Get readable value names for an expression. Only one name is returned unless the expression is a named tuple.
+ */
+ getValueNamesForExpression(node: SdsExpression): string[] {
+ if (isSdsCall(node)) {
+ const callable = this.nodeMapper.callToCallable(node);
+ if (isSdsClass(callable)) {
+ return [callable.name];
+ } else if (isSdsEnumVariant(callable)) {
+ return [callable.name];
+ } else if (isSdsExpressionLambda(callable)) {
+ return [`result`];
+ } else {
+ return getAbstractResults(callable).map((it) => it.name);
+ }
+ } else if (isSdsMemberAccess(node)) {
+ const declarationName = node.member?.target?.ref?.name;
+ if (declarationName) {
+ return [declarationName];
+ }
+ } else if (isSdsReference(node)) {
+ const declarationName = node.target.ref?.name;
+ if (declarationName) {
+ return [declarationName];
+ }
+ }
+
+ return ['expression'];
+ }
+}
diff --git a/packages/safe-ds-lang/src/language/lsp/safe-ds-code-lens-provider.ts b/packages/safe-ds-lang/src/language/lsp/safe-ds-code-lens-provider.ts
index b838e52a0..fe6569fe5 100644
--- a/packages/safe-ds-lang/src/language/lsp/safe-ds-code-lens-provider.ts
+++ b/packages/safe-ds-lang/src/language/lsp/safe-ds-code-lens-provider.ts
@@ -1,24 +1,44 @@
import { CodeLensProvider } from 'langium/lsp';
-import { CancellationToken, CodeLens, type CodeLensParams } from 'vscode-languageserver';
+import { CancellationToken, CodeLens, type CodeLensParams, Range } from 'vscode-languageserver';
import { SafeDsServices } from '../safe-ds-module.js';
import { SafeDsTypeComputer } from '../typing/safe-ds-type-computer.js';
import { AstNode, AstNodeLocator, AstUtils, interruptAndCheck, LangiumDocument } from 'langium';
-import { isSdsModule, isSdsPipeline, SdsModuleMember, SdsPipeline, SdsPlaceholder } from '../generated/ast.js';
+import {
+ isSdsAssignment,
+ isSdsModule,
+ isSdsOutputStatement,
+ isSdsPipeline,
+ isSdsPlaceholder,
+ SdsAssignment,
+ SdsModuleMember,
+ SdsOutputStatement,
+ SdsPipeline,
+ SdsPlaceholder,
+} from '../generated/ast.js';
import { SafeDsRunner } from '../runtime/safe-ds-runner.js';
-import { getModuleMembers, streamPlaceholders } from '../helpers/nodeProperties.js';
+import { getAssignees, getModuleMembers, getStatements } from '../helpers/nodeProperties.js';
import { SafeDsTypeChecker } from '../typing/safe-ds-type-checker.js';
-import { COMMAND_PRINT_VALUE, COMMAND_RUN_PIPELINE, COMMAND_SHOW_IMAGE } from '../communication/commands.js';
+import {
+ COMMAND_EXPLORE_TABLE,
+ COMMAND_PRINT_VALUE,
+ COMMAND_RUN_PIPELINE,
+ COMMAND_SHOW_IMAGE,
+} from '../communication/commands.js';
+import { NamedTupleType, Type } from '../typing/model.js';
+import { SafeDsSyntheticProperties } from '../helpers/safe-ds-synthetic-properties.js';
export class SafeDsCodeLensProvider implements CodeLensProvider {
private readonly astNodeLocator: AstNodeLocator;
private readonly runner: SafeDsRunner;
+ private readonly syntheticProperties: SafeDsSyntheticProperties;
private readonly typeChecker: SafeDsTypeChecker;
private readonly typeComputer: SafeDsTypeComputer;
constructor(services: SafeDsServices) {
this.astNodeLocator = services.workspace.AstNodeLocator;
this.runner = services.runtime.Runner;
+ this.syntheticProperties = services.helpers.SyntheticProperties;
this.typeChecker = services.typing.TypeChecker;
this.typeComputer = services.typing.TypeComputer;
}
@@ -57,9 +77,13 @@ export class SafeDsCodeLensProvider implements CodeLensProvider {
if (isSdsPipeline(node)) {
await this.computeCodeLensForPipeline(node, accept);
- for (const placeholder of streamPlaceholders(node.body)) {
+ for (const statement of getStatements(node.body)) {
await interruptAndCheck(cancelToken);
- await this.computeCodeLensForPlaceholder(placeholder, accept);
+ if (isSdsAssignment(statement)) {
+ await this.computeCodeLensForAssignment(statement, accept);
+ } else if (isSdsOutputStatement(statement)) {
+ await this.computeCodeLensForOutputStatement(statement, accept);
+ }
}
}
}
@@ -81,47 +105,115 @@ export class SafeDsCodeLensProvider implements CodeLensProvider {
});
}
- private async computeCodeLensForPlaceholder(node: SdsPlaceholder, accept: CodeLensAcceptor): Promise {
+ private async computeCodeLensForAssignment(
+ node: SdsAssignment,
+ accept: CodeLensAcceptor,
+ cancelToken: CancellationToken = CancellationToken.None,
+ ): Promise {
+ for (const assignee of getAssignees(node)) {
+ await interruptAndCheck(cancelToken);
+ if (isSdsPlaceholder(assignee)) {
+ await this.computeCodeLensForPlaceholder(node, assignee, accept);
+ }
+ }
+ }
+
+ private async computeCodeLensForPlaceholder(
+ assignment: SdsAssignment,
+ placeholder: SdsPlaceholder,
+ accept: CodeLensAcceptor,
+ ): Promise {
+ const cstNode = placeholder.$cstNode;
+ if (!cstNode) {
+ /* c8 ignore next 2 */
+ return;
+ }
+
+ const type = this.typeComputer.computeType(placeholder);
+ await this.computeCodeLensForValue(
+ type,
+ placeholder.name,
+ this.computeNodeId(assignment),
+ cstNode.range,
+ accept,
+ );
+ }
+
+ private async computeCodeLensForOutputStatement(
+ node: SdsOutputStatement,
+ accept: CodeLensAcceptor,
+ cancelToken: CancellationToken = CancellationToken.None,
+ ): Promise {
const cstNode = node.$cstNode;
if (!cstNode) {
/* c8 ignore next 2 */
return;
}
- if (this.typeChecker.isImage(this.typeComputer.computeType(node))) {
- const documentUri = AstUtils.getDocument(node).uri.toString();
- const nodePath = this.astNodeLocator.getAstNodePath(node);
+ // Compute type of expression and unpack if it is a named tuple
+ const expressionType = this.typeComputer.computeType(node.expression);
+ let unpackedTypes: Type[] = [expressionType];
+ if (expressionType instanceof NamedTupleType) {
+ unpackedTypes = expressionType.entries.map((it) => it.type);
+ }
+
+ // Get names of values
+ const valueNames = this.syntheticProperties.getValueNamesForExpression(node.expression);
+ // Create code lenses for each value
+ for (let i = 0; i < unpackedTypes.length; i++) {
+ await interruptAndCheck(cancelToken);
+
+ await this.computeCodeLensForValue(
+ unpackedTypes[i]!,
+ valueNames[i] ?? 'expression',
+ this.computeNodeId(node),
+ cstNode.range,
+ accept,
+ { fallbackToPrint: true },
+ );
+ }
+ }
+
+ private async computeCodeLensForValue(
+ type: Type,
+ name: string,
+ id: NodeId,
+ range: Range,
+ accept: CodeLensAcceptor,
+ options: CodeLensForValueOptions = {},
+ ): Promise {
+ if (this.typeChecker.isImage(type)) {
accept({
- range: cstNode.range,
+ range,
command: {
- title: `Show ${node.name}`,
+ title: `Show ${name}`,
command: COMMAND_SHOW_IMAGE,
- arguments: [documentUri, nodePath],
+ arguments: [name, id],
},
});
- } else if (this.typeChecker.isTable(this.typeComputer.computeType(node))) {
+ } else if (this.typeChecker.isTable(type)) {
accept({
- range: cstNode.range,
+ range,
command: {
- title: `Explore ${node.name}`,
- command: 'safe-ds.exploreTable',
- arguments: this.computeNodeId(node),
+ title: `Explore ${name}`,
+ command: COMMAND_EXPLORE_TABLE,
+ arguments: [name, id],
},
});
- } else if (this.typeChecker.canBePrinted(this.typeComputer.computeType(node))) {
+ } else if (options.fallbackToPrint || this.typeChecker.canBePrinted(type)) {
accept({
- range: cstNode.range,
+ range,
command: {
- title: `Print ${node.name}`,
+ title: `Print ${name}`,
command: COMMAND_PRINT_VALUE,
- arguments: this.computeNodeId(node),
+ arguments: [name, id],
},
});
}
}
- private computeNodeId(node: AstNode): [string, string] {
+ private computeNodeId(node: AstNode): NodeId {
const documentUri = AstUtils.getDocument(node).uri;
const nodePath = this.astNodeLocator.getAstNodePath(node);
return [documentUri.toString(), nodePath];
@@ -129,3 +221,14 @@ export class SafeDsCodeLensProvider implements CodeLensProvider {
}
type CodeLensAcceptor = (codeLens: CodeLens) => void;
+type NodeId = [string, string];
+
+/**
+ * Options for the `computeCodeLensForValue` method.
+ */
+interface CodeLensForValueOptions {
+ /**
+ * If `true`, a print code lens is created, if no other code lens is applicable.
+ */
+ fallbackToPrint?: boolean;
+}
diff --git a/packages/safe-ds-lang/src/language/lsp/safe-ds-execute-command-handler.ts b/packages/safe-ds-lang/src/language/lsp/safe-ds-execute-command-handler.ts
index 349599d9f..a46297770 100644
--- a/packages/safe-ds-lang/src/language/lsp/safe-ds-execute-command-handler.ts
+++ b/packages/safe-ds-lang/src/language/lsp/safe-ds-execute-command-handler.ts
@@ -1,7 +1,12 @@
import { AbstractExecuteCommandHandler, ExecuteCommandAcceptor } from 'langium/lsp';
import { SafeDsSharedServices } from '../safe-ds-module.js';
import { SafeDsRunner } from '../runtime/safe-ds-runner.js';
-import { COMMAND_PRINT_VALUE, COMMAND_RUN_PIPELINE, COMMAND_SHOW_IMAGE } from '../communication/commands.js';
+import {
+ COMMAND_EXPLORE_TABLE,
+ COMMAND_PRINT_VALUE,
+ COMMAND_RUN_PIPELINE,
+ COMMAND_SHOW_IMAGE,
+} from '../communication/commands.js';
/* c8 ignore start */
export class SafeDsExecuteCommandHandler extends AbstractExecuteCommandHandler {
@@ -15,9 +20,16 @@ export class SafeDsExecuteCommandHandler extends AbstractExecuteCommandHandler {
}
override registerCommands(acceptor: ExecuteCommandAcceptor) {
- acceptor(COMMAND_PRINT_VALUE, ([documentUri, nodePath]) => this.runner.printValue(documentUri, nodePath));
+ acceptor(COMMAND_EXPLORE_TABLE, ([name, [documentUri, nodePath]]) =>
+ this.runner.exploreTable(name, documentUri, nodePath),
+ );
+ acceptor(COMMAND_PRINT_VALUE, ([name, [documentUri, nodePath]]) =>
+ this.runner.printValue(name, documentUri, nodePath),
+ );
acceptor(COMMAND_RUN_PIPELINE, ([documentUri, nodePath]) => this.runner.runPipeline(documentUri, nodePath));
- acceptor(COMMAND_SHOW_IMAGE, ([documentUri, nodePath]) => this.runner.showImage(documentUri, nodePath));
+ acceptor(COMMAND_SHOW_IMAGE, ([name, [documentUri, nodePath]]) =>
+ this.runner.showImage(name, documentUri, nodePath),
+ );
}
}
/* c8 ignore stop */
diff --git a/packages/safe-ds-lang/src/language/lsp/safe-ds-formatter.ts b/packages/safe-ds-lang/src/language/lsp/safe-ds-formatter.ts
index 42dd9c99e..ce145db90 100644
--- a/packages/safe-ds-lang/src/language/lsp/safe-ds-formatter.ts
+++ b/packages/safe-ds-lang/src/language/lsp/safe-ds-formatter.ts
@@ -111,6 +111,8 @@ export class SafeDsFormatter extends AbstractFormatter {
this.formatSdsYield(node);
} else if (ast.isSdsExpressionStatement(node)) {
this.formatSdsExpressionStatement(node);
+ } else if (ast.isSdsOutputStatement(node)) {
+ this.formatSdsOutputStatement(node);
}
// -----------------------------------------------------------------------------
@@ -647,6 +649,13 @@ export class SafeDsFormatter extends AbstractFormatter {
formatter.keyword(';').prepend(noSpace());
}
+ private formatSdsOutputStatement(node: ast.SdsOutputStatement) {
+ const formatter = this.getNodeFormatter(node);
+
+ formatter.keyword('out').append(oneSpace());
+ formatter.keyword(';').prepend(noSpace());
+ }
+
// -----------------------------------------------------------------------------
// Expressions
// -----------------------------------------------------------------------------
diff --git a/packages/safe-ds-lang/src/language/purity/safe-ds-purity-computer.ts b/packages/safe-ds-lang/src/language/purity/safe-ds-purity-computer.ts
index 3fb6b168f..866b0b00d 100644
--- a/packages/safe-ds-lang/src/language/purity/safe-ds-purity-computer.ts
+++ b/packages/safe-ds-lang/src/language/purity/safe-ds-purity-computer.ts
@@ -20,8 +20,8 @@ import {
isSdsExpressionStatement,
isSdsFunction,
isSdsLambda,
+ isSdsOutputStatement,
isSdsParameter,
- isSdsWildcard,
SdsCall,
SdsCallable,
SdsExpression,
@@ -32,7 +32,7 @@ import {
import { EvaluatedEnumVariant, ParameterSubstitutions, StringConstant } from '../partialEvaluation/model.js';
import { SafeDsAnnotations } from '../builtins/safe-ds-annotations.js';
import { SafeDsImpurityReasons } from '../builtins/safe-ds-enums.js';
-import { getAssignees, getParameters } from '../helpers/nodeProperties.js';
+import { getParameters } from '../helpers/nodeProperties.js';
import { isContainedInOrEqual } from '../helpers/astUtils.js';
export class SafeDsPurityComputer {
@@ -135,33 +135,6 @@ export class SafeDsPurityComputer {
return this.getImpurityReasonsForExpression(node, substitutions).some((it) => it.isSideEffect);
}
- /**
- * Returns whether the given statement does something. It must either
- * - create a placeholder,
- * - assign to a result, or
- * - call a function that has side effects.
- *
- * @param node
- * The statement to check.
- *
- * @param substitutions
- * The parameter substitutions to use. These are **not** the argument of a call, but the values of the parameters
- * of any containing callables, i.e. the context of the node.
- */
- statementDoesSomething(node: SdsStatement, substitutions = NO_SUBSTITUTIONS): boolean {
- if (isSdsAssignment(node)) {
- return (
- !getAssignees(node).every(isSdsWildcard) ||
- this.expressionHasSideEffects(node.expression, substitutions)
- );
- } else if (isSdsExpressionStatement(node)) {
- return this.expressionHasSideEffects(node.expression, substitutions);
- } else {
- /* c8 ignore next 2 */
- return false;
- }
- }
-
/**
* Returns the reasons why the given callable is impure.
*
@@ -191,6 +164,8 @@ export class SafeDsPurityComputer {
return this.getImpurityReasonsForExpression(node.expression, substitutions);
} else if (isSdsExpressionStatement(node)) {
return this.getImpurityReasonsForExpression(node.expression, substitutions);
+ } else if (isSdsOutputStatement(node)) {
+ return this.getImpurityReasonsForExpression(node.expression, substitutions);
} else {
/* c8 ignore next 2 */
return [];
diff --git a/packages/safe-ds-lang/src/language/runtime/safe-ds-python-server.ts b/packages/safe-ds-lang/src/language/runtime/safe-ds-python-server.ts
index 28bf9dcc2..73833cdb4 100644
--- a/packages/safe-ds-lang/src/language/runtime/safe-ds-python-server.ts
+++ b/packages/safe-ds-lang/src/language/runtime/safe-ds-python-server.ts
@@ -519,17 +519,15 @@ export class SafeDsPythonServer {
this.messageCallbacks.set(messageType, []);
}
this.messageCallbacks.get(messageType)!.push(<(message: PythonServerMessage) => void>callback);
- return {
- dispose: () => {
- if (!this.messageCallbacks.has(messageType)) {
- return;
- }
- this.messageCallbacks.set(
- messageType,
- this.messageCallbacks.get(messageType)!.filter((storedCallback) => storedCallback !== callback),
- );
- },
- };
+ return Disposable.create(() => {
+ if (!this.messageCallbacks.has(messageType)) {
+ return;
+ }
+ this.messageCallbacks.set(
+ messageType,
+ this.messageCallbacks.get(messageType)!.filter((storedCallback) => storedCallback !== callback),
+ );
+ });
}
/**
@@ -559,7 +557,7 @@ export class SafeDsPythonServer {
try {
await this.doConnectToServer(port);
- } catch (error) {
+ } catch (_error) {
await this.stop();
}
}
diff --git a/packages/safe-ds-lang/src/language/runtime/safe-ds-runner.ts b/packages/safe-ds-lang/src/language/runtime/safe-ds-runner.ts
index 3193aaaa1..0fcbf5202 100644
--- a/packages/safe-ds-lang/src/language/runtime/safe-ds-runner.ts
+++ b/packages/safe-ds-lang/src/language/runtime/safe-ds-runner.ts
@@ -1,5 +1,5 @@
import { SafeDsServices } from '../safe-ds-module.js';
-import { AstNodeLocator, AstUtils, LangiumDocument, LangiumDocuments, URI } from 'langium';
+import { AstNodeLocator, AstUtils, Disposable, LangiumDocument, LangiumDocuments, URI } from 'langium';
import path from 'path';
import {
createPlaceholderQueryMessage,
@@ -12,12 +12,21 @@ import {
import { SourceMapConsumer } from 'source-map-js';
import { SafeDsAnnotations } from '../builtins/safe-ds-annotations.js';
import { SafeDsPythonGenerator } from '../generation/python/safe-ds-python-generator.js';
-import { isSdsModule, isSdsPipeline, isSdsPlaceholder } from '../generated/ast.js';
+import {
+ isSdsAssignment,
+ isSdsModule,
+ isSdsOutputStatement,
+ isSdsPipeline,
+ isSdsStatement,
+ SdsStatement,
+} from '../generated/ast.js';
import { SafeDsLogger, SafeDsMessagingProvider } from '../communication/safe-ds-messaging-provider.js';
import crypto from 'crypto';
import { SafeDsPythonServer } from './safe-ds-python-server.js';
-import { IsRunnerReadyRequest, ShowImageNotification } from '../communication/rpc.js';
+import { ExploreTableNotification, IsRunnerReadyRequest, ShowImageNotification } from '../communication/rpc.js';
import { expandToStringLF, joinToNode } from 'langium/generate';
+import { UUID } from 'node:crypto';
+import { CODEGEN_PREFIX } from '../generation/python/constants.js';
// Most of the functionality cannot be tested automatically as a functioning runner setup would always be required
@@ -57,202 +66,211 @@ export class SafeDsRunner {
}
async runPipeline(documentUri: string, nodePath: string) {
- const uri = URI.parse(documentUri);
- const document = this.langiumDocuments.getDocument(uri);
+ const document = this.getDocument(documentUri);
if (!document) {
- this.messaging.showErrorMessage('Could not find document.');
return;
}
const root = document.parseResult.value;
- const pipeline = this.astNodeLocator.getAstNode(root, nodePath);
- if (!isSdsPipeline(pipeline)) {
+ const node = this.astNodeLocator.getAstNode(root, nodePath);
+ if (!isSdsPipeline(node)) {
this.messaging.showErrorMessage('Selected node is not a pipeline.');
return;
}
- const pipelineExecutionId = crypto.randomUUID();
+ await this.runWithCallbacks(`running pipeline ${node.name} in ${documentUri}`, async (pipelineExecutionId) => {
+ await this.executePipeline(pipelineExecutionId, document, node.name);
+ });
+ }
- const start = Date.now();
- const progress = await this.messaging.showProgress('Safe-DS Runner', 'Starting...');
- this.logger.info(`[${pipelineExecutionId}] Running pipeline "${pipeline.name}" in ${documentUri}.`);
+ async exploreTable(name: string, documentUri: string, nodePath: string) {
+ const document = this.getDocument(documentUri);
+ if (!document) {
+ return;
+ }
- const disposables = [
- this.pythonServer.addMessageCallback('placeholder_type', (message) => {
- if (message.id === pipelineExecutionId) {
- progress.report(`Computed ${message.data.name}`);
- }
- }),
+ const root = document.parseResult.value;
+ const statement = this.astNodeLocator.getAstNode(root, nodePath);
+ if (!isSdsStatement(statement)) {
+ this.messaging.showErrorMessage('Selected node is not a statement.');
+ return;
+ }
- this.pythonServer.addMessageCallback('runtime_error', (message) => {
- if (message.id === pipelineExecutionId) {
- progress?.done();
- disposables.forEach((it) => {
- it.dispose();
- });
- this.messaging.showErrorMessage('An error occurred during pipeline execution.');
- }
- progress.done();
- disposables.forEach((it) => {
- it.dispose();
- });
- }),
+ const placeholderName = this.getPlaceholderName(statement, name);
+ if (!placeholderName) {
+ this.messaging.showErrorMessage('Selected node is not an assignment or output statement.');
+ return;
+ }
- this.pythonServer.addMessageCallback('runtime_progress', (message) => {
- if (message.id === pipelineExecutionId) {
- progress.done();
- const timeElapsed = Date.now() - start;
- this.logger.info(
- `[${pipelineExecutionId}] Finished running pipeline "${pipeline.name}" in ${timeElapsed}ms.`,
- );
- disposables.forEach((it) => {
- it.dispose();
+ const pipeline = AstUtils.getContainerOfType(statement, isSdsPipeline);
+ const pipelineCstNode = pipeline?.$cstNode;
+ if (!pipeline || !pipelineCstNode) {
+ this.messaging.showErrorMessage('Could not find pipeline.');
+ return;
+ }
+
+ await this.runWithCallbacks(
+ `exploring table ${pipeline.name}/${name} in ${documentUri}`,
+ async (pipelineExecutionId) => {
+ await this.executePipeline(pipelineExecutionId, document, pipeline.name, statement.$containerIndex);
+ },
+ async (pipelineExecutionId, currentPlaceholderName) => {
+ if (currentPlaceholderName === placeholderName) {
+ await this.messaging.sendNotification(ExploreTableNotification.type, {
+ pipelineExecutionId,
+ uri: documentUri,
+ pipelineName: pipeline.name,
+ pipelineNodeEndOffset: pipelineCstNode.end,
+ placeholderName,
});
}
- }),
- ];
-
- await this.executePipeline(pipelineExecutionId, document, pipeline.name);
+ },
+ );
}
- async printValue(documentUri: string, nodePath: string) {
- const uri = URI.parse(documentUri);
- const document = this.langiumDocuments.getDocument(uri);
+ async printValue(name: string, documentUri: string, nodePath: string) {
+ const document = this.getDocument(documentUri);
if (!document) {
- this.messaging.showErrorMessage('Could not find document.');
return;
}
const root = document.parseResult.value;
- const placeholder = this.astNodeLocator.getAstNode(root, nodePath);
- if (!isSdsPlaceholder(placeholder)) {
- this.messaging.showErrorMessage('Selected node is not a placeholder.');
+ const statement = this.astNodeLocator.getAstNode(root, nodePath);
+ if (!isSdsStatement(statement)) {
+ this.messaging.showErrorMessage('Selected node is not a statement.');
return;
}
- const pipeline = AstUtils.getContainerOfType(placeholder, isSdsPipeline);
+ const placeholderName = this.getPlaceholderName(statement, name);
+ if (!placeholderName) {
+ this.messaging.showErrorMessage('Selected node is not an assignment or output statement.');
+ return;
+ }
+
+ const pipeline = AstUtils.getContainerOfType(statement, isSdsPipeline);
if (!pipeline) {
this.messaging.showErrorMessage('Could not find pipeline.');
return;
}
- const pipelineExecutionId = crypto.randomUUID();
-
- const start = Date.now();
-
- const progress = await this.messaging.showProgress('Safe-DS Runner', 'Starting...');
-
- this.logger.info(
- `[${pipelineExecutionId}] Printing value "${pipeline.name}/${placeholder.name}" in ${documentUri}.`,
- );
-
- const disposables = [
- this.pythonServer.addMessageCallback('runtime_error', (message) => {
- if (message.id === pipelineExecutionId) {
- progress?.done();
- disposables.forEach((it) => {
- it.dispose();
- });
- this.messaging.showErrorMessage('An error occurred during pipeline execution.');
+ await this.runWithCallbacks(
+ `printing value ${pipeline.name}/${name} in ${documentUri}`,
+ async (pipelineExecutionId) => {
+ await this.executePipeline(pipelineExecutionId, document, pipeline.name, statement.$containerIndex);
+ },
+ async (pipelineExecutionId, currentPlaceholderName) => {
+ if (currentPlaceholderName === placeholderName) {
+ const data = await this.getPlaceholderValue(placeholderName, pipelineExecutionId);
+ this.logger.result(`val ${name} = ${JSON.stringify(data, null, 2)};`);
}
- progress.done();
- disposables.forEach((it) => {
- it.dispose();
- });
- }),
-
- this.pythonServer.addMessageCallback('placeholder_type', async (message) => {
- if (message.id === pipelineExecutionId && message.data.name === placeholder.name) {
- const data = await this.getPlaceholderValue(placeholder.name, pipelineExecutionId);
- this.logger.result(`val ${placeholder.name} = ${JSON.stringify(data, null, 2)};`);
- }
- }),
-
- this.pythonServer.addMessageCallback('runtime_progress', (message) => {
- if (message.id === pipelineExecutionId) {
- progress.done();
- const timeElapsed = Date.now() - start;
- this.logger.info(
- `[${pipelineExecutionId}] Finished printing value "${pipeline.name}/${placeholder.name}" in ${timeElapsed}ms.`,
- );
- disposables.forEach((it) => {
- it.dispose();
- });
- }
- }),
- ];
-
- await this.executePipeline(pipelineExecutionId, document, pipeline.name, [placeholder.name]);
+ },
+ );
}
- async showImage(documentUri: string, nodePath: string) {
- const uri = URI.parse(documentUri);
- const document = this.langiumDocuments.getDocument(uri);
+ async showImage(name: string, documentUri: string, nodePath: string) {
+ const document = this.getDocument(documentUri);
if (!document) {
- this.messaging.showErrorMessage('Could not find document.');
return;
}
const root = document.parseResult.value;
- const placeholder = this.astNodeLocator.getAstNode(root, nodePath);
- if (!isSdsPlaceholder(placeholder)) {
- this.messaging.showErrorMessage('Selected node is not a placeholder.');
+ const statement = this.astNodeLocator.getAstNode(root, nodePath);
+ if (!isSdsStatement(statement)) {
+ this.messaging.showErrorMessage('Selected node is not a statement.');
+ return;
+ }
+
+ const placeholderName = this.getPlaceholderName(statement, name);
+ if (!placeholderName) {
+ this.messaging.showErrorMessage('Selected node is not an assignment or output statement.');
return;
}
- const pipeline = AstUtils.getContainerOfType(placeholder, isSdsPipeline);
+ const pipeline = AstUtils.getContainerOfType(statement, isSdsPipeline);
if (!pipeline) {
this.messaging.showErrorMessage('Could not find pipeline.');
return;
}
- const pipelineExecutionId = crypto.randomUUID();
+ await this.runWithCallbacks(
+ `showing image ${pipeline.name}/${name} in ${documentUri}`,
+ async (pipelineExecutionId) => {
+ await this.executePipeline(pipelineExecutionId, document, pipeline.name, statement.$containerIndex);
+ },
+ async (pipelineExecutionId, currentPlaceholderName) => {
+ if (currentPlaceholderName === placeholderName) {
+ const data = await this.getPlaceholderValue(placeholderName, pipelineExecutionId);
+ await this.messaging.sendNotification(ShowImageNotification.type, { image: data });
+ }
+ },
+ );
+ }
+
+ private getDocument(documentUri: string): LangiumDocument | undefined {
+ const uri = URI.parse(documentUri);
+ const document = this.langiumDocuments.getDocument(uri);
+ if (!document) {
+ this.messaging.showErrorMessage('Could not find document.');
+ this.logger.error(`Could not find document "${documentUri}".`);
+ }
+ return document;
+ }
+ async runWithCallbacks(
+ taskName: string,
+ func: (pipelineExecutionId: UUID) => Promise,
+ onPlaceholderReady?: (pipelineExecutionId: UUID, placeholderName: string) => Promise,
+ ) {
+ const pipelineExecutionId = crypto.randomUUID();
const start = Date.now();
const progress = await this.messaging.showProgress('Safe-DS Runner', 'Starting...');
+ this.logger.info(`[${pipelineExecutionId}] Starting ${taskName}.`);
- this.logger.info(
- `[${pipelineExecutionId}] Showing image "${pipeline.name}/${placeholder.name}" in ${documentUri}.`,
- );
+ let disposables: Disposable[] = [];
+ disposables.push(
+ this.pythonServer.addMessageCallback('placeholder_type', async (message) => {
+ if (message.id === pipelineExecutionId) {
+ progress.report(`Computed ${message.data.name}.`);
+ await onPlaceholderReady?.(pipelineExecutionId, message.data.name);
+ }
+ }),
- const disposables = [
- this.pythonServer.addMessageCallback('runtime_error', (message) => {
+ this.pythonServer.addMessageCallback('runtime_progress', (message) => {
if (message.id === pipelineExecutionId) {
- progress?.done();
disposables.forEach((it) => {
it.dispose();
});
- this.messaging.showErrorMessage('An error occurred during pipeline execution.');
- }
- progress.done();
- disposables.forEach((it) => {
- it.dispose();
- });
- }),
- this.pythonServer.addMessageCallback('placeholder_type', async (message) => {
- if (message.id === pipelineExecutionId && message.data.name === placeholder.name) {
- const data = await this.getPlaceholderValue(placeholder.name, pipelineExecutionId);
- await this.messaging.sendNotification(ShowImageNotification.type, { image: data });
+ progress.done();
+ const timeElapsed = Date.now() - start;
+ this.logger.info(`[${pipelineExecutionId}] Finished ${taskName} in ${timeElapsed}ms.`);
}
}),
- this.pythonServer.addMessageCallback('runtime_progress', (message) => {
+ this.pythonServer.addMessageCallback('runtime_error', (message) => {
if (message.id === pipelineExecutionId) {
- progress.done();
- const timeElapsed = Date.now() - start;
- this.logger.info(
- `[${pipelineExecutionId}] Finished showing image "${pipeline.name}/${placeholder.name}" in ${timeElapsed}ms.`,
- );
disposables.forEach((it) => {
it.dispose();
});
+
+ progress.done();
+ this.messaging.showErrorMessage('An error occurred during pipeline execution.');
}
}),
- ];
+ );
+
+ await func(pipelineExecutionId);
+ }
- await this.executePipeline(pipelineExecutionId, document, pipeline.name, [placeholder.name]);
+ private getPlaceholderName(statement: SdsStatement, name: string): string | undefined {
+ if (isSdsAssignment(statement)) {
+ return name;
+ } else if (isSdsOutputStatement(statement)) {
+ return `${CODEGEN_PREFIX}${statement.$containerIndex}_${name}`;
+ } else {
+ return undefined;
+ }
}
private async getPlaceholderValue(placeholder: string, pipelineExecutionId: string): Promise {
@@ -322,13 +340,13 @@ export class SafeDsRunner {
* @param id A unique id that is used in further communication with this pipeline.
* @param pipelineDocument Document containing the main Safe-DS pipeline to execute.
* @param pipelineName Name of the pipeline that should be run
- * @param targetPlaceholders The names of the target placeholders, used to do partial execution. If undefined is provided, the entire pipeline is run.
+ * @param targetStatements The indices of the target statements, used to do partial execution. If undefined is provided, the entire pipeline is run.
*/
public async executePipeline(
id: string,
pipelineDocument: LangiumDocument,
pipelineName: string,
- targetPlaceholders: string[] | undefined = undefined,
+ targetStatements: number[] | number | undefined = undefined,
) {
const node = pipelineDocument.parseResult.value;
if (!isSdsModule(node)) {
@@ -339,7 +357,7 @@ export class SafeDsRunner {
const mainPackage = mainPythonModuleName === undefined ? node.name.split('.') : [mainPythonModuleName];
const mainModuleName = this.getMainModuleName(pipelineDocument);
// Code generation
- const [codeMap, lastGeneratedSources] = this.generateCodeForRunner(pipelineDocument, targetPlaceholders);
+ const [codeMap, lastGeneratedSources] = this.generateCodeForRunner(pipelineDocument, targetStatements);
// Store information about the run
this.executionInformation.set(id, {
generatedSource: lastGeneratedSources,
@@ -466,13 +484,13 @@ export class SafeDsRunner {
public generateCodeForRunner(
pipelineDocument: LangiumDocument,
- targetPlaceholders: string[] | undefined,
+ targetStatements: number[] | number | undefined,
): [ProgramCodeMap, Map] {
const rootGenerationDir = path.parse(pipelineDocument.uri.fsPath).dir;
const generatedDocuments = this.generator.generate(pipelineDocument, {
destination: URI.file(rootGenerationDir), // actual directory of main module file
createSourceMaps: true,
- targetPlaceholders,
+ targetStatements,
disableRunnerIntegration: false,
});
const lastGeneratedSources = new Map();
diff --git a/packages/safe-ds-lang/src/language/safe-ds-module.ts b/packages/safe-ds-lang/src/language/safe-ds-module.ts
index b2b21155d..766bae884 100644
--- a/packages/safe-ds-lang/src/language/safe-ds-module.ts
+++ b/packages/safe-ds-lang/src/language/safe-ds-module.ts
@@ -56,6 +56,7 @@ import { SafeDsExecuteCommandHandler } from './lsp/safe-ds-execute-command-handl
import { SafeDsServiceRegistry } from './safe-ds-service-registry.js';
import { SafeDsPythonServer } from './runtime/safe-ds-python-server.js';
import { SafeDsSlicer } from './flow/safe-ds-slicer.js';
+import { SafeDsSyntheticProperties } from './helpers/safe-ds-synthetic-properties.js';
/**
* Declaration of custom services - add your own service classes here.
@@ -86,6 +87,7 @@ export type SafeDsAddedServices = {
};
helpers: {
NodeMapper: SafeDsNodeMapper;
+ SyntheticProperties: SafeDsSyntheticProperties;
};
lsp: {
NodeInfoProvider: SafeDsNodeInfoProvider;
@@ -160,6 +162,7 @@ export const SafeDsModule: Module new SafeDsNodeMapper(services),
+ SyntheticProperties: (services) => new SafeDsSyntheticProperties(services),
},
lsp: {
CallHierarchyProvider: (services) => new SafeDsCallHierarchyProvider(services),
diff --git a/packages/safe-ds-lang/src/language/validation/other/statements/outputStatements.ts b/packages/safe-ds-lang/src/language/validation/other/statements/outputStatements.ts
new file mode 100644
index 000000000..2d7323794
--- /dev/null
+++ b/packages/safe-ds-lang/src/language/validation/other/statements/outputStatements.ts
@@ -0,0 +1,39 @@
+import { isSdsBlock, isSdsPipeline, SdsOutputStatement } from '../../../generated/ast.js';
+import { AstUtils, ValidationAcceptor } from 'langium';
+import { SafeDsServices } from '../../../safe-ds-module.js';
+import { NamedTupleType } from '../../../typing/model.js';
+
+export const CODE_OUTPUT_STATEMENT_NO_VALUE = 'output-statement/no-value';
+export const CODE_OUTPUT_STATEMENT_ONLY_IN_PIPELINE = 'output-statement/only-in-pipeline';
+
+export const outputStatementMustHaveValue = (services: SafeDsServices) => {
+ const typeComputer = services.typing.TypeComputer;
+
+ return (node: SdsOutputStatement, accept: ValidationAcceptor): void => {
+ const containingBlock = AstUtils.getContainerOfType(node, isSdsBlock);
+ if (!isSdsPipeline(containingBlock?.$container)) {
+ // We already show another error in this case.
+ return;
+ }
+
+ const expressionType = typeComputer.computeType(node.expression);
+ if (expressionType instanceof NamedTupleType && expressionType.length === 0) {
+ accept('error', 'This expression does not produce a value to output.', {
+ node,
+ property: 'expression',
+ code: CODE_OUTPUT_STATEMENT_NO_VALUE,
+ });
+ }
+ };
+};
+
+export const outputStatementMustOnlyBeUsedInPipeline = (node: SdsOutputStatement, accept: ValidationAcceptor): void => {
+ const containingBlock = AstUtils.getContainerOfType(node, isSdsBlock);
+
+ if (!isSdsPipeline(containingBlock?.$container)) {
+ accept('error', 'Output statements can only be used in a pipeline.', {
+ node,
+ code: CODE_OUTPUT_STATEMENT_ONLY_IN_PIPELINE,
+ });
+ }
+};
diff --git a/packages/safe-ds-lang/src/language/validation/other/statements/statements.ts b/packages/safe-ds-lang/src/language/validation/other/statements/statements.ts
index 438b1483f..7ee670c17 100644
--- a/packages/safe-ds-lang/src/language/validation/other/statements/statements.ts
+++ b/packages/safe-ds-lang/src/language/validation/other/statements/statements.ts
@@ -1,7 +1,8 @@
-import { isSdsExpressionStatement, SdsStatement } from '../../../generated/ast.js';
+import { isSdsAssignment, isSdsExpressionStatement, isSdsWildcard, SdsStatement } from '../../../generated/ast.js';
import { ValidationAcceptor } from 'langium';
import { SafeDsServices } from '../../../safe-ds-module.js';
import { NamedTupleType } from '../../../typing/model.js';
+import { getAssignees } from '../../../helpers/nodeProperties.js';
export const CODE_STATEMENT_HAS_NO_EFFECT = 'statement/has-no-effect';
@@ -10,25 +11,28 @@ export const statementMustDoSomething = (services: SafeDsServices) => {
const typeComputer = services.typing.TypeComputer;
return (node: SdsStatement, accept: ValidationAcceptor): void => {
- if (purityComputer.statementDoesSomething(node)) {
- return;
- }
-
- // Special warning message if an assignment is probably missing
- if (isSdsExpressionStatement(node)) {
- const expressionType = typeComputer.computeType(node.expression);
- if (!(expressionType instanceof NamedTupleType) || expressionType.length > 0) {
- accept('warning', 'This statement does nothing. Did you forget the assignment?', {
+ if (isSdsAssignment(node)) {
+ if (getAssignees(node).every(isSdsWildcard) && !purityComputer.expressionHasSideEffects(node.expression)) {
+ accept('warning', 'This statement does nothing.', {
node,
code: CODE_STATEMENT_HAS_NO_EFFECT,
});
+ }
+ } else if (isSdsExpressionStatement(node)) {
+ if (purityComputer.expressionHasSideEffects(node.expression)) {
return;
}
- }
- accept('warning', 'This statement does nothing.', {
- node,
- code: CODE_STATEMENT_HAS_NO_EFFECT,
- });
+ let message = 'This statement does nothing.';
+ const expressionType = typeComputer.computeType(node.expression);
+ if (!(expressionType instanceof NamedTupleType) || expressionType.length > 0) {
+ message += ' Did you mean to assign or output the result?';
+ }
+
+ accept('warning', message, {
+ node,
+ code: CODE_STATEMENT_HAS_NO_EFFECT,
+ });
+ }
};
};
diff --git a/packages/safe-ds-lang/src/language/validation/safe-ds-validator.ts b/packages/safe-ds-lang/src/language/validation/safe-ds-validator.ts
index 038c438ad..d6879e720 100644
--- a/packages/safe-ds-lang/src/language/validation/safe-ds-validator.ts
+++ b/packages/safe-ds-lang/src/language/validation/safe-ds-validator.ts
@@ -189,6 +189,10 @@ import { tagsShouldNotHaveDuplicateEntries } from './builtins/tags.js';
import { moduleMemberShouldBeUsed } from './other/declarations/moduleMembers.js';
import { pipelinesMustBePrivate } from './other/declarations/pipelines.js';
import { thisMustReferToClassInstance } from './other/expressions/this.js';
+import {
+ outputStatementMustHaveValue,
+ outputStatementMustOnlyBeUsedInPipeline,
+} from './other/statements/outputStatements.js';
/**
* Register custom validation checks.
@@ -335,6 +339,7 @@ export const registerValidationChecks = function (services: SafeDsServices) {
namedTypeTypeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments,
namedTypeTypeArgumentsMustMatchBounds(services),
],
+ SdsOutputStatement: [outputStatementMustHaveValue(services), outputStatementMustOnlyBeUsedInPipeline],
SdsParameter: [
constantParameterMustHaveConstantDefaultValue(services),
constantParameterMustHaveTypeThatCanBeEvaluatedToConstant(services),
diff --git a/packages/safe-ds-lang/src/resources/builtins/safeds/data/tabular/containers/Column.sdsstub b/packages/safe-ds-lang/src/resources/builtins/safeds/data/tabular/containers/Column.sdsstub
index d0ab3d850..f67f95904 100644
--- a/packages/safe-ds-lang/src/resources/builtins/safeds/data/tabular/containers/Column.sdsstub
+++ b/packages/safe-ds-lang/src/resources/builtins/safeds/data/tabular/containers/Column.sdsstub
@@ -44,7 +44,7 @@ class Column(
*/
attr type: DataType
- /*
+ /**
* Return the distinct values in the column.
*
* @param ignoreMissingValues Whether to ignore missing values.
diff --git a/packages/safe-ds-lang/tests/language/flow/safe-ds-slicer.test.ts b/packages/safe-ds-lang/tests/language/flow/safe-ds-slicer.test.ts
index 5c9e8cb3d..e8ab68213 100644
--- a/packages/safe-ds-lang/tests/language/flow/safe-ds-slicer.test.ts
+++ b/packages/safe-ds-lang/tests/language/flow/safe-ds-slicer.test.ts
@@ -1,14 +1,14 @@
import { describe, expect, it } from 'vitest';
import { getNodeOfType } from '../../helpers/nodeFinder.js';
import { isSdsPipeline } from '../../../src/language/generated/ast.js';
-import { createSafeDsServices, getPlaceholderByName, getStatements } from '../../../src/language/index.js';
+import { createSafeDsServices, getStatements } from '../../../src/language/index.js';
import { NodeFileSystem } from 'langium/node';
import { fail } from 'node:assert';
const services = (await createSafeDsServices(NodeFileSystem)).SafeDs;
const slicer = services.flow.Slicer;
-describe('computeBackwardSlice', async () => {
+describe('computeBackwardSliceToTargets', async () => {
const testCases: ComputeBackwardSliceTest[] = [
{
testName: 'no targets',
@@ -17,17 +17,27 @@ describe('computeBackwardSlice', async () => {
val a = 1;
}
`,
- targetNames: [],
+ targetIndices: [],
expectedIndices: [],
},
{
- testName: 'single target',
+ testName: 'single target (assignment)',
code: `
pipeline myPipeline {
val a = 1;
}
`,
- targetNames: ['a'],
+ targetIndices: [0],
+ expectedIndices: [0],
+ },
+ {
+ testName: 'single target (output statement)',
+ code: `
+ pipeline myPipeline {
+ out 1;
+ }
+ `,
+ targetIndices: [0],
expectedIndices: [0],
},
{
@@ -38,7 +48,7 @@ describe('computeBackwardSlice', async () => {
val b = 2;
}
`,
- targetNames: ['a', 'b'],
+ targetIndices: [0, 1],
expectedIndices: [0, 1],
},
{
@@ -50,7 +60,7 @@ describe('computeBackwardSlice', async () => {
val b = a;
}
`,
- targetNames: ['b'],
+ targetIndices: [2],
expectedIndices: [0, 2],
},
{
@@ -61,22 +71,33 @@ describe('computeBackwardSlice', async () => {
val b = a;
}
`,
- targetNames: ['a'],
+ targetIndices: [0],
expectedIndices: [0],
},
{
- testName: 'required due to reference',
+ testName: 'required due to reference (assignment)',
code: `
pipeline myPipeline {
val a = 1;
val b = a + 1;
}
`,
- targetNames: ['b'],
+ targetIndices: [1],
+ expectedIndices: [0, 1],
+ },
+ {
+ testName: 'required due to reference (output statement)',
+ code: `
+ pipeline myPipeline {
+ val a = 1;
+ out a + 1;
+ }
+ `,
+ targetIndices: [1],
expectedIndices: [0, 1],
},
{
- testName: 'required due to impurity reason',
+ testName: 'required due to impurity reason (expression statement)',
code: `
package test
@@ -91,21 +112,38 @@ describe('computeBackwardSlice', async () => {
val a = fileRead();
}
`,
- targetNames: ['a'],
+ targetIndices: [1],
+ expectedIndices: [0, 1],
+ },
+ {
+ testName: 'required due to impurity reason (output statement)',
+ code: `
+ package test
+
+ @Impure([ImpurityReason.FileReadFromConstantPath("a.txt")])
+ fun fileRead() -> content: String
+
+ @Impure([ImpurityReason.FileWriteToConstantPath("a.txt")])
+ fun fileWrite() -> content: String
+
+ pipeline myPipeline {
+ out fileWrite();
+ val a = fileRead();
+ }
+ `,
+ targetIndices: [1],
expectedIndices: [0, 1],
},
];
- it.each(testCases)('$testName', async ({ code, targetNames, expectedIndices }) => {
+ it.each(testCases)('$testName', async ({ code, targetIndices, expectedIndices }) => {
const pipeline = await getNodeOfType(services, code, isSdsPipeline);
const statements = getStatements(pipeline.body);
- const targets = targetNames.map(
- (targetName) =>
- getPlaceholderByName(pipeline.body, targetName) ??
- fail(`Target placeholder "${targetName}" not found.`),
+ const targets = targetIndices.map(
+ (index) => statements[index] ?? fail(`Target index ${index} is out of bounds.`),
);
- const backwardSlice = slicer.computeBackwardSlice(statements, targets);
+ const backwardSlice = slicer.computeBackwardSliceToTargets(statements, targets);
const actualIndices = backwardSlice.map((statement) => statement.$containerIndex);
expect(actualIndices).toStrictEqual(expectedIndices);
@@ -124,9 +162,9 @@ interface ComputeBackwardSliceTest {
code: string;
/**
- * The targets to compute the backward slice for.
+ * The container indices of the target statements.
*/
- targetNames: string[];
+ targetIndices: number[];
/**
* The expected container indices of the statements in the backward slice.
diff --git a/packages/safe-ds-lang/tests/language/generation/safe-ds-python-generator/safe-ds-python-generator.test.ts b/packages/safe-ds-lang/tests/language/generation/safe-ds-python-generator/safe-ds-python-generator.test.ts
index da873aec9..0f4bcdaf1 100644
--- a/packages/safe-ds-lang/tests/language/generation/safe-ds-python-generator/safe-ds-python-generator.test.ts
+++ b/packages/safe-ds-lang/tests/language/generation/safe-ds-python-generator/safe-ds-python-generator.test.ts
@@ -2,9 +2,11 @@ import { describe, expect, it } from 'vitest';
import { NodeFileSystem } from 'langium/node';
import { createPythonGenerationTests } from './creator.js';
import { loadDocuments } from '../../../helpers/testResources.js';
-import { stream, URI } from 'langium';
+import { AstUtils, stream, URI } from 'langium';
import { createSafeDsServices } from '../../../../src/language/index.js';
import { isEmpty } from '../../../../src/helpers/collections.js';
+import { isSdsStatement } from '../../../../src/language/generated/ast.js';
+import { isRangeEqual } from 'langium/test';
const services = (await createSafeDsServices(NodeFileSystem)).SafeDs;
const langiumDocuments = services.shared.workspace.LangiumDocuments;
@@ -22,11 +24,24 @@ describe('generation', async () => {
// Load all documents
const documents = await loadDocuments(services, test.inputUris);
- // Get target placeholder name for "run until"
- let targetNames: string[] | undefined = undefined;
+ // Get target statements for "run until"
+ let targetStatements: number[] | undefined = undefined;
if (test.targets && !isEmpty(test.targets)) {
const document = langiumDocuments.getDocument(URI.parse(test.targets[0]!.uri))!;
- targetNames = test.targets.map((target) => document.textDocument.getText(target.range));
+
+ targetStatements = test.targets.flatMap((target) => {
+ const statements = AstUtils.streamAllContents(document.parseResult.value, {
+ range: target.range,
+ }).filter(isSdsStatement);
+
+ for (const statement of statements) {
+ if (isRangeEqual(statement.$cstNode!.range, target.range)) {
+ return statement.$containerIndex!;
+ }
+ }
+
+ return [];
+ });
}
// Generate code for all documents
@@ -35,7 +50,7 @@ describe('generation', async () => {
pythonGenerator.generate(document, {
destination: test.outputRoot,
createSourceMaps: true,
- targetPlaceholders: targetNames,
+ targetStatements,
disableRunnerIntegration: test.disableRunnerIntegration,
}),
)
diff --git a/packages/safe-ds-lang/tests/language/helpers/safe-ds-synthetic-properties/getValueNamesForExpression.test.ts b/packages/safe-ds-lang/tests/language/helpers/safe-ds-synthetic-properties/getValueNamesForExpression.test.ts
new file mode 100644
index 000000000..7bbf894c1
--- /dev/null
+++ b/packages/safe-ds-lang/tests/language/helpers/safe-ds-synthetic-properties/getValueNamesForExpression.test.ts
@@ -0,0 +1,152 @@
+import { describe, expect, it } from 'vitest';
+import { createSafeDsServices } from '../../../../src/language/index.js';
+import { EmptyFileSystem } from 'langium';
+import { isSdsOutputStatement } from '../../../../src/language/generated/ast.js';
+import { getNodeOfType } from '../../../helpers/nodeFinder.js';
+
+const services = (await createSafeDsServices(EmptyFileSystem, { omitBuiltins: true })).SafeDs;
+const syntheticProperties = services.helpers.SyntheticProperties;
+
+describe('SafeDsSyntheticProperties', () => {
+ describe('getValueNamesForExpression', () => {
+ const tests: GetValueNamesForExpressionTest[] = [
+ {
+ testName: 'literal',
+ code: `
+ pipeline myPipeline {
+ out 1;
+ }
+ `,
+ expected: ['expression'],
+ },
+ {
+ testName: 'reference',
+ code: `
+ pipeline myPipeline {
+ val a = 1;
+ out a;
+ }
+ `,
+ expected: ['a'],
+ },
+ {
+ testName: 'member access',
+ code: `
+ class C {
+ attr a;
+ }
+
+ pipeline myPipeline {
+ out C().a;
+ }
+ `,
+ expected: ['a'],
+ },
+ {
+ testName: 'call to block lambda',
+ code: `
+ pipeline myPipeline {
+ val lambda = () {
+ yield a = 1;
+ yield b = 2;
+ };
+ out lambda();
+ }
+ `,
+ expected: ['a', 'b'],
+ },
+ {
+ testName: 'call to callable type',
+ code: `
+ segment mySegment(f: () -> (a: Int, b: Int)) {
+ out f();
+ }
+ `,
+ expected: ['a', 'b'],
+ },
+ {
+ testName: 'call to class',
+ code: `
+ class C
+
+ pipeline myPipeline {
+ out C();
+ }
+ `,
+ expected: ['C'],
+ },
+ {
+ testName: 'call to enum variant',
+ code: `
+ enum E {
+ V
+ }
+
+ pipeline myPipeline {
+ out E.V();
+ }
+ `,
+ expected: ['V'],
+ },
+ {
+ testName: 'call to expression lambda',
+ code: `
+ pipeline myPipeline {
+ val lambda = () -> 1;
+ out lambda();
+ }
+ `,
+ expected: ['result'],
+ },
+ {
+ testName: 'call to function',
+ code: `
+ fun f() -> (a: Int, b: Int)
+
+ pipeline myPipeline {
+ out f();
+ }
+ `,
+ expected: ['a', 'b'],
+ },
+ {
+ testName: 'call to segment',
+ code: `
+ segment s() -> (a: Int, b: Int) {}
+
+ pipeline myPipeline {
+ out s();
+ }
+ `,
+ expected: ['a', 'b'],
+ },
+ ];
+
+ it.each(tests)('$testName', async ({ code, expected }) => {
+ const outputStatement = await getNodeOfType(services, code, isSdsOutputStatement);
+ const actual = syntheticProperties.getValueNamesForExpression(outputStatement.expression);
+
+ expect(actual).toStrictEqual(expected);
+ });
+ });
+});
+
+/**
+ * A test for {@link SafeDsSyntheticProperties.getValueNamesForExpression}.
+ */
+interface GetValueNamesForExpressionTest {
+ /**
+ * The name of the test.
+ */
+ testName: string;
+
+ /**
+ * The code to test. It must contain exactly one output statement.
+ */
+ code: string;
+
+ /**
+ * The expected value names.
+ */
+ expected: string[];
+}
diff --git a/packages/safe-ds-lang/tests/language/lsp/safe-ds-code-lens-provider.test.ts b/packages/safe-ds-lang/tests/language/lsp/safe-ds-code-lens-provider.test.ts
index c65e34d09..a994c44f4 100644
--- a/packages/safe-ds-lang/tests/language/lsp/safe-ds-code-lens-provider.test.ts
+++ b/packages/safe-ds-lang/tests/language/lsp/safe-ds-code-lens-provider.test.ts
@@ -23,80 +23,227 @@ describe('SafeDsCodeLensProvider', () => {
},
{
testName: 'pipeline with null placeholder',
- code: `pipeline myPipeline {
- val a = null;
- }`,
+ code: `
+ pipeline myPipeline {
+ val a = null;
+ }
+ `,
expectedCodeLensTitles: ['Run myPipeline', 'Print a'],
},
{
testName: 'pipeline with Image placeholder',
- code: `pipeline myPipeline {
- val a = Image.fromFile("test.png");
- }`,
+ code: `
+ pipeline myPipeline {
+ val a = Image.fromFile("test.png");
+ }
+ `,
expectedCodeLensTitles: ['Run myPipeline', 'Show a'],
},
{
testName: 'block lambda with Image placeholder',
- code: `pipeline myPipeline {
- () {
- val a = Image.fromFile("test.png");
- };
- }`,
+ code: `
+ pipeline myPipeline {
+ () {
+ val a = Image.fromFile("test.png");
+ };
+ }
+ `,
expectedCodeLensTitles: ['Run myPipeline'],
},
{
testName: 'segment with Image placeholder',
- code: `segment mySegment {
- val a = Image.fromFile("test.png");
- }`,
+ code: `
+ segment mySegment {
+ val a = Image.fromFile("test.png");
+ }
+ `,
expectedCodeLensTitles: [],
},
{
testName: 'pipeline with Table placeholder',
- code: `pipeline myPipeline {
- val a = Table();
- }`,
+ code: `
+ pipeline myPipeline {
+ val a = Table();
+ }
+ `,
expectedCodeLensTitles: ['Run myPipeline', 'Explore a'],
},
{
testName: 'block lambda with Table placeholder',
- code: `pipeline myPipeline {
- () {
- val a = Table();
- };
- }`,
+ code: `
+ pipeline myPipeline {
+ () {
+ val a = Table();
+ };
+ }
+ `,
expectedCodeLensTitles: ['Run myPipeline'],
},
{
testName: 'segment with Table placeholder',
- code: `segment mySegment {
- val a = Table();
- }`,
+ code: `
+ segment mySegment {
+ val a = Table();
+ }
+ `,
expectedCodeLensTitles: [],
},
{
testName: 'pipeline with printable placeholder',
- code: `pipeline myPipeline {
- val a = 1;
- }`,
+ code: `
+ pipeline myPipeline {
+ val a = 1;
+ }
+ `,
expectedCodeLensTitles: ['Run myPipeline', 'Print a'],
},
{
testName: 'block lambda with printable placeholder',
- code: `pipeline myPipeline {
- () {
- val a = 1;
- };
- }`,
+ code: `
+ pipeline myPipeline {
+ () {
+ val a = 1;
+ };
+ }
+ `,
expectedCodeLensTitles: ['Run myPipeline'],
},
{
testName: 'segment with printable placeholder',
- code: `segment mySegment {
- val a = 1;
- }`,
+ code: `
+ segment mySegment {
+ val a = 1;
+ }
+ `,
+ expectedCodeLensTitles: [],
+ },
+ {
+ testName: 'multiple placeholders in one assignment',
+ code: `
+ @Pure fun f() -> (a: Int, b: Int)
+
+ pipeline myPipeline {
+ val x, val y = f();
+ }
+ `,
+ expectedCodeLensTitles: ['Run myPipeline', 'Print x', 'Print y'],
+ },
+ {
+ testName: 'pipeline with null output',
+ code: `
+ pipeline myPipeline {
+ out null;
+ }
+ `,
+ expectedCodeLensTitles: ['Run myPipeline', 'Print expression'],
+ },
+ {
+ testName: 'pipeline with Image output',
+ code: `
+ pipeline myPipeline {
+ out Image.fromFile("test.png");
+ }
+ `,
+ expectedCodeLensTitles: ['Run myPipeline', 'Show image'],
+ },
+ {
+ testName: 'block lambda with Image output',
+ code: `
+ pipeline myPipeline {
+ () {
+ out Image.fromFile("test.png");
+ };
+ }
+ `,
+ expectedCodeLensTitles: ['Run myPipeline'],
+ },
+ {
+ testName: 'segment with Image output',
+ code: `
+ segment mySegment {
+ out Image.fromFile("test.png");
+ }
+ `,
expectedCodeLensTitles: [],
},
+ {
+ testName: 'pipeline with Table output',
+ code: `
+ pipeline myPipeline {
+ out Table();
+ }
+ `,
+ expectedCodeLensTitles: ['Run myPipeline', 'Explore Table'],
+ },
+ {
+ testName: 'block lambda with Table output',
+ code: `
+ pipeline myPipeline {
+ () {
+ out Table();
+ };
+ }
+ `,
+ expectedCodeLensTitles: ['Run myPipeline'],
+ },
+ {
+ testName: 'segment with Table output',
+ code: `
+ segment mySegment {
+ out Table();
+ }
+ `,
+ expectedCodeLensTitles: [],
+ },
+ {
+ testName: 'pipeline with printable output',
+ code: `
+ pipeline myPipeline {
+ out 1;
+ }
+ `,
+ expectedCodeLensTitles: ['Run myPipeline', 'Print expression'],
+ },
+ {
+ testName: 'block lambda with printable output',
+ code: `
+ pipeline myPipeline {
+ () {
+ out 1;
+ };
+ }
+ `,
+ expectedCodeLensTitles: ['Run myPipeline'],
+ },
+ {
+ testName: 'segment with printable output',
+ code: `
+ segment mySegment {
+ out 1;
+ }
+ `,
+ expectedCodeLensTitles: [],
+ },
+ {
+ testName: 'multiple values in one output statement',
+ code: `
+ @Pure fun f() -> (a: Int, b: Int)
+
+ pipeline myPipeline {
+ out f();
+ }
+ `,
+ expectedCodeLensTitles: ['Run myPipeline', 'Print a', 'Print b'],
+ },
+ {
+ testName: 'member access in output statement',
+ code: `
+ pipeline myPipeline {
+ out Table().rowCount;
+ }
+ `,
+ expectedCodeLensTitles: ['Run myPipeline', 'Print rowCount'],
+ },
];
it.each(testCases)('should compute code lenses ($testName)', async ({ code, expectedCodeLensTitles }) => {
diff --git a/packages/safe-ds-lang/tests/resources/formatting/statements/output statements/in block lambda.sdsdev b/packages/safe-ds-lang/tests/resources/formatting/statements/output statements/in block lambda.sdsdev
new file mode 100644
index 000000000..11a893a3d
--- /dev/null
+++ b/packages/safe-ds-lang/tests/resources/formatting/statements/output statements/in block lambda.sdsdev
@@ -0,0 +1,13 @@
+pipeline myPipeline {
+ () {
+ out call() ;
+ };
+}
+
+// -----------------------------------------------------------------------------
+
+pipeline myPipeline {
+ () {
+ out call();
+ };
+}
diff --git a/packages/safe-ds-lang/tests/resources/formatting/statements/output statements/in pipeline.sdsdev b/packages/safe-ds-lang/tests/resources/formatting/statements/output statements/in pipeline.sdsdev
new file mode 100644
index 000000000..d4e5e02d7
--- /dev/null
+++ b/packages/safe-ds-lang/tests/resources/formatting/statements/output statements/in pipeline.sdsdev
@@ -0,0 +1,9 @@
+pipeline myPipeline {
+ out call() ;
+ }
+
+// -----------------------------------------------------------------------------
+
+pipeline myPipeline {
+ out call();
+}
diff --git a/packages/safe-ds-lang/tests/resources/formatting/statements/output statements/in segment.sdsdev b/packages/safe-ds-lang/tests/resources/formatting/statements/output statements/in segment.sdsdev
new file mode 100644
index 000000000..71ca437c5
--- /dev/null
+++ b/packages/safe-ds-lang/tests/resources/formatting/statements/output statements/in segment.sdsdev
@@ -0,0 +1,9 @@
+segment mySegment() {
+ out call() ;
+}
+
+// -----------------------------------------------------------------------------
+
+segment mySegment() {
+ out call();
+}
diff --git a/packages/safe-ds-lang/tests/resources/generation/python/partial/impure dependency file constant/generated/tests/generator/partialImpureDependencyFileConstant/gen_input.py.map b/packages/safe-ds-lang/tests/resources/generation/python/partial/impure dependency file constant/generated/tests/generator/partialImpureDependencyFileConstant/gen_input.py.map
index 3c21e131f..43330c05a 100644
--- a/packages/safe-ds-lang/tests/resources/generation/python/partial/impure dependency file constant/generated/tests/generator/partialImpureDependencyFileConstant/gen_input.py.map
+++ b/packages/safe-ds-lang/tests/resources/generation/python/partial/impure dependency file constant/generated/tests/generator/partialImpureDependencyFileConstant/gen_input.py.map
@@ -1 +1 @@
-{"version":3,"sources":["input.sdsdev"],"names":["testpipeline","ifilewritea","ifilereada","ifilewriteb","ifilereadb","impurefilereadagain","impurefilereadagainb"],"mappings":"AAAA;;;;;;AAUA,IAASA,YAAY;IAEjB,oCAAsBC,WAAW;IACjC,qCAAuBA,WAAW;IAClC,wCAA0BC,UAAU;IAGpC,qCAAuBC,WAAW;IAClC,sCAAwBA,WAAW;IACnC,yCAA2BC,UAAU;IAGrC,2BAAe,CAAAC,qCAAmB,EAAC,CAAC,EAACC,sCAAoB","file":"gen_input.py"}
\ No newline at end of file
+{"version":3,"sources":["input.sdsdev"],"names":["testpipeline","ifilewritea","ifilereada","ifilewriteb","ifilereadb","impurefilereadagain","impurefilereadagainb"],"mappings":"AAAA;;;;;;AAUA,IAASA,YAAY;IAEjB,oCAAsBC,WAAW;IACjC,qCAAuBA,WAAW;IAClC,wCAA0BC,UAAU;IAGpC,qCAAuBC,WAAW;IAClC,sCAAwBA,WAAW;IACnC,yCAA2BC,UAAU;IAGpC,2BAAa,CAAAC,qCAAmB,EAAC,CAAC,EAACC,sCAAoB","file":"gen_input.py"}
\ No newline at end of file
diff --git a/packages/safe-ds-lang/tests/resources/generation/python/partial/impure dependency file constant/input.sdsdev b/packages/safe-ds-lang/tests/resources/generation/python/partial/impure dependency file constant/input.sdsdev
index 5778ff811..1a798308e 100644
--- a/packages/safe-ds-lang/tests/resources/generation/python/partial/impure dependency file constant/input.sdsdev
+++ b/packages/safe-ds-lang/tests/resources/generation/python/partial/impure dependency file constant/input.sdsdev
@@ -20,5 +20,5 @@ pipeline testPipeline {
val impureFileReadAgainB = iFileReadB();
// $TEST$ target
- val »result« = impureFileReadAgain + impureFileReadAgainB;
+ »val result = impureFileReadAgain + impureFileReadAgainB;«
}
diff --git a/packages/safe-ds-lang/tests/resources/generation/python/partial/impure dependency file parameter/generated/tests/generator/partialImpureDependencyFileParameter/gen_input.py.map b/packages/safe-ds-lang/tests/resources/generation/python/partial/impure dependency file parameter/generated/tests/generator/partialImpureDependencyFileParameter/gen_input.py.map
index 248529607..a77622f88 100644
--- a/packages/safe-ds-lang/tests/resources/generation/python/partial/impure dependency file parameter/generated/tests/generator/partialImpureDependencyFileParameter/gen_input.py.map
+++ b/packages/safe-ds-lang/tests/resources/generation/python/partial/impure dependency file parameter/generated/tests/generator/partialImpureDependencyFileParameter/gen_input.py.map
@@ -1 +1 @@
-{"version":3,"sources":["input.sdsdev"],"names":["testpipeline","ifilewrite","ifileread","impurefilereadagain"],"mappings":"AAAA;;;;;;AAMA,IAASA,YAAY;IAEjB,oCAAsBC,UAAU,CAAC,OAAO;IACxC,qCAAuBA,UAAU,CAAC,OAAO;IACzC,wCAA0BC,SAAS,CAAC,OAAO;IAG3C,2BAAe,CAAAC,qCAAmB,EAAC,CAAC,EAAC,CAAC","file":"gen_input.py"}
\ No newline at end of file
+{"version":3,"sources":["input.sdsdev"],"names":["testpipeline","ifilewrite","ifileread","impurefilereadagain"],"mappings":"AAAA;;;;;;AAMA,IAASA,YAAY;IAEjB,oCAAsBC,UAAU,CAAC,OAAO;IACxC,qCAAuBA,UAAU,CAAC,OAAO;IACzC,wCAA0BC,SAAS,CAAC,OAAO;IAG1C,2BAAa,CAAAC,qCAAmB,EAAC,CAAC,EAAC,CAAC","file":"gen_input.py"}
\ No newline at end of file
diff --git a/packages/safe-ds-lang/tests/resources/generation/python/partial/impure dependency file parameter/input.sdsdev b/packages/safe-ds-lang/tests/resources/generation/python/partial/impure dependency file parameter/input.sdsdev
index 52798dd5b..27cf634af 100644
--- a/packages/safe-ds-lang/tests/resources/generation/python/partial/impure dependency file parameter/input.sdsdev
+++ b/packages/safe-ds-lang/tests/resources/generation/python/partial/impure dependency file parameter/input.sdsdev
@@ -11,5 +11,5 @@ pipeline testPipeline {
val impureFileReadAgain = iFileRead("d.txt");
// $TEST$ target
- val »result« = impureFileReadAgain + 2;
+ »val result = impureFileReadAgain + 2;«
}
diff --git a/packages/safe-ds-lang/tests/resources/generation/python/partial/impure dependency/generated/tests/generator/partialImpureDependency/gen_input.py.map b/packages/safe-ds-lang/tests/resources/generation/python/partial/impure dependency/generated/tests/generator/partialImpureDependency/gen_input.py.map
index 2af1b4b74..c021945b8 100644
--- a/packages/safe-ds-lang/tests/resources/generation/python/partial/impure dependency/generated/tests/generator/partialImpureDependency/gen_input.py.map
+++ b/packages/safe-ds-lang/tests/resources/generation/python/partial/impure dependency/generated/tests/generator/partialImpureDependency/gen_input.py.map
@@ -1 +1 @@
-{"version":3,"sources":["input.sdsdev"],"names":["testpipeline","i1","ifilewrite","nopartialevalint","r","fp","purevalueforimpure2","purevalueforimpure3","impurea2"],"mappings":"AAAA;;;;;;AAeA,IAASA,YAAY;IACjBC,EAAE,CAAC,CAAC;IAGJ,oCAAsBC,UAAU;IAChC,qCAAuBA,UAAU;IAEjC,wCAA0BC,gBAAgB,CAAC,CAAC;IAC5C,wCAA0B,CAAC;IAExB;QACCF,EAAE,CAAC,CAAC;QACJ,0BAAMG,CAAC,GAAG,CAAC;QAFZ,OAEC,0BAAMA,CAAC;IAFXC,EAAE,CAAC;IAIHJ,EAAE,CAAC,CAAC;IACJ,6BAAeA,EAAE,CAACK,qCAAmB;IACrC,6BAAeL,EAAE,CAACE,gBAAgB,CAACI,CAAmB;IACtDN,EAAE,CAAC,CAAC;IAGJ,2BAAeA,EAAE,CAACO,0BAAQ","file":"gen_input.py"}
\ No newline at end of file
+{"version":3,"sources":["input.sdsdev"],"names":["testpipeline","i1","ifilewrite","nopartialevalint","r","fp","purevalueforimpure2","purevalueforimpure3","impurea2"],"mappings":"AAAA;;;;;;AAeA,IAASA,YAAY;IACjBC,EAAE,CAAC,CAAC;IAGJ,oCAAsBC,UAAU;IAChC,qCAAuBA,UAAU;IAEjC,wCAA0BC,gBAAgB,CAAC,CAAC;IAC5C,wCAA0B,CAAC;IAExB;QACCF,EAAE,CAAC,CAAC;QACJ,0BAAMG,CAAC,GAAG,CAAC;QAFZ,OAEC,0BAAMA,CAAC;IAFXC,EAAE,CAAC;IAIHJ,EAAE,CAAC,CAAC;IACJ,6BAAeA,EAAE,CAACK,qCAAmB;IACrC,6BAAeL,EAAE,CAACE,gBAAgB,CAACI,CAAmB;IACtDN,EAAE,CAAC,CAAC;IAGH,2BAAaA,EAAE,CAACO,0BAAQ","file":"gen_input.py"}
\ No newline at end of file
diff --git a/packages/safe-ds-lang/tests/resources/generation/python/partial/impure dependency/input.sdsdev b/packages/safe-ds-lang/tests/resources/generation/python/partial/impure dependency/input.sdsdev
index 4ae8a817b..a610fa5b8 100644
--- a/packages/safe-ds-lang/tests/resources/generation/python/partial/impure dependency/input.sdsdev
+++ b/packages/safe-ds-lang/tests/resources/generation/python/partial/impure dependency/input.sdsdev
@@ -33,7 +33,7 @@ pipeline testPipeline {
i1(4);
// $TEST$ target
- val »result« = i1(impureA2);
+ »val result = i1(impureA2);«
i1(4); // Should not be generated - impure cannot affect result after result is already calculated
val someImpureValue = i1(4); // Should not be generated - impure cannot affect result after result is already calculated
}
diff --git a/packages/safe-ds-lang/tests/resources/generation/python/partial/pure dependency/generated/tests/generator/partialPureDependency/gen_input.py.map b/packages/safe-ds-lang/tests/resources/generation/python/partial/pure dependency/generated/tests/generator/partialPureDependency/gen_input.py.map
index 63741e1ce..005687176 100644
--- a/packages/safe-ds-lang/tests/resources/generation/python/partial/pure dependency/generated/tests/generator/partialPureDependency/gen_input.py.map
+++ b/packages/safe-ds-lang/tests/resources/generation/python/partial/pure dependency/generated/tests/generator/partialPureDependency/gen_input.py.map
@@ -1 +1 @@
-{"version":3,"sources":["input.sdsdev"],"names":["testpipeline","false","null","g","boolean1","ldouble","lint","lnull","lstrmulti","z","f","g2","mapkey","mapvalue","nopartialevalint","listv1","listv3","list3","g3","list","o","value1","mapresult","listresult","g4","listvalue"],"mappings":"AAAA;;;;;;AAcA,IAASA,YAAY;IACjB,2BAAaC,KAAK;IAGlB,4BAAc,IAAI;IAGlB,yBAAW,CAAC;IAGZ,0BAAYC,IAAI;IAGhB,8BAAgB;IAIhB,6BAAe;IAGf,2BAAaC,CAAC,CAACC,IAAQ,EAAEC,IAAO,EAAEC,CAAI,EAAEC,IAAK,EAAEC,aAAS;IAG9C;QACN,sBAAQ,CAAC;QACT,uBAAS,CAAC;QACV,sBAAQ;QACR,uBAAS;QACT,0BAAMC,CAAC,GAAG;QALJ,OAKN,0BAAMA,CAAC;IACJ;eAAM,CAAC;IANd,sBAAQ,CAAAC,CAAC,CAAC,iBAMP,CAAC,EAACA,CAAC,CAAC;IAcP,2BAAa,KAAK;IAClB,6BAAe,OAAO;IACtB,8BAAgBC,EAAE,CAAC,CAACC,KAAM,EAAEC,OAAQ;IAMpC,2BAAa,CAAC;IAEd,2BAAaC,gBAAgB,CAACC,CAAM;IAEpC,yBAAW,CAACA,CAAM;IAElB,0BAAY,CAACC,wBAAM;IACnB,8BAAgBC,uBAAK,CAAC,CAAC;IAEvB,+BAAiBC,EAAE,CAACC,sBAAI;IAIxB,2BAAe,CAAO,CAAP,CAAA,CAAC,CAAAC,mBAAC,GAAC,CAAC,EAAC,CAAC,GAAC,CAAC,EAACC,wBAAM,GAAC,CAAC,EAAwB,CAAvB,CAAAC,2BAAS,EAAC,CAAC,EAACC,4BAAU,GAAC,CAAC,EAACC,EAAE,CAACC,2BAAS","file":"gen_input.py"}
\ No newline at end of file
+{"version":3,"sources":["input.sdsdev"],"names":["testpipeline","false","null","g","boolean1","ldouble","lint","lnull","lstrmulti","z","f","g2","mapkey","mapvalue","nopartialevalint","listv1","listv3","list3","g3","list","o","value1","mapresult","listresult","g4","listvalue"],"mappings":"AAAA;;;;;;AAcA,IAASA,YAAY;IACjB,2BAAaC,KAAK;IAGlB,4BAAc,IAAI;IAGlB,yBAAW,CAAC;IAGZ,0BAAYC,IAAI;IAGhB,8BAAgB;IAIhB,6BAAe;IAGf,2BAAaC,CAAC,CAACC,IAAQ,EAAEC,IAAO,EAAEC,CAAI,EAAEC,IAAK,EAAEC,aAAS;IAG9C;QACN,sBAAQ,CAAC;QACT,uBAAS,CAAC;QACV,sBAAQ;QACR,uBAAS;QACT,0BAAMC,CAAC,GAAG;QALJ,OAKN,0BAAMA,CAAC;IACJ;eAAM,CAAC;IANd,sBAAQ,CAAAC,CAAC,CAAC,iBAMP,CAAC,EAACA,CAAC,CAAC;IAcP,2BAAa,KAAK;IAClB,6BAAe,OAAO;IACtB,8BAAgBC,EAAE,CAAC,CAACC,KAAM,EAAEC,OAAQ;IAMpC,2BAAa,CAAC;IAEd,2BAAaC,gBAAgB,CAACC,CAAM;IAEpC,yBAAW,CAACA,CAAM;IAElB,0BAAY,CAACC,wBAAM;IACnB,8BAAgBC,uBAAK,CAAC,CAAC;IAEvB,+BAAiBC,EAAE,CAACC,sBAAI;IAIvB,2BAAa,CAAO,CAAP,CAAA,CAAC,CAAAC,mBAAC,GAAC,CAAC,EAAC,CAAC,GAAC,CAAC,EAACC,wBAAM,GAAC,CAAC,EAAwB,CAAvB,CAAAC,2BAAS,EAAC,CAAC,EAACC,4BAAU,GAAC,CAAC,EAACC,EAAE,CAACC,2BAAS","file":"gen_input.py"}
\ No newline at end of file
diff --git a/packages/safe-ds-lang/tests/resources/generation/python/partial/pure dependency/input.sdsdev b/packages/safe-ds-lang/tests/resources/generation/python/partial/pure dependency/input.sdsdev
index 3a12c8ac7..655488205 100644
--- a/packages/safe-ds-lang/tests/resources/generation/python/partial/pure dependency/input.sdsdev
+++ b/packages/safe-ds-lang/tests/resources/generation/python/partial/pure dependency/input.sdsdev
@@ -76,7 +76,7 @@ line";
val listResult2 = g3(list2); // Should not be generated
// $TEST$ target
- val »result« = -o + 1 + value1 + mapResult * listResult / g4(listValue);
+ »val result = -o + 1 + value1 + mapResult * listResult / g4(listValue);«
val lDouble3 = 1.0; // Should not be generated - pure cannot affect result after result is already calculated
}
diff --git a/packages/safe-ds-lang/tests/resources/generation/python/partial/redundant impurity/generated/tests/generator/partialRedundantImpurity/gen_input.py.map b/packages/safe-ds-lang/tests/resources/generation/python/partial/redundant impurity/generated/tests/generator/partialRedundantImpurity/gen_input.py.map
index ca437c58d..5b4d81942 100644
--- a/packages/safe-ds-lang/tests/resources/generation/python/partial/redundant impurity/generated/tests/generator/partialRedundantImpurity/gen_input.py.map
+++ b/packages/safe-ds-lang/tests/resources/generation/python/partial/redundant impurity/generated/tests/generator/partialRedundantImpurity/gen_input.py.map
@@ -1 +1 @@
-{"version":3,"sources":["input.sdsdev"],"names":["testpipeline","nopartialevalint","purevalue"],"mappings":"AAAA;;;;;;AAeA,IAASA,YAAY;IAMjB,8BAAgBC,gBAAgB,CAAC,CAAC;IAalC,2BAAe,CAAAC,2BAAS,EAAC,CAAC,EAAC,CAAC","file":"gen_input.py"}
\ No newline at end of file
+{"version":3,"sources":["input.sdsdev"],"names":["testpipeline","nopartialevalint","purevalue"],"mappings":"AAAA;;;;;;AAeA,IAASA,YAAY;IAMjB,8BAAgBC,gBAAgB,CAAC,CAAC;IAajC,2BAAa,CAAAC,2BAAS,EAAC,CAAC,EAAC,CAAC","file":"gen_input.py"}
\ No newline at end of file
diff --git a/packages/safe-ds-lang/tests/resources/generation/python/partial/redundant impurity/input.sdsdev b/packages/safe-ds-lang/tests/resources/generation/python/partial/redundant impurity/input.sdsdev
index 13da7c562..081bf05f6 100644
--- a/packages/safe-ds-lang/tests/resources/generation/python/partial/redundant impurity/input.sdsdev
+++ b/packages/safe-ds-lang/tests/resources/generation/python/partial/redundant impurity/input.sdsdev
@@ -32,7 +32,7 @@ pipeline testPipeline {
i1(4); // Should not be generated - impure can not have effects on future statements as they are pure
// $TEST$ target
- val »result« = pureValue - 1;
+ »val result = pureValue - 1;«
i1(4); // Should not be generated - impure cannot affect result after result is already calculated
val someImpureValue = i1(4); // Should not be generated - impure cannot affect result after result is already calculated
}
diff --git a/packages/safe-ds-lang/tests/resources/generation/python/runner integration/statements/output statement/generated/tests/generator/runnerIntegration/outputStatement/gen_input.py b/packages/safe-ds-lang/tests/resources/generation/python/runner integration/statements/output statement/generated/tests/generator/runnerIntegration/outputStatement/gen_input.py
new file mode 100644
index 000000000..bfbd9d584
--- /dev/null
+++ b/packages/safe-ds-lang/tests/resources/generation/python/runner integration/statements/output statement/generated/tests/generator/runnerIntegration/outputStatement/gen_input.py
@@ -0,0 +1,22 @@
+# Imports ----------------------------------------------------------------------
+
+import safeds_runner
+from tests.generator.runnerIntegration.outputStatement import iFileRead, iFileWrite
+
+# Pipelines --------------------------------------------------------------------
+
+def testPipeline():
+ __gen_placeholder_impureFileWrite = iFileWrite('b.txt')
+ safeds_runner.save_placeholder('impureFileWrite', __gen_placeholder_impureFileWrite)
+ __gen_placeholder_impureFileWrite2 = iFileWrite('c.txt')
+ safeds_runner.save_placeholder('impureFileWrite2', __gen_placeholder_impureFileWrite2)
+ __gen_placeholder_impureFileReadAgain = safeds_runner.memoized_static_call(
+ "tests.generator.runnerIntegration.outputStatement.iFileRead",
+ iFileRead,
+ [safeds_runner.absolute_path('d.txt')],
+ {},
+ [safeds_runner.file_mtime('d.txt')]
+ )
+ safeds_runner.save_placeholder('impureFileReadAgain', __gen_placeholder_impureFileReadAgain)
+ __gen_output_4_expression = (__gen_placeholder_impureFileReadAgain) + (2)
+ safeds_runner.save_placeholder('__gen_4_expression', __gen_output_4_expression)
diff --git a/packages/safe-ds-lang/tests/resources/generation/python/runner integration/statements/output statement/generated/tests/generator/runnerIntegration/outputStatement/gen_input.py.map b/packages/safe-ds-lang/tests/resources/generation/python/runner integration/statements/output statement/generated/tests/generator/runnerIntegration/outputStatement/gen_input.py.map
new file mode 100644
index 000000000..f101af2d7
--- /dev/null
+++ b/packages/safe-ds-lang/tests/resources/generation/python/runner integration/statements/output statement/generated/tests/generator/runnerIntegration/outputStatement/gen_input.py.map
@@ -0,0 +1 @@
+{"version":3,"sources":["input.sdsdev"],"names":["testpipeline","ifilewrite","ifileread","impurefilereadagain"],"mappings":"AAAA;;;;;;;AAMA,IAASA,YAAY;IAEjB,oCAAsBC,UAAU,CAAC,OAAO;IAAxC;IACA,qCAAuBA,UAAU,CAAC,OAAO;IAAzC;IACA,wCAA0B;;QAAAC,SAAS;SAAC,4BAAA,OAAO;;SAAP,yBAAA,OAAO;;IAA3C;IAGC,4BAAI,CAAAC,qCAAmB,EAAC,CAAC,EAAC,CAAC","file":"gen_input.py"}
\ No newline at end of file
diff --git a/packages/safe-ds-lang/tests/resources/generation/python/runner integration/statements/output statement/generated/tests/generator/runnerIntegration/outputStatement/gen_input_testPipeline.py b/packages/safe-ds-lang/tests/resources/generation/python/runner integration/statements/output statement/generated/tests/generator/runnerIntegration/outputStatement/gen_input_testPipeline.py
new file mode 100644
index 000000000..09ca75905
--- /dev/null
+++ b/packages/safe-ds-lang/tests/resources/generation/python/runner integration/statements/output statement/generated/tests/generator/runnerIntegration/outputStatement/gen_input_testPipeline.py
@@ -0,0 +1,4 @@
+from .gen_input import testPipeline
+
+if __name__ == '__main__':
+ testPipeline()
diff --git a/packages/safe-ds-lang/tests/resources/generation/python/runner integration/statements/output statement/input.sdsdev b/packages/safe-ds-lang/tests/resources/generation/python/runner integration/statements/output statement/input.sdsdev
new file mode 100644
index 000000000..949eea974
--- /dev/null
+++ b/packages/safe-ds-lang/tests/resources/generation/python/runner integration/statements/output statement/input.sdsdev
@@ -0,0 +1,15 @@
+package tests.generator.runnerIntegration.outputStatement
+
+@Impure([ImpurityReason.FileReadFromParameterizedPath("path")]) fun iFileRead(path: String) -> q: Int
+
+@Impure([ImpurityReason.FileWriteToParameterizedPath("path")]) fun iFileWrite(path: String) -> q: Int
+
+pipeline testPipeline {
+ val impureFileRead = iFileRead("a.txt"); // Should not be generated - cannot affect result
+ val impureFileWrite = iFileWrite("b.txt");
+ val impureFileWrite2 = iFileWrite("c.txt");
+ val impureFileReadAgain = iFileRead("d.txt");
+
+ // $TEST$ target
+ »out impureFileReadAgain + 2;«
+}
diff --git a/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/bad-in block lambda without expression.sdsdev b/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/bad-in block lambda without expression.sdsdev
new file mode 100644
index 000000000..df520fb00
--- /dev/null
+++ b/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/bad-in block lambda without expression.sdsdev
@@ -0,0 +1,7 @@
+// $TEST$ syntax_error
+
+pipeline myPipeline {
+ () {
+ out;
+ };
+}
diff --git a/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/bad-in block lambda without semicolon.sdsdev b/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/bad-in block lambda without semicolon.sdsdev
new file mode 100644
index 000000000..ec9d0ef7b
--- /dev/null
+++ b/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/bad-in block lambda without semicolon.sdsdev
@@ -0,0 +1,7 @@
+// $TEST$ syntax_error
+
+pipeline myPipeline {
+ () {
+ out call()
+ };
+}
diff --git a/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/bad-in pipeline without expression.sdsdev b/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/bad-in pipeline without expression.sdsdev
new file mode 100644
index 000000000..20eee7d58
--- /dev/null
+++ b/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/bad-in pipeline without expression.sdsdev
@@ -0,0 +1,5 @@
+// $TEST$ syntax_error
+
+pipeline myPipeline {
+ out;
+}
diff --git a/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/bad-in pipeline without semicolon.sdsdev b/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/bad-in pipeline without semicolon.sdsdev
new file mode 100644
index 000000000..8cfafcf17
--- /dev/null
+++ b/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/bad-in pipeline without semicolon.sdsdev
@@ -0,0 +1,5 @@
+// $TEST$ syntax_error
+
+pipeline myPipeline {
+ out call()
+}
diff --git a/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/bad-in segment without expression.sdsdev b/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/bad-in segment without expression.sdsdev
new file mode 100644
index 000000000..490219847
--- /dev/null
+++ b/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/bad-in segment without expression.sdsdev
@@ -0,0 +1,5 @@
+// $TEST$ syntax_error
+
+segment mySegment() {
+ out;
+}
diff --git a/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/bad-in segment without semicolon.sdsdev b/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/bad-in segment without semicolon.sdsdev
new file mode 100644
index 000000000..334128faa
--- /dev/null
+++ b/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/bad-in segment without semicolon.sdsdev
@@ -0,0 +1,5 @@
+// $TEST$ syntax_error
+
+segment mySegment() {
+ out call()
+}
diff --git a/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/good-in block lambda.sdsdev b/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/good-in block lambda.sdsdev
new file mode 100644
index 000000000..ca6f6245d
--- /dev/null
+++ b/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/good-in block lambda.sdsdev
@@ -0,0 +1,7 @@
+// $TEST$ no_syntax_error
+
+pipeline myPipeline {
+ () {
+ out call();
+ };
+}
diff --git a/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/good-in pipeline.sdsdev b/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/good-in pipeline.sdsdev
new file mode 100644
index 000000000..13312cda6
--- /dev/null
+++ b/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/good-in pipeline.sdsdev
@@ -0,0 +1,5 @@
+// $TEST$ no_syntax_error
+
+pipeline myPipeline {
+ out call();
+}
diff --git a/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/good-in segment.sdsdev b/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/good-in segment.sdsdev
new file mode 100644
index 000000000..0ca11b937
--- /dev/null
+++ b/packages/safe-ds-lang/tests/resources/grammar/statements/output statements/good-in segment.sdsdev
@@ -0,0 +1,5 @@
+// $TEST$ no_syntax_error
+
+segment mySegment() {
+ out call();
+}
diff --git a/packages/safe-ds-lang/tests/resources/validation/other/statements/expression statements/has no effect/main.sdsdev b/packages/safe-ds-lang/tests/resources/validation/other/statements/expression statements/has no effect/main.sdsdev
index 39d35aa38..b8378192f 100644
--- a/packages/safe-ds-lang/tests/resources/validation/other/statements/expression statements/has no effect/main.sdsdev
+++ b/packages/safe-ds-lang/tests/resources/validation/other/statements/expression statements/has no effect/main.sdsdev
@@ -30,9 +30,9 @@ segment recursiveB() {
}
segment mySegment() {
- // $TEST$ warning "This statement does nothing. Did you forget the assignment?"
+ // $TEST$ warning "This statement does nothing. Did you mean to assign or output the result?"
»1 + 2;«
- // $TEST$ warning "This statement does nothing. Did you forget the assignment?"
+ // $TEST$ warning "This statement does nothing. Did you mean to assign or output the result?"
»pureFunctionWithResults();«
// $TEST$ warning "This statement does nothing."
»MyClass().pureFunctionWithoutResults();«
@@ -43,9 +43,9 @@ segment mySegment() {
»MyClass().impureFunction();«
() {
- // $TEST$ warning "This statement does nothing. Did you forget the assignment?"
+ // $TEST$ warning "This statement does nothing. Did you mean to assign or output the result?"
»1 + 2;«
- // $TEST$ warning "This statement does nothing. Did you forget the assignment?"
+ // $TEST$ warning "This statement does nothing. Did you mean to assign or output the result?"
»pureFunctionWithResults();«
// $TEST$ warning "This statement does nothing."
»MyClass().pureFunctionWithoutResults();«
diff --git a/packages/safe-ds-lang/tests/resources/validation/other/statements/output statements/has no effect/main.sdsdev b/packages/safe-ds-lang/tests/resources/validation/other/statements/output statements/has no effect/main.sdsdev
new file mode 100644
index 000000000..6480ac7ef
--- /dev/null
+++ b/packages/safe-ds-lang/tests/resources/validation/other/statements/output statements/has no effect/main.sdsdev
@@ -0,0 +1,16 @@
+package tests.validation.other.statements.outputStatements.hasNoEffect
+
+pipeline myPipeline {
+ // $TEST$ no warning r"This statement does nothing.*"
+ »out 1 + 2;«
+
+ () {
+ // $TEST$ no warning r"This statement does nothing.*"
+ »out 1 + 2;«
+ };
+}
+
+segment mySegment() {
+ // $TEST$ no warning r"This statement does nothing.*"
+ »out 1 + 2;«
+}
diff --git a/packages/safe-ds-lang/tests/resources/validation/other/statements/output statements/no value/main.sdsdev b/packages/safe-ds-lang/tests/resources/validation/other/statements/output statements/no value/main.sdsdev
new file mode 100644
index 000000000..105df2bd8
--- /dev/null
+++ b/packages/safe-ds-lang/tests/resources/validation/other/statements/output statements/no value/main.sdsdev
@@ -0,0 +1,24 @@
+package tests.validation.other.statements.outputStatements.noValue
+
+@Pure fun noResults()
+
+pipeline myPipeline {
+ // $TEST$ no error 'This expression does not produce a value to output.'
+ out »1 + 2«;
+ // $TEST$ error 'This expression does not produce a value to output.'
+ out »noResults()«;
+
+ () {
+ // $TEST$ no error 'This expression does not produce a value to output.'
+ out »1 + 2«;
+ // $TEST$ no error 'This expression does not produce a value to output.'
+ out »noResults()«;
+ };
+}
+
+segment mySegment() {
+ // $TEST$ no error 'This expression does not produce a value to output.'
+ out »1 + 2«;
+ // $TEST$ no error 'This expression does not produce a value to output.'
+ out »noResults()«;
+}
diff --git a/packages/safe-ds-lang/tests/resources/validation/other/statements/output statements/only in pipeline/main.sdsdev b/packages/safe-ds-lang/tests/resources/validation/other/statements/output statements/only in pipeline/main.sdsdev
new file mode 100644
index 000000000..319da4c87
--- /dev/null
+++ b/packages/safe-ds-lang/tests/resources/validation/other/statements/output statements/only in pipeline/main.sdsdev
@@ -0,0 +1,16 @@
+package tests.validation.other.statements.outputStatements.onlyInPipeline
+
+pipeline myPipeline {
+ // $TEST$ no error "Output statements can only be used in a pipeline."
+ »out 1 + 2;«
+
+ () {
+ // $TEST$ error "Output statements can only be used in a pipeline."
+ »out 1 + 2;«
+ };
+}
+
+segment mySegment() {
+ // $TEST$ error "Output statements can only be used in a pipeline."
+ »out 1 + 2;«
+}
diff --git a/packages/safe-ds-vscode/package.json b/packages/safe-ds-vscode/package.json
index 65159958e..c8b392fba 100644
--- a/packages/safe-ds-vscode/package.json
+++ b/packages/safe-ds-vscode/package.json
@@ -255,11 +255,6 @@
"title": "Open Diagnostics Dumps in New VS Code Window",
"category": "Safe-DS"
},
- {
- "command": "safe-ds.refreshWebview",
- "title": "Refresh Webview",
- "category": "Safe-DS"
- },
{
"command": "safe-ds.updateRunner",
"title": "Update the Safe-DS Runner",
diff --git a/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts
index 73d93be9f..80dbee103 100644
--- a/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts
+++ b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts
@@ -10,8 +10,8 @@ import {
ProfilingDetailStatistical,
Table,
} from '@safe-ds/eda/types/state.js';
-import { ast, CODEGEN_PREFIX, messages, SafeDsServices } from '@safe-ds/lang';
-import { LangiumDocument } from 'langium';
+import { CODEGEN_PREFIX, messages, SafeDsServices } from '@safe-ds/lang';
+import { AstUtils, LangiumDocument } from 'langium';
import * as vscode from 'vscode';
import crypto from 'crypto';
import { getPipelineDocument } from '../../mainClient.ts';
@@ -21,12 +21,19 @@ import {
MultipleRunnerExecutionResultMessage,
RunnerExecutionResultMessage,
} from '@safe-ds/eda/types/messaging.ts';
+import {
+ isSdsOutputStatement,
+ isSdsPipeline,
+ isSdsStatement,
+ SdsModule,
+} from '../../../../../safe-ds-lang/src/language/generated/ast.js';
+import { getModuleMembers, getPlaceholderByName } from '../../../../../safe-ds-lang/src/language/index.js';
export class RunnerApi {
services: SafeDsServices;
pipelinePath: vscode.Uri;
pipelineName: string;
- pipelineNode: ast.SdsPipeline;
+ pipelineNodeEndOffset: number;
tablePlaceholder: string;
baseDocument: LangiumDocument | undefined;
placeholderCounter = 0;
@@ -35,16 +42,16 @@ export class RunnerApi {
services: SafeDsServices,
pipelinePath: vscode.Uri,
pipelineName: string,
- pipelineNode: ast.SdsPipeline,
+ pipelineNodeEndOffset: number,
tablePlaceholder: string,
) {
this.services = services;
this.pipelinePath = pipelinePath;
this.pipelineName = pipelineName;
- this.pipelineNode = pipelineNode;
+ this.pipelineNodeEndOffset = pipelineNodeEndOffset;
this.tablePlaceholder = tablePlaceholder;
getPipelineDocument(this.pipelinePath).then((doc) => {
- // Get here to avoid issues because of chanigng file
+ // Get here to avoid issues because of changing file
// Make sure to create new instance of RunnerApi if pipeline execution of fresh pipeline is needed
// (e.g. launching of extension on table with existing state but no current panel)
this.baseDocument = doc;
@@ -65,11 +72,7 @@ export class RunnerApi {
const documentText = this.baseDocument.textDocument.getText();
- const endOfPipeline = this.pipelineNode.$cstNode?.end;
- if (!endOfPipeline) {
- reject('Pipeline not found');
- return;
- }
+ const endOfPipeline = this.pipelineNodeEndOffset;
let newDocumentText;
@@ -79,18 +82,38 @@ export class RunnerApi {
const afterPipelineEnd = documentText.substring(endOfPipeline - 1);
newDocumentText = beforePipelineEnd + addedLines + afterPipelineEnd;
- const newDoc = this.services.shared.workspace.LangiumDocumentFactory.fromString(
+ let newDoc = this.services.shared.workspace.LangiumDocumentFactory.fromString(
+ newDocumentText,
+ this.pipelinePath,
+ );
+
+ newDocumentText = this.replaceOutputStatements(newDoc);
+ safeDsLogger.debug(newDocumentText);
+ newDoc = this.services.shared.workspace.LangiumDocumentFactory.fromString(
newDocumentText,
this.pipelinePath,
);
await this.services.shared.workspace.DocumentBuilder.build([newDoc]);
+ let targetStatements: number[] = [];
+ for (const moduleMember of getModuleMembers(newDoc.parseResult.value as SdsModule)) {
+ if (isSdsPipeline(moduleMember) && moduleMember.name === this.pipelineName) {
+ for (const name of placeholderNames ?? []) {
+ const placeholder = getPlaceholderByName(moduleMember.body, name);
+ const statement = AstUtils.getContainerOfType(placeholder, isSdsStatement);
+ if (statement) {
+ targetStatements.push(statement.$containerIndex!);
+ }
+ }
+ }
+ }
+
safeDsLogger.debug(`Executing pipeline ${this.pipelineName} with added lines`);
await this.services.runtime.Runner.executePipeline(
pipelineExecutionId,
newDoc,
this.pipelineName,
- placeholderNames,
+ targetStatements,
);
this.services.shared.workspace.LangiumDocuments.deleteDocument(this.pipelinePath);
@@ -126,6 +149,36 @@ export class RunnerApi {
}
//#endregion
+ private replaceOutputStatements(doc: LangiumDocument): string {
+ const outputStatements = AstUtils.streamAst(doc.parseResult.value)
+ .filter(isSdsOutputStatement)
+ .toArray()
+ .reverse();
+
+ let documentText = doc.textDocument.getText();
+
+ for (const outputStatement of outputStatements) {
+ const cstNode = outputStatement.$cstNode;
+ const index = outputStatement.$containerIndex;
+ const expressionCstNode = outputStatement.expression.$cstNode;
+ if (!cstNode || !index || !expressionCstNode) {
+ continue;
+ }
+
+ const assignees = this.services.helpers.SyntheticProperties.getValueNamesForExpression(
+ outputStatement.expression,
+ )
+ .map((valueName) => `val ${CODEGEN_PREFIX}${index}_${valueName}`)
+ .join(', ');
+
+ const replacement = `${assignees} = ${expressionCstNode.text};`;
+ documentText =
+ documentText.substring(0, cstNode.offset) + replacement + documentText.substring(cstNode.end);
+ }
+
+ return documentText;
+ }
+
//#region Helpers
private runnerResultToTable(tableName: string, runnerResult: any, columnIsNumeric: Map): Table {
const table: Table = {
diff --git a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts
index 74eac4344..4a6069f40 100644
--- a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts
+++ b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts
@@ -2,7 +2,7 @@ import * as vscode from 'vscode';
import { ToExtensionMessage } from '@safe-ds/eda/types/messaging.js';
import * as webviewApi from './apis/webviewApi.ts';
import { Table } from '@safe-ds/eda/types/state.ts';
-import { SafeDsServices, ast } from '@safe-ds/lang';
+import { SafeDsServices } from '@safe-ds/lang';
import { RunnerApi } from './apis/runnerApi.ts';
import { safeDsLogger } from '../helpers/logging.js';
@@ -33,14 +33,14 @@ export class EDAPanel {
startPipelineExecutionId: string,
pipelinePath: vscode.Uri,
pipelineName: string,
- pipelineNode: ast.SdsPipeline,
+ pipelineNodeEndOffset: number,
tableName: string,
) {
this.tableIdentifier = pipelineName + '.' + tableName;
this.panel = panel;
this.extensionUri = extensionUri;
this.startPipelineExecutionId = startPipelineExecutionId;
- this.runnerApi = new RunnerApi(EDAPanel.services, pipelinePath, pipelineName, pipelineNode, tableName);
+ this.runnerApi = new RunnerApi(EDAPanel.services, pipelinePath, pipelineName, pipelineNodeEndOffset, tableName);
this.tableName = tableName;
// Set the webview's initial html content
@@ -265,7 +265,7 @@ export class EDAPanel {
services: SafeDsServices,
pipelinePath: vscode.Uri,
pipelineName: string,
- pipelineNode: ast.SdsPipeline,
+ pipelineNodeEndOffset: number,
tableName: string,
): Promise {
EDAPanel.context = context;
@@ -282,7 +282,7 @@ export class EDAPanel {
panel.panel.reveal(panel.column);
panel.tableIdentifier = tableIdentifier;
panel.startPipelineExecutionId = startPipelineExecutionId;
- panel.runnerApi = new RunnerApi(services, pipelinePath, pipelineName, pipelineNode, tableName);
+ panel.runnerApi = new RunnerApi(services, pipelinePath, pipelineName, pipelineNodeEndOffset, tableName);
panel.tableName = tableName;
EDAPanel.panelsMap.set(tableIdentifier, panel);
@@ -312,7 +312,7 @@ export class EDAPanel {
startPipelineExecutionId,
pipelinePath,
pipelineName,
- pipelineNode,
+ pipelineNodeEndOffset,
tableName,
);
EDAPanel.panelsMap.set(tableIdentifier, edaPanel);
@@ -414,7 +414,7 @@ export class EDAPanel {
try {
await vscode.workspace.fs.stat(scriptPath);
safeDsLogger.info('Using EDA build from EDA package.');
- } catch (error) {
+ } catch (_error) {
// If not use the static one from the dist folder here
safeDsLogger.info('Using EDA build from local dist.');
scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'dist', 'eda-webview', 'main.js'));
diff --git a/packages/safe-ds-vscode/src/extension/mainClient.ts b/packages/safe-ds-vscode/src/extension/mainClient.ts
index c389db6bc..987ee7f3a 100644
--- a/packages/safe-ds-vscode/src/extension/mainClient.ts
+++ b/packages/safe-ds-vscode/src/extension/mainClient.ts
@@ -3,14 +3,12 @@ import * as vscode from 'vscode';
import { Uri } from 'vscode';
import type { LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node.js';
import { LanguageClient, TransportKind } from 'vscode-languageclient/node.js';
-import { ast, createSafeDsServices, getModuleMembers, messages, rpc, SafeDsServices } from '@safe-ds/lang';
+import { ast, createSafeDsServices, rpc, SafeDsServices } from '@safe-ds/lang';
import { NodeFileSystem } from 'langium/node';
-import crypto from 'crypto';
-import { AstUtils, LangiumDocument } from 'langium';
+import { LangiumDocument } from 'langium';
import { EDAPanel } from './eda/edaPanel.ts';
import { dumpDiagnostics } from './actions/dumpDiagnostics.js';
import { openDiagnosticsDumps } from './actions/openDiagnosticsDumps.js';
-import { isSdsPlaceholder } from '../../../safe-ds-lang/src/language/generated/ast.js';
import { installRunner } from './actions/installRunner.js';
import { updateRunner } from './actions/updateRunner.js';
import { safeDsLogger } from './helpers/logging.js';
@@ -18,11 +16,6 @@ import { showImage } from './actions/showImage.js';
let client: LanguageClient;
let services: SafeDsServices;
-let lastFinishedPipelineExecutionId: string | undefined;
-let lastSuccessfulPipelineName: string | undefined;
-let lastSuccessfulTableName: string | undefined;
-let lastSuccessfulPipelinePath: vscode.Uri | undefined;
-let lastSuccessfulPipelineNode: ast.SdsPipeline | undefined;
/**
* This function is called when the extension is activated.
@@ -106,6 +99,7 @@ const registerNotificationListeners = function (context: vscode.ExtensionContext
client.onNotification(rpc.UpdateRunnerNotification.type, async () => {
await updateRunner(context, client)();
}),
+ client.onNotification(rpc.ExploreTableNotification.type, exploreTable(context)),
client.onNotification(rpc.ShowImageNotification.type, showImage(context)),
);
};
@@ -113,181 +107,24 @@ const registerNotificationListeners = function (context: vscode.ExtensionContext
const registerCommands = function (context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('safe-ds.dumpDiagnostics', dumpDiagnostics(context)),
- vscode.commands.registerCommand('safe-ds.exploreTable', exploreTable(context)),
vscode.commands.registerCommand('safe-ds.installRunner', installRunner(client)),
vscode.commands.registerCommand('safe-ds.openDiagnosticsDumps', openDiagnosticsDumps(context)),
- vscode.commands.registerCommand('safe-ds.refreshWebview', refreshWebview(context)),
vscode.commands.registerCommand('safe-ds.updateRunner', updateRunner(context, client)),
);
};
-const refreshWebview = function (context: vscode.ExtensionContext) {
- return async () => {
- if (
- !lastSuccessfulPipelinePath ||
- !lastFinishedPipelineExecutionId ||
- !lastSuccessfulPipelineName ||
- !lastSuccessfulTableName ||
- !lastSuccessfulPipelineNode
- ) {
- vscode.window.showErrorMessage('No EDA Panel to refresh!');
- return;
- }
- EDAPanel.kill(lastSuccessfulPipelineName! + '.' + lastSuccessfulTableName!);
- setTimeout(() => {
- EDAPanel.createOrShow(
- context.extensionUri,
- context,
- lastFinishedPipelineExecutionId!,
- services,
- lastSuccessfulPipelinePath!,
- lastSuccessfulPipelineName!,
- lastSuccessfulPipelineNode!,
- lastSuccessfulTableName!,
- );
- }, 100);
- setTimeout(() => {
- vscode.commands.executeCommand('workbench.action.webview.openDeveloperTools');
- }, 100);
- };
-};
-
-const doRunPipelineFile = async function (
- filePath: vscode.Uri | undefined,
- pipelineExecutionId: string,
- knownPipelineName?: string,
- placeholderNames?: string[],
-) {
- const document = await getPipelineDocument(filePath);
-
- if (document) {
- // Run it
- let pipelineName;
- if (!knownPipelineName) {
- const firstPipeline = getModuleMembers(document.parseResult.value).find(ast.isSdsPipeline);
- if (firstPipeline === undefined) {
- safeDsLogger.error('Cannot execute: no pipeline found');
- vscode.window.showErrorMessage('The current file cannot be executed, as no pipeline could be found.');
- return;
- }
- pipelineName = services.builtins.Annotations.getPythonName(firstPipeline) ?? firstPipeline.name;
- } else {
- pipelineName = knownPipelineName;
- }
-
- safeDsLogger.info(`Launching Pipeline (${pipelineExecutionId}): ${filePath} - ${pipelineName}`);
-
- await services.runtime.Runner.executePipeline(pipelineExecutionId, document, pipelineName, placeholderNames);
- }
-};
-
const exploreTable = (context: vscode.ExtensionContext) => {
- return async (documentUri: string, nodePath: string) => {
- await vscode.workspace.saveAll();
-
- const uri = Uri.parse(documentUri);
-
- const document = await getPipelineDocument(Uri.parse(documentUri));
- if (!document) {
- vscode.window.showErrorMessage('Could not find document.');
- return;
- }
-
- const root = document.parseResult.value;
- const placeholderNode = services.workspace.AstNodeLocator.getAstNode(root, nodePath);
- if (!isSdsPlaceholder(placeholderNode)) {
- vscode.window.showErrorMessage('Selected node is not a placeholder.');
- return;
- }
-
- const pipelineNode = AstUtils.getContainerOfType(placeholderNode, ast.isSdsPipeline);
- if (!pipelineNode) {
- vscode.window.showErrorMessage('Selected placeholder is not in a pipeline.');
- return;
- }
-
- const pipelineName = pipelineNode.name;
- const requestedPlaceholderName = placeholderNode.name;
-
- // gen custom id for pipeline
- const pipelineExecutionId = crypto.randomUUID();
-
- let loadingInProgress = true; // Flag to track loading status
- // Show progress indicator
- vscode.window.withProgress(
- {
- location: vscode.ProgressLocation.Window,
- title: 'Loading Table...',
- },
- (progress, _) => {
- progress.report({ increment: 0 });
- return new Promise((resolve) => {
- // Resolve the promise when loading is no longer in progress
- const checkInterval = setInterval(() => {
- if (!loadingInProgress) {
- clearInterval(checkInterval);
- resolve();
- }
- }, 1000); // Check every second
- });
- },
+ return async (data: rpc.ExploreTableNotification) => {
+ await EDAPanel.createOrShow(
+ context.extensionUri,
+ context,
+ data.pipelineExecutionId,
+ services,
+ Uri.parse(data.uri),
+ data.pipelineName,
+ data.pipelineNodeEndOffset,
+ data.placeholderName,
);
- const cleanupLoadingIndication = () => {
- loadingInProgress = false;
- };
-
- const placeholderTypeCallback = function (message: messages.PlaceholderTypeMessage) {
- safeDsLogger.info(
- `Placeholder was calculated (${message.id}): ${message.data.name} of type ${message.data.type}`,
- );
- if (message.id === pipelineExecutionId && message.data.name === requestedPlaceholderName) {
- lastFinishedPipelineExecutionId = pipelineExecutionId;
- lastSuccessfulPipelinePath = uri;
- lastSuccessfulTableName = requestedPlaceholderName;
- lastSuccessfulPipelineName = pipelineName;
- lastSuccessfulPipelineNode = pipelineNode;
- EDAPanel.createOrShow(
- context.extensionUri,
- context,
- pipelineExecutionId,
- services,
- uri,
- pipelineName,
- pipelineNode,
- message.data.name,
- );
- services.runtime.PythonServer.removeMessageCallback('placeholder_type', placeholderTypeCallback);
- cleanupLoadingIndication();
- }
- };
- services.runtime.PythonServer.addMessageCallback('placeholder_type', placeholderTypeCallback);
-
- const runtimeProgressCallback = function (message: messages.RuntimeProgressMessage) {
- safeDsLogger.info(`Runner-Progress (${message.id}): ${message.data}`);
- if (
- message.id === pipelineExecutionId &&
- message.data === 'done' &&
- lastFinishedPipelineExecutionId !== pipelineExecutionId
- ) {
- lastFinishedPipelineExecutionId = pipelineExecutionId;
- vscode.window.showErrorMessage(`Selected text is not a placeholder!`);
- services.runtime.PythonServer.removeMessageCallback('runtime_progress', runtimeProgressCallback);
- cleanupLoadingIndication();
- }
- };
- services.runtime.PythonServer.addMessageCallback('runtime_progress', runtimeProgressCallback);
-
- const runtimeErrorCallback = function (message: messages.RuntimeErrorMessage) {
- if (message.id === pipelineExecutionId && lastFinishedPipelineExecutionId !== pipelineExecutionId) {
- lastFinishedPipelineExecutionId = pipelineExecutionId;
- vscode.window.showErrorMessage(`Pipeline ran into an Error!`);
- services.runtime.PythonServer.removeMessageCallback('runtime_error', runtimeErrorCallback);
- cleanupLoadingIndication();
- }
- };
- services.runtime.PythonServer.addMessageCallback('runtime_error', runtimeErrorCallback);
-
- await doRunPipelineFile(uri, pipelineExecutionId, pipelineName, [requestedPlaceholderName]);
};
};
diff --git a/packages/safe-ds-vscode/syntaxes/safe-ds.tmLanguage.json b/packages/safe-ds-vscode/syntaxes/safe-ds.tmLanguage.json
index adbacd5b5..3cfc67340 100644
--- a/packages/safe-ds-vscode/syntaxes/safe-ds.tmLanguage.json
+++ b/packages/safe-ds-vscode/syntaxes/safe-ds.tmLanguage.json
@@ -13,7 +13,7 @@
},
{
"name": "storage.modifier.safe-ds",
- "match": "\\b(const|internal|private)\\b"
+ "match": "\\b(const|internal|out|private)\\b"
},
{
"name": "keyword.operator.expression.safe-ds",