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: type checker service #722

Merged
merged 9 commits into from
Nov 4, 2023
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AstNode, getContainerOfType, Stream, stream } from 'langium';
import {
isSdsAnnotation,
isSdsArgumentList,
Expand Down Expand Up @@ -57,7 +58,6 @@ import {
SdsTypeParameter,
SdsTypeParameterList,
} from '../generated/ast.js';
import { AstNode, getContainerOfType, Stream, stream } from 'langium';

// -------------------------------------------------------------------------------------------------
// Checks
Expand Down
2 changes: 0 additions & 2 deletions packages/safe-ds-lang/src/language/typing/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,9 @@ export class NamedTupleType<T extends SdsDeclaration> extends Type {
/**
* The length of this tuple.
*/
/* c8 ignore start */
get length(): number {
return this.entries.length;
}
/* c8 ignore stop */

/**
* Returns the type of the entry at the given index. If the index is out of bounds, returns `undefined`.
Expand Down
161 changes: 89 additions & 72 deletions packages/safe-ds-lang/src/language/typing/safe-ds-type-checker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getContainerOfType } from 'langium';
import type { SafeDsClasses } from '../builtins/safe-ds-classes.js';
import { isSdsEnum, SdsDeclaration } from '../generated/ast.js';
import {
BooleanConstant,
Expand All @@ -19,16 +20,18 @@ import {
StaticType,
Type,
UnionType,
UnknownType,
} from './model.js';
import { SafeDsClassHierarchy } from './safe-ds-class-hierarchy.js';
import { SafeDsCoreTypes } from './safe-ds-core-types.js';

/* c8 ignore start */
export class SafeDsTypeChecker {
private readonly builtinClasses: SafeDsClasses;
private readonly classHierarchy: SafeDsClassHierarchy;
private readonly coreTypes: SafeDsCoreTypes;

constructor(services: SafeDsServices) {
this.builtinClasses = services.builtins.Classes;
this.classHierarchy = services.types.ClassHierarchy;
this.coreTypes = services.types.CoreTypes;
}
Expand All @@ -37,6 +40,12 @@ export class SafeDsTypeChecker {
* Checks whether {@link type} is assignable {@link other}.
*/
isAssignableTo(type: Type, other: Type): boolean {
if (type === UnknownType || other === UnknownType) {
return false;
} else if (other instanceof UnionType) {
return other.possibleTypes.some((it) => this.isAssignableTo(type, it));
}

if (type instanceof CallableType) {
return this.callableTypeIsAssignableTo(type, other);
} else if (type instanceof ClassType) {
Expand All @@ -53,48 +62,66 @@ export class SafeDsTypeChecker {
return this.staticTypeIsAssignableTo(type, other);
} else if (type instanceof UnionType) {
return this.unionTypeIsAssignableTo(type, other);
} else {
return false;
}
} /* c8 ignore start */ else {
throw new Error(`Unexpected type: ${type.constructor.name}`);
} /* c8 ignore stop */
}

private callableTypeIsAssignableTo(type: CallableType, other: Type): boolean {
// return when (val unwrappedOther = unwrapVariadicType(other)) {
// is CallableType -> {
// // TODO: We need to compare names of parameters & results and can allow additional optional parameters
//
// // Sizes must match (too strict requirement -> should be loosened later)
// if (this.parameters.size != unwrappedOther.parameters.size || this.results.size != this.results.size) {
// return false
// }
//
// // Actual parameters must be supertypes of expected parameters (contravariance)
// this.parameters.zip(unwrappedOther.parameters).forEach { (thisParameter, otherParameter) ->
// if (!otherParameter.isSubstitutableFor(thisParameter)) {
// return false
// }
// }
//
// // Expected results must be subtypes of expected results (covariance)
// this.results.zip(unwrappedOther.results).forEach { (thisResult, otherResult) ->
// if (!thisResult.isSubstitutableFor(otherResult)) {
// return false
// }
// }
//
// true
// }
// is ClassType -> {
// unwrappedOther.sdsClass.qualifiedNameOrNull() == StdlibClasses.Any
// }
// is UnionType -> {
// unwrappedOther.possibleTypes.any { this.isSubstitutableFor(it) }
// }
// else -> false
// }
// }
if (other instanceof ClassType) {
return other.declaration === this.builtinClasses.Any;
} else if (other instanceof CallableType) {
// Must accept at least as many parameters and produce at least as many results
if (type.inputType.length < other.inputType.length || type.outputType.length < other.outputType.length) {
return false;
}

return type.equals(other);
// Check expected parameters
for (let i = 0; i < other.inputType.length; i++) {
const typeEntry = type.inputType.entries[i];
const otherEntry = other.inputType.entries[i];

// Names must match
if (typeEntry.name !== otherEntry.name) {
return false;
}

// Types must be contravariant
if (!this.isAssignableTo(otherEntry.type, typeEntry.type)) {
return false;
}
}

// Additional parameters must be optional
for (let i = other.inputType.length; i < type.inputType.length; i++) {
const typeEntry = type.inputType.entries[i];
if (!typeEntry.declaration?.defaultValue) {
return false;
}
}

// Check expected results
for (let i = 0; i < other.outputType.length; i++) {
const typeEntry = type.outputType.entries[i];
const otherEntry = other.outputType.entries[i];

// Names must match
if (typeEntry.name !== otherEntry.name) {
return false;
}

// Types must be covariant
if (!this.isAssignableTo(typeEntry.type, otherEntry.type)) {
return false;
}
}

// Additional results are OK

return true;
} else {
return false;
}
}

private classTypeIsAssignableTo(type: ClassType, other: Type): boolean {
Expand All @@ -104,8 +131,6 @@ export class SafeDsTypeChecker {

if (other instanceof ClassType) {
return this.classHierarchy.isEqualToOrSubclassOf(type.declaration, other.declaration);
} else if (other instanceof UnionType) {
return other.possibleTypes.some((it) => this.isAssignableTo(type, it));
} else {
return false;
}
Expand All @@ -116,45 +141,30 @@ export class SafeDsTypeChecker {
return false;
}

if (other instanceof EnumType) {
if (other instanceof ClassType) {
return other.declaration === this.builtinClasses.Any;
} else if (other instanceof EnumType) {
return type.declaration === other.declaration;
} else {
return false;
}

// return when (val unwrappedOther = unwrapVariadicType(other)) {
// is ClassType -> {
// (!this.isNullable || unwrappedOther.isNullable) &&
// unwrappedOther.sdsClass.qualifiedNameOrNull() == StdlibClasses.Any
// }
// is UnionType -> {
// unwrappedOther.possibleTypes.any { this.isSubstitutableFor(it) }
// }
// else -> false
// }

return type.equals(other);
}

private enumVariantTypeIsAssignableTo(type: EnumVariantType, other: Type): boolean {
if (type.isNullable && !other.isNullable) {
return false;
}

if (other instanceof EnumType) {
if (other instanceof ClassType) {
return other.declaration === this.builtinClasses.Any;
} else if (other instanceof EnumType) {
const containingEnum = getContainerOfType(type.declaration, isSdsEnum);
return containingEnum === other.declaration;
} else if (other instanceof EnumVariantType) {
return type.declaration === other.declaration;
} else {
return false;
}
// return when (val unwrappedOther = unwrapVariadicType(other)) {
// is ClassType -> {
// (!this.isNullable || unwrappedOther.isNullable) &&
// unwrappedOther.sdsClass.qualifiedNameOrNull() == StdlibClasses.Any
// }
// is UnionType -> unwrappedOther.possibleTypes.any { this.isSubstitutableFor(it) }
// else -> false
// }

return type.equals(other);
}

private literalTypeIsAssignableTo(type: LiteralType, other: Type): boolean {
Expand All @@ -173,7 +183,6 @@ export class SafeDsTypeChecker {
other.constants.some((otherConstant) => constant.equals(otherConstant)),
);
} else {
// TODO: union type
return false;
}
}
Expand All @@ -197,18 +206,26 @@ export class SafeDsTypeChecker {
return this.isAssignableTo(classType, other);
}

/* c8 ignore start */
private namedTupleTypeIsAssignableTo(type: NamedTupleType<SdsDeclaration>, other: Type): boolean {
return type.equals(other);
if (other instanceof NamedTupleType) {
return (
type.length === other.length &&
type.entries.every((typeEntry, index) => {
const otherEntry = other.entries[index];
// We deliberately ignore the declarations here
return typeEntry.name === otherEntry.name && this.isAssignableTo(typeEntry.type, otherEntry.type);
})
);
} else {
return false;
}
}

private staticTypeIsAssignableTo(type: Type, other: Type): boolean {
return type.equals(other);
}

private unionTypeIsAssignableTo(type: UnionType, other: Type): boolean {
// return this.possibleTypes.all { it.isSubstitutableFor(other) }
return type.equals(other);
return type.possibleTypes.every((it) => this.isAssignableTo(it, other));
}
}
/* c8 ignore stop */
Loading