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: compute type of elvis operators with nullable left operand #715

Merged
merged 8 commits into from
Oct 31, 2023
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
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