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: improve type simplification #871

Merged
merged 4 commits into from
Feb 10, 2024
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
27 changes: 23 additions & 4 deletions packages/safe-ds-lang/src/language/typing/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,20 @@ export class CallableType extends Type {

export class LiteralType extends Type {
readonly constants: Constant[];
override readonly isNullable: boolean;
private _isNullable: boolean | undefined;

constructor(...constants: Constant[]) {
super();

this.constants = constants;
this.isNullable = constants.some((it) => it === NullConstant);
}

override get isNullable(): boolean {
if (this._isNullable === undefined) {
this._isNullable = this.constants.some((it) => it === NullConstant);
}

return this._isNullable;
}

override equals(other: unknown): boolean {
Expand Down Expand Up @@ -356,6 +363,11 @@ export class ClassType extends NamedType<SdsClass> {
return new ClassType(this.declaration, newSubstitutions, this.isNullable);
}

override unwrap(): ClassType {
const newSubstitutions = new Map(stream(this.substitutions).map(([key, value]) => [key, value.unwrap()]));
return new ClassType(this.declaration, newSubstitutions, this.isNullable);
}

override updateNullability(isNullable: boolean): ClassType {
if (this.isNullable === isNullable) {
return this;
Expand Down Expand Up @@ -511,13 +523,20 @@ export class StaticType extends Type {

export class UnionType extends Type {
readonly possibleTypes: Type[];
override readonly isNullable: boolean;
private _isNullable: boolean | undefined;

constructor(...possibleTypes: Type[]) {
super();

this.possibleTypes = possibleTypes;
this.isNullable = possibleTypes.some((it) => it.isNullable);
}

override get isNullable(): boolean {
if (this._isNullable === undefined) {
this._isNullable = this.possibleTypes.some((it) => it.isNullable);
}

return this._isNullable;
}

override equals(other: unknown): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,12 @@ export class SafeDsTypeChecker {
private classTypeIsAssignableTo(type: ClassType, other: Type, ignoreTypeParameters: boolean): boolean {
if (type.isNullable && !other.isNullable) {
return false;
} else if (type.declaration === this.builtinClasses.Nothing) {
return true;
}

if (other instanceof ClassType) {
if (type.declaration === this.builtinClasses.Nothing) {
return true;
} else if (!this.classHierarchy.isEqualToOrSubclassOf(type.declaration, other.declaration)) {
if (!this.classHierarchy.isEqualToOrSubclassOf(type.declaration, other.declaration)) {
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -665,19 +665,41 @@ export class SafeDsTypeComputer {
// Simplify possible types
const newPossibleTypes = type.possibleTypes.map((it) => this.simplifyType(it));

// Remove types that are subtypes of others. We do this back-to-front to keep the first occurrence of duplicate
// types. It's also makes splicing easier.
// Merge literal types and remove types that are subtypes of others. We do this back-to-front to keep the first
// occurrence of duplicate types. It's also makes splicing easier.
for (let i = newPossibleTypes.length - 1; i >= 0; i--) {
const currentType = newPossibleTypes[i]!;

for (let j = 0; j < newPossibleTypes.length; j++) {
for (let j = newPossibleTypes.length - 1; j >= 0; j--) {
if (i === j) {
continue;
}

let otherType = newPossibleTypes[j]!;
otherType = otherType.updateNullability(currentType.isNullable || otherType.isNullable);

// Don't merge `Nothing?` into callable types, named tuple types or static types, since that would
// create another union type.
if (
currentType.equals(this.coreTypes.NothingOrNull) &&
(otherType instanceof CallableType ||
otherType instanceof NamedTupleType ||
otherType instanceof StaticType)
) {
continue;
}

// Merge literal types
if (currentType instanceof LiteralType && otherType instanceof LiteralType) {
// Other type always occurs before current type
const newConstants = [...otherType.constants, ...currentType.constants];
const newLiteralType = this.simplifyType(new LiteralType(...newConstants));
newPossibleTypes.splice(j, 1, newLiteralType);
newPossibleTypes.splice(i, 1);
break;
}

// Remove subtypes of other types
otherType = otherType.updateNullability(currentType.isNullable || otherType.isNullable);
if (this.typeChecker.isAssignableTo(currentType, otherType)) {
newPossibleTypes.splice(j, 1, otherType); // Update nullability
newPossibleTypes.splice(i, 1);
Expand Down
4 changes: 4 additions & 0 deletions packages/safe-ds-lang/tests/language/typing/model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,10 @@ describe('type model', async () => {
type: new ClassType(class1, new Map(), false),
expectedType: new ClassType(class1, new Map(), false),
},
{
type: new ClassType(class1, new Map([[typeParameter1, new UnionType(UnknownType)]]), false),
expectedType: new ClassType(class1, new Map([[typeParameter1, UnknownType]]), false),
},
{
type: new EnumType(enum1, false),
expectedType: new EnumType(enum1, false),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package tests.typing.simplification.mergeLiteralTypesInUnionTypes

class C(
// $TEST$ serialization literal<1>
p1: »union<literal<1, 1>, literal<1, 1>>«,

// $TEST$ serialization literal<1, 2>
p2: »union<literal<1, 1>, literal<1, 2>>«,

// $TEST$ serialization literal<1, 2, 3>
p3: »union<literal<1, 2>, literal<2, 3>>«,

// $TEST$ serialization literal<1, 2, 3>
p4: »union<literal<1>, literal<2>, literal<3>>«,

// $TEST$ serialization union<literal<1, 2, 3>, String>
p5: »union<literal<1, 1>, String, literal<2, 3>>«,
)
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,23 @@ class C(
// $TEST$ serialization Any?
p16: »union<Any, String?, Int>«,
)

class TestsInvolvingNothing(
// $TEST$ serialization Any
p1: »union<Any, Nothing>«,

// $TEST$ serialization Any?
p2: »union<Any, Nothing?>«,

// $TEST$ serialization literal<1>
p3: »union<literal<1>, Nothing>«,

// $TEST$ serialization literal<1, null>
p4: »union<literal<1>, Nothing?>«,

// $TEST$ serialization () -> ()
p5: »union<() -> (), Nothing>«,

// $TEST$ serialization union<() -> (), Nothing?>
p6: »union<() -> (), Nothing?>«,
)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package tests.typing.simplification.replaceLiteralTypesThatAllowOnlyNullWithNothingNullable
package tests.typing.simplification.replaceLiteralTypesThatAllowOnlyNullWithNothingOrNull

class C(
// $TEST$ serialization Nothing?
Expand Down