From 011ba31ec1d54629ec083807c23e6de04a004580 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Sun, 10 Nov 2024 16:12:55 +0100 Subject: [PATCH] feat: output statement (#1262) Closes #1259 ### Summary of Changes Add a new kind of statement, the output statement, to inspect results of expressions without creating useless placeholders. --- .../safeds/data/tabular/containers/Column.md | 18 +- docs/mkdocs.yml | 1 + docs/pipeline-language/statements/README.md | 12 +- .../statements/assignments.md | 6 +- .../statements/output-statements.md | 26 ++ packages/safe-ds-cli/src/cli/generate.ts | 2 +- .../src/language/communication/commands.ts | 1 + .../src/language/communication/rpc.ts | 34 ++ .../src/language/flow/safe-ds-slicer.ts | 27 +- .../python/safe-ds-python-generator.ts | 113 ++++++- .../src/language/grammar/safe-ds.langium | 13 +- .../helpers/safe-ds-synthetic-properties.ts | 50 +++ .../lsp/safe-ds-code-lens-provider.ts | 149 +++++++-- .../lsp/safe-ds-execute-command-handler.ts | 18 +- .../src/language/lsp/safe-ds-formatter.ts | 9 + .../purity/safe-ds-purity-computer.ts | 33 +- .../language/runtime/safe-ds-python-server.ts | 22 +- .../src/language/runtime/safe-ds-runner.ts | 292 ++++++++++-------- .../src/language/safe-ds-module.ts | 3 + .../other/statements/outputStatements.ts | 39 +++ .../validation/other/statements/statements.ts | 34 +- .../language/validation/safe-ds-validator.ts | 5 + .../data/tabular/containers/Column.sdsstub | 2 +- .../language/flow/safe-ds-slicer.test.ts | 78 +++-- .../safe-ds-python-generator.test.ts | 25 +- .../getValueNamesForExpression.test.ts | 152 +++++++++ .../lsp/safe-ds-code-lens-provider.test.ts | 219 ++++++++++--- .../output statements/in block lambda.sdsdev | 13 + .../output statements/in pipeline.sdsdev | 9 + .../output statements/in segment.sdsdev | 9 + .../gen_input.py.map | 2 +- .../input.sdsdev | 2 +- .../gen_input.py.map | 2 +- .../input.sdsdev | 2 +- .../partialImpureDependency/gen_input.py.map | 2 +- .../partial/impure dependency/input.sdsdev | 2 +- .../partialPureDependency/gen_input.py.map | 2 +- .../partial/pure dependency/input.sdsdev | 2 +- .../partialRedundantImpurity/gen_input.py.map | 2 +- .../partial/redundant impurity/input.sdsdev | 2 +- .../outputStatement/gen_input.py | 22 ++ .../outputStatement/gen_input.py.map | 1 + .../outputStatement/gen_input_testPipeline.py | 4 + .../statements/output statement/input.sdsdev | 15 + ...-in block lambda without expression.sdsdev | 7 + ...d-in block lambda without semicolon.sdsdev | 7 + .../bad-in pipeline without expression.sdsdev | 5 + .../bad-in pipeline without semicolon.sdsdev | 5 + .../bad-in segment without expression.sdsdev | 5 + .../bad-in segment without semicolon.sdsdev | 5 + .../good-in block lambda.sdsdev | 7 + .../output statements/good-in pipeline.sdsdev | 5 + .../output statements/good-in segment.sdsdev | 5 + .../has no effect/main.sdsdev | 8 +- .../has no effect/main.sdsdev | 16 + .../output statements/no value/main.sdsdev | 24 ++ .../only in pipeline/main.sdsdev | 16 + packages/safe-ds-vscode/package.json | 5 - .../src/extension/eda/apis/runnerApi.ts | 79 ++++- .../src/extension/eda/edaPanel.ts | 14 +- .../src/extension/mainClient.ts | 189 +----------- .../syntaxes/safe-ds.tmLanguage.json | 2 +- 62 files changed, 1348 insertions(+), 532 deletions(-) create mode 100644 docs/pipeline-language/statements/output-statements.md create mode 100644 packages/safe-ds-lang/src/language/helpers/safe-ds-synthetic-properties.ts create mode 100644 packages/safe-ds-lang/src/language/validation/other/statements/outputStatements.ts create mode 100644 packages/safe-ds-lang/tests/language/helpers/safe-ds-synthetic-properties/getValueNamesForExpression.test.ts create mode 100644 packages/safe-ds-lang/tests/resources/formatting/statements/output statements/in block lambda.sdsdev create mode 100644 packages/safe-ds-lang/tests/resources/formatting/statements/output statements/in pipeline.sdsdev create mode 100644 packages/safe-ds-lang/tests/resources/formatting/statements/output statements/in segment.sdsdev create mode 100644 packages/safe-ds-lang/tests/resources/generation/python/runner integration/statements/output statement/generated/tests/generator/runnerIntegration/outputStatement/gen_input.py create mode 100644 packages/safe-ds-lang/tests/resources/generation/python/runner integration/statements/output statement/generated/tests/generator/runnerIntegration/outputStatement/gen_input.py.map create mode 100644 packages/safe-ds-lang/tests/resources/generation/python/runner integration/statements/output statement/generated/tests/generator/runnerIntegration/outputStatement/gen_input_testPipeline.py create mode 100644 packages/safe-ds-lang/tests/resources/generation/python/runner integration/statements/output statement/input.sdsdev create mode 100644 packages/safe-ds-lang/tests/resources/grammar/statements/output statements/bad-in block lambda without expression.sdsdev create mode 100644 packages/safe-ds-lang/tests/resources/grammar/statements/output statements/bad-in block lambda without semicolon.sdsdev create mode 100644 packages/safe-ds-lang/tests/resources/grammar/statements/output statements/bad-in pipeline without expression.sdsdev create mode 100644 packages/safe-ds-lang/tests/resources/grammar/statements/output statements/bad-in pipeline without semicolon.sdsdev create mode 100644 packages/safe-ds-lang/tests/resources/grammar/statements/output statements/bad-in segment without expression.sdsdev create mode 100644 packages/safe-ds-lang/tests/resources/grammar/statements/output statements/bad-in segment without semicolon.sdsdev create mode 100644 packages/safe-ds-lang/tests/resources/grammar/statements/output statements/good-in block lambda.sdsdev create mode 100644 packages/safe-ds-lang/tests/resources/grammar/statements/output statements/good-in pipeline.sdsdev create mode 100644 packages/safe-ds-lang/tests/resources/grammar/statements/output statements/good-in segment.sdsdev create mode 100644 packages/safe-ds-lang/tests/resources/validation/other/statements/output statements/has no effect/main.sdsdev create mode 100644 packages/safe-ds-lang/tests/resources/validation/other/statements/output statements/no value/main.sdsdev create mode 100644 packages/safe-ds-lang/tests/resources/validation/other/statements/output statements/only in pipeline/main.sdsdev 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",