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: scoping of direct references to declarations in same file #580

Merged
merged 28 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
08b2a9c
refactor: move test resource
lars-reimann Sep 27, 2023
98a508b
test: no direct reference to annotation
lars-reimann Sep 27, 2023
d99d4c9
test: no direct reference to pipelines
lars-reimann Sep 27, 2023
5e89f2c
test: unresolved references
lars-reimann Sep 27, 2023
ee4e1f9
feat: references to segments in same file
lars-reimann Sep 27, 2023
b710746
test: references to functions in same file
lars-reimann Sep 27, 2023
8f67b00
test: no references to schemas
lars-reimann Sep 27, 2023
d74430a
feat: resolve references to annotations, pipelines, and schemas but s…
lars-reimann Sep 27, 2023
9811e88
test: first matching global wins
lars-reimann Sep 27, 2023
207ed79
test: references to global class
lars-reimann Sep 27, 2023
f7de6e3
test: references to global enum
lars-reimann Sep 27, 2023
21a15fd
test: references to type parameters
lars-reimann Sep 27, 2023
e9c5ed4
test: references to block lambda results
lars-reimann Sep 27, 2023
c038a76
test: references to results
lars-reimann Sep 27, 2023
1573df8
test: references to parameters
lars-reimann Sep 27, 2023
383c40d
feat: references to parameters
lars-reimann Sep 27, 2023
6971cb6
perf: use precomputed scope for global declarations in same file
lars-reimann Sep 28, 2023
831e3ef
perf: only stream nodes that intersect the given range
lars-reimann Sep 28, 2023
acf3cba
test: yield points to first result
lars-reimann Sep 28, 2023
366211d
refactor: rename a file
lars-reimann Sep 28, 2023
5c21774
test: point to first parameter if redeclared
lars-reimann Sep 28, 2023
954feee
test: references to placeholders (currently failing since implementat…
lars-reimann Sep 28, 2023
cae0505
feat: references to placeholders
lars-reimann Sep 28, 2023
3c8487d
test: add another test case for references to placeholders of a block…
lars-reimann Sep 28, 2023
84784e0
fix: build error
lars-reimann Sep 28, 2023
f871ae0
style: apply automated linter fixes
megalinter-bot Sep 28, 2023
67bbfcf
test: ignore lines for coverage that cannot be covered
lars-reimann Sep 28, 2023
92f003a
refactor: minor changes
lars-reimann Sep 28, 2023
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
8 changes: 4 additions & 4 deletions src/language/ast/shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ export const classMembersOrEmpty = function (node: SdsClass | undefined): SdsCla
return node?.body?.members ?? [];
};

export const enumVariantsOrEmpty = function (node: SdsEnum | undefined): SdsEnumVariant[] {
return node?.body?.variants ?? [];
};

export const parametersOrEmpty = function (node: SdsParameterList | undefined): SdsParameter[] {
return node?.parameters ?? [];
};
Expand Down Expand Up @@ -90,7 +94,3 @@ export const typeArgumentsOrEmpty = function (node: SdsTypeArgumentList | undefi
export const typeParametersOrEmpty = function (node: SdsTypeParameterList | undefined): SdsTypeParameter[] {
return node?.typeParameters ?? [];
};

export const variantsOrEmpty = function (node: SdsEnum | undefined): SdsEnumVariant[] {
return node?.body?.variants ?? [];
};
8 changes: 8 additions & 0 deletions src/language/ast/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { AstNode, hasContainerOfType } from 'langium';

/**
* Returns whether the inner node is contained in the outer node. If the nodes are equal, this function returns `true`.
*/
export const isContainedIn = (inner: AstNode | undefined, outer: AstNode | undefined): boolean => {
return hasContainerOfType(inner, (it) => it === outer);
};
4 changes: 2 additions & 2 deletions src/language/grammar/safe-ds.langium
Original file line number Diff line number Diff line change
Expand Up @@ -757,11 +757,11 @@ SdsString returns SdsString:
;

interface SdsReference extends SdsExpression {
declaration?: @SdsDeclaration
target?: @SdsDeclaration
}

SdsReference returns SdsReference:
declaration=[SdsDeclaration:ID]
target=[SdsDeclaration:ID]
;

interface SdsParenthesizedExpression extends SdsExpression {
Expand Down
2 changes: 1 addition & 1 deletion src/language/partialEvaluation/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class SdsIntermediateRecord extends SdsIntermediateExpression {
}

getSubstitutionByReferenceOrNull(reference: SdsReference): SdsSimplifiedExpression | null {
const referencedDeclaration = reference.declaration;
const referencedDeclaration = reference.target;
if (!isSdsAbstractResult(referencedDeclaration)) {
return null;
}
Expand Down
230 changes: 224 additions & 6 deletions src/language/scoping/safe-ds-scope-provider.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,59 @@
import { DefaultScopeProvider, EMPTY_SCOPE, getContainerOfType, ReferenceInfo, Scope } from 'langium';
import {
AstNode,
DefaultScopeProvider,
EMPTY_SCOPE,
getContainerOfType,
getDocument,
ReferenceInfo,
Scope,
} from 'langium';
import {
isSdsAssignment,
isSdsBlock,
isSdsCallable,
isSdsClass,
isSdsEnum,
isSdsLambda,
isSdsMemberAccess,
isSdsMemberType,
isSdsModule,
isSdsNamedType,
isSdsNamedTypeDeclaration,
isSdsPlaceholder,
isSdsReference,
isSdsSegment,
isSdsStatement,
isSdsYield,
SdsMemberAccess,
SdsMemberType,
SdsNamedTypeDeclaration,
SdsPlaceholder,
SdsReference,
SdsStatement,
SdsType,
SdsYield,
} from '../generated/ast.js';
import { resultsOrEmpty } from '../ast/shortcuts.js';
import { assigneesOrEmpty, parametersOrEmpty, resultsOrEmpty, statementsOrEmpty } from '../ast/shortcuts.js';
import { isContainedIn } from '../ast/utils.js';

export class SafeDsScopeProvider extends DefaultScopeProvider {
override getScope(context: ReferenceInfo): Scope {
if (isSdsNamedType(context.container) && context.property === 'declaration') {
const node = context.container;
const node = context.container;

if (isSdsNamedType(node) && context.property === 'declaration') {
if (isSdsMemberType(node.$container) && node.$containerProperty === 'member') {
return this.getScopeForMemberTypeMember(node.$container);
} else {
return super.getScope(context);
}
} else if (isSdsYield(context.container) && context.property === 'result') {
return this.getScopeForYieldResult(context.container);
} else if (isSdsReference(node) && context.property === 'target') {
if (isSdsMemberAccess(node.$container) && node.$containerProperty === 'member') {
return this.getScopeForMemberAccessMember(node.$container);
} else {
return this.getScopeForDirectReferenceTarget(node);
}
} else if (isSdsYield(node) && context.property === 'result') {
return this.getScopeForYieldResult(node);
} else {
return super.getScope(context);
}
Expand Down Expand Up @@ -65,6 +93,196 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
}
}

private getScopeForMemberAccessMember(_node: SdsMemberAccess): Scope {
return EMPTY_SCOPE;
}

private getScopeForDirectReferenceTarget(node: SdsReference): Scope {
// Declarations in this file
const currentScope = this.globalDeclarationsInSameFile(node, EMPTY_SCOPE);

// Declarations in containing blocks
return this.localDeclarations(node, currentScope);
}

private globalDeclarationsInSameFile(node: AstNode, outerScope: Scope): Scope {
const module = getContainerOfType(node, isSdsModule);
if (!module) {
/* c8 ignore next 2 */
return outerScope;
}

const precomputed = getDocument(module).precomputedScopes?.get(module);
if (!precomputed) {
/* c8 ignore next 2 */
return outerScope;
}

return this.createScope(precomputed, outerScope);
}

private localDeclarations(node: AstNode, outerScope: Scope): Scope {
// Parameters
const containingCallable = getContainerOfType(node.$container, isSdsCallable);
const parameters = parametersOrEmpty(containingCallable?.parameterList);

// Placeholders up to the containing statement
const containingStatement = getContainerOfType(node.$container, isSdsStatement);

let placeholders: Iterable<SdsPlaceholder>;
if (!containingCallable || isContainedIn(containingStatement, containingCallable)) {
placeholders = this.placeholdersUpToStatement(containingStatement);
} else {
// Placeholders are further away than the parameters
placeholders = [];
}

// Local declarations
const localDeclarations = [...parameters, ...placeholders];

// Lambdas can be nested
if (isSdsLambda(containingCallable)) {
return this.createScopeForNodes(localDeclarations, this.localDeclarations(containingCallable, outerScope));
} else {
return this.createScopeForNodes(localDeclarations, outerScope);
}
}

private *placeholdersUpToStatement(
statement: SdsStatement | undefined,
): Generator<SdsPlaceholder, void, undefined> {
if (!statement) {
return;
}

const containingBlock = getContainerOfType(statement, isSdsBlock);
for (const current of statementsOrEmpty(containingBlock)) {
if (current === statement) {
return;
}

if (isSdsAssignment(current)) {
yield* assigneesOrEmpty(current).filter(isSdsPlaceholder);
}
}
}

// private fun scopeForReferenceDeclaration(context: SdsReference): IScope {
// val resource = context.eResource()
// val packageName = context.containingCompilationUnitOrNull()?.qualifiedNameOrNull()
//
// // Declarations in other files
// var result: IScope = FilteringScope(
// super.delegateGetScope(context, SafeDSPackage.Literals.SDS_REFERENCE__DECLARATION),
// ) {
// it.isReferencableExternalDeclaration(resource, packageName)
// }
//
// // Declarations in this file
// result = declarationsInSameFile(resource, result)
//
// // Declarations in containing classes
// context.containingClassOrNull()?.let {
// result = classMembers(it, result)
// }
//
// // Declarations in containing blocks
// localDeclarations(context, result)
// }
// }
// }
//
// /**
// * Removes declarations in this [Resource], [SdsAnnotation]s, and internal [SdsStep]s located in other
// * [SdsCompilationUnit]s.
// */
// private fun IEObjectDescription?.isReferencableExternalDeclaration(
// fromResource: Resource,
// fromPackageWithQualifiedName: QualifiedName?,
// ): Boolean {
// // Resolution failed in delegate scope
// if (this == null) return false
//
// val obj = this.eObjectOrProxy
//
// // Local declarations are added later using custom scoping rules
// if (obj.eResource() == fromResource) return false
//
// // Annotations cannot be referenced
// if (obj is SdsAnnotation) return false
//
// // Internal steps in another package cannot be referenced
// return !(
// obj is SdsStep &&
// obj.visibility() == SdsVisibility.Internal &&
// obj.containingCompilationUnitOrNull()?.qualifiedNameOrNull() != fromPackageWithQualifiedName
// )
// }
//
// private fun scopeForMemberAccessDeclaration(context: SdsMemberAccess): IScope {
// val receiver = context.receiver
//
// // Static access
// val receiverDeclaration = when (receiver) {
// is SdsReference -> receiver.declaration
// is SdsMemberAccess -> receiver.member.declaration
// else -> null
// }
// if (receiverDeclaration != null) {
// when (receiverDeclaration) {
// is SdsClass -> {
// val members = receiverDeclaration.classMembersOrEmpty().filter { it.isStatic() }
// val superTypeMembers = receiverDeclaration.superClassMembers()
// .filter { it.isStatic() }
// .toList()
//
// return Scopes.scopeFor(members, Scopes.scopeFor(superTypeMembers))
// }
// is SdsEnum -> {
// return Scopes.scopeFor(receiverDeclaration.variantsOrEmpty())
// }
// }
// }
//
// // Call results
// var resultScope = IScope.NULLSCOPE
// if (receiver is SdsCall) {
// val results = receiver.resultsOrNull()
// when {
// results == null -> return IScope.NULLSCOPE
// results.size > 1 -> return Scopes.scopeFor(results)
// results.size == 1 -> resultScope = Scopes.scopeFor(results)
// }
// }
//
// // Members
// val type = (receiver.type() as? NamedType) ?: return resultScope
//
// return when {
// type.isNullable && !context.isNullSafe -> resultScope
// type is ClassType -> {
// val members = type.sdsClass.classMembersOrEmpty().filter { !it.isStatic() }
// val superTypeMembers = type.sdsClass.superClassMembers()
// .filter { !it.isStatic() }
// .toList()
//
// Scopes.scopeFor(members, Scopes.scopeFor(superTypeMembers, resultScope))
// }
// type is EnumVariantType -> Scopes.scopeFor(type.sdsEnumVariant.parametersOrEmpty())
// else -> resultScope
// }
// }
//
// private fun classMembers(context: SdsClass, parentScope: IScope): IScope {
// return when (val containingClassOrNull = context.containingClassOrNull()) {
// is SdsClass -> Scopes.scopeFor(
// context.classMembersOrEmpty(),
// classMembers(containingClassOrNull, parentScope),
// )
// else -> Scopes.scopeFor(context.classMembersOrEmpty(), parentScope)
// }
// }

private getScopeForYieldResult(node: SdsYield): Scope {
const containingSegment = getContainerOfType(node, isSdsSegment);
if (!containingSegment) {
Expand Down
4 changes: 2 additions & 2 deletions src/language/validation/names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
placeholdersOrEmpty,
resultsOrEmpty,
typeParametersOrEmpty,
variantsOrEmpty,
enumVariantsOrEmpty,
} from '../ast/shortcuts.js';

export const CODE_NAME_BLOCK_LAMBDA_PREFIX = 'name/block-lambda-prefix';
Expand Down Expand Up @@ -185,7 +185,7 @@ export const classMustContainUniqueNames = (node: SdsClass, accept: ValidationAc
};

export const enumMustContainUniqueNames = (node: SdsEnum, accept: ValidationAcceptor): void => {
namesMustBeUnique(variantsOrEmpty(node), (name) => `A variant with name '${name}' exists already.`, accept);
namesMustBeUnique(enumVariantsOrEmpty(node), (name) => `A variant with name '${name}' exists already.`, accept);
};

export const enumVariantMustContainUniqueNames = (node: SdsEnumVariant, accept: ValidationAcceptor): void => {
Expand Down
28 changes: 28 additions & 0 deletions src/language/validation/other/expressions/references.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { isSdsAnnotation, isSdsPipeline, isSdsSchema, SdsReference } from '../../../generated/ast.js';
import { ValidationAcceptor } from 'langium';

export const CODE_REFERENCE_TARGET = 'reference/target';

export const referenceTargetMustNotBeAnnotationPipelineOrSchema = (
node: SdsReference,
accept: ValidationAcceptor,
): void => {
const target = node.target?.ref;

if (isSdsAnnotation(target)) {
accept('error', 'An annotation must not be the target of a reference.', {
node,
code: CODE_REFERENCE_TARGET,
});
} else if (isSdsPipeline(target)) {
accept('error', 'A pipeline must not be the target of a reference.', {
node,
code: CODE_REFERENCE_TARGET,
});
} else if (isSdsSchema(target)) {
accept('error', 'A schema must not be the target of a reference.', {
node,
code: CODE_REFERENCE_TARGET,
});
}
};
2 changes: 2 additions & 0 deletions src/language/validation/safe-ds-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { callableTypeMustNotHaveOptionalParameters } from './other/types/callabl
import { typeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments } from './other/types/typeArgumentLists.js';
import { argumentListMustNotHavePositionalArgumentsAfterNamedArguments } from './other/argumentLists.js';
import { parameterMustNotBeVariadicAndOptional } from './other/declarations/parameters.js';
import { referenceTargetMustNotBeAnnotationPipelineOrSchema } from './other/expressions/references.js';

/**
* Register custom validation checks.
Expand Down Expand Up @@ -75,6 +76,7 @@ export const registerValidationChecks = function (services: SafeDsServices) {
parameterListVariadicParameterMustBeLast,
],
SdsPipeline: [pipelineMustContainUniqueNames],
SdsReference: [referenceTargetMustNotBeAnnotationPipelineOrSchema],
SdsResult: [resultMustHaveTypeHint],
SdsSegment: [segmentMustContainUniqueNames, segmentResultListShouldNotBeEmpty],
SdsTemplateString: [templateStringMustHaveExpressionBetweenTwoStringParts],
Expand Down
2 changes: 1 addition & 1 deletion tests/helpers/location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export const getNodeByLocation = (services: SafeDsServices, location: Location):
});
}

for (const node of streamAllContents(root)) {
for (const node of streamAllContents(root, { range: location.range })) {
// Entire node matches the range
const actualRange = node.$cstNode?.range;
if (actualRange && isRangeEqual(actualRange, location.range)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package tests.partialValidation.baseCases.stringLiteralWithInterpolation
package tests.partialValidation.simpleRecursiveCases.templateString

pipeline test {
// $TEST$ constant serialization "start 1 inner1 true inner2 test end"
Expand Down
Loading