diff --git a/package.json b/package.json index 690fdd4d59..f95a647b76 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "doc": "typedoc", "test": "nyc --no-clean mocha", "performance-test": "func() { cd test/performance/ && bash run-all-suites.sh $1 $2 $3; cd ../../; }; func", - "test-full": "npm run test -- --test-installation" + "test-full": "npm run test -- --test-installation", + "detect-circular-deps": "npx madge --extensions ts,tsx --circular src/" }, "keywords": [ "static code analysis", diff --git a/src/abstract-interpretation/domain.ts b/src/abstract-interpretation/domain.ts deleted file mode 100644 index e7c011b6b1..0000000000 --- a/src/abstract-interpretation/domain.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { assertUnreachable, guard } from '../util/assert' - -interface IntervalBound { - readonly value: number, - readonly inclusive: boolean -} - -export class Interval { - /** - * Build a new interval from the given bounds. - * If the interval represents a scalar, the min and max bounds should be equal, as well as their inclusivity. - * @param min - The minimum bound of the interval. - * @param max - The maximum bound of the interval. - */ - constructor(readonly min: IntervalBound, readonly max: IntervalBound) { - guard(min.value <= max.value, () => `The interval ${this.toString()} has a minimum that is greater than its maximum`) - guard(min.value !== max.value || (min.inclusive === max.inclusive), `The bound ${min.value} cannot be in- and exclusive at the same time`) - } - - toString(): string { - return `${this.min.inclusive ? '[' : '('}${this.min.value}, ${this.max.value}${this.max.inclusive ? ']' : ')'}` - } -} - -/** - * A domain represents a set of intervals describing a range of possible values a variable may hold. - * In the future we may want to extend this to support more complex types. - */ -export class Domain { - /** set of intervals to hold */ - private readonly _intervals: Set - - private constructor(intervals: readonly Interval[] = []) { - this._intervals = new Set(unifyOverlappingIntervals(intervals)) - } - - static bottom(): Domain { - return new Domain() - } - - static fromIntervals(intervals: readonly Interval[] | ReadonlySet): Domain { - return new Domain(Array.from(intervals)) - } - - static fromScalar(n: number): Domain { - return new Domain([new Interval( - { value: n, inclusive: true }, - { value: n, inclusive: true } - )]) - } - - get intervals(): Set { - return this._intervals - } - - private set intervals(intervals: Interval[]) { - this._intervals.clear() - for(const interval of intervals) { - this._intervals.add(interval) - } - } - - addInterval(interval: Interval): void { - this.intervals = unifyOverlappingIntervals([...this.intervals, interval]) - } - - toString(): string { - return `{${Array.from(this.intervals).join(', ')}}` - } -} - -const enum CompareType { - /** If qual, the bound that's inclusive is the smaller one */ - Min, - /** If equal, the bound that's inclusive is the greater one */ - Max, - /** Equality is only based on the "raw" values */ - IgnoreInclusivity -} - -function compareIntervals(compareType: CompareType, interval1: IntervalBound, interval2: IntervalBound): number { - const diff = interval1.value - interval2.value - if(diff !== 0 || compareType === CompareType.IgnoreInclusivity) { - return diff - } - switch(compareType) { - case CompareType.Min: - return Number(!interval1.inclusive) - Number(!interval2.inclusive) - case CompareType.Max: - return Number(interval1.inclusive) - Number(interval2.inclusive) - default: - assertUnreachable(compareType) - } -} - -function compareIntervalsByTheirMinimum(interval1: Interval, interval2: Interval): number { - return compareIntervals(CompareType.Min, interval1.min, interval2.min) -} - -function compareIntervalsByTheirMaximum(interval1: Interval, interval2: Interval): number { - return compareIntervals(CompareType.Max, interval1.max, interval2.max) -} - -/** - * Returns true if the given intervals overlap, checking for inclusivity. - */ -export function doIntervalsOverlap(interval1: Interval, interval2: Interval): boolean { - const diff1 = compareIntervals(CompareType.IgnoreInclusivity, interval1.max, interval2.min) - const diff2 = compareIntervals(CompareType.IgnoreInclusivity, interval2.max, interval1.min) - - // If one interval ends before the other starts, they don't overlap - if(diff1 < 0 || diff2 < 0) { - return false - } - // If their end and start are equal, they only overlap if both are inclusive - if(diff1 === 0) { - return interval1.max.inclusive && interval2.min.inclusive - } - if(diff2 === 0) { - return interval2.max.inclusive && interval1.min.inclusive - } - - return true -} - -/** - * Unifies the given domains by creating a new domain that contains all values from all given domains. - */ -export function unifyDomains(domains: readonly Domain[]): Domain { - const unifiedIntervals = unifyOverlappingIntervals(domains.flatMap(domain => Array.from(domain.intervals))) - return Domain.fromIntervals(unifiedIntervals) -} - -/** - * Unify all intervals which overlap with each other to one. - */ -export function unifyOverlappingIntervals(intervals: readonly Interval[]): Interval[] { - if(intervals.length === 0) { - return [] - } - const sortedIntervals = [...intervals].sort(compareIntervalsByTheirMinimum) - - const unifiedIntervals: Interval[] = [] - let currentInterval = sortedIntervals[0] - for(const nextInterval of sortedIntervals) { - if(doIntervalsOverlap(currentInterval, nextInterval)) { - const intervalWithEarlierStart = compareIntervalsByTheirMinimum(currentInterval, nextInterval) < 0 ? currentInterval : nextInterval - const intervalWithLaterEnd = compareIntervalsByTheirMaximum(currentInterval, nextInterval) > 0 ? currentInterval : nextInterval - currentInterval = new Interval(intervalWithEarlierStart.min, intervalWithLaterEnd.max) - } else { - unifiedIntervals.push(currentInterval) - currentInterval = nextInterval - } - } - unifiedIntervals.push(currentInterval) - return unifiedIntervals -} - -/** - * Returns domain1 + domain2, mapping the inclusivity. - * - * @see subtractDomains - */ -export function addDomains(domain1: Domain, domain2: Domain): Domain { - const intervals = new Set() - for(const interval1 of domain1.intervals) { - for(const interval2 of domain2.intervals) { - intervals.add(new Interval({ - value: interval1.min.value + interval2.min.value, - inclusive: interval1.min.inclusive && interval2.min.inclusive - }, { - value: interval1.max.value + interval2.max.value, - inclusive: interval1.max.inclusive && interval2.max.inclusive - })) - } - } - return Domain.fromIntervals(intervals) -} - -/** - * Returns domain1 - domain2, mapping the inclusivity. - * - * @see addDomains - */ -export function subtractDomains(domain1: Domain, domain2: Domain): Domain { - const intervals = new Set() - for(const interval1 of domain1.intervals) { - for(const interval2 of domain2.intervals) { - intervals.add(new Interval({ - value: interval1.min.value - interval2.max.value, - inclusive: interval1.min.inclusive && interval2.max.inclusive - }, { - value: interval1.max.value - interval2.min.value, - inclusive: interval1.max.inclusive && interval2.min.inclusive - })) - } - } - return Domain.fromIntervals(intervals) -} \ No newline at end of file diff --git a/src/abstract-interpretation/handler/binop/binop.ts b/src/abstract-interpretation/handler/binop/binop.ts deleted file mode 100644 index 49d5d41af0..0000000000 --- a/src/abstract-interpretation/handler/binop/binop.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { Handler } from '../handler' -import type { AINode } from '../../processor' -import { aiLogger } from '../../processor' -import { guard } from '../../../util/assert' -import { operators } from './operators' -import type { ParentInformation } from '../../../r-bridge/lang-4.x/ast/model/processing/decorate' -import type { RBinaryOp } from '../../../r-bridge/lang-4.x/ast/model/nodes/r-binary-op' - -export type BinaryOpProcessor = (lhs: AINode, rhs: AINode, node: RBinaryOp) => AINode - -export class BinOp implements Handler { - lhs: AINode | undefined - rhs: AINode | undefined - - constructor(readonly node: RBinaryOp) {} - - getName(): string { - return `Bin Op (${this.node.operator})` - } - - enter(): void { - aiLogger.trace(`Entered ${this.getName()}`) - } - - exit(): AINode { - aiLogger.trace(`Exited ${this.getName()}`) - guard(this.lhs !== undefined, `No LHS found for assignment ${this.node.info.id}`) - guard(this.rhs !== undefined, `No RHS found for assignment ${this.node.info.id}`) - const processor: BinaryOpProcessor | undefined = operators[this.node.operator] - guard(processor !== undefined, `No processor found for binary operator ${this.node.operator}`) - return processor(this.lhs, this.rhs, this.node) - } - - next(node: AINode): void { - aiLogger.trace(`${this.getName()} received`) - if(this.lhs === undefined) { - this.lhs = node - } else if(this.rhs === undefined) { - this.rhs = node - } else { - guard(false, `BinOp ${this.node.info.id} already has both LHS and RHS`) - } - } -} diff --git a/src/abstract-interpretation/handler/binop/operators.ts b/src/abstract-interpretation/handler/binop/operators.ts deleted file mode 100644 index 42c35e284b..0000000000 --- a/src/abstract-interpretation/handler/binop/operators.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { BinaryOpProcessor } from './binop' -import { addDomains, subtractDomains } from '../../domain' - -export const operators: Record = { - '<-': (lhs, rhs, node) => { - return { - id: lhs.id, - domain: rhs.domain, - astNode: node.lhs, - } - }, - '+': (lhs, rhs, node) => { - return { - id: lhs.id, - domain: addDomains(lhs.domain, rhs.domain), - astNode: node, - } - }, - '-': (lhs, rhs, node) => { - return { - id: lhs.id, - domain: subtractDomains(lhs.domain, rhs.domain), - astNode: node, - } - } -} diff --git a/src/abstract-interpretation/handler/handler.ts b/src/abstract-interpretation/handler/handler.ts deleted file mode 100644 index 805613fe03..0000000000 --- a/src/abstract-interpretation/handler/handler.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface Handler { - getName: () => string, - enter: () => void - exit: () => ValueType - next: (value: ValueType) => void -} \ No newline at end of file diff --git a/src/abstract-interpretation/processor.ts b/src/abstract-interpretation/processor.ts deleted file mode 100644 index f9419a2d50..0000000000 --- a/src/abstract-interpretation/processor.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { DataflowInformation } from '../dataflow/info' -import { CfgVertexType, extractCFG } from '../util/cfg/cfg' -import { visitCfg } from '../util/cfg/visitor' -import { guard } from '../util/assert' - -import type { Handler } from './handler/handler' -import { BinOp } from './handler/binop/binop' -import { Domain, unifyDomains } from './domain' -import { log } from '../util/log' -import type { NodeId } from '../r-bridge/lang-4.x/ast/model/processing/node-id' -import type { - NormalizedAst, - ParentInformation, - RNodeWithParent -} from '../r-bridge/lang-4.x/ast/model/processing/decorate' -import type { DataflowGraphVertexInfo } from '../dataflow/graph/vertex' -import type { OutgoingEdges } from '../dataflow/graph/graph' -import { edgeIncludesType, EdgeType } from '../dataflow/graph/edge' -import { RType } from '../r-bridge/lang-4.x/ast/model/type' - -export const aiLogger = log.getSubLogger({ name: 'abstract-interpretation' }) - -export interface AINode { - readonly id: NodeId - readonly domain: Domain - readonly astNode: RNodeWithParent -} - -class Stack { - private backingStore: ElementType[] = [] - - size(): number { - return this.backingStore.length - } - peek(): ElementType | undefined { - return this.backingStore[this.size() - 1] - } - pop(): ElementType | undefined { - return this.backingStore.pop() - } - push(item: ElementType): ElementType { - this.backingStore.push(item) - return item - } -} - -function getDomainOfDfgChild(node: NodeId, dfg: DataflowInformation, nodeMap: Map): Domain { - const dfgNode: [DataflowGraphVertexInfo, OutgoingEdges] | undefined = dfg.graph.get(node) - guard(dfgNode !== undefined, `No DFG-Node found with ID ${node}`) - const [_, children] = dfgNode - const ids = Array.from(children.entries()) - .filter(([_, edge]) => edgeIncludesType(edge.types, EdgeType.Reads)) - .map(([id, _]) => id) - const domains: Domain[] = [] - for(const id of ids) { - const domain = nodeMap.get(id)?.domain - guard(domain !== undefined, `No domain found for ID ${id}`) - domains.push(domain) - } - return unifyDomains(domains) -} - -export function runAbstractInterpretation(ast: NormalizedAst, dfg: DataflowInformation): DataflowInformation { - const cfg = extractCFG(ast) - const operationStack = new Stack>() - const nodeMap = new Map() - visitCfg(cfg, (node, _) => { - const astNode = ast.idMap.get(node.id) - if(astNode?.type === RType.BinaryOp) { - operationStack.push(new BinOp(astNode)).enter() - } else if(astNode?.type === RType.Symbol) { - operationStack.peek()?.next({ - id: astNode.info.id, - domain: getDomainOfDfgChild(node.id, dfg, nodeMap), - astNode: astNode, - }) - } else if(astNode?.type === RType.Number){ - const num = astNode.content.num - operationStack.peek()?.next({ - id: astNode.info.id, - domain: Domain.fromScalar(num), - astNode: astNode, - }) - } else if(node.type === CfgVertexType.EndMarker) { - const operation = operationStack.pop() - if(operation === undefined) { - return - } - const operationResult = operation.exit() - guard(!nodeMap.has(operationResult.id), `Domain for ID ${operationResult.id} already exists`) - nodeMap.set(operationResult.id, operationResult) - operationStack.peek()?.next(operationResult) - } else { - aiLogger.warn(`Unknown node type ${node.type}`) - } - }) - return dfg -} diff --git a/src/dataflow/environments/built-in.ts b/src/dataflow/environments/built-in.ts index 1f897b1f75..3648ae48eb 100644 --- a/src/dataflow/environments/built-in.ts +++ b/src/dataflow/environments/built-in.ts @@ -2,11 +2,10 @@ import type { DataflowProcessorInformation } from '../processor' import { ExitPointType } from '../info' import type { DataflowInformation } from '../info' import { processKnownFunctionCall } from '../internal/process/functions/call/known-call-handling' -import { processSourceCall } from '../internal/process/functions/call/built-in/built-in-source' import { processAccess } from '../internal/process/functions/call/built-in/built-in-access' import { processIfThenElse } from '../internal/process/functions/call/built-in/built-in-if-then-else' import { processAssignment } from '../internal/process/functions/call/built-in/built-in-assignment' -import { processSpecialBinOp } from '../internal/process/functions/call/built-in/built-in-logical-bin-op' +import { processSpecialBinOp } from '../internal/process/functions/call/built-in/built-in-special-bin-op' import { processPipe } from '../internal/process/functions/call/built-in/built-in-pipe' import { processForLoop } from '../internal/process/functions/call/built-in/built-in-for-loop' import { processRepeatLoop } from '../internal/process/functions/call/built-in/built-in-repeat-loop' @@ -24,6 +23,7 @@ import type { RSymbol } from '../../r-bridge/lang-4.x/ast/model/nodes/r-symbol' import type { NodeId } from '../../r-bridge/lang-4.x/ast/model/processing/node-id' import { EdgeType } from '../graph/edge' import { processLibrary } from '../internal/process/functions/call/built-in/built-in-library' +import { processSourceCall } from '../internal/process/functions/call/built-in/built-in-source' export const BuiltIn = 'built-in' @@ -59,7 +59,7 @@ function defaultBuiltInProcessor( args: readonly RFunctionArgument[], rootId: NodeId, data: DataflowProcessorInformation, - config: { returnsNthArgument?: number | 'last', cfg?: ExitPointType, readAllArguments?: boolean } + config: { returnsNthArgument?: number | 'last', cfg?: ExitPointType, readAllArguments?: boolean, hasUnknownSideEffects?: boolean } ): DataflowInformation { const { information: res, processedArguments } = processKnownFunctionCall({ name, args, rootId, data }) if(config.returnsNthArgument !== undefined) { @@ -76,6 +76,10 @@ function defaultBuiltInProcessor( } } + if(config.hasUnknownSideEffects) { + res.graph.markIdForUnknownSideEffects(rootId) + } + if(config.cfg !== undefined) { res.exitPoints = [...res.exitPoints, { type: config.cfg, nodeId: rootId, controlDependencies: data.controlDependencies }] } @@ -83,12 +87,13 @@ function defaultBuiltInProcessor( } export function registerBuiltInFunctions>( - both: boolean, - names: readonly Identifier[], + both: boolean, + names: readonly Identifier[], processor: Proc, - config: Config + config: Config ): void { for(const name of names) { + guard(processor !== undefined, `Processor for ${name} is undefined, maybe you have an import loop? You may run 'npm run detect-circular-deps' - although by far not all are bad`) guard(!BuiltInMemory.has(name), `Built-in ${name} already defined`) const d: IdentifierDefinition[] = [{ kind: 'built-in-function', @@ -151,12 +156,12 @@ function registerBuiltInConstant(both: boolean, name: Identifier, value: T): export const BuiltInMemory = new Map() export const EmptyBuiltInMemory = new Map() -registerBuiltInConstant(true, 'NULL', null) -registerBuiltInConstant(true, 'NA', null) -registerBuiltInConstant(true, 'TRUE', true) -registerBuiltInConstant(true, 'T', true) +registerBuiltInConstant(true, 'NULL', null) +registerBuiltInConstant(true, 'NA', null) +registerBuiltInConstant(true, 'TRUE', true) +registerBuiltInConstant(true, 'T', true) registerBuiltInConstant(true, 'FALSE', false) -registerBuiltInConstant(true, 'F', false) +registerBuiltInConstant(true, 'F', false) registerSimpleFunctions( '~', '+', '-', '*', '/', '^', '!', '?', '**', '==', '!=', '>', '<', '>=', '<=', '%%', '%/%', '%*%', '%in%', ':', 'list', 'c', 'rep', 'seq', 'seq_len', 'seq_along', 'seq.int', 'gsub', 'which', 'class', 'dimnames', 'min', 'max', @@ -165,9 +170,9 @@ registerSimpleFunctions( 'apply', 'lapply', 'sapply', 'tapply', 'mapply', 'do.call', 'rbind', 'nrow', 'ncol', 'tryCatch', 'expression', 'factor', 'missing', 'as.data.frame', 'data.frame', 'na.omit', 'rownames', 'names', 'order', 'length', 'any', 'dim', 'matrix', 'cbind', 'nchar', 't' ) -registerBuiltInFunctions(true, ['print', '('], defaultBuiltInProcessor, { returnsNthArgument: 0 }) - -registerBuiltInFunctions(false, ['cat', 'switch'], defaultBuiltInProcessor, {}) /* returns null */ +registerBuiltInFunctions(true, ['print', '('], defaultBuiltInProcessor, { returnsNthArgument: 0 } ) +registerBuiltInFunctions(true, ['load', 'load_all'], defaultBuiltInProcessor, { hasUnknownSideEffects: true } ) +registerBuiltInFunctions(false, ['cat', 'switch'], defaultBuiltInProcessor, {} ) /* returns null */ registerBuiltInFunctions(true, ['return'], defaultBuiltInProcessor, { returnsNthArgument: 0, cfg: ExitPointType.Return } ) registerBuiltInFunctions(true, ['break'], defaultBuiltInProcessor, { cfg: ExitPointType.Break } ) registerBuiltInFunctions(true, ['next'], defaultBuiltInProcessor, { cfg: ExitPointType.Next } ) diff --git a/src/dataflow/graph/diff.ts b/src/dataflow/graph/diff.ts index 0509a6d3a5..8a82977d28 100644 --- a/src/dataflow/graph/diff.ts +++ b/src/dataflow/graph/diff.ts @@ -129,6 +129,7 @@ function diffOutgoingEdges(ctx: DataflowDiffContext): void { function diffRootVertices(ctx: DataflowDiffContext): void { setDifference(ctx.left.rootIds(), ctx.right.rootIds(), { ...ctx, position: `${ctx.position}Root vertices differ in graphs. ` }) + setDifference(ctx.left.unknownSideEffects, ctx.right.unknownSideEffects, { ...ctx, position: `${ctx.position}Unknown side effects differ in graphs. ` }) } diff --git a/src/dataflow/graph/graph.ts b/src/dataflow/graph/graph.ts index efe986c9b5..c8c563d011 100644 --- a/src/dataflow/graph/graph.ts +++ b/src/dataflow/graph/graph.ts @@ -1,10 +1,8 @@ import { guard } from '../../util/assert' import type { DataflowGraphEdge } from './edge' import { EdgeType } from './edge' - import type { DataflowInformation } from '../info' -import type { DataflowDifferenceReport } from './diff' -import { diffOfDataflowGraphs, equalFunctionArguments } from './diff' +import { equalFunctionArguments } from './diff' import type { DataflowGraphVertexArgument, DataflowGraphVertexFunctionCall, @@ -18,6 +16,7 @@ import { arrayEqual } from '../../util/arrays' import { EmptyArgument } from '../../r-bridge/lang-4.x/ast/model/nodes/r-function-call' import type { IdentifierDefinition, IdentifierReference } from '../environments/identifier' import type { NodeId } from '../../r-bridge/lang-4.x/ast/model/processing/node-id' +import { normalizeIdToNumberIfPossible } from '../../r-bridge/lang-4.x/ast/model/processing/node-id' import type { REnvironmentInformation } from '../environments/environment' import { initializeCleanEnvironments } from '../environments/environment' import type { AstIdMap } from '../../r-bridge/lang-4.x/ast/model/processing/decorate' @@ -88,6 +87,8 @@ export class DataflowGraph< > { private static DEFAULT_ENVIRONMENT: REnvironmentInformation | undefined = undefined private _idMap: AstIdMap | undefined + /* Set of vertices which have sideEffects that we do not know anything about */ + private _unknownSideEffects: Set = new Set() // this should be linked separately public readonly functionCache = new Map>() @@ -152,6 +153,13 @@ export class DataflowGraph< return this._idMap } + /** + * Retrieves the set of vertices which have side effects that we do not know anything about. + */ + public get unknownSideEffects(): ReadonlySet { + return this._unknownSideEffects + } + /** Allows setting the id-map explicitly (which should only be used when, e.g., you plan to compare two dataflow graphs on the same AST-basis) */ public setIdMap(idMap: AstIdMap): void { this._idMap = idMap @@ -159,11 +167,11 @@ export class DataflowGraph< /** - * @param includeDefinedFunctions - If true this will iterate over function definitions as well and not just the toplevel - * @returns the ids of all toplevel vertices in the graph together with their vertex information + * @param includeDefinedFunctions - If true this will iterate over function definitions as well and not just the toplevel + * @returns the ids of all toplevel vertices in the graph together with their vertex information * * @see #edges - */ + */ public* vertices(includeDefinedFunctions: boolean): IterableIterator<[NodeId, Vertex]> { if(includeDefinedFunctions) { yield* this.vertexInformation.entries() @@ -205,15 +213,15 @@ export class DataflowGraph< } /** - * Adds a new vertex to the graph, for ease of use, some arguments are optional and filled automatically. - * + * Adds a new vertex to the graph, for ease of use, some arguments are optional and filled automatically. + * * @param vertex - The vertex to add * @param asRoot - If false, this will only add the vertex but do not add it to the {@link rootIds|root vertices} of the graph. * This is probably only of use, when you construct dataflow graphs for tests. * - * @see DataflowGraphVertexInfo - * @see DataflowGraphVertexArgument - */ + * @see DataflowGraphVertexInfo + * @see DataflowGraphVertexArgument + */ public addVertex(vertex: DataflowGraphVertexArgument & Omit, asRoot = true): this { const oldVertex = this.vertexInformation.get(vertex.id) if(oldVertex !== undefined) { @@ -225,7 +233,6 @@ export class DataflowGraph< const environment = vertex.environment === undefined ? fallback : cloneEnvironmentInformation(vertex.environment) - this.vertexInformation.set(vertex.id, { ...vertex, environment @@ -243,11 +250,11 @@ export class DataflowGraph< /** {@inheritDoc} */ public addEdge(from: NodeId | ReferenceForEdge, to: NodeId | ReferenceForEdge, edgeInfo: EdgeData): this /** - * Will insert a new edge into the graph, - * if the direction of the edge is of no importance (`same-read-read` or `same-def-def`), source - * and target will be sorted so that `from` has the lower, and `to` the higher id (default ordering). - * Please note, that this will never make edges to {@link BuiltIn} as they are not part of the graph. - */ + * Will insert a new edge into the graph, + * if the direction of the edge is of no importance (`same-read-read` or `same-def-def`), source + * and target will be sorted so that `from` has the lower, and `to` the higher id (default ordering). + * Please note, that this will never make edges to {@link BuiltIn} as they are not part of the graph. + */ public addEdge(from: NodeId | ReferenceForEdge, to: NodeId | ReferenceForEdge, edgeInfo: EdgeData): this { const { fromId, toId } = extractEdgeIds(from, to) const { type, ...rest } = edgeInfo @@ -310,6 +317,10 @@ export class DataflowGraph< } } + for(const unknown of otherGraph.unknownSideEffects) { + this._unknownSideEffects.add(unknown) + } + for(const [id, info] of otherGraph.vertexInformation) { const currentInfo = this.vertexInformation.get(id) this.vertexInformation.set(id, currentInfo === undefined ? info : mergeNodeInfos(currentInfo, info)) @@ -337,17 +348,6 @@ export class DataflowGraph< } } - public equals(other: DataflowGraph, diff: true, names?: { left: string, right: string }): DataflowDifferenceReport - public equals(other: DataflowGraph, diff?: false, names?: { left: string, right: string }): boolean - public equals(other: DataflowGraph, diff = false, names = { left: 'left', right: 'right' }): boolean | DataflowDifferenceReport { - const report = diffOfDataflowGraphs({ name: names.left, graph: this }, { name: names.right, graph: other }) - if(diff) { - return report - } else { - return report.isEqual() - } - } - /** * Marks a vertex in the graph to be a definition * @param reference - The reference to the vertex to mark as definition @@ -361,6 +361,24 @@ export class DataflowGraph< this.vertexInformation.set(reference.nodeId, { ...vertex, tag: 'variable-definition' }) } } + + /** If you do not pass the `to` node, this will just mark the node as maybe */ + public addControlDependency(from: NodeId, to?: NodeId, when?: boolean): this { + to = to ? normalizeIdToNumberIfPossible(to) : undefined + const vertex = this.getVertex(from, true) + guard(vertex !== undefined, () => `node must be defined for ${from} to add control dependency`) + vertex.controlDependencies ??= [] + if(to && vertex.controlDependencies.every(({ id, when: cond }) => id !== to && when !== cond)) { + vertex.controlDependencies.push({ id: to, when }) + } + return this + } + + /** Marks the given node as having unknown side effects */ + public markIdForUnknownSideEffects(id: NodeId): this { + this._unknownSideEffects.add(normalizeIdToNumberIfPossible(id)) + return this + } } function mergeNodeInfos(current: Vertex, next: Vertex): Vertex { diff --git a/src/dataflow/info.ts b/src/dataflow/info.ts index ee99d64354..703fab08c2 100644 --- a/src/dataflow/info.ts +++ b/src/dataflow/info.ts @@ -78,7 +78,7 @@ export function initializeCleanDataflowInformation(entryPoint: NodeId, data: } } -export function happensInEveryBranch(controlDependencies: ControlDependency[] | undefined): boolean { +export function happensInEveryBranch(controlDependencies: readonly ControlDependency[] | undefined): boolean { if(controlDependencies === undefined) { /* the cds are unconstrained */ return true diff --git a/src/dataflow/internal/process/functions/call/built-in/built-in-library.ts b/src/dataflow/internal/process/functions/call/built-in/built-in-library.ts index 9fb66882c1..2518da9e2f 100644 --- a/src/dataflow/internal/process/functions/call/built-in/built-in-library.ts +++ b/src/dataflow/internal/process/functions/call/built-in/built-in-library.ts @@ -12,6 +12,7 @@ import { RType } from '../../../../../../r-bridge/lang-4.x/ast/model/type' import { wrapArgumentsUnnamed } from '../argument/make-argument' +/* we currently do not mark this as an unknown side effect, as we can enable/disable this with a toggle */ export function processLibrary( name: RSymbol, args: readonly RFunctionArgument[], diff --git a/src/dataflow/internal/process/functions/call/built-in/built-in-source.ts b/src/dataflow/internal/process/functions/call/built-in/built-in-source.ts index 6b08061841..9f7c957101 100644 --- a/src/dataflow/internal/process/functions/call/built-in/built-in-source.ts +++ b/src/dataflow/internal/process/functions/call/built-in/built-in-source.ts @@ -13,8 +13,7 @@ import type { ParentInformation } from '../../../../../../r-bridge/lang-4.x/ast/model/processing/decorate' import { - deterministicPrefixIdGenerator - , + deterministicPrefixIdGenerator, sourcedDeterministicCountingIdGenerator } from '../../../../../../r-bridge/lang-4.x/ast/model/processing/decorate' import type { RFunctionArgument } from '../../../../../../r-bridge/lang-4.x/ast/model/nodes/r-function-call' @@ -25,6 +24,7 @@ import { dataflowLogger } from '../../../../../logger' import { RType } from '../../../../../../r-bridge/lang-4.x/ast/model/type' import { overwriteEnvironment } from '../../../../../environments/overwrite' import type { NoInfo } from '../../../../../../r-bridge/lang-4.x/ast/model/model' +import { expensiveTrace } from '../../../../../../util/log' let sourceProvider = requestProviderFromFile() @@ -40,7 +40,7 @@ export function processSourceCall( config: { /** should this produce an explicit source function call in the graph? */ includeFunctionCall?: boolean, - /** should this function call be followed, even when the configuratio disables it? */ + /** should this function call be followed, even when the configuration disables it? */ forceFollow?: boolean } ): DataflowInformation { @@ -51,28 +51,31 @@ export function processSourceCall( const sourceFile = args[0] if(!config.forceFollow && getConfig().ignoreSourceCalls) { - dataflowLogger.info(`Skipping source call ${JSON.stringify(sourceFile)} (disabled in config file)`) + expensiveTrace(dataflowLogger, () => `Skipping source call ${JSON.stringify(sourceFile)} (disabled in config file)`) + information.graph.markIdForUnknownSideEffects(rootId) return information } - if(sourceFile !== EmptyArgument && sourceFile?.value?.type == RType.String) { + if(sourceFile !== EmptyArgument && sourceFile?.value?.type === RType.String) { const path = removeRQuotes(sourceFile.lexeme) const request = sourceProvider.createRequest(path) // check if the sourced file has already been dataflow analyzed, and if so, skip it if(data.referenceChain.includes(requestFingerprint(request))) { - dataflowLogger.info(`Found loop in dataflow analysis for ${JSON.stringify(request)}: ${JSON.stringify(data.referenceChain)}, skipping further dataflow analysis`) + expensiveTrace(dataflowLogger, () => `Found loop in dataflow analysis for ${JSON.stringify(request)}: ${JSON.stringify(data.referenceChain)}, skipping further dataflow analysis`) + information.graph.markIdForUnknownSideEffects(rootId) return information } - return sourceRequest(request, data, information, sourcedDeterministicCountingIdGenerator(path, name.location)) + return sourceRequest(rootId, request, data, information, sourcedDeterministicCountingIdGenerator(path, name.location)) } else { - dataflowLogger.info(`Non-constant argument ${JSON.stringify(sourceFile)} for source is currently not supported, skipping`) + expensiveTrace(dataflowLogger, () => `Non-constant argument ${JSON.stringify(sourceFile)} for source is currently not supported, skipping`) + information.graph.markIdForUnknownSideEffects(rootId) return information } } -export function sourceRequest(request: RParseRequest, data: DataflowProcessorInformation, information: DataflowInformation, getId: IdGenerator): DataflowInformation { +export function sourceRequest(rootId: NodeId, request: RParseRequest, data: DataflowProcessorInformation, information: DataflowInformation, getId: IdGenerator): DataflowInformation { const executor = new RShellExecutor() // parse, normalize and dataflow the sourced file @@ -89,9 +92,16 @@ export function sourceRequest(request: RParseRequest, data: DataflowP }) } catch(e) { dataflowLogger.warn(`Failed to analyze sourced file ${JSON.stringify(request)}, skipping: ${(e as Error).message}`) + information.graph.markIdForUnknownSideEffects(rootId) return information } + // take the entry point as well as all the written references, and give them a control dependency to the source call to show that they are conditional + dataflow.graph.addControlDependency(dataflow.entryPoint, rootId) + for(const out of dataflow.out) { + dataflow.graph.addControlDependency(out.nodeId, rootId) + } + // update our graph with the sourced file's information const newInformation = { ...information } newInformation.environment = overwriteEnvironment(information.environment, dataflow.environment) @@ -118,10 +128,11 @@ export function standaloneSourceFile( // check if the sourced file has already been dataflow analyzed, and if so, skip it if(data.referenceChain.includes(fingerprint)) { dataflowLogger.info(`Found loop in dataflow analysis for ${JSON.stringify(request)}: ${JSON.stringify(data.referenceChain)}, skipping further dataflow analysis`) + information.graph.markIdForUnknownSideEffects(uniqueSourceId) return information } - return sourceRequest(request, { + return sourceRequest(uniqueSourceId, request, { ...data, currentRequest: request, environment: information.environment, diff --git a/src/dataflow/internal/process/functions/call/built-in/built-in-logical-bin-op.ts b/src/dataflow/internal/process/functions/call/built-in/built-in-special-bin-op.ts similarity index 100% rename from src/dataflow/internal/process/functions/call/built-in/built-in-logical-bin-op.ts rename to src/dataflow/internal/process/functions/call/built-in/built-in-special-bin-op.ts diff --git a/src/slicing/static/static-slicer.ts b/src/slicing/static/static-slicer.ts index dc01fefc38..7dc131f8e0 100644 --- a/src/slicing/static/static-slicer.ts +++ b/src/slicing/static/static-slicer.ts @@ -46,6 +46,12 @@ export function staticSlicing(graph: DataflowGraph, { idMap }: NormalizedAst, cr minDepth = Math.min(minDepth, idMap.get(startId)?.info.depth ?? minDepth) sliceSeedIds.add(startId) } + /* additionally, + * include all the implicit side effects that we have to consider as we are unable to narrow them down + */ + for(const id of graph.unknownSideEffects) { + queue.add(id, emptyEnv, basePrint, true) + } } while(queue.nonEmpty()) { diff --git a/test/functionality/abstract-interpretation/abstract-interpretation.spec.ts b/test/functionality/abstract-interpretation/abstract-interpretation.spec.ts deleted file mode 100644 index aa6008123d..0000000000 --- a/test/functionality/abstract-interpretation/abstract-interpretation.spec.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { - addDomains, - doIntervalsOverlap, - Domain, - Interval, - subtractDomains, - unifyOverlappingIntervals -} from '../../../src/abstract-interpretation/domain' -import { assert } from 'chai' - -describe('Abstract Interpretation', () => { - it('Interval overlapping', () => { - assert.isFalse(doIntervalsOverlap( - new Interval({ value: 1, inclusive: true }, { value: 2, inclusive: true }), - new Interval({ value: 3, inclusive: true }, { value: 4, inclusive: true }) - ), 'Trivially non-overlapping') - - assert.isTrue(doIntervalsOverlap( - new Interval({ value: 1, inclusive: true }, { value: 3, inclusive: true }), - new Interval({ value: 2, inclusive: true }, { value: 4, inclusive: false }) - ), 'Trivially overlapping') - - assert.isTrue(doIntervalsOverlap( - new Interval({ value: 1, inclusive: true }, { value: 3, inclusive: true }), - new Interval({ value: 3, inclusive: true }, { value: 4, inclusive: false }) - ), 'Intervals touching, with the touching bounds being inclusive are overlapping') - - assert.isFalse(doIntervalsOverlap( - new Interval({ value: 1, inclusive: true }, { value: 3, inclusive: false }), - new Interval({ value: 3, inclusive: true }, { value: 4, inclusive: false }) - ), 'Intervals touching, with one of the touching bounds being exclusive are not overlapping') - - assert.isFalse(doIntervalsOverlap( - new Interval({ value: 1, inclusive: true }, { value: 3, inclusive: true }), - new Interval({ value: 3, inclusive: false }, { value: 4, inclusive: false }) - ), 'Intervals touching, with one of the touching bounds being exclusive are not overlapping') - - assert.isFalse(doIntervalsOverlap( - new Interval({ value: 1, inclusive: true }, { value: 3, inclusive: false }), - new Interval({ value: 3, inclusive: false }, { value: 4, inclusive: false }) - ), 'Intervals touching, with both touching bounds being exclusive are not overlapping') - }) - - it('Interval unification', () => { - assert.isEmpty(unifyOverlappingIntervals([]), 'Unifying no intervals results in nothing') - - let intervals = [...Domain.fromScalar(1).intervals, ...Domain.fromScalar(2).intervals] - assert.deepEqual( - unifyOverlappingIntervals(intervals), - intervals, - 'Unifying two non overlapping intervals results in no change' - ) - - intervals = [ - new Interval({ value: 1, inclusive: true }, { value: 3, inclusive: true }), - new Interval({ value: 5, inclusive: true }, { value: 7, inclusive: true }), - new Interval({ value: 2, inclusive: true }, { value: 4, inclusive: true }), - new Interval({ value: 6, inclusive: true }, { value: 8, inclusive: true }), - ] - assert.deepEqual( - unifyOverlappingIntervals(intervals), - [ - new Interval({ value: 1, inclusive: true }, { value: 4, inclusive: true }), - new Interval({ value: 5, inclusive: true }, { value: 8, inclusive: true }), - ], - ) - }) - - it('Domain addition', () => { - assert.isEmpty(addDomains(Domain.bottom(), Domain.bottom()).intervals, 'Adding two empty domains results in an empty domain') - - let domain1 = Domain.fromScalar(4) - let domain2 = Domain.fromScalar(2) - assert.deepEqual( - addDomains(domain1, domain2), - Domain.fromScalar(6), - 'Adding two domains of a scalar, results in a domain containing the sum of the scalars' - ) - - domain2 = Domain.fromIntervals([new Interval({ value: 6, inclusive: true }, { value: 9, inclusive: true })]) - assert.deepEqual( - addDomains(domain1, domain2), - Domain.fromIntervals([new Interval({ value: 10, inclusive: true }, { value: 13, inclusive: true })]), - 'Adding one scalar-domain to a wider domain, adds the scalar to the start and end of the wider domain' - ) - - domain1 = Domain.fromIntervals([new Interval({ value: 6, inclusive: true }, { value: 9, inclusive: true })]) - domain2 = Domain.fromIntervals([new Interval({ value: 4, inclusive: true }, { value: 7, inclusive: true })]) - assert.deepEqual( - addDomains(domain1, domain2), - Domain.fromIntervals([new Interval({ value: 10, inclusive: true }, { value: 16, inclusive: true })]), - 'Adding two domains with overlapping intervals, adds the intervals' - ) - }) - - it('Domain subtraction', () => { - assert.isEmpty(subtractDomains(Domain.bottom(), Domain.bottom()).intervals, 'Subtracting two empty domains results in an empty domain') - - let domain1 = Domain.fromScalar(4) - let domain2 = Domain.fromScalar(2) - assert.deepEqual( - subtractDomains(domain1, domain2), - Domain.fromIntervals([new Interval({ value: 2, inclusive: true }, { value: 2, inclusive: true })]), - 'Subtracting two domains of a scalar, results in a domain containing the difference of the scalars' - ) - - domain2 = Domain.fromIntervals([new Interval({ value: 6, inclusive: true }, { value: 9, inclusive: true })]) - assert.deepEqual( - subtractDomains(domain2, domain1), - Domain.fromIntervals([new Interval({ value: 2, inclusive: true }, { value: 5, inclusive: true })]), - 'Subtracting a scalar-domain from a wider domain, subtracts the scalar from the start and end of the wider domain' - ) - - domain2 = Domain.fromIntervals([new Interval({ value: 1, inclusive: true }, { value: 3, inclusive: true })]) - assert.deepEqual( - subtractDomains(domain1, domain2), - Domain.fromIntervals([new Interval({ value: 1, inclusive: true }, { value: 3, inclusive: true })]), - 'Subtracting a wider domain from a scalar-domain, subtracts the start and end of the wider domain from the scalar' - ) - - domain1 = Domain.fromIntervals([new Interval({ value: 6, inclusive: true }, { value: 9, inclusive: true })]) - domain2 = Domain.fromIntervals([new Interval({ value: 4, inclusive: true }, { value: 5, inclusive: true })]) - assert.deepEqual( - subtractDomains(domain1, domain2), - Domain.fromIntervals([new Interval({ value: 1, inclusive: true }, { value: 5, inclusive: true })]), - 'Subtracting two domains with overlapping intervals, subtracts the intervals' - ) - }) -}) diff --git a/test/functionality/dataflow/processing-of-elements/functions/dataflow-source-tests.ts b/test/functionality/dataflow/processing-of-elements/functions/dataflow-source-tests.ts index 889d6466ec..05dec1de95 100644 --- a/test/functionality/dataflow/processing-of-elements/functions/dataflow-source-tests.ts +++ b/test/functionality/dataflow/processing-of-elements/functions/dataflow-source-tests.ts @@ -24,10 +24,12 @@ describe('source', withShell(shell => { .reads('5', 'simple-1:1-1:6-0') .call('3', 'source', [argumentInCall('1')], { returns: [], reads: [BuiltIn] }) .call('simple-1:1-1:6-2', '<-', [argumentInCall('simple-1:1-1:6-0'), argumentInCall('simple-1:1-1:6-1')], { returns: ['simple-1:1-1:6-0'], reads: [BuiltIn] }) + .addControlDependency('simple-1:1-1:6-2', '3') .call('7', 'cat', [argumentInCall('5')], { returns: [], reads: [BuiltIn], environment: defaultEnv().defineVariable('N', 'simple-1:1-1:6-0', 'simple-1:1-1:6-2') }) .constant('1') .constant('simple-1:1-1:6-1') .defineVariable('simple-1:1-1:6-0', 'N', { definedBy: ['simple-1:1-1:6-1', 'simple-1:1-1:6-2'] }) + .addControlDependency('simple-1:1-1:6-0', '3') ) assertDataflow(label('multiple source', ['sourcing-external-files', 'strings', 'unnamed-arguments', 'normal-definition', 'newlines']), shell, 'source("simple")\nN <- 0\nsource("simple")\ncat(N)', emptyGraph() @@ -35,18 +37,22 @@ describe('source', withShell(shell => { .reads('12', 'simple-3:1-3:6-0') .call('3', 'source', [argumentInCall('1')], { returns: [], reads: [BuiltIn] }) .call('simple-1:1-1:6-2', '<-', [argumentInCall('simple-1:1-1:6-0'), argumentInCall('simple-1:1-1:6-1')], { returns: ['simple-1:1-1:6-0'], reads: [BuiltIn] }) + .addControlDependency('simple-1:1-1:6-2', '3') .call('6', '<-', [argumentInCall('4'), argumentInCall('5')], { returns: ['4'], reads: [BuiltIn], environment: defaultEnv().defineVariable('N', 'simple-1:1-1:6-0', 'simple-1:1-1:6-2') }) .call('10', 'source', [argumentInCall('8')], { returns: [], reads: [BuiltIn], environment: defaultEnv().defineVariable('N', '4', '6') }) .call('simple-3:1-3:6-2', '<-', [argumentInCall('simple-3:1-3:6-0'), argumentInCall('simple-3:1-3:6-1')], { returns: ['simple-3:1-3:6-0'], reads: [BuiltIn], environment: defaultEnv().defineVariable('N', '4', '6') }) + .addControlDependency('simple-3:1-3:6-2', '10') .call('14', 'cat', [argumentInCall('12')], { returns: [], reads: [BuiltIn], environment: defaultEnv().defineVariable('N', 'simple-3:1-3:6-0', 'simple-3:1-3:6-2') }) .constant('1') .constant('simple-1:1-1:6-1') .defineVariable('simple-1:1-1:6-0', 'N', { definedBy: ['simple-1:1-1:6-1', 'simple-1:1-1:6-2'] }) + .addControlDependency('simple-1:1-1:6-0', '3') .constant('5') .defineVariable('4', 'N', { definedBy: ['5', '6'] }) .constant('8') .constant('simple-3:1-3:6-1') .defineVariable('simple-3:1-3:6-0', 'N', { definedBy: ['simple-3:1-3:6-1', 'simple-3:1-3:6-2'] }) + .addControlDependency('simple-3:1-3:6-0', '10') ) assertDataflow(label('conditional', ['if', 'name-normal', 'sourcing-external-files', 'unnamed-arguments', 'strings']), shell, 'if (x) { source("simple") }\ncat(N)', emptyGraph() @@ -55,18 +61,21 @@ describe('source', withShell(shell => { .reads('10', 'simple-1:10-1:15-0') .call('6', 'source', [argumentInCall('4')], { returns: [], reads: [BuiltIn], controlDependencies: [{ id: '8', when: true }] }) .call('simple-1:10-1:15-2', '<-', [argumentInCall('simple-1:10-1:15-0'), argumentInCall('simple-1:10-1:15-1')], { returns: ['simple-1:10-1:15-0'], reads: [BuiltIn] }) + .addControlDependency('simple-1:10-1:15-2', '6') .call('7', '{', [argumentInCall('6')], { returns: ['6'], reads: [BuiltIn], controlDependencies: [{ id: '8', when: true }] }) .call('8', 'if', [argumentInCall('0'), argumentInCall('7'), EmptyArgument], { returns: ['7'], reads: ['0', BuiltIn], onlyBuiltIn: true }) .call('12', 'cat', [argumentInCall('10')], { returns: [], reads: [BuiltIn], environment: defaultEnv().defineVariable('N', 'simple-1:10-1:15-0', 'simple-1:10-1:15-2') }) .constant('4') .constant('simple-1:10-1:15-1') .defineVariable('simple-1:10-1:15-0', 'N', { definedBy: ['simple-1:10-1:15-1', 'simple-1:10-1:15-2'] }) + .addControlDependency('simple-1:10-1:15-0', '6') ) // missing sources should just be ignored assertDataflow(label('missing source', ['unnamed-arguments', 'strings', 'sourcing-external-files']), shell, 'source("missing")', emptyGraph() .call('3', 'source', [argumentInCall('1')], { returns: [], reads: [BuiltIn] }) .constant('1') + .markIdForUnknownSideEffects('3') ) assertDataflow(label('recursive source', ['name-normal', ...OperatorDatabase['<-'].capabilities, 'numbers', 'unnamed-arguments', 'strings', 'sourcing-external-files', 'newlines']), shell, sources.recursive1, emptyGraph() @@ -75,11 +84,14 @@ describe('source', withShell(shell => { .call('2', '<-', [argumentInCall('0'), argumentInCall('1')], { returns: ['0'], reads: [BuiltIn] }) .call('6', 'source', [argumentInCall('4')], { returns: [], reads: [BuiltIn], environment: defaultEnv().defineVariable('x', '0', '2') }) .call('recursive2-2:1-2:6-3', 'cat', [argumentInCall('recursive2-2:1-2:6-1')], { returns: [], reads: [BuiltIn], environment: defaultEnv().defineVariable('x', '0', '2') }) + .addControlDependency('recursive2-2:1-2:6-3', '6') .call('recursive2-2:1-2:6-7', 'source', [argumentInCall('recursive2-2:1-2:6-5')], { returns: [], reads: [BuiltIn], environment: defaultEnv().defineVariable('x', '0', '2') }) .constant('1') .defineVariable('0', 'x', { definedBy: ['1', '2'] }) .constant('4') .constant('recursive2-2:1-2:6-5') + /* indicate recursion */ + .markIdForUnknownSideEffects('recursive2-2:1-2:6-7') ) // we currently don't support (and ignore) source calls with non-constant arguments! @@ -90,6 +102,7 @@ describe('source', withShell(shell => { .call('6', 'source', [argumentInCall('4')], { returns: [], reads: [BuiltIn], environment: defaultEnv().defineVariable('x', '0', '2') }) .constant('1') .defineVariable('0', 'x', { definedBy: ['1', '2'] }) + .markIdForUnknownSideEffects('6') ) assertDataflow(label('sourcing a closure', ['name-normal', ...OperatorDatabase['<-'].capabilities, 'sourcing-external-files', 'newlines', 'normal-definition', 'implicit-return', 'closures', 'numbers']), @@ -97,6 +110,7 @@ describe('source', withShell(shell => { .call('3', 'source', [argumentInCall('1')], { returns: [], reads: [BuiltIn] }) .argument('3', '1') .call('closure1-1:1-1:6-8', '<-', [argumentInCall('closure1-1:1-1:6-0'), argumentInCall('closure1-1:1-1:6-7')], { returns: ['closure1-1:1-1:6-0'], reads: [BuiltIn] }) + .addControlDependency('closure1-1:1-1:6-8', '3') .argument('closure1-1:1-1:6-8', ['closure1-1:1-1:6-7', 'closure1-1:1-1:6-0']) .call('6', 'f', [], { returns: ['closure1-1:1-1:6-5'], reads: ['closure1-1:1-1:6-0'], environment: defaultEnv().defineFunction('f', 'closure1-1:1-1:6-0', 'closure1-1:1-1:6-8') }) .calls('6', 'closure1-1:1-1:6-7') @@ -126,6 +140,7 @@ describe('source', withShell(shell => { environment: defaultEnv().pushEnv() }) .defineVariable('closure1-1:1-1:6-0', 'f', { definedBy: ['closure1-1:1-1:6-7', 'closure1-1:1-1:6-8'] }) + .addControlDependency('closure1-1:1-1:6-0', '3') .defineVariable('4', 'g', { definedBy: ['6', '7'] })) assertDataflow(label('sourcing a closure w/ side effects', ['name-normal', ...OperatorDatabase['<-'].capabilities, 'sourcing-external-files', 'newlines', 'normal-definition', 'implicit-return', 'closures', 'numbers', ...OperatorDatabase['<<-'].capabilities]), shell, 'x <- 2\nsource("closure2")\nf()\nprint(x)', emptyGraph() @@ -138,6 +153,7 @@ describe('source', withShell(shell => { .call('closure2-2:1-2:6-5', '<<-', [argumentInCall('closure2-2:1-2:6-3'), argumentInCall('closure2-2:1-2:6-4')], { returns: ['closure2-2:1-2:6-3'], reads: [BuiltIn], environment: defaultEnv().pushEnv() }, false) .argument('closure2-2:1-2:6-5', ['closure2-2:1-2:6-4', 'closure2-2:1-2:6-3']) .call('closure2-2:1-2:6-8', '<-', [argumentInCall('closure2-2:1-2:6-0'), argumentInCall('closure2-2:1-2:6-7')], { returns: ['closure2-2:1-2:6-0'], reads: [BuiltIn], environment: defaultEnv().defineVariable('x', '0', '2') }) + .addControlDependency('closure2-2:1-2:6-8', '6') .argument('closure2-2:1-2:6-8', ['closure2-2:1-2:6-7', 'closure2-2:1-2:6-0']) .call('8', 'f', [], { returns: ['closure2-2:1-2:6-5'], reads: ['closure2-2:1-2:6-0'], environment: defaultEnv().defineVariable('x', '0', '2').defineFunction('f', 'closure2-2:1-2:6-0', 'closure2-2:1-2:6-8') }) .calls('8', 'closure2-2:1-2:6-7') @@ -157,5 +173,7 @@ describe('source', withShell(shell => { graph: new Set(['closure2-2:1-2:6-4', 'closure2-2:1-2:6-3', 'closure2-2:1-2:6-5']), environment: defaultEnv().defineVariable('x', 'closure2-2:1-2:6-3', 'closure2-2:1-2:6-5').pushEnv() }, { environment: defaultEnv().defineVariable('x', 'closure2-2:1-2:6-3', 'closure2-2:1-2:6-5') }) - .defineVariable('closure2-2:1-2:6-0', 'f', { definedBy: ['closure2-2:1-2:6-7', 'closure2-2:1-2:6-8'] })) + .defineVariable('closure2-2:1-2:6-0', 'f', { definedBy: ['closure2-2:1-2:6-7', 'closure2-2:1-2:6-8'] }) + .addControlDependency('closure2-2:1-2:6-0', '6') + ) })) diff --git a/test/functionality/dataflow/processing-of-elements/multiple-files/simple-definitions-tests.ts b/test/functionality/dataflow/processing-of-elements/multiple-files/simple-definitions-tests.ts index 1039f9d46a..4a8e92ac4e 100644 --- a/test/functionality/dataflow/processing-of-elements/multiple-files/simple-definitions-tests.ts +++ b/test/functionality/dataflow/processing-of-elements/multiple-files/simple-definitions-tests.ts @@ -19,15 +19,18 @@ describe('Simple Defs in Multiple Files', withShell(shell => { .call('2', '<-', [argumentInCall('0'), argumentInCall('1')], { returns: ['0'], reads: [], onlyBuiltIn: true }) .argument('2', ['1', '0']) .call('-inline-@root-1-2', '<-', [argumentInCall('-inline-@root-1-0'), argumentInCall('-inline-@root-1-1')], { returns: ['-inline-@root-1-0'], reads: [], onlyBuiltIn: true }) + .addControlDependency('-inline-@root-1-2', 'root-1') .argument('-inline-@root-1-2', ['-inline-@root-1-1', '-inline-@root-1-0']) .argument('-inline-@root-2-3', '-inline-@root-2-1') .argument('-inline-@root-2-3', '-inline-@root-2-2') .call('-inline-@root-2-3', '+', [argumentInCall('-inline-@root-2-1'), argumentInCall('-inline-@root-2-2')], { returns: [], reads: ['-inline-@root-2-1', '-inline-@root-2-2'], onlyBuiltIn: true }) .argument('-inline-@root-2-5', '-inline-@root-2-3') .call('-inline-@root-2-5', 'print', [argumentInCall('-inline-@root-2-3')], { returns: ['-inline-@root-2-3'], reads: [], onlyBuiltIn: true }) + .addControlDependency('-inline-@root-2-5', 'root-2') .constant('1') .defineVariable('0', 'x', { definedBy: ['1', '2'] }) .constant('-inline-@root-1-1') .defineVariable('-inline-@root-1-0', 'y', { definedBy: ['-inline-@root-1-1', '-inline-@root-1-2'] }) + .addControlDependency('-inline-@root-1-0', 'root-1') ) })) diff --git a/test/functionality/slicing/static-program-slices/load-tests.ts b/test/functionality/slicing/static-program-slices/load-tests.ts new file mode 100644 index 0000000000..7ad48bea7a --- /dev/null +++ b/test/functionality/slicing/static-program-slices/load-tests.ts @@ -0,0 +1,12 @@ +import { assertSliced, withShell } from '../../_helper/shell' +import { label } from '../../_helper/label' +import { OperatorDatabase } from '../../../../src/r-bridge/lang-4.x/ast/model/operators' + +describe('load', withShell(shell => { + /* in this case, we assume that it may have an impact! */ + assertSliced(label('simple loading', ['name-normal', ...OperatorDatabase['<-'].capabilities, 'numbers', 'unnamed-arguments', 'strings', 'built-in', 'newlines']), + shell, 'load("x.RData")\ncat(N)', ['2@N'], 'load("x.RData")\nN') + /* currently we cannot narrow this down */ + assertSliced(label('multiple loads', ['name-normal', ...OperatorDatabase['<-'].capabilities, 'numbers', 'unnamed-arguments', 'strings', 'built-in', 'newlines']), + shell, 'load("z.RData")\nload("x.RData")\ncat(N)\nload("y.RData")', ['3@cat'], 'load("z.RData")\nload("x.RData")\ncat(N)\nload("y.RData")') +})) diff --git a/test/functionality/slicing/static-program-slices/source-tests.ts b/test/functionality/slicing/static-program-slices/source-tests.ts index 6ba41b8c6f..9f9067bc5a 100644 --- a/test/functionality/slicing/static-program-slices/source-tests.ts +++ b/test/functionality/slicing/static-program-slices/source-tests.ts @@ -13,11 +13,16 @@ describe('source', withShell(shell => { before(() => setSourceProvider(requestProviderFromText(sources))) after(() => setSourceProvider(requestProviderFromFile())) - // these are incorrect - where is the content from the sourced file? (see https://github.com/flowr-analysis/flowr/issues/822) assertSliced(label('simple source', ['name-normal', ...OperatorDatabase['<-'].capabilities, 'numbers', 'unnamed-arguments', 'strings', 'sourcing-external-files','newlines']), - shell, 'source("simple")\ncat(N)', ['2@N'], 'N') + shell, 'source("simple")\ncat(N)', ['2@N'], 'source("simple")\nN') + assertSliced(label('do not always include source', ['name-normal', ...OperatorDatabase['<-'].capabilities, 'numbers', 'unnamed-arguments', 'strings', 'sourcing-external-files','newlines']), + shell, 'source("simple")\ncat(N)\nx <- 3', ['3@x'], 'x <- 3') assertSliced(label('sourcing a closure', ['name-normal', ...OperatorDatabase['<-'].capabilities, 'sourcing-external-files', 'newlines', 'normal-definition', 'implicit-return', 'closures', 'numbers']), - shell, 'source("closure1")\ng <- f()\nprint(g())', ['3@g'], 'g <- f()\ng()') + shell, 'source("closure1")\ng <- f()\nprint(g())', ['3@g'], 'source("closure1")\ng <- f()\ng()') assertSliced(label('sourcing a closure w/ side effects', ['name-normal', ...OperatorDatabase['<-'].capabilities, 'sourcing-external-files', 'newlines', 'normal-definition', 'implicit-return', 'closures', 'numbers', ...OperatorDatabase['<<-'].capabilities]), - shell, 'x <- 2\nsource("closure2")\nf()\nprint(x)', ['4@x'], 'f()\nx') + shell, 'x <- 2\nsource("closure2")\nf()\nprint(x)', ['4@x'], 'source("closure2")\nf()\nx') + assertSliced(label('multiple sources', ['name-normal', ...OperatorDatabase['<-'].capabilities, 'numbers', 'unnamed-arguments', 'strings', 'sourcing-external-files','newlines']), + shell, 'source("simple")\nsource("closure1")\ncat(N + f())', ['3@cat'], 'source("simple")\nsource("closure1")\ncat(N + f())') + assertSliced(label('Include unresolved sources', ['name-normal', ...OperatorDatabase['<-'].capabilities, 'numbers', 'unnamed-arguments', 'strings', 'sourcing-external-files','newlines']), + shell, 'source("unknown")\ncat(N)', ['2@cat'], 'source("unknown")\ncat(N)') }))