From 026e86d918aceacc24d9f3689c1f24369d5dcad8 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Fri, 10 Nov 2023 22:43:41 +0100 Subject: [PATCH 1/6] refactor: extract method to compute a class type for a literal type --- .../language/typing/safe-ds-type-checker.ts | 25 ++------------ .../language/typing/safe-ds-type-computer.ts | 34 ++++++++++++++++++- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/packages/safe-ds-lang/src/language/typing/safe-ds-type-checker.ts b/packages/safe-ds-lang/src/language/typing/safe-ds-type-checker.ts index 7aea9d873..9dd319b84 100644 --- a/packages/safe-ds-lang/src/language/typing/safe-ds-type-checker.ts +++ b/packages/safe-ds-lang/src/language/typing/safe-ds-type-checker.ts @@ -2,14 +2,7 @@ import { getContainerOfType } from 'langium'; import type { SafeDsClasses } from '../builtins/safe-ds-classes.js'; import { isSdsEnum, type SdsAbstractResult, SdsDeclaration } from '../generated/ast.js'; import { getParameters } from '../helpers/nodeProperties.js'; -import { - BooleanConstant, - Constant, - FloatConstant, - IntConstant, - NullConstant, - StringConstant, -} from '../partialEvaluation/model.js'; +import { Constant } from '../partialEvaluation/model.js'; import { SafeDsServices } from '../safe-ds-module.js'; import { CallableType, @@ -190,21 +183,7 @@ export class SafeDsTypeChecker { } private constantIsAssignableToClassType(constant: Constant, other: ClassType): boolean { - let classType: Type; - if (constant instanceof BooleanConstant) { - classType = this.coreTypes.Boolean; - } else if (constant instanceof FloatConstant) { - classType = this.coreTypes.Float; - } else if (constant instanceof IntConstant) { - classType = this.coreTypes.Int; - } else if (constant === NullConstant) { - classType = this.coreTypes.NothingOrNull; - } else if (constant instanceof StringConstant) { - classType = this.coreTypes.String; - } /* c8 ignore start */ else { - throw new Error(`Unexpected constant type: ${constant.constructor.name}`); - } /* c8 ignore stop */ - + const classType = this.typeComputer().computeClassTypeForConstant(constant); return this.isAssignableTo(classType, other); } diff --git a/packages/safe-ds-lang/src/language/typing/safe-ds-type-computer.ts b/packages/safe-ds-lang/src/language/typing/safe-ds-type-computer.ts index 5b4acc3d3..53025e1d1 100644 --- a/packages/safe-ds-lang/src/language/typing/safe-ds-type-computer.ts +++ b/packages/safe-ds-lang/src/language/typing/safe-ds-type-computer.ts @@ -70,7 +70,15 @@ import { streamBlockLambdaResults, } from '../helpers/nodeProperties.js'; import { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js'; -import { Constant, isConstant } from '../partialEvaluation/model.js'; +import { + BooleanConstant, + Constant, + FloatConstant, + IntConstant, + isConstant, + NullConstant, + StringConstant, +} from '../partialEvaluation/model.js'; import { SafeDsPartialEvaluator } from '../partialEvaluation/safe-ds-partial-evaluator.js'; import { SafeDsServices } from '../safe-ds-module.js'; import { @@ -482,6 +490,30 @@ export class SafeDsTypeComputer { } /* c8 ignore stop */ } + // ----------------------------------------------------------------------------------------------------------------- + // Compute class types for literal types and their constants + // ----------------------------------------------------------------------------------------------------------------- + + computeClassTypeForLiteralType(literalType: LiteralType): Type { + return this.lowestCommonSupertype(...literalType.constants.map((it) => this.computeClassTypeForConstant(it))); + } + + computeClassTypeForConstant(constant: Constant): Type { + if (constant instanceof BooleanConstant) { + return this.coreTypes.Boolean; + } else if (constant instanceof FloatConstant) { + return this.coreTypes.Float; + } else if (constant instanceof IntConstant) { + return this.coreTypes.Int; + } else if (constant === NullConstant) { + return this.coreTypes.NothingOrNull; + } else if (constant instanceof StringConstant) { + return this.coreTypes.String; + } /* c8 ignore start */ else { + throw new Error(`Unexpected constant type: ${constant.constructor.name}`); + } /* c8 ignore stop */ + } + // ----------------------------------------------------------------------------------------------------------------- // Lowest common supertype // ----------------------------------------------------------------------------------------------------------------- From 506fb6f83410bcc3d449cc14f4b3c0340ade3465 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Fri, 10 Nov 2023 22:56:51 +0100 Subject: [PATCH 2/6] test: allow selected warnings in builtin files --- .../builtins/builtinFilesCorrectness.test.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/safe-ds-lang/tests/language/builtins/builtinFilesCorrectness.test.ts b/packages/safe-ds-lang/tests/language/builtins/builtinFilesCorrectness.test.ts index 81e434905..0e86e8d7b 100644 --- a/packages/safe-ds-lang/tests/language/builtins/builtinFilesCorrectness.test.ts +++ b/packages/safe-ds-lang/tests/language/builtins/builtinFilesCorrectness.test.ts @@ -9,10 +9,25 @@ import { locationToString } from '../../helpers/location.js'; import { AssertionError } from 'assert'; import { isEmpty } from '../../../src/helpers/collectionUtils.js'; import { loadDocuments } from '../../helpers/testResources.js'; +import { CODE_EXPERIMENTAL_LANGUAGE_FEATURE } from '../../../src/language/validation/experimentalLanguageFeatures.js'; +import { + CODE_EXPERIMENTAL_ASSIGNED_RESULT, + CODE_EXPERIMENTAL_CALLED_ANNOTATION, + CODE_EXPERIMENTAL_CORRESPONDING_PARAMETER, + CODE_EXPERIMENTAL_REFERENCED_DECLARATION, +} from '../../../src/language/validation/builtins/experimental.js'; const services = createSafeDsServices(NodeFileSystem).SafeDs; const builtinFiles = listBuiltinFiles(); +const ignoredWarnings: (number | string | undefined)[] = [ + CODE_EXPERIMENTAL_LANGUAGE_FEATURE, + CODE_EXPERIMENTAL_ASSIGNED_RESULT, + CODE_EXPERIMENTAL_CALLED_ANNOTATION, + CODE_EXPERIMENTAL_CORRESPONDING_PARAMETER, + CODE_EXPERIMENTAL_REFERENCED_DECLARATION, +]; + describe('builtin files', () => { beforeAll(async () => { await loadDocuments(services, builtinFiles, { validation: true }); @@ -22,6 +37,7 @@ describe('builtin files', () => { uri, shortenedResourceName: uriToShortenedResourceName(uri, 'builtins'), })); + it.each(testCases)('[$shortenedResourceName] should have no errors or warnings', async ({ uri }) => { const document = services.shared.workspace.LangiumDocuments.getOrCreateDocument(uri); @@ -29,7 +45,7 @@ describe('builtin files', () => { document.diagnostics?.filter( (diagnostic) => diagnostic.severity === DiagnosticSeverity.Error || - diagnostic.severity === DiagnosticSeverity.Warning, + (diagnostic.severity === DiagnosticSeverity.Warning && !ignoredWarnings.includes(diagnostic.code)), ) ?? []; if (!isEmpty(errorsOrWarnings)) { From 603e3e965a39dde9541ba9412100054152016e67 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Fri, 10 Nov 2023 23:05:01 +0100 Subject: [PATCH 3/6] test: always add the members of `Any` --- .../tests/language/typing/safe-ds-class-hierarchy.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/safe-ds-lang/tests/language/typing/safe-ds-class-hierarchy.test.ts b/packages/safe-ds-lang/tests/language/typing/safe-ds-class-hierarchy.test.ts index 1f7271901..f2cfebbe4 100644 --- a/packages/safe-ds-lang/tests/language/typing/safe-ds-class-hierarchy.test.ts +++ b/packages/safe-ds-lang/tests/language/typing/safe-ds-class-hierarchy.test.ts @@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { isSdsClass, SdsClass } from '../../../src/language/generated/ast.js'; import { createSafeDsServices } from '../../../src/language/index.js'; import { getNodeOfType } from '../../helpers/nodeFinder.js'; +import { getMatchingClassMembers } from '../../../src/language/helpers/nodeProperties.js'; const services = createSafeDsServices(NodeFileSystem).SafeDs; const builtinClasses = services.builtins.Classes; @@ -197,7 +198,8 @@ describe('SafeDsClassHierarchy', async () => { it.each(testCases)('$testName', async ({ code, index, expected }) => { const firstClass = await getNodeOfType(services, code, isSdsClass, index); - expect(superclassMemberNames(firstClass)).toStrictEqual(expected); + const anyMembers = getMatchingClassMembers(builtinClasses.Any).map((member) => member.name); + expect(superclassMemberNames(firstClass)).toStrictEqual(expected.concat(anyMembers)); }); }); }); From f62f78dbafd1f8a539c648bb2ecedf46a558c71c Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Fri, 10 Nov 2023 23:05:19 +0100 Subject: [PATCH 4/6] feat: add `toString` method to `Any` --- .../resources/builtins/safeds/lang/coreClasses.sdsstub | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/safe-ds-lang/src/resources/builtins/safeds/lang/coreClasses.sdsstub b/packages/safe-ds-lang/src/resources/builtins/safeds/lang/coreClasses.sdsstub index a3fe9a769..39c815b02 100644 --- a/packages/safe-ds-lang/src/resources/builtins/safeds/lang/coreClasses.sdsstub +++ b/packages/safe-ds-lang/src/resources/builtins/safeds/lang/coreClasses.sdsstub @@ -3,7 +3,14 @@ package safeds.lang /** * The common superclass of all classes. */ -class Any +class Any { + + /** + * Returns a string representation of the object. + */ + @PythonCall("str($this)") + fun toString() -> s: String +} /** * The common subclass of all classes. From b9450f7a0708767d2b8d3a88a27e62a274cc1451 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Sat, 11 Nov 2023 09:57:20 +0100 Subject: [PATCH 5/6] test: `computeClassTypeForLiteralType` --- .../src/language/typing/safe-ds-core-types.ts | 4 + .../computeClassTypeForLiteralType.test.ts | 108 ++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 packages/safe-ds-lang/tests/language/typing/type computer/computeClassTypeForLiteralType.test.ts diff --git a/packages/safe-ds-lang/src/language/typing/safe-ds-core-types.ts b/packages/safe-ds-lang/src/language/typing/safe-ds-core-types.ts index bbb0c1a81..e6dec7671 100644 --- a/packages/safe-ds-lang/src/language/typing/safe-ds-core-types.ts +++ b/packages/safe-ds-lang/src/language/typing/safe-ds-core-types.ts @@ -49,6 +49,10 @@ export class SafeDsCoreTypes { return this.createCoreType(this.builtinClasses.Nothing, true); } + get Number(): Type { + return this.createCoreType(this.builtinClasses.Number); + } + get String(): Type { return this.createCoreType(this.builtinClasses.String); } diff --git a/packages/safe-ds-lang/tests/language/typing/type computer/computeClassTypeForLiteralType.test.ts b/packages/safe-ds-lang/tests/language/typing/type computer/computeClassTypeForLiteralType.test.ts new file mode 100644 index 000000000..8c422af27 --- /dev/null +++ b/packages/safe-ds-lang/tests/language/typing/type computer/computeClassTypeForLiteralType.test.ts @@ -0,0 +1,108 @@ +import { NodeFileSystem } from 'langium/node'; +import { describe, expect, it } from 'vitest'; +import { createSafeDsServicesWithBuiltins } from '../../../../src/language/index.js'; +import { + BooleanConstant, + FloatConstant, + IntConstant, + NullConstant, + StringConstant, +} from '../../../../src/language/partialEvaluation/model.js'; +import { LiteralType, Type } from '../../../../src/language/typing/model.js'; + +const services = (await createSafeDsServicesWithBuiltins(NodeFileSystem)).SafeDs; +const coreTypes = services.types.CoreTypes; +const typeComputer = services.types.TypeComputer; + +const tests: ComputeClassTypeForLiteralTypeTest[] = [ + // Base cases + { + literalType: new LiteralType(), + expected: coreTypes.Nothing, + }, + { + literalType: new LiteralType(new BooleanConstant(false)), + expected: coreTypes.Boolean, + }, + { + literalType: new LiteralType(new FloatConstant(1.5)), + expected: coreTypes.Float, + }, + { + literalType: new LiteralType(new IntConstant(1n)), + expected: coreTypes.Int, + }, + { + literalType: new LiteralType(NullConstant), + expected: coreTypes.NothingOrNull, + }, + { + literalType: new LiteralType(new StringConstant('')), + expected: coreTypes.String, + }, + // Nullable types + { + literalType: new LiteralType(new BooleanConstant(false), NullConstant), + expected: coreTypes.Boolean.updateNullability(true), + }, + { + literalType: new LiteralType(new FloatConstant(1.5), NullConstant), + expected: coreTypes.Float.updateNullability(true), + }, + { + literalType: new LiteralType(new IntConstant(1n), NullConstant), + expected: coreTypes.Int.updateNullability(true), + }, + { + literalType: new LiteralType(new StringConstant(''), NullConstant), + expected: coreTypes.String.updateNullability(true), + }, + // Other combinations + { + literalType: new LiteralType(new BooleanConstant(false), new FloatConstant(1.5)), + expected: coreTypes.Any, + }, + { + literalType: new LiteralType(new FloatConstant(1.5), new IntConstant(1n)), + expected: coreTypes.Number, + }, + { + literalType: new LiteralType(new IntConstant(1n), new StringConstant('')), + expected: coreTypes.Any, + }, + { + literalType: new LiteralType(new BooleanConstant(false), new FloatConstant(1.5), NullConstant), + expected: coreTypes.AnyOrNull, + }, + { + literalType: new LiteralType(new FloatConstant(1.5), new IntConstant(1n), NullConstant), + expected: coreTypes.Number.updateNullability(true), + }, + { + literalType: new LiteralType(new IntConstant(1n), new StringConstant(''), NullConstant), + expected: coreTypes.AnyOrNull, + }, +]; + +describe.each(tests)('computeClassTypeForLiteralType', ({ literalType, expected }) => { + it(`should return the class type for a literal type (${literalType})`, () => { + expect(typeComputer.computeClassTypeForLiteralType(literalType)).toSatisfy((actual: Type) => + actual.equals(expected), + ); + }); +}); + +/** + * A test case for {@link computeClassTypeForLiteralType}. + */ +interface ComputeClassTypeForLiteralTypeTest { + /** + * The literal type to compute the class type for. + */ + literalType: LiteralType; + + /** + * The expected type. + */ + expected: Type; +} From 7965988c99db03803a963bd9138cf8a713c50f0a Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Sat, 11 Nov 2023 10:18:01 +0100 Subject: [PATCH 6/6] feat: scoping for member access on literals and literal types --- .../scoping/safe-ds-scope-provider.ts | 5 ++- .../tests/language/scoping/creator.ts | 14 ++++---- .../tests/language/scoping/scoping.test.ts | 36 +++++++++++++++---- 3 files changed, 40 insertions(+), 15 deletions(-) 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 7fa1a54d5..0da66db24 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 @@ -70,7 +70,7 @@ import { } from '../helpers/nodeProperties.js'; import { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js'; import { SafeDsServices } from '../safe-ds-module.js'; -import { ClassType, EnumVariantType } from '../typing/model.js'; +import { ClassType, EnumVariantType, LiteralType } from '../typing/model.js'; import type { SafeDsClassHierarchy } from '../typing/safe-ds-class-hierarchy.js'; import { SafeDsTypeComputer } from '../typing/safe-ds-type-computer.js'; import { SafeDsPackageManager } from '../workspace/safe-ds-package-manager.js'; @@ -210,6 +210,9 @@ export class SafeDsScopeProvider extends DefaultScopeProvider { // Members let receiverType = this.typeComputer.computeType(node.receiver); + if (receiverType instanceof LiteralType) { + receiverType = this.typeComputer.computeClassTypeForLiteralType(receiverType); + } if (receiverType instanceof ClassType) { const ownInstanceMembers = getMatchingClassMembers(receiverType.declaration, (it) => !isStatic(it)); diff --git a/packages/safe-ds-lang/tests/language/scoping/creator.ts b/packages/safe-ds-lang/tests/language/scoping/creator.ts index 6781f4379..1c82a3eff 100644 --- a/packages/safe-ds-lang/tests/language/scoping/creator.ts +++ b/packages/safe-ds-lang/tests/language/scoping/creator.ts @@ -1,14 +1,14 @@ -import { - listTestSafeDsFilesGroupedByParentDirectory, - uriToShortenedTestResourceName, -} from '../../helpers/testResources.js'; import fs from 'fs'; -import { findTestChecks } from '../../helpers/testChecks.js'; +import { EmptyFileSystem, URI } from 'langium'; import { Location } from 'vscode-languageserver'; +import { createSafeDsServices } from '../../../src/language/index.js'; import { getSyntaxErrors, SyntaxErrorsInCodeError } from '../../helpers/diagnostics.js'; -import { EmptyFileSystem, URI } from 'langium'; -import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; +import { findTestChecks } from '../../helpers/testChecks.js'; import { TestDescription, TestDescriptionError } from '../../helpers/testDescription.js'; +import { + listTestSafeDsFilesGroupedByParentDirectory, + uriToShortenedTestResourceName, +} from '../../helpers/testResources.js'; const services = createSafeDsServices(EmptyFileSystem).SafeDs; const rootResourceName = 'scoping'; diff --git a/packages/safe-ds-lang/tests/language/scoping/scoping.test.ts b/packages/safe-ds-lang/tests/language/scoping/scoping.test.ts index 761faae23..5d5473245 100644 --- a/packages/safe-ds-lang/tests/language/scoping/scoping.test.ts +++ b/packages/safe-ds-lang/tests/language/scoping/scoping.test.ts @@ -1,13 +1,13 @@ -import { afterEach, beforeEach, describe, it } from 'vitest'; -import { createSafeDsServices } from '../../../src/language/index.js'; -import { LangiumDocument, Reference, URI } from 'langium'; -import { NodeFileSystem } from 'langium/node'; -import { clearDocuments, isRangeEqual } from 'langium/test'; import { AssertionError } from 'assert'; -import { isLocationEqual, locationToString } from '../../helpers/location.js'; -import { createScopingTests, ExpectedReference } from './creator.js'; +import { DocumentValidator, LangiumDocument, Reference, URI } from 'langium'; +import { NodeFileSystem } from 'langium/node'; +import { clearDocuments, isRangeEqual, validationHelper } from 'langium/test'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { Location } from 'vscode-languageserver'; +import { createSafeDsServices } from '../../../src/language/index.js'; +import { isLocationEqual, locationToString } from '../../helpers/location.js'; import { loadDocuments } from '../../helpers/testResources.js'; +import { createScopingTests, ExpectedReference } from './creator.js'; const services = createSafeDsServices(NodeFileSystem).SafeDs; @@ -68,6 +68,28 @@ describe('scoping', async () => { } } }); + + it('should resolve members on literals', async () => { + const code = ` + pipeline myPipeline { + 1.toString(); + } + `; + const { diagnostics } = await validationHelper(services)(code); + const linkingError = diagnostics.filter((d) => d.data?.code === DocumentValidator.LinkingError); + expect(linkingError).toStrictEqual([]); + }); + + it('should resolve members on literal types', async () => { + const code = ` + segment mySegment(p: literal<"">) { + p.toString(); + } + `; + const { diagnostics } = await validationHelper(services)(code); + const linkingError = diagnostics.filter((d) => d.data?.code === DocumentValidator.LinkingError); + expect(linkingError).toStrictEqual([]); + }); }); /**