Skip to content

Commit

Permalink
feat: compute type of elvis operators with nullable left operand (#715)
Browse files Browse the repository at this point in the history
Closes #541.

### Summary of Changes

Add logic to compute the lowest common supertype of several types. This
is needed to compute the type of elvis operators with a nullable left
operand. It's also later needed for the inference of type parameters.
  • Loading branch information
lars-reimann authored Oct 31, 2023
1 parent 3b3a495 commit 376b083
Show file tree
Hide file tree
Showing 9 changed files with 921 additions and 279 deletions.
Original file line number Diff line number Diff line change
@@ -1,21 +1,5 @@
import { SafeDsServices } from '../safe-ds-module.js';
import { AstNode, AstNodeLocator, getDocument, WorkspaceCache } from 'langium';
import {
BooleanConstant,
EvaluatedEnumVariant,
EvaluatedList,
EvaluatedMap,
EvaluatedMapEntry,
EvaluatedNode,
FloatConstant,
IntConstant,
isConstant,
NullConstant,
NumberConstant,
ParameterSubstitutions,
StringConstant,
UnknownEvaluatedNode,
} from './model.js';
import { isEmpty } from '../../helpers/collectionUtils.js';
import {
isSdsArgument,
isSdsBlockLambda,
Expand Down Expand Up @@ -55,7 +39,23 @@ import {
} from '../generated/ast.js';
import { getArguments, getParameters } from '../helpers/nodeProperties.js';
import { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js';
import { isEmpty } from '../../helpers/collectionUtils.js';
import { SafeDsServices } from '../safe-ds-module.js';
import {
BooleanConstant,
EvaluatedEnumVariant,
EvaluatedList,
EvaluatedMap,
EvaluatedMapEntry,
EvaluatedNode,
FloatConstant,
IntConstant,
isConstant,
NullConstant,
NumberConstant,
ParameterSubstitutions,
StringConstant,
UnknownEvaluatedNode,
} from './model.js';

export class SafeDsPartialEvaluator {
private readonly astNodeLocator: AstNodeLocator;
Expand Down Expand Up @@ -141,7 +141,7 @@ export class SafeDsPartialEvaluator {
} else if (isSdsTemplateString(node)) {
return this.evaluateTemplateString(node, substitutions);
} /* c8 ignore start */ else {
return UnknownEvaluatedNode;
throw new Error(`Unexpected node type: ${node.$type}`);
} /* c8 ignore stop */
}

Expand Down Expand Up @@ -269,7 +269,7 @@ export class SafeDsPartialEvaluator {

/* c8 ignore next 2 */
default:
return UnknownEvaluatedNode;
throw new Error(`Unexpected operator: ${node.operator}`);
}
}

Expand Down
129 changes: 99 additions & 30 deletions packages/safe-ds-lang/src/language/typing/model.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isEmpty } from '../../helpers/collectionUtils.js';
import {
SdsAbstractResult,
SdsCallable,
Expand Down Expand Up @@ -29,19 +30,14 @@ export abstract class Type {
abstract toString(): string;

/**
* Returns a copy of this type with the given nullability. If nullability is fixed for a type, it may return the
* instance itself.
* Removes any unnecessary containers from the type.
*/
updateNullability(_isNullable: boolean): Type {
return this;
}
abstract unwrap(): Type;

/**
* Removes any unnecessary containers from the type.
* Returns a copy of this type with the given nullability.
*/
unwrap(): Type {
return this;
}
abstract updateNullability(isNullable: boolean): Type;
}

export class CallableType extends Type {
Expand Down Expand Up @@ -79,14 +75,32 @@ export class CallableType extends Type {
override toString(): string {
return `${this.inputType} -> ${this.outputType}`;
}

override unwrap(): CallableType {
return new CallableType(
this.callable,
new NamedTupleType(...this.inputType.entries.map((it) => it.unwrap())),
new NamedTupleType(...this.outputType.entries.map((it) => it.unwrap())),
);
}

override updateNullability(isNullable: boolean): Type {
if (!isNullable) {
return this;
}

return new UnionType(this, new LiteralType(NullConstant));
}
}

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

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

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

Expand All @@ -107,28 +121,39 @@ export class LiteralType extends Type {
return `literal<${this.constants.join(', ')}>`;
}

override unwrap(): LiteralType {
return this;
}

override updateNullability(isNullable: boolean): LiteralType {
if (this.isNullable && !isNullable) {
return new LiteralType(this.constants.filter((it) => it !== NullConstant));
return new LiteralType(...this.constants.filter((it) => it !== NullConstant));
} else if (!this.isNullable && isNullable) {
return new LiteralType([...this.constants, NullConstant]);
return new LiteralType(...this.constants, NullConstant);
} else {
return this;
}
}
}

export class NamedTupleType<T extends SdsDeclaration> extends Type {
readonly entries: NamedTupleEntry<T>[];
override readonly isNullable = false;

constructor(readonly entries: NamedTupleEntry<T>[]) {
constructor(...entries: NamedTupleEntry<T>[]) {
super();

this.entries = entries;
}

/**
* The length of this tuple.
*/
readonly length: number = this.entries.length;
/* 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 Expand Up @@ -159,10 +184,18 @@ export class NamedTupleType<T extends SdsDeclaration> extends Type {
*/
override unwrap(): Type {
if (this.entries.length === 1) {
return this.entries[0].type;
return this.entries[0].type.unwrap();
}

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

override updateNullability(isNullable: boolean): Type {
if (!isNullable) {
return this;
}

return new UnionType(this, new LiteralType(NullConstant));
}
}

Expand All @@ -180,6 +213,10 @@ export class NamedTupleEntry<T extends SdsDeclaration> {
toString(): string {
return `${this.name}: ${this.type}`;
}

unwrap(): NamedTupleEntry<T> {
return new NamedTupleEntry(this.declaration, this.name, this.type.unwrap());
}
}

export abstract class NamedType<T extends SdsDeclaration> extends Type {
Expand All @@ -196,6 +233,10 @@ export abstract class NamedType<T extends SdsDeclaration> extends Type {
}

abstract override updateNullability(isNullable: boolean): NamedType<T>;

unwrap(): NamedType<T> {
return this;
}
}

export class ClassType extends NamedType<SdsClass> {
Expand Down Expand Up @@ -290,14 +331,28 @@ export class StaticType extends Type {
override toString(): string {
return `$type<${this.instanceType}>`;
}

override unwrap(): Type {
return this;
}

override updateNullability(isNullable: boolean): Type {
if (!isNullable) {
return this;
}

return new UnionType(this, new LiteralType(NullConstant));
}
}

export class UnionType extends Type {
readonly possibleTypes: Type[];
override readonly isNullable: boolean;

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

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

Expand All @@ -317,6 +372,28 @@ export class UnionType extends Type {
override toString(): string {
return `union<${this.possibleTypes.join(', ')}>`;
}

override unwrap(): Type {
if (this.possibleTypes.length === 1) {
return this.possibleTypes[0].unwrap();
}

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

override updateNullability(isNullable: boolean): Type {
if (this.isNullable && !isNullable) {
return new UnionType(...this.possibleTypes.map((it) => it.updateNullability(false)));
} else if (!this.isNullable && isNullable) {
if (isEmpty(this.possibleTypes)) {
return new LiteralType(NullConstant);
} else {
return new UnionType(...this.possibleTypes.map((it) => it.updateNullability(true)));
}
} else {
return this;
}
}
}

class UnknownTypeClass extends Type {
Expand All @@ -329,22 +406,14 @@ class UnknownTypeClass extends Type {
override toString(): string {
return '?';
}
}

export const UnknownType = new UnknownTypeClass();

/* c8 ignore start */
class NotImplementedTypeClass extends Type {
override readonly isNullable = false;

override equals(other: Type): boolean {
return other instanceof NotImplementedTypeClass;
override unwrap(): Type {
return this;
}

override toString(): string {
return '$NotImplemented';
override updateNullability(_isNullable: boolean): Type {
return this;
}
}

export const NotImplementedType = new NotImplementedTypeClass();
/* c8 ignore stop */
export const UnknownType = new UnknownTypeClass();
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
import { EMPTY_STREAM, stream, Stream } from 'langium';
import { SafeDsClasses } from '../builtins/safe-ds-classes.js';
import { SdsClass, type SdsClassMember } from '../generated/ast.js';
import { isSdsClass, isSdsNamedType, SdsClass, type SdsClassMember } from '../generated/ast.js';
import { getMatchingClassMembers, getParentTypes } from '../helpers/nodeProperties.js';
import { SafeDsServices } from '../safe-ds-module.js';
import { ClassType } from './model.js';
import { SafeDsTypeComputer } from './safe-ds-type-computer.js';

export class SafeDsClassHierarchy {
private readonly builtinClasses: SafeDsClasses;
private readonly typeComputer: SafeDsTypeComputer;

constructor(services: SafeDsServices) {
this.builtinClasses = services.builtins.Classes;
this.typeComputer = services.types.TypeComputer;
}

/**
* Returns `true` if the given node is equal to or a subclass of the given other node. If one of the nodes is
* undefined, `false` is returned.
* `undefined`, `false` is returned.
*/
isEqualToOrSubclassOf(node: SdsClass | undefined, other: SdsClass | undefined): boolean {
if (!node || !other) {
Expand Down Expand Up @@ -74,9 +70,11 @@ export class SafeDsClassHierarchy {
*/
private parentClassOrUndefined(node: SdsClass | undefined): SdsClass | undefined {
const [firstParentType] = getParentTypes(node);
const computedType = this.typeComputer.computeType(firstParentType);
if (computedType instanceof ClassType) {
return computedType.declaration;
if (isSdsNamedType(firstParentType)) {
const declaration = firstParentType.declaration?.ref;
if (isSdsClass(declaration)) {
return declaration;
}
}

return undefined;
Expand Down
10 changes: 8 additions & 2 deletions packages/safe-ds-lang/src/language/typing/safe-ds-core-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ export class SafeDsCoreTypes {
this.cache = new WorkspaceCache(services.shared);
}

get Any(): Type {
return this.createCoreType(this.builtinClasses.Any, false);
}

get AnyOrNull(): Type {
return this.createCoreType(this.builtinClasses.Any, true);
}
Expand All @@ -37,11 +41,13 @@ export class SafeDsCoreTypes {
return this.createCoreType(this.builtinClasses.Map);
}

/* c8 ignore start */
get Nothing(): Type {
return this.createCoreType(this.builtinClasses.Nothing, false);
}

get NothingOrNull(): Type {
return this.createCoreType(this.builtinClasses.Nothing, true);
}
/* c8 ignore stop */

get String(): Type {
return this.createCoreType(this.builtinClasses.String);
Expand Down
Loading

0 comments on commit 376b083

Please sign in to comment.