Skip to content

Commit

Permalink
feat: improve type simplification (#871)
Browse files Browse the repository at this point in the history
### Summary of Changes

* Merge literal types in union types (`union<literal<1>, literal<2>`
becomes `literal<1, 2>`).
* Unwrap type parameter substitutions in class type (`C<union<Int>>`
becomes `C<Int>`).
  • Loading branch information
lars-reimann authored Feb 10, 2024
1 parent 6035b76 commit 0daafb9
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 12 deletions.
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
30 changes: 26 additions & 4 deletions packages/safe-ds-lang/src/language/typing/safe-ds-type-computer.ts
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

0 comments on commit 0daafb9

Please sign in to comment.