Skip to content

Commit

Permalink
feat: handle type parameters (part 1) (#856)
Browse files Browse the repository at this point in the history
Closes partially #23

### Summary of Changes

* New kind of type `TypeParameterType` to represent usages of type
parameter references as types.
* Store substitutions for type parameters in `ClassType` and take them
into account for serialization and equality checks.
* Compute element types for list literals.
* Compute key/value types for map literals.
* Compute type of indexed accesses on lists/maps.
* Type checking for the indexed passed to an indexed access on maps (it
already worked for lists).
* Consider type parameter values when checking the assignability of
class types.

### Deferred to future PRs

There are additional features needed to fully handle type parameters,
including

* Inference of type parameter values for calls.
* Substitution of type parameters for types of class members (e.g. for
member access on an attribute).
* Finding the lowest common superclass of class types with type
parameters.

These features will be added later in separate PRs.
  • Loading branch information
lars-reimann authored Feb 5, 2024
1 parent 4ebae94 commit 8a35558
Show file tree
Hide file tree
Showing 83 changed files with 2,091 additions and 975 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AstNode } from 'langium';
import { SymbolTag } from 'vscode-languageserver';
import type { SafeDsAnnotations } from '../builtins/safe-ds-annotations.js';
import { isSdsAnnotatedObject, isSdsFunction, isSdsSegment } from '../generated/ast.js';
import { isSdsAnnotatedObject, isSdsAttribute, isSdsFunction, isSdsSegment } from '../generated/ast.js';
import type { SafeDsServices } from '../safe-ds-module.js';
import { SafeDsTypeComputer } from '../typing/safe-ds-type-computer.js';

Expand All @@ -19,11 +19,13 @@ export class SafeDsNodeInfoProvider {
* hierarchies.
*/
getDetails(node: AstNode): string | undefined {
if (isSdsFunction(node) || isSdsSegment(node)) {
const type = this.typeComputer.computeType(node);
return type?.toString();
if (isSdsAttribute(node)) {
return `: ${this.typeComputer.computeType(node)}`;
} else if (isSdsFunction(node) || isSdsSegment(node)) {
return this.typeComputer.computeType(node)?.toString();
} else {
return undefined;
}
return undefined;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export class SafeDsScopeComputation extends DefaultScopeComputation {

if (isSdsClass(containingDeclaration)) {
this.addToScopesIfKeyIsDefined(scopes, containingDeclaration.parameterList, description);
this.addToScopesIfKeyIsDefined(scopes, containingDeclaration.parentTypeList, description);
this.addToScopesIfKeyIsDefined(scopes, containingDeclaration.constraintList, description);
this.addToScopesIfKeyIsDefined(scopes, containingDeclaration.body, description);
} else if (isSdsFunction(containingDeclaration)) {
Expand Down
167 changes: 163 additions & 4 deletions packages/safe-ds-lang/src/language/typing/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ import {
SdsEnum,
SdsEnumVariant,
SdsParameter,
SdsTypeParameter,
} from '../generated/ast.js';
import { Parameter } from '../helpers/nodeProperties.js';
import { getTypeParameters, Parameter } from '../helpers/nodeProperties.js';
import { Constant, NullConstant } from '../partialEvaluation/model.js';
import { stream } from 'langium';

export type TypeParameterSubstitutions = Map<SdsTypeParameter, Type>;

/**
* The type of an AST node.
Expand All @@ -30,6 +34,11 @@ export abstract class Type {
*/
abstract toString(): string;

/**
* Returns a copy of this type with the given type parameters substituted.
*/
abstract substituteTypeParameters(substitutions: TypeParameterSubstitutions): Type;

/**
* Removes any unnecessary containers from the type.
*/
Expand Down Expand Up @@ -83,6 +92,19 @@ export class CallableType extends Type {
return `(${inputTypeString}) -> ${this.outputType}`;
}

override substituteTypeParameters(substitutions: TypeParameterSubstitutions): CallableType {
if (isEmpty(substitutions)) {
return this;
}

return new CallableType(
this.callable,
this.parameter,
this.inputType.substituteTypeParameters(substitutions),
this.outputType.substituteTypeParameters(substitutions),
);
}

override unwrap(): CallableType {
return new CallableType(
this.callable,
Expand Down Expand Up @@ -129,6 +151,10 @@ export class LiteralType extends Type {
return `literal<${this.constants.join(', ')}>`;
}

override substituteTypeParameters(_substitutions: TypeParameterSubstitutions): Type {
return this;
}

override unwrap(): LiteralType {
return this;
}
Expand Down Expand Up @@ -185,6 +211,14 @@ export class NamedTupleType<T extends SdsDeclaration> extends Type {
return `(${this.entries.join(', ')})`;
}

override substituteTypeParameters(substitutions: TypeParameterSubstitutions): NamedTupleType<T> {
if (isEmpty(substitutions)) {
return this;
}

return new NamedTupleType(...this.entries.map((it) => it.substituteTypeParameters(substitutions)));
}

/**
* If this only has one entry, returns its type. Otherwise, returns this.
*/
Expand Down Expand Up @@ -228,6 +262,15 @@ export class NamedTupleEntry<T extends SdsDeclaration> {
return `${this.name}: ${this.type}`;
}

substituteTypeParameters(substitutions: TypeParameterSubstitutions): NamedTupleEntry<T> {
if (isEmpty(substitutions)) {
/* c8 ignore next 2 */
return this;
}

return new NamedTupleEntry(this.declaration, this.name, this.type.substituteTypeParameters(substitutions));
}

unwrap(): NamedTupleEntry<T> {
return new NamedTupleEntry(this.declaration, this.name, this.type.unwrap());
}
Expand Down Expand Up @@ -256,23 +299,65 @@ export abstract class NamedType<T extends SdsDeclaration> extends Type {
export class ClassType extends NamedType<SdsClass> {
constructor(
declaration: SdsClass,
readonly substitutions: TypeParameterSubstitutions,
override readonly isNullable: boolean,
) {
super(declaration);
}

getTypeParameterTypeByIndex(index: number): Type {
const typeParameter = getTypeParameters(this.declaration)[index];
if (!typeParameter) {
return UnknownType;
}

return this.substitutions.get(typeParameter) ?? UnknownType;
}

override equals(other: unknown): boolean {
if (other === this) {
return true;
} else if (!(other instanceof ClassType)) {
return false;
}

return other.declaration === this.declaration && other.isNullable === this.isNullable;
return (
other.declaration === this.declaration &&
other.isNullable === this.isNullable &&
substitutionsAreEqual(other.substitutions, this.substitutions)
);
}

override toString(): string {
let result = this.declaration.name;

if (this.substitutions.size > 0) {
result += `<${Array.from(this.substitutions.values())
.map((value) => value.toString())
.join(', ')}>`;
}

if (this.isNullable) {
result += '?';
}

return result;
}

override substituteTypeParameters(substitutions: TypeParameterSubstitutions): ClassType {
if (isEmpty(substitutions)) {
return this;
}

const newSubstitutions = new Map(
stream(this.substitutions).map(([key, value]) => [key, value.substituteTypeParameters(substitutions)]),
);

return new ClassType(this.declaration, newSubstitutions, this.isNullable);
}

override updateNullability(isNullable: boolean): ClassType {
return new ClassType(this.declaration, isNullable);
return new ClassType(this.declaration, this.substitutions, isNullable);
}
}

Expand All @@ -294,6 +379,10 @@ export class EnumType extends NamedType<SdsEnum> {
return other.declaration === this.declaration && other.isNullable === this.isNullable;
}

override substituteTypeParameters(_substitutions: TypeParameterSubstitutions): Type {
return this;
}

override updateNullability(isNullable: boolean): EnumType {
return new EnumType(this.declaration, isNullable);
}
Expand All @@ -317,11 +406,42 @@ export class EnumVariantType extends NamedType<SdsEnumVariant> {
return other.declaration === this.declaration && other.isNullable === this.isNullable;
}

override substituteTypeParameters(_substitutions: TypeParameterSubstitutions): Type {
return this;
}

override updateNullability(isNullable: boolean): EnumVariantType {
return new EnumVariantType(this.declaration, isNullable);
}
}

export class TypeParameterType extends NamedType<SdsTypeParameter> {
constructor(
declaration: SdsTypeParameter,
override readonly isNullable: boolean,
) {
super(declaration);
}

override equals(other: unknown): boolean {
if (other === this) {
return true;
} else if (!(other instanceof TypeParameterType)) {
return false;
}

return other.declaration === this.declaration && other.isNullable === this.isNullable;
}

override substituteTypeParameters(substitutions: TypeParameterSubstitutions): Type {
return substitutions.get(this.declaration) ?? this;
}

override updateNullability(isNullable: boolean): TypeParameterType {
return new TypeParameterType(this.declaration, isNullable);
}
}

/**
* A type that represents an actual class, enum, or enum variant instead of an instance of it.
*/
Expand All @@ -346,6 +466,12 @@ export class StaticType extends Type {
return `$type<${this.instanceType}>`;
}

override substituteTypeParameters(_substitutions: TypeParameterSubstitutions): StaticType {
// The substitutions are only meaningful for instances of a declaration, not for the declaration itself. Hence,
// we don't substitute anything here.
return this;
}

override unwrap(): Type {
return this;
}
Expand Down Expand Up @@ -387,6 +513,14 @@ export class UnionType extends Type {
return `union<${this.possibleTypes.join(', ')}>`;
}

override substituteTypeParameters(substitutions: TypeParameterSubstitutions): UnionType {
if (isEmpty(substitutions)) {
return this;
}

return new UnionType(...this.possibleTypes.map((it) => it.substituteTypeParameters(substitutions)));
}

override unwrap(): Type {
if (this.possibleTypes.length === 1) {
return this.possibleTypes[0]!.unwrap();
Expand Down Expand Up @@ -418,7 +552,11 @@ class UnknownTypeClass extends Type {
}

override toString(): string {
return '?';
return '$unknown';
}

override substituteTypeParameters(_substitutions: TypeParameterSubstitutions): Type {
return this;
}

override unwrap(): Type {
Expand All @@ -431,3 +569,24 @@ class UnknownTypeClass extends Type {
}

export const UnknownType = new UnknownTypeClass();

// -------------------------------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------------------------------

const substitutionsAreEqual = (
a: Map<SdsDeclaration, Type> | undefined,
b: Map<SdsDeclaration, Type> | undefined,
): boolean => {
if (a?.size !== b?.size) {
return false;
}

const aEntries = Array.from(a?.entries() ?? []);
const bEntries = Array.from(b?.entries() ?? []);

return aEntries.every(([aEntry, aValue], i) => {
const [bEntry, bValue] = bEntries[i]!;
return aEntry === bEntry && aValue.equals(bValue);
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ export class SafeDsClassHierarchy {
return stream(this.superclassesGenerator(node));
}

private *superclassesGenerator(node: SdsClass | undefined): Generator<SdsClass, void> {
private *superclassesGenerator(node: SdsClass): Generator<SdsClass, void> {
const visited = new Set<SdsClass>();
let current = this.parentClassOrUndefined(node);
let current = this.parentClass(node);
while (current && !visited.has(current)) {
yield current;
visited.add(current);
current = this.parentClassOrUndefined(current);
current = this.parentClass(current);
}

const anyClass = this.builtinClasses.Any;
Expand All @@ -68,7 +68,7 @@ export class SafeDsClassHierarchy {
* Returns the parent class of the given class, or undefined if there is no parent class. Only the first parent
* type is considered, i.e. multiple inheritance is not supported.
*/
private parentClassOrUndefined(node: SdsClass | undefined): SdsClass | undefined {
private parentClass(node: SdsClass | undefined): SdsClass | undefined {
const [firstParentType] = getParentTypes(node);
if (isSdsNamedType(firstParentType)) {
const declaration = firstParentType.declaration?.ref;
Expand Down
Loading

0 comments on commit 8a35558

Please sign in to comment.