Skip to content

Commit e60e456

Browse files
authored
feat: scoping for member access on literals and literal types (#754)
Closes #80 ### Summary of Changes Resolved accessed members on literals and literal types.
1 parent d48e1e0 commit e60e456

File tree

10 files changed

+215
-42
lines changed

10 files changed

+215
-42
lines changed

packages/safe-ds-lang/src/language/scoping/safe-ds-scope-provider.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ import {
7070
} from '../helpers/nodeProperties.js';
7171
import { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js';
7272
import { SafeDsServices } from '../safe-ds-module.js';
73-
import { ClassType, EnumVariantType } from '../typing/model.js';
73+
import { ClassType, EnumVariantType, LiteralType } from '../typing/model.js';
7474
import type { SafeDsClassHierarchy } from '../typing/safe-ds-class-hierarchy.js';
7575
import { SafeDsTypeComputer } from '../typing/safe-ds-type-computer.js';
7676
import { SafeDsPackageManager } from '../workspace/safe-ds-package-manager.js';
@@ -210,6 +210,9 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
210210

211211
// Members
212212
let receiverType = this.typeComputer.computeType(node.receiver);
213+
if (receiverType instanceof LiteralType) {
214+
receiverType = this.typeComputer.computeClassTypeForLiteralType(receiverType);
215+
}
213216

214217
if (receiverType instanceof ClassType) {
215218
const ownInstanceMembers = getMatchingClassMembers(receiverType.declaration, (it) => !isStatic(it));

packages/safe-ds-lang/src/language/typing/safe-ds-core-types.ts

+4
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ export class SafeDsCoreTypes {
4949
return this.createCoreType(this.builtinClasses.Nothing, true);
5050
}
5151

52+
get Number(): Type {
53+
return this.createCoreType(this.builtinClasses.Number);
54+
}
55+
5256
get String(): Type {
5357
return this.createCoreType(this.builtinClasses.String);
5458
}

packages/safe-ds-lang/src/language/typing/safe-ds-type-checker.ts

+2-23
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,7 @@ import { getContainerOfType } from 'langium';
22
import type { SafeDsClasses } from '../builtins/safe-ds-classes.js';
33
import { isSdsEnum, type SdsAbstractResult, SdsDeclaration } from '../generated/ast.js';
44
import { getParameters } from '../helpers/nodeProperties.js';
5-
import {
6-
BooleanConstant,
7-
Constant,
8-
FloatConstant,
9-
IntConstant,
10-
NullConstant,
11-
StringConstant,
12-
} from '../partialEvaluation/model.js';
5+
import { Constant } from '../partialEvaluation/model.js';
136
import { SafeDsServices } from '../safe-ds-module.js';
147
import {
158
CallableType,
@@ -190,21 +183,7 @@ export class SafeDsTypeChecker {
190183
}
191184

192185
private constantIsAssignableToClassType(constant: Constant, other: ClassType): boolean {
193-
let classType: Type;
194-
if (constant instanceof BooleanConstant) {
195-
classType = this.coreTypes.Boolean;
196-
} else if (constant instanceof FloatConstant) {
197-
classType = this.coreTypes.Float;
198-
} else if (constant instanceof IntConstant) {
199-
classType = this.coreTypes.Int;
200-
} else if (constant === NullConstant) {
201-
classType = this.coreTypes.NothingOrNull;
202-
} else if (constant instanceof StringConstant) {
203-
classType = this.coreTypes.String;
204-
} /* c8 ignore start */ else {
205-
throw new Error(`Unexpected constant type: ${constant.constructor.name}`);
206-
} /* c8 ignore stop */
207-
186+
const classType = this.typeComputer().computeClassTypeForConstant(constant);
208187
return this.isAssignableTo(classType, other);
209188
}
210189

packages/safe-ds-lang/src/language/typing/safe-ds-type-computer.ts

+33-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,15 @@ import {
7070
streamBlockLambdaResults,
7171
} from '../helpers/nodeProperties.js';
7272
import { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js';
73-
import { Constant, isConstant } from '../partialEvaluation/model.js';
73+
import {
74+
BooleanConstant,
75+
Constant,
76+
FloatConstant,
77+
IntConstant,
78+
isConstant,
79+
NullConstant,
80+
StringConstant,
81+
} from '../partialEvaluation/model.js';
7482
import { SafeDsPartialEvaluator } from '../partialEvaluation/safe-ds-partial-evaluator.js';
7583
import { SafeDsServices } from '../safe-ds-module.js';
7684
import {
@@ -482,6 +490,30 @@ export class SafeDsTypeComputer {
482490
} /* c8 ignore stop */
483491
}
484492

493+
// -----------------------------------------------------------------------------------------------------------------
494+
// Compute class types for literal types and their constants
495+
// -----------------------------------------------------------------------------------------------------------------
496+
497+
computeClassTypeForLiteralType(literalType: LiteralType): Type {
498+
return this.lowestCommonSupertype(...literalType.constants.map((it) => this.computeClassTypeForConstant(it)));
499+
}
500+
501+
computeClassTypeForConstant(constant: Constant): Type {
502+
if (constant instanceof BooleanConstant) {
503+
return this.coreTypes.Boolean;
504+
} else if (constant instanceof FloatConstant) {
505+
return this.coreTypes.Float;
506+
} else if (constant instanceof IntConstant) {
507+
return this.coreTypes.Int;
508+
} else if (constant === NullConstant) {
509+
return this.coreTypes.NothingOrNull;
510+
} else if (constant instanceof StringConstant) {
511+
return this.coreTypes.String;
512+
} /* c8 ignore start */ else {
513+
throw new Error(`Unexpected constant type: ${constant.constructor.name}`);
514+
} /* c8 ignore stop */
515+
}
516+
485517
// -----------------------------------------------------------------------------------------------------------------
486518
// Lowest common supertype
487519
// -----------------------------------------------------------------------------------------------------------------

packages/safe-ds-lang/src/resources/builtins/safeds/lang/coreClasses.sdsstub

+8-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@ package safeds.lang
33
/**
44
* The common superclass of all classes.
55
*/
6-
class Any
6+
class Any {
7+
8+
/**
9+
* Returns a string representation of the object.
10+
*/
11+
@PythonCall("str($this)")
12+
fun toString() -> s: String
13+
}
714

815
/**
916
* The common subclass of all classes.

packages/safe-ds-lang/tests/language/builtins/builtinFilesCorrectness.test.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,25 @@ import { locationToString } from '../../helpers/location.js';
99
import { AssertionError } from 'assert';
1010
import { isEmpty } from '../../../src/helpers/collectionUtils.js';
1111
import { loadDocuments } from '../../helpers/testResources.js';
12+
import { CODE_EXPERIMENTAL_LANGUAGE_FEATURE } from '../../../src/language/validation/experimentalLanguageFeatures.js';
13+
import {
14+
CODE_EXPERIMENTAL_ASSIGNED_RESULT,
15+
CODE_EXPERIMENTAL_CALLED_ANNOTATION,
16+
CODE_EXPERIMENTAL_CORRESPONDING_PARAMETER,
17+
CODE_EXPERIMENTAL_REFERENCED_DECLARATION,
18+
} from '../../../src/language/validation/builtins/experimental.js';
1219

1320
const services = createSafeDsServices(NodeFileSystem).SafeDs;
1421
const builtinFiles = listBuiltinFiles();
1522

23+
const ignoredWarnings: (number | string | undefined)[] = [
24+
CODE_EXPERIMENTAL_LANGUAGE_FEATURE,
25+
CODE_EXPERIMENTAL_ASSIGNED_RESULT,
26+
CODE_EXPERIMENTAL_CALLED_ANNOTATION,
27+
CODE_EXPERIMENTAL_CORRESPONDING_PARAMETER,
28+
CODE_EXPERIMENTAL_REFERENCED_DECLARATION,
29+
];
30+
1631
describe('builtin files', () => {
1732
beforeAll(async () => {
1833
await loadDocuments(services, builtinFiles, { validation: true });
@@ -22,14 +37,15 @@ describe('builtin files', () => {
2237
uri,
2338
shortenedResourceName: uriToShortenedResourceName(uri, 'builtins'),
2439
}));
40+
2541
it.each(testCases)('[$shortenedResourceName] should have no errors or warnings', async ({ uri }) => {
2642
const document = services.shared.workspace.LangiumDocuments.getOrCreateDocument(uri);
2743

2844
const errorsOrWarnings =
2945
document.diagnostics?.filter(
3046
(diagnostic) =>
3147
diagnostic.severity === DiagnosticSeverity.Error ||
32-
diagnostic.severity === DiagnosticSeverity.Warning,
48+
(diagnostic.severity === DiagnosticSeverity.Warning && !ignoredWarnings.includes(diagnostic.code)),
3349
) ?? [];
3450

3551
if (!isEmpty(errorsOrWarnings)) {

packages/safe-ds-lang/tests/language/scoping/creator.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import {
2-
listTestSafeDsFilesGroupedByParentDirectory,
3-
uriToShortenedTestResourceName,
4-
} from '../../helpers/testResources.js';
51
import fs from 'fs';
6-
import { findTestChecks } from '../../helpers/testChecks.js';
2+
import { EmptyFileSystem, URI } from 'langium';
73
import { Location } from 'vscode-languageserver';
4+
import { createSafeDsServices } from '../../../src/language/index.js';
85
import { getSyntaxErrors, SyntaxErrorsInCodeError } from '../../helpers/diagnostics.js';
9-
import { EmptyFileSystem, URI } from 'langium';
10-
import { createSafeDsServices } from '../../../src/language/safe-ds-module.js';
6+
import { findTestChecks } from '../../helpers/testChecks.js';
117
import { TestDescription, TestDescriptionError } from '../../helpers/testDescription.js';
8+
import {
9+
listTestSafeDsFilesGroupedByParentDirectory,
10+
uriToShortenedTestResourceName,
11+
} from '../../helpers/testResources.js';
1212

1313
const services = createSafeDsServices(EmptyFileSystem).SafeDs;
1414
const rootResourceName = 'scoping';

packages/safe-ds-lang/tests/language/scoping/scoping.test.ts

+29-7
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import { afterEach, beforeEach, describe, it } from 'vitest';
2-
import { createSafeDsServices } from '../../../src/language/index.js';
3-
import { LangiumDocument, Reference, URI } from 'langium';
4-
import { NodeFileSystem } from 'langium/node';
5-
import { clearDocuments, isRangeEqual } from 'langium/test';
61
import { AssertionError } from 'assert';
7-
import { isLocationEqual, locationToString } from '../../helpers/location.js';
8-
import { createScopingTests, ExpectedReference } from './creator.js';
2+
import { DocumentValidator, LangiumDocument, Reference, URI } from 'langium';
3+
import { NodeFileSystem } from 'langium/node';
4+
import { clearDocuments, isRangeEqual, validationHelper } from 'langium/test';
5+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
96
import { Location } from 'vscode-languageserver';
7+
import { createSafeDsServices } from '../../../src/language/index.js';
8+
import { isLocationEqual, locationToString } from '../../helpers/location.js';
109
import { loadDocuments } from '../../helpers/testResources.js';
10+
import { createScopingTests, ExpectedReference } from './creator.js';
1111

1212
const services = createSafeDsServices(NodeFileSystem).SafeDs;
1313

@@ -68,6 +68,28 @@ describe('scoping', async () => {
6868
}
6969
}
7070
});
71+
72+
it('should resolve members on literals', async () => {
73+
const code = `
74+
pipeline myPipeline {
75+
1.toString();
76+
}
77+
`;
78+
const { diagnostics } = await validationHelper(services)(code);
79+
const linkingError = diagnostics.filter((d) => d.data?.code === DocumentValidator.LinkingError);
80+
expect(linkingError).toStrictEqual([]);
81+
});
82+
83+
it('should resolve members on literal types', async () => {
84+
const code = `
85+
segment mySegment(p: literal<"">) {
86+
p.toString();
87+
}
88+
`;
89+
const { diagnostics } = await validationHelper(services)(code);
90+
const linkingError = diagnostics.filter((d) => d.data?.code === DocumentValidator.LinkingError);
91+
expect(linkingError).toStrictEqual([]);
92+
});
7193
});
7294

7395
/**

packages/safe-ds-lang/tests/language/typing/safe-ds-class-hierarchy.test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
44
import { isSdsClass, SdsClass } from '../../../src/language/generated/ast.js';
55
import { createSafeDsServices } from '../../../src/language/index.js';
66
import { getNodeOfType } from '../../helpers/nodeFinder.js';
7+
import { getMatchingClassMembers } from '../../../src/language/helpers/nodeProperties.js';
78

89
const services = createSafeDsServices(NodeFileSystem).SafeDs;
910
const builtinClasses = services.builtins.Classes;
@@ -197,7 +198,8 @@ describe('SafeDsClassHierarchy', async () => {
197198

198199
it.each(testCases)('$testName', async ({ code, index, expected }) => {
199200
const firstClass = await getNodeOfType(services, code, isSdsClass, index);
200-
expect(superclassMemberNames(firstClass)).toStrictEqual(expected);
201+
const anyMembers = getMatchingClassMembers(builtinClasses.Any).map((member) => member.name);
202+
expect(superclassMemberNames(firstClass)).toStrictEqual(expected.concat(anyMembers));
201203
});
202204
});
203205
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { NodeFileSystem } from 'langium/node';
2+
import { describe, expect, it } from 'vitest';
3+
import { createSafeDsServicesWithBuiltins } from '../../../../src/language/index.js';
4+
import {
5+
BooleanConstant,
6+
FloatConstant,
7+
IntConstant,
8+
NullConstant,
9+
StringConstant,
10+
} from '../../../../src/language/partialEvaluation/model.js';
11+
import { LiteralType, Type } from '../../../../src/language/typing/model.js';
12+
13+
const services = (await createSafeDsServicesWithBuiltins(NodeFileSystem)).SafeDs;
14+
const coreTypes = services.types.CoreTypes;
15+
const typeComputer = services.types.TypeComputer;
16+
17+
const tests: ComputeClassTypeForLiteralTypeTest[] = [
18+
// Base cases
19+
{
20+
literalType: new LiteralType(),
21+
expected: coreTypes.Nothing,
22+
},
23+
{
24+
literalType: new LiteralType(new BooleanConstant(false)),
25+
expected: coreTypes.Boolean,
26+
},
27+
{
28+
literalType: new LiteralType(new FloatConstant(1.5)),
29+
expected: coreTypes.Float,
30+
},
31+
{
32+
literalType: new LiteralType(new IntConstant(1n)),
33+
expected: coreTypes.Int,
34+
},
35+
{
36+
literalType: new LiteralType(NullConstant),
37+
expected: coreTypes.NothingOrNull,
38+
},
39+
{
40+
literalType: new LiteralType(new StringConstant('')),
41+
expected: coreTypes.String,
42+
},
43+
// Nullable types
44+
{
45+
literalType: new LiteralType(new BooleanConstant(false), NullConstant),
46+
expected: coreTypes.Boolean.updateNullability(true),
47+
},
48+
{
49+
literalType: new LiteralType(new FloatConstant(1.5), NullConstant),
50+
expected: coreTypes.Float.updateNullability(true),
51+
},
52+
{
53+
literalType: new LiteralType(new IntConstant(1n), NullConstant),
54+
expected: coreTypes.Int.updateNullability(true),
55+
},
56+
{
57+
literalType: new LiteralType(new StringConstant(''), NullConstant),
58+
expected: coreTypes.String.updateNullability(true),
59+
},
60+
// Other combinations
61+
{
62+
literalType: new LiteralType(new BooleanConstant(false), new FloatConstant(1.5)),
63+
expected: coreTypes.Any,
64+
},
65+
{
66+
literalType: new LiteralType(new FloatConstant(1.5), new IntConstant(1n)),
67+
expected: coreTypes.Number,
68+
},
69+
{
70+
literalType: new LiteralType(new IntConstant(1n), new StringConstant('')),
71+
expected: coreTypes.Any,
72+
},
73+
{
74+
literalType: new LiteralType(new BooleanConstant(false), new FloatConstant(1.5), NullConstant),
75+
expected: coreTypes.AnyOrNull,
76+
},
77+
{
78+
literalType: new LiteralType(new FloatConstant(1.5), new IntConstant(1n), NullConstant),
79+
expected: coreTypes.Number.updateNullability(true),
80+
},
81+
{
82+
literalType: new LiteralType(new IntConstant(1n), new StringConstant(''), NullConstant),
83+
expected: coreTypes.AnyOrNull,
84+
},
85+
];
86+
87+
describe.each(tests)('computeClassTypeForLiteralType', ({ literalType, expected }) => {
88+
it(`should return the class type for a literal type (${literalType})`, () => {
89+
expect(typeComputer.computeClassTypeForLiteralType(literalType)).toSatisfy((actual: Type) =>
90+
actual.equals(expected),
91+
);
92+
});
93+
});
94+
95+
/**
96+
* A test case for {@link computeClassTypeForLiteralType}.
97+
*/
98+
interface ComputeClassTypeForLiteralTypeTest {
99+
/**
100+
* The literal type to compute the class type for.
101+
*/
102+
literalType: LiteralType;
103+
104+
/**
105+
* The expected type.
106+
*/
107+
expected: Type;
108+
}

0 commit comments

Comments
 (0)