From a237c346a3674588eb2059473d6541b7bab9a159 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 20 Nov 2023 17:31:32 +0100 Subject: [PATCH 1/4] refactor: rename method --- packages/safe-ds-lang/src/language/helpers/astUtils.ts | 4 ++-- .../src/language/scoping/safe-ds-scope-provider.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/safe-ds-lang/src/language/helpers/astUtils.ts b/packages/safe-ds-lang/src/language/helpers/astUtils.ts index 8a82b3865..bc7add5da 100644 --- a/packages/safe-ds-lang/src/language/helpers/astUtils.ts +++ b/packages/safe-ds-lang/src/language/helpers/astUtils.ts @@ -1,8 +1,8 @@ import { AstNode, hasContainerOfType } from 'langium'; /** - * Returns whether the inner node is contained in the outer node. If the nodes are equal, this function returns `true`. + * Returns whether the inner node is contained in the outer node or equal to it. */ -export const isContainedIn = (inner: AstNode | undefined, outer: AstNode | undefined): boolean => { +export const isContainedInOrEqual = (inner: AstNode | undefined, outer: AstNode | undefined): boolean => { return hasContainerOfType(inner, (it) => it === outer); }; diff --git a/packages/safe-ds-lang/src/language/scoping/safe-ds-scope-provider.ts b/packages/safe-ds-lang/src/language/scoping/safe-ds-scope-provider.ts index 64377ec2d..a0fd47437 100644 --- a/packages/safe-ds-lang/src/language/scoping/safe-ds-scope-provider.ts +++ b/packages/safe-ds-lang/src/language/scoping/safe-ds-scope-provider.ts @@ -53,7 +53,7 @@ import { SdsTypeArgument, SdsYield, } from '../generated/ast.js'; -import { isContainedIn } from '../helpers/astUtils.js'; +import { isContainedInOrEqual } from '../helpers/astUtils.js'; import { getAbstractResults, getAnnotationCallTarget, @@ -336,7 +336,7 @@ export class SafeDsScopeProvider extends DefaultScopeProvider { const containingStatement = getContainerOfType(node.$container, isSdsStatement); let placeholders: Iterable; - if (!containingCallable || isContainedIn(containingStatement, containingCallable)) { + if (!containingCallable || isContainedInOrEqual(containingStatement, containingCallable)) { placeholders = this.placeholdersUpToStatement(containingStatement); } else { // Placeholders are further away than the parameters From 940db30c6391312586221de0ac150377871d35a8 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 20 Nov 2023 17:34:04 +0100 Subject: [PATCH 2/4] test: fix wrong test --- .../resources/call graph/callable/block lambda.sdstest | 4 ++-- .../call graph/callable/expression lambda.sdstest | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/safe-ds-lang/tests/resources/call graph/callable/block lambda.sdstest b/packages/safe-ds-lang/tests/resources/call graph/callable/block lambda.sdstest index dcf02d836..891b02668 100644 --- a/packages/safe-ds-lang/tests/resources/call graph/callable/block lambda.sdstest +++ b/packages/safe-ds-lang/tests/resources/call graph/callable/block lambda.sdstest @@ -3,7 +3,7 @@ package tests.callGraph.callable.blockLambda @Pure fun f() -> r: Any @Pure fun g() -> r: Any -// $TEST$ ["$expressionLambda", "f", "g"] pipeline myPipeline { - »(param: Any = f()) -> g()«; + // $TEST$ ["$blockLambda", "f", "g"] + »(param: Any = f()) { g(); }«; } diff --git a/packages/safe-ds-lang/tests/resources/call graph/callable/expression lambda.sdstest b/packages/safe-ds-lang/tests/resources/call graph/callable/expression lambda.sdstest index 4c36fa783..1b6af2fd3 100644 --- a/packages/safe-ds-lang/tests/resources/call graph/callable/expression lambda.sdstest +++ b/packages/safe-ds-lang/tests/resources/call graph/callable/expression lambda.sdstest @@ -1 +1,9 @@ package tests.callGraph.callable.expressionLambda + +@Pure fun f() -> r: Any +@Pure fun g() -> r: Any + +pipeline myPipeline { + // $TEST$ ["$expressionLambda", "f", "g"] + »(param: Any = f()) -> g()«; +} From ae6b7f5de20fa6a9a648140fd655e5c59af74391 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 20 Nov 2023 18:04:34 +0100 Subject: [PATCH 3/4] feat: check purity/side effects of expressions --- .../purity/safe-ds-purity-computer.ts | 113 ++++++-- .../purity/safe-ds-purity-computer.test.ts | 241 +++++++++++++++--- 2 files changed, 300 insertions(+), 54 deletions(-) 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 d0be04234..f4abed6be 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 @@ -1,4 +1,12 @@ -import { type AstNode, type AstNodeLocator, EMPTY_STREAM, getDocument, Stream, WorkspaceCache } from 'langium'; +import { + type AstNode, + type AstNodeLocator, + EMPTY_STREAM, + getContainerOfType, + getDocument, + Stream, + WorkspaceCache, +} from 'langium'; import { isEmpty } from '../../helpers/collectionUtils.js'; import type { SafeDsCallGraphComputer } from '../flow/safe-ds-call-graph-computer.js'; import type { SafeDsServices } from '../safe-ds-module.js'; @@ -9,11 +17,20 @@ import { OtherImpurityReason, PotentiallyImpureParameterCall, } from './model.js'; -import { isSdsFunction, SdsCall, SdsCallable, SdsFunction, SdsParameter } from '../generated/ast.js'; +import { + isSdsFunction, + isSdsLambda, + SdsCall, + SdsCallable, + SdsExpression, + SdsFunction, + SdsParameter, +} from '../generated/ast.js'; 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 { getParameters } from '../helpers/nodeProperties.js'; +import { isContainedInOrEqual } from '../helpers/astUtils.js'; export class SafeDsPurityComputer { private readonly astNodeLocator: AstNodeLocator; @@ -32,45 +49,95 @@ export class SafeDsPurityComputer { this.reasonsCache = new WorkspaceCache(services.shared); } + // We need separate methods for callables and expressions because lambdas are both. The caller must decide whether + // the lambda should get "executed" (***Callable methods) when computing the impurity reasons or not (***Expression + // methods). + + /** + * Returns whether the given callable is pure. + * + * @param node + * The callable 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. + */ + isPureCallable(node: SdsCallable, substitutions = NO_SUBSTITUTIONS): boolean { + return isEmpty(this.getImpurityReasonsForCallable(node, substitutions)); + } + + /** + * Returns whether the given expression is pure. + * + * @param node + * The expression 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. + */ + isPureExpression(node: SdsExpression, substitutions = NO_SUBSTITUTIONS): boolean { + return isEmpty(this.getImpurityReasonsForExpression(node, substitutions)); + } + + /** + * Returns whether the given callable has side effects. + * + * @param node + * The callable 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. + */ + callableHasSideEffects(node: SdsCallable, substitutions = NO_SUBSTITUTIONS): boolean { + return this.getImpurityReasonsForCallable(node, substitutions).some((it) => it.isSideEffect); + } + /** - * Returns whether the given call/callable is pure. + * Returns whether the given expression has side effects. * * @param node - * The call/callable to check. + * The expression to check. * * @param substitutions - * The parameter substitutions to use. These are **not** the argument of the call, but the values of the parameters - * of any containing callables, i.e. the context of the call/callable. + * 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. */ - isPure(node: SdsCall | SdsCallable, substitutions = NO_SUBSTITUTIONS): boolean { - return isEmpty(this.getImpurityReasons(node, substitutions)); + expressionHasSideEffects(node: SdsExpression, substitutions = NO_SUBSTITUTIONS): boolean { + return this.getImpurityReasonsForExpression(node, substitutions).some((it) => it.isSideEffect); } /** - * Returns whether the given call/callable has side effects. + * Returns the reasons why the given callable is impure. * * @param node - * The call/callable to check. + * The callable to check. * * @param substitutions - * The parameter substitutions to use. These are **not** the argument of the call, but the values of the parameters - * of any containing callables, i.e. the context of the call/callable. + * 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. */ - hasSideEffects(node: SdsCall | SdsCallable, substitutions = NO_SUBSTITUTIONS): boolean { - return this.getImpurityReasons(node, substitutions).some((it) => it.isSideEffect); + getImpurityReasonsForCallable(node: SdsCallable, substitutions = NO_SUBSTITUTIONS): ImpurityReason[] { + return this.getImpurityReasons(node, substitutions); } /** - * Returns the reasons why the given call/callable is impure. + * Returns the reasons why the given expression is impure. * * @param node - * The call/callable to check. + * The expression to check. * * @param substitutions - * The parameter substitutions to use. These are **not** the argument of the call, but the values of the parameters - * of any containing callables, i.e. the context of the call/callable. + * 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. */ - getImpurityReasons(node: SdsCall | SdsCallable, substitutions = NO_SUBSTITUTIONS): ImpurityReason[] { + getImpurityReasonsForExpression(node: SdsExpression, substitutions = NO_SUBSTITUTIONS): ImpurityReason[] { + return this.getExecutedCallsInExpression(node).flatMap((it) => this.getImpurityReasons(it, substitutions)); + } + + private getImpurityReasons(node: SdsCall | SdsCallable, substitutions = NO_SUBSTITUTIONS): ImpurityReason[] { const key = this.getNodeId(node); return this.reasonsCache.get(key, () => { return this.callGraphComputer @@ -87,6 +154,14 @@ export class SafeDsPurityComputer { }); } + private getExecutedCallsInExpression(expression: SdsExpression): SdsCall[] { + return this.callGraphComputer.getAllContainedCalls(expression).filter((it) => { + // Keep only calls that are not contained in a lambda inside the expression + const containingLambda = getContainerOfType(it, isSdsLambda); + return !containingLambda || !isContainedInOrEqual(containingLambda, expression); + }); + } + private getImpurityReasonsForFunction(node: SdsFunction): Stream { return this.builtinAnnotations.streamImpurityReasons(node).flatMap((it) => { switch (it.variant) { diff --git a/packages/safe-ds-lang/tests/language/purity/safe-ds-purity-computer.test.ts b/packages/safe-ds-lang/tests/language/purity/safe-ds-purity-computer.test.ts index 3b29efd6f..79f4ed14f 100644 --- a/packages/safe-ds-lang/tests/language/purity/safe-ds-purity-computer.test.ts +++ b/packages/safe-ds-lang/tests/language/purity/safe-ds-purity-computer.test.ts @@ -1,152 +1,242 @@ import { describe, expect, it } from 'vitest'; import { NodeFileSystem } from 'langium/node'; import { createSafeDsServicesWithBuiltins } from '../../../src/language/index.js'; -import { isSdsCall, isSdsCallable } from '../../../src/language/generated/ast.js'; +import { isSdsCallable, isSdsExpression } from '../../../src/language/generated/ast.js'; import { getNodeOfType } from '../../helpers/nodeFinder.js'; const services = (await createSafeDsServicesWithBuiltins(NodeFileSystem)).SafeDs; const purityComputer = services.purity.PurityComputer; describe('SafeDsPurityComputer', async () => { - describe('isPure', () => { + describe('isPureCallable', () => { it.each([ { + testName: 'pure function', code: ` package test @Pure fun f() + `, + expected: true, + }, + { + testName: 'impure function with reasons', + code: ` + package test - pipeline myPipeline { - f(); - } + @Impure([]) + fun f() `, expected: true, }, { + testName: 'impure function with reasons', code: ` package test @Impure([ImpurityReason.Other]) fun f() - - pipeline myPipeline { - f(); - } `, expected: false, }, - ])('should return whether a call is pure (%#)', async ({ code, expected }) => { - const call = await getNodeOfType(services, code, isSdsCall); - expect(purityComputer.isPure(call)).toBe(expected); + ])('should return whether a callable is pure (#testName)', async ({ code, expected }) => { + const callable = await getNodeOfType(services, code, isSdsCallable); + expect(purityComputer.isPureCallable(callable)).toBe(expected); }); + }); + describe('isPureExpression', () => { it.each([ { + testName: 'call of pure function', code: ` package test + pipeline myPipeline { + f(); + } + @Pure fun f() `, expected: true, }, { + testName: 'call of impure function without reasons', + code: ` + package test + + pipeline myPipeline { + f(); + } + + @Impure([]) + fun f() + `, + expected: true, + }, + { + testName: 'call of impure function with reasons', code: ` package test + pipeline myPipeline { + f(); + } + @Impure([ImpurityReason.Other]) fun f() `, expected: false, }, - ])('should return whether a callable is pure (%#)', async ({ code, expected }) => { - const callable = await getNodeOfType(services, code, isSdsCallable); - expect(purityComputer.isPure(callable)).toBe(expected); + { + testName: 'lambda', + code: ` + package test + + pipeline myPipeline { + () -> f(); + } + + @Impure([ImpurityReason.Other]) + fun f() + `, + expected: true, + }, + ])('should return whether an expression is pure ($testName)', async ({ code, expected }) => { + const expression = await getNodeOfType(services, code, isSdsExpression); + expect(purityComputer.isPureExpression(expression)).toBe(expected); }); }); - describe('hasSideEffects', () => { + describe('callableHasSideEffects', () => { it.each([ { + testName: 'pure function', code: ` package test @Pure fun f() + `, + expected: false, + }, + { + testName: 'impure function without reasons', + code: ` + package test - pipeline myPipeline { - f(); - } + @Impure([]) + fun f() `, expected: false, }, { + testName: 'impure function with reasons but no side effects', code: ` package test @Impure([ImpurityReason.FileReadFromConstantPath("file.txt")]) fun f() - - pipeline myPipeline { - f(); - } `, expected: false, }, { + testName: 'impure function with reasons and side effects', code: ` package test @Impure([ImpurityReason.Other]) fun f() - - pipeline myPipeline { - f(); - } `, expected: true, }, - ])('should return whether a call has side effects (%#)', async ({ code, expected }) => { - const call = await getNodeOfType(services, code, isSdsCall); - expect(purityComputer.hasSideEffects(call)).toBe(expected); + ])('should return whether a callable has side effects (#testName)', async ({ code, expected }) => { + const callable = await getNodeOfType(services, code, isSdsCallable); + expect(purityComputer.callableHasSideEffects(callable)).toBe(expected); }); + }); + describe('expressionHasSideEffects', () => { it.each([ { + testName: 'call of pure function', code: ` package test + pipeline myPipeline { + f(); + } + @Pure fun f() `, expected: false, }, { + testName: 'call of impure function without reasons', code: ` package test + pipeline myPipeline { + f(); + } + + @Impure([]) + fun f() + `, + expected: false, + }, + { + testName: 'call of impure function with reasons but no side effects', + code: ` + package test + + pipeline myPipeline { + f(); + } + @Impure([ImpurityReason.FileReadFromConstantPath("file.txt")]) fun f() `, expected: false, }, { + testName: 'call of impure function with reasons and side effects', code: ` package test + pipeline myPipeline { + f(); + } + @Impure([ImpurityReason.Other]) fun f() `, expected: true, }, - ])('should return whether a callable has side effects (%#)', async ({ code, expected }) => { - const callable = await getNodeOfType(services, code, isSdsCallable); - expect(purityComputer.hasSideEffects(callable)).toBe(expected); + { + testName: 'lambda', + code: ` + package test + + pipeline myPipeline { + () -> f(); + } + + @Impure([ImpurityReason.Other]) + fun f() + `, + expected: false, + }, + ])('should return whether a call has side effects (%#)', async ({ code, expected }) => { + const expression = await getNodeOfType(services, code, isSdsExpression); + expect(purityComputer.expressionHasSideEffects(expression)).toBe(expected); }); }); - describe('getImpurityReasons', () => { + describe('getImpurityReasonsForCallable', () => { it.each([ { testName: 'pure function', @@ -240,7 +330,88 @@ describe('SafeDsPurityComputer', async () => { }, ])('should return the impurity reasons of a callable ($testName)', async ({ code, expected }) => { const callable = await getNodeOfType(services, code, isSdsCallable); - const actual = purityComputer.getImpurityReasons(callable).map((reason) => reason.toString()); + const actual = purityComputer.getImpurityReasonsForCallable(callable).map((reason) => reason.toString()); + expect(actual).toStrictEqual(expected); + }); + }); + + describe('getImpurityReasonsForExpression', () => { + it.each([ + { + testName: 'call of pure function', + code: ` + package test + + pipeline myPipeline { + f(); + } + + @Pure + fun f() + `, + expected: [], + }, + { + testName: 'call of impure function without reasons', + code: ` + package test + + pipeline myPipeline { + f(); + } + + @Impure([]) + fun f() + `, + expected: [], + }, + { + testName: 'call of impure function with reasons but no side effects', + code: ` + package test + + pipeline myPipeline { + f(); + } + + @Impure([ImpurityReason.FileReadFromConstantPath("file.txt")]) + fun f() + `, + expected: ['File read from "file.txt"'], + }, + { + testName: 'call of impure function with reasons and side effects', + code: ` + package test + + pipeline myPipeline { + f(); + } + + @Impure([ImpurityReason.Other]) + fun f() + `, + expected: ['Other'], + }, + { + testName: 'lambda', + code: ` + package test + + pipeline myPipeline { + () -> f(); + } + + @Impure([ImpurityReason.Other]) + fun f() + `, + expected: [], + }, + ])('should return the impurity reasons of a callable ($testName)', async ({ code, expected }) => { + const expression = await getNodeOfType(services, code, isSdsExpression); + const actual = purityComputer + .getImpurityReasonsForExpression(expression) + .map((reason) => reason.toString()); expect(actual).toStrictEqual(expected); }); }); From eeaf0c9b030f170f77b1912ece7e144dabc18334 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 20 Nov 2023 18:17:49 +0100 Subject: [PATCH 4/4] docs: how to write call graph tests --- docs/development/call-graph-testing.md | 27 +++++++++++++++++ docs/development/formatting-testing.md | 4 +-- docs/development/grammar-testing.md | 2 +- docs/development/langium-quickstart.md | 41 -------------------------- docs/mkdocs.yml | 8 ++--- 5 files changed, 34 insertions(+), 48 deletions(-) create mode 100644 docs/development/call-graph-testing.md delete mode 100644 docs/development/langium-quickstart.md diff --git a/docs/development/call-graph-testing.md b/docs/development/call-graph-testing.md new file mode 100644 index 000000000..ef73c0d7a --- /dev/null +++ b/docs/development/call-graph-testing.md @@ -0,0 +1,27 @@ +# Call Graph Testing + +Call graph tests are data-driven instead of being specified explicitly. This document explains how to add a new call +graph test. + +## Adding a call graph test + +1. Create a new file with the extension `.sdstest` in the `tests/resources/call graph` directory or any subdirectory. + Give the file a descriptive name, since the file name becomes part of the test name. + + !!! tip "Skipping a test" + + If you want to skip a test, add the prefix `skip-` to the file name. + +2. Add the Safe-DS code that you want to test to the file. +3. Surround calls or callables for which you want to compute a call graph with test markers, e.g. `»f()«`. Add a + comment in the preceding line with the following format: + ```ts + // $TEST$ ["f", "$blockLambda", "$expressionLambda", "undefined"] + ``` + The comment must contain an array with the names of the callables that are expected to be called. The order must + match the actual call order. The names must be: + * The quoted name of a named callable, e.g. `"f"`. + * The string `"$blockLambda"` for a block lambda. + * The string `"$expressionLambda"` for an expression lambda. + * The string `"undefined"` for an undefined callable. +4. Run the tests. The test runner will automatically pick up the new test. diff --git a/docs/development/formatting-testing.md b/docs/development/formatting-testing.md index fc6306dda..5d1d066c7 100644 --- a/docs/development/formatting-testing.md +++ b/docs/development/formatting-testing.md @@ -5,8 +5,8 @@ formatting test. ## Adding a formatting test -1. Create a new file with extension `.sdstest` in the `tests/resources/formatting` directory or any - subdirectory. Give the file a descriptive name, since the file name becomes part of the test name. +1. Create a new file with the extension `.sdstest` in the `tests/resources/formatting` directory or any subdirectory. + Give the file a descriptive name, since the file name becomes part of the test name. !!! tip "Skipping a test" diff --git a/docs/development/grammar-testing.md b/docs/development/grammar-testing.md index 15c13eb86..823182aa2 100644 --- a/docs/development/grammar-testing.md +++ b/docs/development/grammar-testing.md @@ -5,7 +5,7 @@ test. ## Adding a grammar test -1. Create a new file with extension `.sdstest` in the `tests/resources/grammar` directory or any subdirectory. Give +1. Create a new file with the extension `.sdstest` in the `tests/resources/grammar` directory or any subdirectory. Give the file a descriptive name, since the file name becomes part of the test name. !!! note "Naming convention" diff --git a/docs/development/langium-quickstart.md b/docs/development/langium-quickstart.md deleted file mode 100644 index 1dbf58fed..000000000 --- a/docs/development/langium-quickstart.md +++ /dev/null @@ -1,41 +0,0 @@ -# Welcome to your Langium VS Code Extension - -## What's in the folder - -The project folder contains all necessary files for your language extension. - -* `package.json` - the manifest file in which you declare your language support. -* `language-configuration.json` - the language configuration used in the VS Code editor, defining the tokens that are used for comments and brackets. -* `src/extension/main.ts` - the main code of the extension, which is responsible for launching a language server and client. -* `src/language/grammar/safe-ds.langium` - the grammar definition of your language. -* `src/language/main.ts` - the entry point of the language server process. -* `src/language/safe-ds-module.ts` - the dependency injection module of your language implementation. Use this to register overridden and added services. -* `src/language/validation/safe-ds-validator.ts` - an example validator. You should change it to reflect the semantics of your language. -* `src/cli/main.ts` - the entry point of the command line interface (CLI) of your language. -* `src/cli/generator.ts` - the code generator used by the CLI to write output files from DSL documents. -* `src/cli/cli-util.ts` - utility code for the CLI. - -## Get up and running straight away - -* Run `npm run langium:generate` to generate TypeScript code from the grammar definition. -* Run `npm run build` to compile all TypeScript code. -* Press `F5` to open a new window with your extension loaded. -* Create a new file with a file name suffix matching your language. -* Verify that syntax highlighting, validation, completion etc. are working as expected. -* Run `./bin/cli` to see options for the CLI; `./bin/cli generate ` generates code for a given DSL file. - -## Make changes - -* Run `npm run watch` to have the TypeScript compiler run automatically after every change of the source files. -* Run `npm run langium:watch` to have the Langium generator run automatically after every change of the grammar declaration. -* You can relaunch the extension from the debug toolbar after making changes to the files listed above. -* You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. - -## Install your extension - -* To start using your extension with VS Code, copy it into the `/.vscode/extensions` folder and restart Code. -* To share your extension with the world, read the [VS Code documentation](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) about publishing an extension. - -## To Go Further - -Documentation about the Langium framework is available at https://langium.org diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 8cacba9de..7503045dd 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -40,14 +40,14 @@ nav: - stdlib/README.md - safeds.lang: stdlib/safeds_lang.md - Development: + - Call Graph Testing: development/call-graph-testing.md + - Formatting Testing: development/formatting-testing.md + - Generation Testing: development/generation-testing.md - Grammar Testing: development/grammar-testing.md + - Partial Evaluation Testing: development/partial-evaluation-testing.md - Scoping Testing: development/scoping-testing.md - Typing Testing: development/typing-testing.md - - Partial Evaluation Testing: development/partial-evaluation-testing.md - Validation Testing: development/validation-testing.md - - Generation Testing: development/generation-testing.md - - Formatting Testing: development/formatting-testing.md - - Langium Quickstart: development/langium-quickstart.md # Configuration of MkDocs & Material for MkDocs --------------------------------