Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: scoping for member access on literals and literal types #754

Merged
merged 6 commits into from
Nov 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
// -----------------------------------------------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -22,14 +37,15 @@ 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);

const errorsOrWarnings =
document.diagnostics?.filter(
(diagnostic) =>
diagnostic.severity === DiagnosticSeverity.Error ||
diagnostic.severity === DiagnosticSeverity.Warning,
(diagnostic.severity === DiagnosticSeverity.Warning && !ignoredWarnings.includes(diagnostic.code)),
) ?? [];

if (!isEmpty(errorsOrWarnings)) {
Expand Down
14 changes: 7 additions & 7 deletions packages/safe-ds-lang/tests/language/scoping/creator.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
36 changes: 29 additions & 7 deletions packages/safe-ds-lang/tests/language/scoping/scoping.test.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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([]);
});
});

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
});
});
});
Original file line number Diff line number Diff line change
@@ -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;
}