Skip to content

Commit

Permalink
feat: node mapper (#602)
Browse files Browse the repository at this point in the history
### Summary of Changes

Add a service that can map
* arguments to parameters,
* type arguments to type parameters,
* calls to callables.

---------

Co-authored-by: megalinter-bot <[email protected]>
  • Loading branch information
lars-reimann and megalinter-bot authored Oct 5, 2023
1 parent 6b486cd commit a13e5b5
Show file tree
Hide file tree
Showing 21 changed files with 934 additions and 200 deletions.
10 changes: 10 additions & 0 deletions src/language/helpers/checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,24 @@ import {
isSdsEnum,
isSdsFunction,
isSdsSegment,
SdsArgument,
SdsClassMember,
SdsDeclaration,
SdsTypeArgument,
} from '../generated/ast.js';

export const isInternal = (node: SdsDeclaration): boolean => {
return isSdsSegment(node) && node.visibility === 'internal';
};

export const isNamedArgument = (node: SdsArgument): boolean => {
return Boolean(node.parameter);
};

export const isNamedTypeArgument = (node: SdsTypeArgument): boolean => {
return Boolean(node.typeParameter);
};

export const isStatic = (node: SdsClassMember): boolean => {
if (isSdsClass(node) || isSdsEnum(node)) {
return true;
Expand Down
139 changes: 139 additions & 0 deletions src/language/helpers/safe-ds-node-mapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { SafeDsServices } from '../safe-ds-module.js';
import { SafeDsTypeComputer } from '../typing/safe-ds-type-computer.js';
import {
isSdsAbstractCall,
isSdsAnnotationCall,
isSdsCall,
isSdsCallable,
isSdsClass,
isSdsEnumVariant,
isSdsNamedType,
isSdsType,
SdsAbstractCall,
SdsArgument,
SdsCallable,
SdsNamedTypeDeclaration,
SdsParameter,
SdsTypeArgument,
SdsTypeParameter,
} from '../generated/ast.js';
import { CallableType, StaticType } from '../typing/model.js';
import { getContainerOfType } from 'langium';
import { argumentsOrEmpty, parametersOrEmpty, typeArgumentsOrEmpty, typeParametersOrEmpty } from './shortcuts.js';
import { isNamedArgument, isNamedTypeArgument } from './checks.js';

export class SafeDsNodeMapper {
private readonly typeComputer: SafeDsTypeComputer;

constructor(services: SafeDsServices) {
this.typeComputer = new SafeDsTypeComputer(services);
}

/**
* Returns the callable that is called by the given call. If no callable can be found, returns undefined.
*/
callToCallableOrUndefined(node: SdsAbstractCall | undefined): SdsCallable | undefined {
if (isSdsAnnotationCall(node)) {
return node.annotation?.ref;
} else if (isSdsCall(node)) {
const receiverType = this.typeComputer.computeType(node.receiver);
if (receiverType instanceof CallableType) {
return receiverType.sdsCallable;
} else if (receiverType instanceof StaticType) {
const declaration = receiverType.instanceType.sdsDeclaration;
if (isSdsCallable(declaration)) {
return declaration;
}
}
}

return undefined;
}

/**
* Returns the parameter that the argument is assigned to. If there is no matching parameter, returns undefined.
*/
argumentToParameterOrUndefined(node: SdsArgument | undefined): SdsParameter | undefined {
if (!node) {
return undefined;
}

// Named argument
if (node.parameter) {
return node.parameter.ref;
}

// Positional argument
const containingAbstractCall = getContainerOfType(node, isSdsAbstractCall)!;
const args = argumentsOrEmpty(containingAbstractCall);
const argumentPosition = node.$containerIndex ?? -1;

// A prior argument is named
for (let i = 0; i < argumentPosition; i++) {
if (isNamedArgument(args[i])) {
return undefined;
}
}

// Find parameter at the same position
const callable = this.callToCallableOrUndefined(containingAbstractCall);
const parameters = parametersOrEmpty(callable);
if (argumentPosition < parameters.length) {
return parameters[argumentPosition];
}

// If no parameter is found, check if the last parameter is variadic
const lastParameter = parameters[parameters.length - 1];
if (lastParameter?.isVariadic) {
return lastParameter;
}

return undefined;
}

/**
* Returns the type parameter that the type argument is assigned to. If there is no matching type parameter, returns
* undefined.
*/
typeArgumentToTypeParameterOrUndefined(node: SdsTypeArgument | undefined): SdsTypeParameter | undefined {
if (!node) {
return undefined;
}

// Named type argument
if (node.typeParameter) {
return node.typeParameter.ref;
}

// Positional type argument
const containingType = getContainerOfType(node, isSdsType);
if (!isSdsNamedType(containingType)) {
return undefined;
}

const typeArguments = typeArgumentsOrEmpty(containingType.typeArgumentList);
const typeArgumentPosition = node.$containerIndex ?? -1;

// A prior type argument is named
for (let i = 0; i < typeArgumentPosition; i++) {
if (isNamedTypeArgument(typeArguments[i])) {
return undefined;
}
}

// Find type parameter at the same position
const namedTypeDeclaration = containingType.declaration.ref;
const typeParameters = this.typeParametersOfNamedTypeDeclarationOrEmpty(namedTypeDeclaration);
return typeParameters[typeArgumentPosition];
}

private typeParametersOfNamedTypeDeclarationOrEmpty(node: SdsNamedTypeDeclaration | undefined): SdsTypeParameter[] {
if (isSdsClass(node)) {
return typeParametersOrEmpty(node.typeParameterList);
} else if (isSdsEnumVariant(node)) {
return typeParametersOrEmpty(node.typeParameterList);
} else {
return [];
}
}
}
16 changes: 11 additions & 5 deletions src/language/helpers/shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import {
isSdsModule,
isSdsModuleMember,
isSdsPlaceholder,
SdsAbstractCall,
SdsAnnotatedObject,
SdsAnnotationCall,
SdsArgument,
SdsAssignee,
SdsAssignment,
SdsBlock,
SdsBlockLambda,
SdsBlockLambdaResult,
SdsCallable,
SdsClass,
SdsClassMember,
SdsEnum,
Expand All @@ -23,7 +26,6 @@ import {
SdsModule,
SdsModuleMember,
SdsParameter,
SdsParameterList,
SdsPlaceholder,
SdsQualifiedImport,
SdsResult,
Expand All @@ -50,6 +52,10 @@ export const annotationCallsOrEmpty = function (node: SdsAnnotatedObject | undef
}
};

export const argumentsOrEmpty = function (node: SdsAbstractCall | undefined): SdsArgument[] {
return node?.argumentList?.arguments ?? [];
};

export const assigneesOrEmpty = function (node: SdsAssignment | undefined): SdsAssignee[] {
return node?.assigneeList?.assignees ?? [];
};
Expand Down Expand Up @@ -89,12 +95,12 @@ export const moduleMembersOrEmpty = function (node: SdsModule | undefined): SdsM
return node?.members?.filter(isSdsModuleMember) ?? [];
};

export const packageNameOrNull = function (node: AstNode | undefined): string | null {
return getContainerOfType(node, isSdsModule)?.name ?? null;
export const packageNameOrUndefined = function (node: AstNode | undefined): string | undefined {
return getContainerOfType(node, isSdsModule)?.name;
};

export const parametersOrEmpty = function (node: SdsParameterList | undefined): SdsParameter[] {
return node?.parameters ?? [];
export const parametersOrEmpty = function (node: SdsCallable | undefined): SdsParameter[] {
return node?.parameterList?.parameters ?? [];
};

export const placeholdersOrEmpty = function (node: SdsBlock | undefined): SdsPlaceholder[] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import {
/**
* Tries to evaluate this expression. On success an SdsConstantExpression is returned, otherwise `null`.
*/
export const toConstantExpressionOrNull = (node: AstNode): SdsConstantExpression | null => {
export const toConstantExpressionOrUndefined = (node: AstNode): SdsConstantExpression | null => {
return toConstantExpressionOrNullImpl(node, new Map());
};

Expand Down Expand Up @@ -155,8 +155,8 @@ const simplifyInfixOperation = (
_substitutions: ParameterSubstitutions,
): SdsConstantExpression | null => {
// // By design none of the operators are short-circuited
// val constantLeft = leftOperand.toConstantExpressionOrNull(substitutions) ?: return null
// val constantRight = rightOperand.toConstantExpressionOrNull(substitutions) ?: return null
// val constantLeft = leftOperand.toConstantExpressionOrUndefined(substitutions) ?: return null
// val constantRight = rightOperand.toConstantExpressionOrUndefined(substitutions) ?: return null
//
// return when (operator()) {
// Or -> simplifyLogicalOp(constantLeft, Boolean::or, constantRight)
Expand Down Expand Up @@ -281,7 +281,7 @@ const simplifyPrefixOperation = (
_node: SdsPrefixOperation,
_substitutions: ParameterSubstitutions,
): SdsConstantExpression | null => {
// val constantOperand = operand.toConstantExpressionOrNull(substitutions) ?: return null
// val constantOperand = operand.toConstantExpressionOrUndefined(substitutions) ?: return null
//
// return when (operator()) {
// Not -> when (constantOperand) {
Expand Down
7 changes: 7 additions & 0 deletions src/language/safe-ds-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { SafeDsValueConverter } from './grammar/safe-ds-value-converter.js';
import { SafeDsTypeComputer } from './typing/safe-ds-type-computer.js';
import { SafeDsCoreClasses } from './builtins/safe-ds-core-classes.js';
import { SafeDsPackageManager } from './workspace/safe-ds-package-manager.js';
import { SafeDsNodeMapper } from './helpers/safe-ds-node-mapper.js';

/**
* Declaration of custom services - add your own service classes here.
Expand All @@ -27,6 +28,9 @@ export type SafeDsAddedServices = {
builtins: {
CoreClasses: SafeDsCoreClasses;
};
helpers: {
NodeMapper: SafeDsNodeMapper;
};
types: {
TypeComputer: SafeDsTypeComputer;
};
Expand All @@ -50,6 +54,9 @@ export const SafeDsModule: Module<SafeDsServices, PartialLangiumServices & SafeD
builtins: {
CoreClasses: (services) => new SafeDsCoreClasses(services),
},
helpers: {
NodeMapper: (services) => new SafeDsNodeMapper(services),
},
lsp: {
Formatter: () => new SafeDsFormatter(),
},
Expand Down
46 changes: 14 additions & 32 deletions src/language/scoping/safe-ds-scope-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@ import {
} from 'langium';
import {
isSdsAbstractCall,
isSdsAnnotationCall,
isSdsArgument,
isSdsAssignment,
isSdsBlock,
isSdsCall,
isSdsCallable,
isSdsClass,
isSdsEnum,
Expand Down Expand Up @@ -55,7 +53,7 @@ import {
enumVariantsOrEmpty,
importedDeclarationsOrEmpty,
importsOrEmpty,
packageNameOrNull,
packageNameOrUndefined,
parametersOrEmpty,
resultsOrEmpty,
statementsOrEmpty,
Expand All @@ -66,17 +64,19 @@ import { isStatic } from '../helpers/checks.js';
import { SafeDsServices } from '../safe-ds-module.js';
import { SafeDsTypeComputer } from '../typing/safe-ds-type-computer.js';
import { SafeDsPackageManager } from '../workspace/safe-ds-package-manager.js';
import { CallableType, StaticType } from '../typing/model.js';
import { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js';

export class SafeDsScopeProvider extends DefaultScopeProvider {
private readonly astReflection: AstReflection;
private readonly nodeMapper: SafeDsNodeMapper;
private readonly packageManager: SafeDsPackageManager;
private readonly typeComputer: SafeDsTypeComputer;

constructor(services: SafeDsServices) {
super(services);

this.astReflection = services.shared.AstReflection;
this.nodeMapper = services.helpers.NodeMapper;
this.packageManager = services.workspace.PackageManager;
this.typeComputer = services.types.TypeComputer;
}
Expand Down Expand Up @@ -111,35 +111,17 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {

private getScopeForArgumentParameter(node: SdsArgument): Scope {
const containingAbstractCall = getContainerOfType(node, isSdsAbstractCall);
if (isSdsAnnotationCall(containingAbstractCall)) {
const annotation = containingAbstractCall.annotation?.ref;
if (!annotation) {
return EMPTY_SCOPE;
}

const parameters = parametersOrEmpty(annotation.parameterList);
return this.createScopeForNodes(parameters);
} else if (isSdsCall(containingAbstractCall)) {
const receiverType = this.typeComputer.computeType(containingAbstractCall.receiver);
if (receiverType instanceof CallableType) {
const parameters = parametersOrEmpty(receiverType.sdsCallable.parameterList);
return this.createScopeForNodes(parameters);
} else if (receiverType instanceof StaticType) {
const declaration = receiverType.instanceType.sdsDeclaration;
if (isSdsCallable(declaration)) {
const parameters = parametersOrEmpty(declaration.parameterList);
return this.createScopeForNodes(parameters);
}
}

return EMPTY_SCOPE;
} /* c8 ignore start */ else {
const callable = this.nodeMapper.callToCallableOrUndefined(containingAbstractCall);
if (!callable) {
return EMPTY_SCOPE;
} /* c8 ignore stop */
}

const parameters = parametersOrEmpty(callable);
return this.createScopeForNodes(parameters);
}

private getScopeForImportedDeclarationDeclaration(node: SdsImportedDeclaration): Scope {
const ownPackageName = packageNameOrNull(node);
const ownPackageName = packageNameOrUndefined(node);

const containingQualifiedImport = getContainerOfType(node, isSdsQualifiedImport);
if (!containingQualifiedImport) {
Expand Down Expand Up @@ -297,7 +279,7 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
private localDeclarations(node: AstNode, outerScope: Scope): Scope {
// Parameters
const containingCallable = getContainerOfType(node.$container, isSdsCallable);
const parameters = parametersOrEmpty(containingCallable?.parameterList);
const parameters = parametersOrEmpty(containingCallable);

// Placeholders up to the containing statement
const containingStatement = getContainerOfType(node.$container, isSdsStatement);
Expand Down Expand Up @@ -375,7 +357,7 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
}

private getGlobalScopeForNode(referenceType: string, node: AstNode): Scope {
const ownPackageName = packageNameOrNull(node);
const ownPackageName = packageNameOrUndefined(node);

// Builtin declarations
const builtinDeclarations = this.builtinDeclarations(referenceType);
Expand All @@ -397,7 +379,7 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
});
}

private declarationsInSamePackage(packageName: string | null, referenceType: string): AstNodeDescription[] {
private declarationsInSamePackage(packageName: string | undefined, referenceType: string): AstNodeDescription[] {
if (!packageName) {
return [];
}
Expand Down
Loading

0 comments on commit a13e5b5

Please sign in to comment.