From 2e6be9fc7a00fac752d49ce249b561373db7bfc2 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Tue, 26 Sep 2023 15:03:23 +0200 Subject: [PATCH] test: data-driven tests for partial evaluation (#578) Closes partially #433. ### Summary of Changes * Implement a data-driven way to test the partial evaluator * Add a very basic port of our implementation from Xtext and lay out the general structure of the full implementation --------- Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> --- .../development/partial-evaluation-testing.md | 28 ++ docs/development/typing-testing.md | 11 +- mkdocs.yml | 1 + .../grammar/safe-ds-value-converter.ts | 16 + src/language/partialEvaluation/model.ts | 210 ++++++++ .../toConstantExpressionOrNull.ts | 458 ++++++++++++++++++ src/language/safe-ds-module.ts | 4 + .../__snapshots__/location.test.ts.snap | 3 + tests/helpers/location.test.ts | 58 ++- tests/helpers/location.ts | 57 ++- tests/language/partialEvaluation/creator.ts | 217 +++++++++ .../testPartialEvaluation.test.ts | 101 ++++ tests/language/typing/testTyping.test.ts | 52 +- .../base cases/boolean literal/main.sdstest | 9 + .../base cases/float literal/main.sdstest | 9 + .../base cases/int literal/main.sdstest | 6 + .../base cases/null literal/main.sdstest | 6 + .../main.sdstest | 12 + .../main.sdstest | 9 + tsconfig.json | 1 + 20 files changed, 1213 insertions(+), 55 deletions(-) create mode 100644 docs/development/partial-evaluation-testing.md create mode 100644 src/language/grammar/safe-ds-value-converter.ts create mode 100644 src/language/partialEvaluation/model.ts create mode 100644 src/language/partialEvaluation/toConstantExpressionOrNull.ts create mode 100644 tests/helpers/__snapshots__/location.test.ts.snap create mode 100644 tests/language/partialEvaluation/creator.ts create mode 100644 tests/language/partialEvaluation/testPartialEvaluation.test.ts create mode 100644 tests/resources/partial evaluation/base cases/boolean literal/main.sdstest create mode 100644 tests/resources/partial evaluation/base cases/float literal/main.sdstest create mode 100644 tests/resources/partial evaluation/base cases/int literal/main.sdstest create mode 100644 tests/resources/partial evaluation/base cases/null literal/main.sdstest create mode 100644 tests/resources/partial evaluation/base cases/string literal (with interpolation)/main.sdstest create mode 100644 tests/resources/partial evaluation/base cases/string literal (without interpolation)/main.sdstest diff --git a/docs/development/partial-evaluation-testing.md b/docs/development/partial-evaluation-testing.md new file mode 100644 index 000000000..d852913e0 --- /dev/null +++ b/docs/development/partial-evaluation-testing.md @@ -0,0 +1,28 @@ +# Partial Evaluation Testing + +Partial evaluation tests are data-driven instead of being specified explicitly. This document explains how to add a new +partial evaluation test. + +## Adding a partial evaluation test + +1. Create a new **folder** (not just a file!) in the `tests/resources/partial evaluation` directory or any subdirectory. + Give the folder a descriptive name, since the folder name becomes part of the test name. + + !!! tip "Skipping a test" + + If you want to skip a test, add the prefix `skip-` to the folder name. + +2. Add files with the extension `.sdstest` **directly inside the folder**. All files in a folder will be loaded into the + same workspace, so they can reference each other. Files in different folders are loaded into different workspaces, so + they cannot reference each other. +3. Add the Safe-DS code that you want to test to the file. +4. Surround entire nodes whose value you want to check with test markers, e.g. `1 + 2`. +5. For each pair of test markers, add a test comment with one of the formats listed below. Test comments and test + markers are mapped to each other by their position in the file, i.e. the first test comment corresponds to the first + test marker, the second test comment corresponds to the second test marker, etc. + * `// $TEST$ constant equivalence_class `: Assert that all nodes with the same `` get partially evaluated + successfully to the same constant expression. + * `// $TEST$ constant serialization `: Assert that the node gets partially evaluated to a constant expression + that serializes to ``. + * `// $TEST$ not constant`: Assert that the node cannot be evaluated to a constant expression. +6. Run the tests. The test runner will automatically pick up the new test. diff --git a/docs/development/typing-testing.md b/docs/development/typing-testing.md index 7373998c6..7adf3d687 100644 --- a/docs/development/typing-testing.md +++ b/docs/development/typing-testing.md @@ -8,7 +8,7 @@ test. 1. Create a new **folder** (not just a file!) in the `tests/resources/typing` directory or any subdirectory. Give the folder a descriptive name, since the folder name becomes part of the test name. - !!! tip "Skipping a test" + !!! tip "Skipping a test" If you want to skip a test, add the prefix `skip-` to the folder name. @@ -18,9 +18,10 @@ test. 3. Add the Safe-DS code that you want to test to the file. 4. Surround entire nodes whose type you want to check with test markers, e.g. `1 + 2`. For declarations, it is also possible to surround only their name, e.g. `class »C«`. -5. For each pair of test markers, add a test comment with one of the formats listed below. Test comments and test markers are - mapped to each other by their position in the file, i.e. the first test comment corresponds to the first test marker, - the second test comment corresponds to the second test marker, etc. - * `// $TEST$ equivalence_class `: Assert that all nodes with the same `` have the same type. All equivalence classes must have at least two entries. +5. For each pair of test markers, add a test comment with one of the formats listed below. Test comments and test + markers are mapped to each other by their position in the file, i.e. the first test comment corresponds to the first + test marker, the second test comment corresponds to the second test marker, etc. + * `// $TEST$ equivalence_class `: Assert that all nodes with the same `` have the same type. All equivalence + classes must have at least two entries. * `// $TEST$ serialization `: Assert that the serialized type of the node is ``. 6. Run the tests. The test runner will automatically pick up the new test. diff --git a/mkdocs.yml b/mkdocs.yml index b61fb36d3..0681bfac7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -39,6 +39,7 @@ nav: - Grammar Testing: development/grammar-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 - Formatting Testing: development/formatting-testing.md - Langium Quickstart: development/langium-quickstart.md diff --git a/src/language/grammar/safe-ds-value-converter.ts b/src/language/grammar/safe-ds-value-converter.ts new file mode 100644 index 000000000..b34fc6c0e --- /dev/null +++ b/src/language/grammar/safe-ds-value-converter.ts @@ -0,0 +1,16 @@ +import { convertString, CstNode, DefaultValueConverter, GrammarAST, ValueType } from 'langium'; + +export class SafeDsValueConverter extends DefaultValueConverter { + protected override runConverter(rule: GrammarAST.AbstractRule, input: string, cstNode: CstNode): ValueType { + switch (rule.name.toUpperCase()) { + case 'TEMPLATE_STRING_START': + return convertString(input.substring(0, input.length - 1)); + case 'TEMPLATE_STRING_INNER': + return convertString(input.substring(1, input.length - 1)); + case 'TEMPLATE_STRING_END': + return convertString(input.substring(1)); + default: + return super.runConverter(rule, input, cstNode); + } + } +} diff --git a/src/language/partialEvaluation/model.ts b/src/language/partialEvaluation/model.ts new file mode 100644 index 000000000..23f211d15 --- /dev/null +++ b/src/language/partialEvaluation/model.ts @@ -0,0 +1,210 @@ +import { + isSdsAbstractResult, + SdsAbstractResult, + SdsBlockLambdaResult, + SdsEnumVariant, + SdsExpression, + SdsParameter, + SdsReference, + SdsResult, +} from '../generated/ast.js'; + +/* c8 ignore start */ +export type ParameterSubstitutions = Map; +export type ResultSubstitutions = Map; + +export abstract class SdsSimplifiedExpression { + /** + * Removes any unnecessary containers from the expression. + */ + unwrap(): SdsSimplifiedExpression { + return this; + } +} + +export abstract class SdsIntermediateExpression extends SdsSimplifiedExpression {} + +export abstract class SdsIntermediateCallable extends SdsIntermediateExpression {} + +export class SdsIntermediateBlockLambda extends SdsIntermediateCallable { + constructor( + readonly parameters: SdsParameter[], + readonly results: SdsBlockLambdaResult[], + readonly substitutionsOnCreation: ParameterSubstitutions, + ) { + super(); + } +} + +export class SdsIntermediateExpressionLambda extends SdsIntermediateCallable { + constructor( + readonly parameters: SdsParameter[], + readonly result: SdsExpression, + readonly substitutionsOnCreation: ParameterSubstitutions, + ) { + super(); + } +} + +export class SdsIntermediateStep extends SdsIntermediateCallable { + constructor( + readonly parameters: SdsParameter[], + readonly results: SdsResult[], + ) { + super(); + } +} + +export class SdsIntermediateRecord extends SdsIntermediateExpression { + constructor(readonly resultSubstitutions: ResultSubstitutions) { + super(); + } + + getSubstitutionByReferenceOrNull(reference: SdsReference): SdsSimplifiedExpression | null { + const referencedDeclaration = reference.declaration; + if (!isSdsAbstractResult(referencedDeclaration)) { + return null; + } + + return this.resultSubstitutions.get(referencedDeclaration) ?? null; + } + + getSubstitutionByIndexOrNull(index: number | null): SdsSimplifiedExpression | null { + if (index === null) { + return null; + } + return Array.from(this.resultSubstitutions.values())[index] ?? null; + } + + /** + * If the record contains exactly one substitution its value is returned. Otherwise, it returns `this`. + */ + override unwrap(): SdsSimplifiedExpression { + if (this.resultSubstitutions.size === 1) { + return this.resultSubstitutions.values().next().value; + } else { + return this; + } + } + + override toString(): string { + const entryString = Array.from(this.resultSubstitutions, ([result, value]) => `${result.name}=${value}`).join( + ', ', + ); + return `{${entryString}}`; + } +} + +export class SdsIntermediateVariadicArguments extends SdsIntermediateExpression { + constructor(readonly arguments_: (SdsSimplifiedExpression | null)[]) { + super(); + } + + getArgumentByIndexOrNull(index: number | null): SdsSimplifiedExpression | null { + if (index === null) { + return null; + } + return this.arguments_[index] ?? null; + } +} + +export abstract class SdsConstantExpression extends SdsSimplifiedExpression { + abstract equals(other: SdsConstantExpression): boolean; + + abstract override toString(): string; + + /** + * Returns the string representation of the constant expression if it occurs in a string template. + */ + toInterpolationString(): string { + return this.toString(); + } +} + +export class SdsConstantBoolean extends SdsConstantExpression { + constructor(readonly value: boolean) { + super(); + } + + equals(other: SdsConstantExpression): boolean { + return other instanceof SdsConstantBoolean && this.value === other.value; + } + + toString(): string { + return this.value.toString(); + } +} + +export class SdsConstantEnumVariant extends SdsConstantExpression { + constructor(readonly value: SdsEnumVariant) { + super(); + } + + equals(other: SdsConstantExpression): boolean { + return other instanceof SdsConstantEnumVariant && this.value === other.value; + } + + toString(): string { + return this.value.name; + } +} + +export abstract class SdsConstantNumber extends SdsConstantExpression {} + +export class SdsConstantFloat extends SdsConstantNumber { + constructor(readonly value: number) { + super(); + } + + equals(other: SdsConstantExpression): boolean { + return other instanceof SdsConstantFloat && this.value === other.value; + } + + toString(): string { + return this.value.toString(); + } +} + +export class SdsConstantInt extends SdsConstantNumber { + constructor(readonly value: bigint) { + super(); + } + + equals(other: SdsConstantExpression): boolean { + return other instanceof SdsConstantInt && this.value === other.value; + } + + toString(): string { + return this.value.toString(); + } +} + +export class SdsConstantNull extends SdsConstantExpression { + equals(other: SdsConstantExpression): boolean { + return other instanceof SdsConstantNull; + } + + toString(): string { + return 'null'; + } +} + +export class SdsConstantString extends SdsConstantExpression { + constructor(readonly value: string) { + super(); + } + + equals(other: SdsConstantExpression): boolean { + return other instanceof SdsConstantString && this.value === other.value; + } + + toString(): string { + return `"${this.value}"`; + } + + override toInterpolationString(): string { + return this.value; + } +} + +/* c8 ignore stop */ diff --git a/src/language/partialEvaluation/toConstantExpressionOrNull.ts b/src/language/partialEvaluation/toConstantExpressionOrNull.ts new file mode 100644 index 000000000..10bfa4719 --- /dev/null +++ b/src/language/partialEvaluation/toConstantExpressionOrNull.ts @@ -0,0 +1,458 @@ +import { + ParameterSubstitutions, + SdsConstantBoolean, + SdsConstantExpression, + SdsConstantFloat, + SdsConstantNull, + SdsConstantString, + SdsIntermediateBlockLambda, + SdsIntermediateExpressionLambda, + SdsSimplifiedExpression, +} from './model.js'; +import { AstNode } from 'langium'; +import { + isSdsArgument, + isSdsBlockLambda, + isSdsBoolean, + isSdsCall, + isSdsExpression, + isSdsExpressionLambda, + isSdsFloat, + isSdsIndexedAccess, + isSdsInfixOperation, + isSdsInt, + isSdsMemberAccess, + isSdsNull, + isSdsParenthesizedExpression, + isSdsPrefixOperation, + isSdsReference, + isSdsString, + isSdsTemplateString, + isSdsTemplateStringEnd, + isSdsTemplateStringInner, + isSdsTemplateStringStart, + SdsBlockLambda, + SdsCall, + SdsExpressionLambda, + SdsIndexedAccess, + SdsInfixOperation, + SdsMemberAccess, + SdsPrefixOperation, + SdsReference, + SdsTemplateString, +} from '../generated/ast.js'; + +/** + * Tries to evaluate this expression. On success an SdsConstantExpression is returned, otherwise `null`. + */ +export const toConstantExpressionOrNull = (node: AstNode): SdsConstantExpression | null => { + return toConstantExpressionOrNullImpl(node, new Map()); +}; + +/* c8 ignore start */ +const toConstantExpressionOrNullImpl = ( + node: AstNode, + substitutions: ParameterSubstitutions, +): SdsConstantExpression | null => { + const simplifiedExpression = simplify(node, substitutions)?.unwrap(); + if (simplifiedExpression instanceof SdsConstantExpression) { + return simplifiedExpression; + } else { + return null; + } +}; + +const simplify = (node: AstNode, substitutions: ParameterSubstitutions): SdsSimplifiedExpression | null => { + // Only expressions have a value + if (!isSdsExpression(node)) { + return null; + } + + // Base cases + if (isSdsBoolean(node)) { + return new SdsConstantBoolean(node.value); + } else if (isSdsFloat(node)) { + return new SdsConstantFloat(node.value); + } else if (isSdsInt(node)) { + return new SdsConstantFloat(node.value); + } else if (isSdsNull(node)) { + return new SdsConstantNull(); + } else if (isSdsString(node)) { + return new SdsConstantString(node.value); + } else if (isSdsTemplateStringStart(node)) { + return new SdsConstantString(node.value); + } else if (isSdsTemplateStringInner(node)) { + return new SdsConstantString(node.value); + } else if (isSdsTemplateStringEnd(node)) { + return new SdsConstantString(node.value); + } else if (isSdsBlockLambda(node)) { + return simplifyBlockLambda(node, substitutions); + } else if (isSdsExpressionLambda(node)) { + return simplifyExpressionLambda(node, substitutions); + } + + // Simple recursive cases + else if (isSdsArgument(node)) { + return simplify(node.value, substitutions); + } else if (isSdsInfixOperation(node)) { + return simplifyInfixOperation(node, substitutions); + } else if (isSdsParenthesizedExpression(node)) { + return simplify(node.expression, substitutions); + } else if (isSdsPrefixOperation(node)) { + return simplifyPrefixOperation(node, substitutions); + } else if (isSdsTemplateString(node)) { + return simplifyTemplateString(node, substitutions); + } + + // Complex recursive cases + else if (isSdsCall(node)) { + return simplifyCall(node, substitutions); + } else if (isSdsIndexedAccess(node)) { + return simplifyIndexedAccess(node, substitutions); + } else if (isSdsMemberAccess(node)) { + return simplifyMemberAccess(node, substitutions); + } else if (isSdsReference(node)) { + return simplifyReference(node, substitutions); + } + + // Raise if case is missing (should not happen) + /* c8 ignore next */ + throw new Error(`Missing case to handle ${node.$type}.`); +}; + +const simplifyBlockLambda = ( + _node: SdsBlockLambda, + _substitutions: ParameterSubstitutions, +): SdsIntermediateBlockLambda | null => { + // return when { + // callableHasNoSideEffects(resultIfUnknown = true) -> SdsIntermediateBlockLambda( + // parameters = parametersOrEmpty(), + // results = blockLambdaResultsOrEmpty(), + // substitutionsOnCreation = substitutions + // ) + // else -> null + // } + return null; +}; + +const simplifyExpressionLambda = ( + _node: SdsExpressionLambda, + _substitutions: ParameterSubstitutions, +): SdsIntermediateExpressionLambda | null => { + // return when { + // callableHasNoSideEffects(resultIfUnknown = true) -> SdsIntermediateExpressionLambda( + // parameters = parametersOrEmpty(), + // result = result, + // substitutionsOnCreation = substitutions + // ) + // else -> null + // } + return null; +}; + +const simplifyInfixOperation = ( + _node: SdsInfixOperation, + _substitutions: ParameterSubstitutions, +): SdsConstantExpression | null => { + // // By design none of the operators are short-circuited + // val constantLeft = leftOperand.toConstantExpressionOrNull(substitutions) ?: return null + // val constantRight = rightOperand.toConstantExpressionOrNull(substitutions) ?: return null + // + // return when (operator()) { + // Or -> simplifyLogicalOp(constantLeft, Boolean::or, constantRight) + // And -> simplifyLogicalOp(constantLeft, Boolean::and, constantRight) + // Equals -> SdsConstantBoolean(constantLeft == constantRight) + // NotEquals -> SdsConstantBoolean(constantLeft != constantRight) + // IdenticalTo -> SdsConstantBoolean(constantLeft == constantRight) + // NotIdenticalTo -> SdsConstantBoolean(constantLeft != constantRight) + // LessThan -> simplifyComparisonOp( + // constantLeft, + // { a, b -> a < b }, + // { a, b -> a < b }, + // constantRight + // ) + // LessThanOrEquals -> simplifyComparisonOp( + // constantLeft, + // { a, b -> a <= b }, + // { a, b -> a <= b }, + // constantRight + // ) + // GreaterThanOrEquals -> simplifyComparisonOp( + // constantLeft, + // { a, b -> a >= b }, + // { a, b -> a >= b }, + // constantRight + // ) + // GreaterThan -> simplifyComparisonOp( + // constantLeft, + // { a, b -> a > b }, + // { a, b -> a > b }, + // constantRight + // ) + // Plus -> simplifyArithmeticOp( + // constantLeft, + // { a, b -> a + b }, + // { a, b -> a + b }, + // constantRight + // ) + // InfixMinus -> simplifyArithmeticOp( + // constantLeft, + // { a, b -> a - b }, + // { a, b -> a - b }, + // constantRight + // ) + // Times -> simplifyArithmeticOp( + // constantLeft, + // { a, b -> a * b }, + // { a, b -> a * b }, + // constantRight + // ) + // By -> { + // if (constantRight == SdsConstantFloat(0.0) || constantRight == SdsConstantInt(0)) { + // return null + // } + // + // simplifyArithmeticOp( + // constantLeft, + // { a, b -> a / b }, + // { a, b -> a / b }, + // constantRight + // ) + // } + // Elvis -> when (constantLeft) { + // SdsConstantNull -> constantRight + // else -> constantLeft + // } + // } + return null; +}; + +// private fun simplifyLogicalOp( +// leftOperand: SdsConstantExpression, +// operation: (Boolean, Boolean) -> Boolean, +// rightOperand: SdsConstantExpression, +// ): SdsConstantExpression? { +// +// return when { +// leftOperand is SdsConstantBoolean && rightOperand is SdsConstantBoolean -> { +// SdsConstantBoolean(operation(leftOperand.value, rightOperand.value)) +// } +// else -> null +// } +// } +// +// private fun simplifyComparisonOp( +// leftOperand: SdsConstantExpression, +// doubleOperation: (Double, Double) -> Boolean, +// intOperation: (Int, Int) -> Boolean, +// rightOperand: SdsConstantExpression, +// ): SdsConstantExpression? { +// +// return when { +// leftOperand is SdsConstantInt && rightOperand is SdsConstantInt -> { +// SdsConstantBoolean(intOperation(leftOperand.value, rightOperand.value)) +// } +// leftOperand is SdsConstantNumber && rightOperand is SdsConstantNumber -> { +// SdsConstantBoolean(doubleOperation(leftOperand.value.toDouble(), rightOperand.value.toDouble())) +// } +// else -> null +// } +// } +// +// private fun simplifyArithmeticOp( +// leftOperand: SdsConstantExpression, +// doubleOperation: (Double, Double) -> Double, +// intOperation: (Int, Int) -> Int, +// rightOperand: SdsConstantExpression, +// ): SdsConstantExpression? { +// +// return when { +// leftOperand is SdsConstantInt && rightOperand is SdsConstantInt -> { +// SdsConstantInt(intOperation(leftOperand.value, rightOperand.value)) +// } +// leftOperand is SdsConstantNumber && rightOperand is SdsConstantNumber -> { +// SdsConstantFloat(doubleOperation(leftOperand.value.toDouble(), rightOperand.value.toDouble())) +// } +// else -> null +// } +// } + +const simplifyPrefixOperation = ( + _node: SdsPrefixOperation, + _substitutions: ParameterSubstitutions, +): SdsConstantExpression | null => { + // val constantOperand = operand.toConstantExpressionOrNull(substitutions) ?: return null + // + // return when (operator()) { + // Not -> when (constantOperand) { + // is SdsConstantBoolean -> SdsConstantBoolean(!constantOperand.value) + // else -> null + // } + // PrefixMinus -> when (constantOperand) { + // is SdsConstantFloat -> SdsConstantFloat(-constantOperand.value) + // is SdsConstantInt -> SdsConstantInt(-constantOperand.value) + // else -> null + // } + // } + return null; +}; + +const simplifyTemplateString = ( + node: SdsTemplateString, + substitutions: ParameterSubstitutions, +): SdsConstantExpression | null => { + const constantExpressions = node.expressions.map((it) => toConstantExpressionOrNullImpl(it, substitutions)); + if (constantExpressions.some((it) => it === null)) { + return null; + } + + return new SdsConstantString(constantExpressions.map((it) => it!.toInterpolationString()).join('')); +}; + +const simplifyCall = (_node: SdsCall, _substitutions: ParameterSubstitutions): SdsSimplifiedExpression | null => { + // val simpleReceiver = simplifyReceiver(substitutions) ?: return null + // val newSubstitutions = buildNewSubstitutions(simpleReceiver, substitutions) + // + // return when (simpleReceiver) { + // is SdsIntermediateBlockLambda -> { + // SdsIntermediateRecord( + // simpleReceiver.results.map { + // it to it.simplifyAssignee(newSubstitutions) + // } + // ) + // } + // is SdsIntermediateExpressionLambda -> simpleReceiver.result.simplify(newSubstitutions) + // is SdsIntermediateStep -> { + // SdsIntermediateRecord( + // simpleReceiver.results.map { + // it to it.uniqueYieldOrNull()?.simplifyAssignee(newSubstitutions) + // } + // ) + // } + // } + return null; +}; + +// private fun SdsCall.simplifyReceiver(substitutions: ParameterSubstitutions): SdsIntermediateCallable? { +// return when (val simpleReceiver = receiver.simplify(substitutions)) { +// is SdsIntermediateRecord -> simpleReceiver.unwrap() as? SdsIntermediateCallable +// is SdsIntermediateCallable -> simpleReceiver +// else -> return null +// } +// } +// +// private fun SdsCall.buildNewSubstitutions( +// simpleReceiver: SdsIntermediateCallable, +// oldSubstitutions: ParameterSubstitutions +// ): ParameterSubstitutions { +// +// val substitutionsOnCreation = when (simpleReceiver) { +// is SdsIntermediateBlockLambda -> simpleReceiver.substitutionsOnCreation +// is SdsIntermediateExpressionLambda -> simpleReceiver.substitutionsOnCreation +// else -> emptyMap() +// } +// +// val substitutionsOnCall = argumentsOrEmpty() +// .groupBy { it.parameterOrNull() } +// .mapValues { (parameter, arguments) -> +// when { +// parameter == null -> null +// parameter.isVariadic -> SdsIntermediateVariadicArguments( +// arguments.map { it.simplify(oldSubstitutions) } +// ) +// else -> arguments.uniqueOrNull()?.simplify(oldSubstitutions) +// } +// } +// +// return buildMap { +// putAll(substitutionsOnCreation) +// substitutionsOnCall.entries.forEach { (parameter, argument) -> +// if (parameter != null) { +// put(parameter, argument) +// } +// } +// } +// } + +const simplifyIndexedAccess = ( + _node: SdsIndexedAccess, + _substitutions: ParameterSubstitutions, +): SdsSimplifiedExpression | null => { + // val simpleReceiver = receiver.simplify(substitutions) as? SdsIntermediateVariadicArguments ?: return null + // val simpleIndex = index.simplify(substitutions) as? SdsConstantInt ?: return null + // + // return simpleReceiver.getArgumentByIndexOrNull(simpleIndex.value) + // } + return null; +}; + +const simplifyMemberAccess = ( + _node: SdsMemberAccess, + _substitutions: ParameterSubstitutions, +): SdsSimplifiedExpression | null => { + // private fun SdsMemberAccess.simplifyMemberAccess(substitutions: ParameterSubstitutions): SdsSimplifiedExpression? { + // if (member.declaration is SdsEnumVariant) { + // return member.simplifyReference(substitutions) + // } + // + // return when (val simpleReceiver = receiver.simplify(substitutions)) { + // SdsConstantNull -> when { + // isNullSafe -> SdsConstantNull + // else -> null + // } + // is SdsIntermediateRecord -> simpleReceiver.getSubstitutionByReferenceOrNull(member) + // else -> null + // } + return null; +}; + +const simplifyReference = ( + _node: SdsReference, + _substitutions: ParameterSubstitutions, +): SdsSimplifiedExpression | null => { + // return when (val declaration = this.declaration) { + // is SdsEnumVariant -> when { + // declaration.parametersOrEmpty().isEmpty() -> SdsConstantEnumVariant(declaration) + // else -> null + // } + // is SdsPlaceholder -> declaration.simplifyAssignee(substitutions) + // is SdsParameter -> declaration.simplifyParameter(substitutions) + // is SdsStep -> declaration.simplifyStep() + // else -> null + // } + return null; +}; + +// private fun SdsAbstractAssignee.simplifyAssignee(substitutions: ParameterSubstitutions): SdsSimplifiedExpression? { +// val simpleFullAssignedExpression = closestAncestorOrNull() +// ?.expression +// ?.simplify(substitutions) +// ?: return null +// +// return when (simpleFullAssignedExpression) { +// is SdsIntermediateRecord -> simpleFullAssignedExpression.getSubstitutionByIndexOrNull(indexOrNull()) +// else -> when { +// indexOrNull() == 0 -> simpleFullAssignedExpression +// else -> null +// } +// } +// } +// +// private fun SdsParameter.simplifyParameter(substitutions: ParameterSubstitutions): SdsSimplifiedExpression? { +// return when { +// this in substitutions -> substitutions[this] +// isOptional() -> defaultValue?.simplify(substitutions) +// else -> null +// } +// } +// +// private fun SdsStep.simplifyStep(): SdsIntermediateStep? { +// return when { +// callableHasNoSideEffects(resultIfUnknown = true) -> SdsIntermediateStep( +// parameters = parametersOrEmpty(), +// results = resultsOrEmpty() +// ) +// else -> null +// } +// } +/* c8 ignore stop */ diff --git a/src/language/safe-ds-module.ts b/src/language/safe-ds-module.ts index dcf3f38c4..4a2c5257a 100644 --- a/src/language/safe-ds-module.ts +++ b/src/language/safe-ds-module.ts @@ -15,6 +15,7 @@ import { SafeDsFormatter } from './formatting/safe-ds-formatter.js'; import { SafeDsWorkspaceManager } from './builtins/safe-ds-workspace-manager.js'; import { SafeDsScopeComputation } from './scoping/safe-ds-scope-computation.js'; import { SafeDsScopeProvider } from './scoping/safe-ds-scope-provider.js'; +import { SafeDsValueConverter } from './grammar/safe-ds-value-converter.js'; /** * Declaration of custom services - add your own service classes here. @@ -40,6 +41,9 @@ export const SafeDsModule: Module new SafeDsFormatter(), }, + parser: { + ValueConverter: () => new SafeDsValueConverter(), + }, references: { ScopeComputation: (services) => new SafeDsScopeComputation(services), ScopeProvider: (services) => new SafeDsScopeProvider(services), diff --git a/tests/helpers/__snapshots__/location.test.ts.snap b/tests/helpers/__snapshots__/location.test.ts.snap new file mode 100644 index 000000000..f7c403a28 --- /dev/null +++ b/tests/helpers/__snapshots__/location.test.ts.snap @@ -0,0 +1,3 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`getNodeByLocation > should raise if no document is found 1`] = `"No document found at file:///test.sdstest"`; diff --git a/tests/helpers/location.test.ts b/tests/helpers/location.test.ts index 4d9ff1f1e..7253386ee 100644 --- a/tests/helpers/location.test.ts +++ b/tests/helpers/location.test.ts @@ -1,5 +1,10 @@ -import { describe, it, expect } from 'vitest'; -import { isLocationEqual, locationToString, positionToString, rangeToString } from './location.js'; +import { describe, expect, it } from 'vitest'; +import { getNodeByLocation, isLocationEqual, locationToString, positionToString, rangeToString } from './location.js'; +import { createSafeDsServices } from '../../src/language/safe-ds-module.js'; +import { EmptyFileSystem } from 'langium'; +import { AssertionError } from 'assert'; +import { parseHelper } from 'langium/test'; +import { isSdsClass } from '../../src/language/generated/ast.js'; describe('positionToString', () => { it.each([ @@ -94,3 +99,52 @@ describe('isLocationEqual', () => { expect(isLocationEqual(location1, location2)).toBe(expected); }); }); + +describe('getNodeByLocation', () => { + it('should raise if no document is found', () => { + const services = createSafeDsServices(EmptyFileSystem).SafeDs; + + expect(() => { + getNodeByLocation(services, { + uri: 'file:///test.sdstest', + range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }, + }); + }).toThrowErrorMatchingSnapshot(); + }); + + it('should raise if no node is found', async () => { + const services = createSafeDsServices(EmptyFileSystem).SafeDs; + const document = await parseHelper(services)(`class C`); + + expect(() => { + getNodeByLocation(services, { + uri: document.uri.toString(), + range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }, + }); + }).toThrow(AssertionError); + }); + + it('should return the node that fills the range completely', async () => { + const services = createSafeDsServices(EmptyFileSystem).SafeDs; + const document = await parseHelper(services)(`class C`); + + expect( + getNodeByLocation(services, { + uri: document.uri.toString(), + range: { start: { line: 0, character: 0 }, end: { line: 0, character: 7 } }, + }), + ).to.satisfy(isSdsClass); + }); + + it('should return the node whose name fills the range completely', async () => { + const services = createSafeDsServices(EmptyFileSystem).SafeDs; + const document = await parseHelper(services)(`class C`); + + expect( + getNodeByLocation(services, { + uri: document.uri.toString(), + range: { start: { line: 0, character: 6 }, end: { line: 0, character: 7 } }, + }), + ).to.satisfy(isSdsClass); + }); +}); diff --git a/tests/helpers/location.ts b/tests/helpers/location.ts index 1c31b483d..2300bcbc2 100644 --- a/tests/helpers/location.ts +++ b/tests/helpers/location.ts @@ -1,5 +1,9 @@ import { Location, Position, Range } from 'vscode-languageserver'; import { isRangeEqual } from 'langium/test'; +import { SafeDsServices } from '../../src/language/safe-ds-module.js'; +import { AstNode, streamAllContents, URI } from 'langium'; +import { isSdsModule } from '../../src/language/generated/ast.js'; +import { AssertionError } from 'assert'; /** * Converts a position to a string. @@ -32,7 +36,7 @@ export const locationToString = (location: Location) => { }; /** - * Compare two locations for equality.ts. + * Compare two locations for equality. * * @param location1 The first location. * @param location2 The second location. @@ -41,3 +45,54 @@ export const locationToString = (location: Location) => { export const isLocationEqual = (location1: Location, location2: Location): boolean => { return location1.uri === location2.uri && isRangeEqual(location1.range, location2.range); }; + +/** + * Find the AstNode at the given location. It must fill the range exactly. + * + * @param services The services to use. + * @param location The location of the node to find. + * @returns The node at the given location. + * @throws AssertionError If no matching node was found. + */ +export const getNodeByLocation = (services: SafeDsServices, location: Location): AstNode => { + const langiumDocuments = services.shared.workspace.LangiumDocuments; + const uri = URI.parse(location.uri); + + if (!langiumDocuments.hasDocument(uri)) { + throw new AssertionError({ + message: `No document found at ${location.uri}`, + }); + } + + const document = langiumDocuments.getOrCreateDocument(URI.parse(location.uri)); + const root = document.parseResult.value; + + if (!isSdsModule(root)) { + throw new AssertionError({ + message: `The root node of ${location.uri} is not a SdsModule`, + }); + } + + for (const node of streamAllContents(root)) { + // Entire node matches the range + const actualRange = node.$cstNode?.range; + if (actualRange && isRangeEqual(actualRange, location.range)) { + return node; + } + + // The node has a name node that matches the range + const actualNameRange = getNameRange(services, node); + if (actualNameRange && isRangeEqual(actualNameRange, location.range)) { + return node; + } + } + + throw new AssertionError({ message: `Expected to find a node at ${locationToString(location)} but found none.` }); +}; + +/** + * Returns the range of the name of the given node or undefined if the node has no name. + */ +const getNameRange = (services: SafeDsServices, node: AstNode): Range | undefined => { + return services.references.NameProvider.getNameNode(node)?.range; +}; diff --git a/tests/language/partialEvaluation/creator.ts b/tests/language/partialEvaluation/creator.ts new file mode 100644 index 000000000..af6255edd --- /dev/null +++ b/tests/language/partialEvaluation/creator.ts @@ -0,0 +1,217 @@ +import { + listTestsResourcesGroupedByParentDirectory, + resolvePathRelativeToResources, +} from '../../helpers/testResources.js'; +import path from 'path'; +import fs from 'fs'; +import { findTestChecks } from '../../helpers/testChecks.js'; +import { Location } from 'vscode-languageserver'; +import { URI } from 'vscode-uri'; +import { getSyntaxErrors, SyntaxErrorsInCodeError } from '../../helpers/diagnostics.js'; +import { EmptyFileSystem } from 'langium'; +import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; + +const services = createSafeDsServices(EmptyFileSystem).SafeDs; +const root = 'partial evaluation'; + +export const createPartialEvaluationTests = (): Promise => { + const pathsGroupedByParentDirectory = listTestsResourcesGroupedByParentDirectory(root); + const testCases = Object.entries(pathsGroupedByParentDirectory).map(([dirname, paths]) => + createPartialEvaluationTest(dirname, paths), + ); + + return Promise.all(testCases); +}; + +const createPartialEvaluationTest = async ( + relativeParentDirectoryPath: string, + relativeResourcePaths: string[], +): Promise => { + const uris: string[] = []; + const groupIdToLocations: Map = new Map(); + const serializationAssertions: SerializationAssertion[] = []; + const undefinedAssertions: UndefinedAssertion[] = []; + + for (const relativeResourcePath of relativeResourcePaths) { + const absolutePath = resolvePathRelativeToResources(path.join(root, relativeResourcePath)); + const uri = URI.file(absolutePath).toString(); + uris.push(uri); + + const code = fs.readFileSync(absolutePath).toString(); + + // File must not contain any syntax errors + const syntaxErrors = await getSyntaxErrors(services, code); + if (syntaxErrors.length > 0) { + return invalidTest( + `INVALID TEST FILE [${relativeResourcePath}]`, + new SyntaxErrorsInCodeError(syntaxErrors), + ); + } + + const checksResult = findTestChecks(code, uri, { failIfFewerRangesThanComments: true }); + + // Something went wrong when finding test checks + if (checksResult.isErr) { + return invalidTest(`INVALID TEST FILE [${relativeResourcePath}]`, checksResult.error); + } + + for (const check of checksResult.value) { + // Expected unresolved reference + const equivalenceClassMatch = /constant equivalence_class (?.*)/gu.exec(check.comment); + if (equivalenceClassMatch) { + const id = equivalenceClassMatch.groups!.id; + const priorLocationsInEquivalenceClass = groupIdToLocations.get(id) ?? []; + priorLocationsInEquivalenceClass.push(check.location!); + groupIdToLocations.set(id, priorLocationsInEquivalenceClass); + continue; + } + + // Expected that reference is resolved and points to the target id + const serializationMatch = /constant serialization (?.*)/gu.exec(check.comment); + if (serializationMatch) { + const expectedValue = serializationMatch.groups!.expectedValue; + serializationAssertions.push({ + location: check.location!, + expectedValue, + }); + continue; + } + + // Expected that reference is resolved and points to the target id + const undefinedMatch = /not constant/gu.exec(check.comment); + if (undefinedMatch) { + undefinedAssertions.push({ + location: check.location!, + }); + continue; + } + + return invalidTest(`INVALID TEST FILE [${relativeResourcePath}]`, new InvalidCommentError(check.comment)); + } + } + + // Check that all equivalence classes have at least two locations + for (const [id, locations] of groupIdToLocations) { + if (locations.length < 2) { + return invalidTest( + `INVALID TEST SUITE [${relativeParentDirectoryPath}]`, + new SingletonEquivalenceClassError(id), + ); + } + } + + return { + testName: `[${relativeParentDirectoryPath}] should be partially evaluated correctly`, + uris, + equivalenceClassAssertions: [...groupIdToLocations.values()].map((locations) => ({ locations })), + serializationAssertions, + undefinedAssertions, + }; +}; + +/** + * Report a test that has errors. + * + * @param testName The name of the test. + * @param error The error that occurred. + */ +const invalidTest = (testName: string, error: Error): PartialEvaluationTest => { + return { + testName, + uris: [], + equivalenceClassAssertions: [], + serializationAssertions: [], + undefinedAssertions: [], + error, + }; +}; + +/** + * A description of a partial evaluation test. + */ +interface PartialEvaluationTest { + /** + * The name of the test. + */ + testName: string; + + /** + * The URIs of the files that should be loaded into the workspace. + */ + uris: string[]; + + /** + * All nodes in an equivalence class should evaluate to the same constant expression. + */ + equivalenceClassAssertions: EquivalenceClassAssertion[]; + + /** + * The serialized constant expression of a node should match the expected value. + */ + serializationAssertions: SerializationAssertion[]; + + /** + * The node should not evaluate to a constant expression. + */ + undefinedAssertions: UndefinedAssertion[]; + + /** + * An error that occurred while creating the test. If this is undefined, the test is valid. + */ + error?: Error; +} + +/** + * A set of nodes should all evaluate to the same constant expression. + */ +interface EquivalenceClassAssertion { + /** + * The locations of the nodes that should all evaluate to the same constant expression. + */ + locations: Location[]; +} + +/** + * The serialized constant expression of a node should match the expected value. + */ +interface SerializationAssertion { + /** + * The location of the node whose serialized constant expression should be checked. + */ + location: Location; + + /** + * The expected serialized constant expression of the node. + */ + expectedValue: string; +} + +/** + * The node should not evaluate to a constant expression. + */ +interface UndefinedAssertion { + /** + * The location of the node to check. + */ + location: Location; +} + +/** + * A test comment did not match the expected format. + */ +class InvalidCommentError extends Error { + constructor(readonly comment: string) { + super( + `Invalid test comment (valid values are 'constant equivalence_class ', 'constant serialization ', and 'not constant'): ${comment}`, + ); + } +} + +/** + * An equivalence class test contains only a single location. + */ +class SingletonEquivalenceClassError extends Error { + constructor(readonly id: string) { + super(`Equivalence class '${id}' only contains a single location. Such an assertion always succeeds.`); + } +} diff --git a/tests/language/partialEvaluation/testPartialEvaluation.test.ts b/tests/language/partialEvaluation/testPartialEvaluation.test.ts new file mode 100644 index 000000000..993450986 --- /dev/null +++ b/tests/language/partialEvaluation/testPartialEvaluation.test.ts @@ -0,0 +1,101 @@ +import { afterEach, describe, it } from 'vitest'; +import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; +import { URI } from 'vscode-uri'; +import { NodeFileSystem } from 'langium/node'; +import { clearDocuments } from 'langium/test'; +import { AssertionError } from 'assert'; +import { getNodeByLocation, locationToString } from '../../helpers/location.js'; +import { createPartialEvaluationTests } from './creator.js'; +import { toConstantExpressionOrNull } from '../../../src/language/partialEvaluation/toConstantExpressionOrNull.js'; +import { Location } from 'vscode-languageserver'; + +const services = createSafeDsServices(NodeFileSystem).SafeDs; + +describe('partial evaluation', async () => { + afterEach(async () => { + await clearDocuments(services); + }); + + it.each(await createPartialEvaluationTests())('$testName', async (test) => { + // Test is invalid + if (test.error) { + throw test.error; + } + + // Load all documents + const documents = test.uris.map((uri) => + services.shared.workspace.LangiumDocuments.getOrCreateDocument(URI.parse(uri)), + ); + await services.shared.workspace.DocumentBuilder.build(documents); + + // Ensure all nodes in the equivalence class get evaluated to the same constant expression + for (const equivalenceClassAssertion of test.equivalenceClassAssertions) { + if (equivalenceClassAssertion.locations.length > 1) { + const firstLocation = equivalenceClassAssertion.locations[0]; + const firstNode = getNodeByLocation(services, firstLocation); + const firstValue = toConstantExpressionOrNull(firstNode); + if (!firstValue) { + return reportUndefinedValue(firstLocation); + } + + for (const currentLocation of equivalenceClassAssertion.locations.slice(1)) { + const currentNode = getNodeByLocation(services, currentLocation); + const currentValue = toConstantExpressionOrNull(currentNode); + if (!currentValue) { + return reportUndefinedValue(currentLocation); + } + + if (!currentValue.equals(firstValue)) { + throw new AssertionError({ + message: `Two nodes in the same equivalence class evaluate to different constant expressions.\n Current location: ${locationToString( + currentLocation, + )}\n First location: ${locationToString(firstLocation)}`, + actual: currentValue.toString(), + expected: firstValue.toString(), + }); + } + } + } + } + + // Ensure the serialized constant expression matches the expected one + for (const serializationAssertion of test.serializationAssertions) { + const node = getNodeByLocation(services, serializationAssertion.location); + const actualValue = toConstantExpressionOrNull(node); + if (!actualValue) { + return reportUndefinedValue(serializationAssertion.location); + } + + if (actualValue.toString() !== serializationAssertion.expectedValue) { + throw new AssertionError({ + message: `A node has the wrong serialized constant expression.\n Location: ${locationToString( + serializationAssertion.location, + )}`, + actual: actualValue.toString(), + expected: serializationAssertion.expectedValue, + }); + } + } + + // Ensure the node does not evaluate to a constant expression + for (const undefinedAssertion of test.undefinedAssertions) { + const node = getNodeByLocation(services, undefinedAssertion.location); + const actualValue = toConstantExpressionOrNull(node); + if (actualValue) { + throw new AssertionError({ + message: `A node evaluates to a constant expression, but it should not.\n Location: ${locationToString( + undefinedAssertion.location, + )}`, + actual: actualValue.toString(), + expected: 'undefined', + }); + } + } + }); +}); + +const reportUndefinedValue = (location: Location) => { + throw new AssertionError({ + message: `A node could not be evaluated to a constant expression.\n Location: ${locationToString(location)}`, + }); +}; diff --git a/tests/language/typing/testTyping.test.ts b/tests/language/typing/testTyping.test.ts index 7075609c3..8ed1e159d 100644 --- a/tests/language/typing/testTyping.test.ts +++ b/tests/language/typing/testTyping.test.ts @@ -2,13 +2,10 @@ import { afterEach, beforeEach, describe, it } from 'vitest'; import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; import { URI } from 'vscode-uri'; import { NodeFileSystem } from 'langium/node'; -import { clearDocuments, isRangeEqual } from 'langium/test'; +import { clearDocuments } from 'langium/test'; import { AssertionError } from 'assert'; -import { locationToString } from '../../helpers/location.js'; +import { getNodeByLocation, locationToString } from '../../helpers/location.js'; import { createTypingTests } from './creator.js'; -import { AstNode, streamAllContents } from 'langium'; -import { Location, Range } from 'vscode-languageserver'; -import { isSdsModule } from '../../../src/language/generated/ast.js'; import { computeType } from '../../../src/language/typing/typeComputer.js'; const services = createSafeDsServices(NodeFileSystem).SafeDs; @@ -39,11 +36,11 @@ describe('typing', async () => { for (const equivalenceClassAssertion of test.equivalenceClassAssertions) { if (equivalenceClassAssertion.locations.length > 1) { const firstLocation = equivalenceClassAssertion.locations[0]; - const firstNode = getNodeByLocation(firstLocation); + const firstNode = getNodeByLocation(services, firstLocation); const firstType = computeType(firstNode); for (const currentLocation of equivalenceClassAssertion.locations.slice(1)) { - const currentNode = getNodeByLocation(currentLocation); + const currentNode = getNodeByLocation(services, currentLocation); const currentType = computeType(currentNode); if (!currentType.equals(firstType)) { @@ -61,7 +58,7 @@ describe('typing', async () => { // Ensure the serialized type of the node matches the expected type for (const serializationAssertion of test.serializationAssertions) { - const node = getNodeByLocation(serializationAssertion.location); + const node = getNodeByLocation(services, serializationAssertion.location); const actualType = computeType(node); if (actualType.toString() !== serializationAssertion.expectedType) { @@ -76,42 +73,3 @@ describe('typing', async () => { } }); }); - -/** - * Find the AstNode at the given location. It must fill the range exactly. - * - * @param location The location of the node to find. - * @returns The node at the given location. - * @throws AssertionError If no matching node was found. - */ -const getNodeByLocation = (location: Location): AstNode => { - const document = services.shared.workspace.LangiumDocuments.getOrCreateDocument(URI.parse(location.uri)); - const root = document.parseResult.value; - - if (!isSdsModule(root)) { - throw new AssertionError({ - message: `The root node of ${location.uri} is not a SdsModule`, - }); - } - - for (const node of streamAllContents(root)) { - const actualRange = node.$cstNode?.range; - if (actualRange && isRangeEqual(actualRange, location.range)) { - return node; - } - - const actualNameRange = getNameRange(node); - if (actualNameRange && isRangeEqual(actualNameRange, location.range)) { - return node; - } - } - - throw new AssertionError({ message: `Expected to find a node at ${locationToString(location)} but found none.` }); -}; - -/** - * Returns the range of the name of the given node or undefined if the node has no name. - */ -const getNameRange = (node: AstNode): Range | undefined => { - return services.references.NameProvider.getNameNode(node)?.range; -}; diff --git a/tests/resources/partial evaluation/base cases/boolean literal/main.sdstest b/tests/resources/partial evaluation/base cases/boolean literal/main.sdstest new file mode 100644 index 000000000..913cb39b8 --- /dev/null +++ b/tests/resources/partial evaluation/base cases/boolean literal/main.sdstest @@ -0,0 +1,9 @@ +package tests.partialValidation.baseCases.booleanLiteral + +pipeline test { + // $TEST$ constant serialization false + »false«; + + // $TEST$ constant serialization true + »true«; +} diff --git a/tests/resources/partial evaluation/base cases/float literal/main.sdstest b/tests/resources/partial evaluation/base cases/float literal/main.sdstest new file mode 100644 index 000000000..2910be3b8 --- /dev/null +++ b/tests/resources/partial evaluation/base cases/float literal/main.sdstest @@ -0,0 +1,9 @@ +package tests.partialValidation.baseCases.floatLiteral + +pipeline test { + // $TEST$ constant serialization 1.25 + »1.25«; + + // $TEST$ constant serialization 0.02 + »2e-2«; +} diff --git a/tests/resources/partial evaluation/base cases/int literal/main.sdstest b/tests/resources/partial evaluation/base cases/int literal/main.sdstest new file mode 100644 index 000000000..a3e4abd55 --- /dev/null +++ b/tests/resources/partial evaluation/base cases/int literal/main.sdstest @@ -0,0 +1,6 @@ +package tests.partialValidation.baseCases.intLiteral + +pipeline test { + // $TEST$ constant serialization 123 + »123«; +} diff --git a/tests/resources/partial evaluation/base cases/null literal/main.sdstest b/tests/resources/partial evaluation/base cases/null literal/main.sdstest new file mode 100644 index 000000000..7b7e0a892 --- /dev/null +++ b/tests/resources/partial evaluation/base cases/null literal/main.sdstest @@ -0,0 +1,6 @@ +package tests.partialValidation.baseCases.nullLiteral + +pipeline test { + // $TEST$ constant serialization null + »null«; +} diff --git a/tests/resources/partial evaluation/base cases/string literal (with interpolation)/main.sdstest b/tests/resources/partial evaluation/base cases/string literal (with interpolation)/main.sdstest new file mode 100644 index 000000000..c3ae42683 --- /dev/null +++ b/tests/resources/partial evaluation/base cases/string literal (with interpolation)/main.sdstest @@ -0,0 +1,12 @@ +package tests.partialValidation.baseCases.stringLiteralWithInterpolation + +pipeline test { + // $TEST$ constant serialization "start 1 inner1 true inner2 test end" + »"start {{ 1 }} inner1 {{ true }} inner2 {{ "test" }} end"«; + + // $TEST$ constant serialization "start 1 inner1 true inner2 test end " + »"start\t{{ 1 }} inner1\t{{ true }} inner2\t{{ "test" }} end\t"«; + + // $TEST$ not constant + »"start {{ call() }} end"«; +} diff --git a/tests/resources/partial evaluation/base cases/string literal (without interpolation)/main.sdstest b/tests/resources/partial evaluation/base cases/string literal (without interpolation)/main.sdstest new file mode 100644 index 000000000..4d835aa45 --- /dev/null +++ b/tests/resources/partial evaluation/base cases/string literal (without interpolation)/main.sdstest @@ -0,0 +1,9 @@ +package tests.partialValidation.baseCases.stringLiteralWithoutInterpolation + +pipeline test { + // $TEST$ constant serialization "test" + »"test"«; + + // $TEST$ constant serialization "test " + »"test\t"«; +} diff --git a/tsconfig.json b/tsconfig.json index 0ac163e0d..0a01d76a8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,7 @@ "moduleResolution": "Node16", "esModuleInterop": true, "skipLibCheck": true, + "strictNullChecks": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*.ts"],