Skip to content

Commit

Permalink
feat: scoping for member access on literals and literal types (#754)
Browse files Browse the repository at this point in the history
Closes #80

### Summary of Changes

Resolved accessed members on literals and literal types.
  • Loading branch information
lars-reimann authored Nov 11, 2023
1 parent d48e1e0 commit e60e456
Show file tree
Hide file tree
Showing 10 changed files with 215 additions and 42 deletions.
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
25 changes: 2 additions & 23 deletions packages/safe-ds-lang/src/language/typing/safe-ds-type-checker.ts
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;
}

0 comments on commit e60e456

Please sign in to comment.