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: improved control dependencies #818

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion src/dataflow/environments/built-in.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,8 @@ registerBuiltInFunctions(['delayedAssign'], processAssignment,
registerBuiltInFunctions(['<<-'], processAssignment, { superAssignment: true, canBeReplacement: true } )
registerBuiltInFunctions(['->'], processAssignment, { swapSourceAndTarget: true, canBeReplacement: true } )
registerBuiltInFunctions(['->>'], processAssignment, { superAssignment: true, swapSourceAndTarget: true, canBeReplacement: true } )
registerBuiltInFunctions(['&&', '||', '&', '|'], processSpecialBinOp, { lazy: true } )
registerBuiltInFunctions(['&&', '&'], processSpecialBinOp, { lazy: true, evalRhsWhen: true } )
registerBuiltInFunctions(['||', '|'], processSpecialBinOp, { lazy: true, evalRhsWhen: false } )
registerBuiltInFunctions(['|>', '%>%'], processPipe, {} )
registerBuiltInFunctions(['function', '\\'], processFunctionDefinition, {} )
registerBuiltInFunctions(['quote', 'substitute', 'bquote'], processQuote, { quoteArgumentsWithIndex: 0 } )
Expand Down
10 changes: 3 additions & 7 deletions src/dataflow/environments/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { setDifference } from '../../util/diff'
import type { IEnvironment, REnvironmentInformation } from './environment'
import { jsonReplacer } from '../../util/json'
import type { IdentifierReference } from './identifier'
import { arrayEqual } from '../../util/arrays'
import { diffControlDependencies } from '../info'

export function diffIdentifierReferences<Report extends WriteableDifferenceReport>(a: IdentifierReference | undefined, b: IdentifierReference | undefined, info: GenericDifferenceInformation<Report>): void {
if(a === undefined || b === undefined) {
Expand All @@ -18,9 +18,7 @@ export function diffIdentifierReferences<Report extends WriteableDifferenceRepor
if(a.nodeId !== b.nodeId) {
info.report.addComment(`${info.position}Different nodeIds: ${info.leftname}: ${a.nodeId} vs. ${info.rightname}: ${b.nodeId}`)
}
if(!arrayEqual(a.controlDependencies, b.controlDependencies)) {
info.report.addComment(`${info.position}Different control dependency: ${info.leftname}: ${JSON.stringify(a.controlDependencies)} vs. ${info.rightname}: ${JSON.stringify(b.controlDependencies)}`)
}
diffControlDependencies(a.controlDependencies, b.controlDependencies, info)
}

function diffMemory<Report extends WriteableDifferenceReport>(a: IEnvironment, b: IEnvironment, info: GenericDifferenceInformation<Report>) {
Expand All @@ -44,9 +42,7 @@ function diffMemory<Report extends WriteableDifferenceReport>(a: IEnvironment, b
if(aVal.nodeId !== bVal.nodeId) {
info.report.addComment(`${info.position}Different ids for ${key}. ${info.leftname}: ${aVal.nodeId} vs. ${info.rightname}: ${bVal.nodeId}`)
}
if(!arrayEqual(aVal.controlDependencies, bVal.controlDependencies)) {
info.report.addComment(`${info.position}Different controlDependency for ${key} (${aVal.nodeId}). ${info.leftname}: ${JSON.stringify(aVal.controlDependencies)} vs. ${info.rightname}: ${JSON.stringify(bVal.controlDependencies)}`)
}
diffControlDependencies(aVal.controlDependencies, bVal.controlDependencies, { ...info, position: `${info.position} For ${key}. ` })
if(aVal.definedAt !== bVal.definedAt) {
info.report.addComment(`${info.position}Different definition ids (definedAt) for ${key} (${aVal.nodeId}). ${info.leftname}: ${aVal.definedAt} vs. ${info.rightname}: ${bVal.definedAt}`)
}
Expand Down
8 changes: 4 additions & 4 deletions src/dataflow/environments/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ import type { Identifier, IdentifierDefinition, IdentifierReference } from './id
import { BuiltInMemory } from './built-in'
import type { DataflowGraph } from '../graph/graph'
import { resolveByName } from './resolve-by-name'
import type { NodeId } from '../../r-bridge/lang-4.x/ast/model/processing/node-id'
import type { ControlDependency } from '../info'


export function makeReferenceMaybe(ref: IdentifierReference, graph: DataflowGraph, environments: REnvironmentInformation, includeDefs: boolean, defaultCd: NodeId | undefined = undefined): IdentifierReference {
export function makeReferenceMaybe(ref: IdentifierReference, graph: DataflowGraph, environments: REnvironmentInformation, includeDefs: boolean, defaultCd: ControlDependency | undefined = undefined): IdentifierReference {
const node = graph.get(ref.nodeId, true)
if(includeDefs) {
const definitions = ref.name ? resolveByName(ref.name, environments) : undefined
for(const definition of definitions ?? []) {
if(definition.kind !== 'built-in-function' && definition.kind !== 'built-in-value') {
if(definition.controlDependencies && defaultCd && !definition.controlDependencies.includes(defaultCd)) {
if(definition.controlDependencies && defaultCd && !definition.controlDependencies.find(c => c.id === defaultCd.id)) {
definition.controlDependencies.push(defaultCd)
} else {
definition.controlDependencies = defaultCd ? [defaultCd] : []
Expand All @@ -35,7 +35,7 @@ export function makeReferenceMaybe(ref: IdentifierReference, graph: DataflowGrap
return { ...ref, controlDependencies: [...ref.controlDependencies ?? [], ...(defaultCd ? [defaultCd]: []) ] }
}

export function makeAllMaybe(references: readonly IdentifierReference[] | undefined, graph: DataflowGraph, environments: REnvironmentInformation, includeDefs: boolean, defaultCd: NodeId | undefined = undefined): IdentifierReference[] {
export function makeAllMaybe(references: readonly IdentifierReference[] | undefined, graph: DataflowGraph, environments: REnvironmentInformation, includeDefs: boolean, defaultCd: ControlDependency | undefined = undefined): IdentifierReference[] {
if(references === undefined) {
return []
}
Expand Down
3 changes: 2 additions & 1 deletion src/dataflow/environments/identifier.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { BuiltInIdentifierConstant, BuiltInIdentifierDefinition } from './built-in'
import type { NodeId } from '../../r-bridge/lang-4.x/ast/model/processing/node-id'
import type { ControlDependency } from '../info'

export type Identifier = string & { __brand?: 'identifier' }

Expand Down Expand Up @@ -28,5 +29,5 @@ export interface IdentifierReference {
* If the reference is only effective if, e.g. an if-then-else condition is true, this references the root of the `if`.
* As a hackey intermediate solution (until we have pointer-analysis), an empty array may indicate a `maybe` which is due to pointer access (e.g., in `a[x] <- 3`).
*/
controlDependencies: NodeId[] | undefined
controlDependencies: ControlDependency[] | undefined
}
15 changes: 3 additions & 12 deletions src/dataflow/graph/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { recoverName } from '../../r-bridge/lang-4.x/ast/model/processing/node-i
import type { IdentifierReference } from '../environments/identifier'
import { diffEnvironmentInformation, diffIdentifierReferences } from '../environments/diff'
import { EmptyArgument } from '../../r-bridge/lang-4.x/ast/model/nodes/r-function-call'
import { diffControlDependencies } from '../info'

interface ProblematicVertex {
tag: 'vertex',
Expand Down Expand Up @@ -183,12 +184,7 @@ export function diffFunctionArguments(fn: NodeId, a: false | readonly FunctionAr
if(aArg.name !== bArg.name) {
ctx.report.addComment(`${ctx.position}In argument #${i} (of ${ctx.leftname}, unnamed) the name differs: ${aArg.name} vs ${bArg.name}.`)
}
if(!arrayEqual(aArg.controlDependencies, bArg.controlDependencies)) {
ctx.report.addComment(
`${ctx.position}In argument #${i} (of ${ctx.leftname}, unnamed) the control dependency differs: ${JSON.stringify(aArg.controlDependencies)} vs ${JSON.stringify(bArg.controlDependencies)}.`,
{ tag: 'vertex', id: fn }
)
}
diffControlDependencies(aArg.controlDependencies, bArg.controlDependencies, { ...ctx, position: `${ctx.position}In argument #${i} (of ${ctx.leftname}, unnamed) the control dependency differs: ${JSON.stringify(aArg.controlDependencies)} vs ${JSON.stringify(bArg.controlDependencies)}.` })
}
}
}
Expand Down Expand Up @@ -224,12 +220,7 @@ export function diffVertices(ctx: DataflowDiffContext): void {
})
}
}
if(!arrayEqual(lInfo.controlDependencies, rInfo.controlDependencies)) {
ctx.report.addComment(
`Vertex ${id} differs in controlDependency. ${ctx.leftname}: ${JSON.stringify(lInfo.controlDependencies)} vs ${ctx.rightname}: ${JSON.stringify(rInfo.controlDependencies)}`,
{ tag: 'vertex', id }
)
}
diffControlDependencies(lInfo.controlDependencies, rInfo.controlDependencies, { ...ctx, position: `Vertex ${id} differs in controlDependency. ` })

diffEnvironmentInformation(lInfo.environment, rInfo.environment, { ...ctx, position: `${ctx.position}Vertex ${id} differs in environment. ` })

Expand Down
10 changes: 3 additions & 7 deletions src/dataflow/graph/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,8 @@ export class DataflowGraph<Vertex extends DataflowGraphVertexInfo = DataflowGrap
// keep a clone of the original environment
const environment = vertex.environment === undefined ? fallback : cloneEnvironmentInformation(vertex.environment)



this.vertexInformation.set(vertex.id, {
...vertex,
when: vertex.controlDependencies ?? 'always',
Expand All @@ -238,7 +238,7 @@ export class DataflowGraph<Vertex extends DataflowGraphVertexInfo = DataflowGrap
public addEdge(from: NodeId | ReferenceForEdge, to: NodeId | ReferenceForEdge, edgeInfo: EdgeData<Edge>): this {
const { fromId, toId } = extractEdgeIds(from, to)
const { type, ...rest } = edgeInfo

if(fromId === toId) {
return this
}
Expand Down Expand Up @@ -343,10 +343,6 @@ export class DataflowGraph<Vertex extends DataflowGraphVertexInfo = DataflowGrap
const vertex = this.getVertex(reference.nodeId, true)
guard(vertex !== undefined, () => `node must be defined for ${JSON.stringify(reference)} to set reference`)
if(vertex.tag === VertexType.FunctionDefinition || vertex.tag === VertexType.VariableDefinition) {
guard(vertex.controlDependencies !== undefined
|| reference.controlDependencies !== undefined
|| arrayEqual(vertex.controlDependencies, reference.controlDependencies),
() => `node ${JSON.stringify(vertex)} must not be previously defined at position or have same scope for ${JSON.stringify(reference)}`)
vertex.controlDependencies = reference.controlDependencies
} else {
this.vertexInformation.set(reference.nodeId, { ...vertex, tag: 'variable-definition' })
Expand Down
3 changes: 2 additions & 1 deletion src/dataflow/graph/vertex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { MergeableRecord } from '../../util/objects'
import type { DataflowFunctionFlowInformation, FunctionArgument } from './graph'
import type { NodeId } from '../../r-bridge/lang-4.x/ast/model/processing/node-id'
import type { REnvironmentInformation } from '../environments/environment'
import type { ControlDependency } from '../info'

export type DataflowGraphVertices<Vertex extends DataflowGraphVertexInfo = DataflowGraphVertexInfo> = Map<NodeId, Vertex>

Expand Down Expand Up @@ -37,7 +38,7 @@ interface DataflowGraphVertexBase extends MergeableRecord {
/**
* See {@link IdentifierReference}
*/
controlDependencies: NodeId[] | undefined
controlDependencies: ControlDependency[] | undefined
}

export interface DataflowGraphValue extends DataflowGraphVertexBase {
Expand Down
67 changes: 65 additions & 2 deletions src/dataflow/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { NodeId } from '../r-bridge/lang-4.x/ast/model/processing/node-id'
import type { IdentifierReference } from './environments/identifier'
import type { REnvironmentInformation } from './environments/environment'
import { DataflowGraph } from './graph/graph'
import type { GenericDifferenceInformation, WriteableDifferenceReport } from '../util/diff'

export const enum ExitPointType {
Default = 0,
Expand All @@ -11,10 +12,17 @@ export const enum ExitPointType {
Next = 3
}

export interface ControlDependency {
readonly id: NodeId,
/** when does this control dependency trigger (if the condition is true or false)? */
readonly when?: boolean
}


export interface ExitPoint {
readonly type: ExitPointType,
readonly nodeId: NodeId,
readonly controlDependencies: NodeId[] | undefined
readonly controlDependencies: ControlDependency[] | undefined
}

export function addNonDefaultExitPoints(existing: ExitPoint[], add: readonly ExitPoint[]): void {
Expand Down Expand Up @@ -64,10 +72,65 @@ export function initializeCleanDataflowInformation<T>(entryPoint: NodeId, data:
}
}

export function happensInEveryBranch(controlDependencies: ControlDependency[] | undefined): boolean {
if(controlDependencies === undefined) {
/* the cds are unconstrained */
return true
} else if(controlDependencies.length === 0) {
/* this happens only when we have no idea and require more analysis */
return false
}

const trues = []
const falseSet = new Set()

for(const { id, when } of controlDependencies) {
if(when) {
trues.push(id)
} else {
falseSet.add(id)
}
}

return trues.every(id => falseSet.has(id))
}

export function alwaysExits(data: DataflowInformation): boolean {
return data.exitPoints?.some(e => e.type !== ExitPointType.Default && e.controlDependencies === undefined) ?? false
return data.exitPoints?.some(
e => e.type !== ExitPointType.Default && happensInEveryBranch(e.controlDependencies)
) ?? false
}

export function filterOutLoopExitPoints(exitPoints: readonly ExitPoint[]): readonly ExitPoint[] {
return exitPoints.filter(({ type }) => type === ExitPointType.Return || type === ExitPointType.Default)
}

export function diffControlDependency<Report extends WriteableDifferenceReport>(a: ControlDependency | undefined, b: ControlDependency | undefined, info: GenericDifferenceInformation<Report>): void {
if(a === undefined || b === undefined) {
if(a !== b) {
info.report.addComment(`${info.position}Different control dependencies. ${info.leftname}: ${JSON.stringify(a)} vs. ${info.rightname}: ${JSON.stringify(b)}`)
}
return
}
if(a.id !== b.id) {
info.report.addComment(`${info.position}Different control dependency ids. ${info.leftname}: ${a.id} vs. ${info.rightname}: ${b.id}`)
}
if(a.when !== b.when) {
info.report.addComment(`${info.position}Different control dependency when. ${info.leftname}: ${a.when} vs. ${info.rightname}: ${b.when}`)
}
}

export function diffControlDependencies<Report extends WriteableDifferenceReport>(a: ControlDependency[] | undefined, b: ControlDependency[] | undefined, info: GenericDifferenceInformation<Report>): void {
if(a === undefined || b === undefined) {
if(a !== b) {
info.report.addComment(`${info.position}Different control dependencies: ${JSON.stringify(a)} vs. ${JSON.stringify(b)}`)
}
return
}
if(a.length !== b.length) {
info.report.addComment(`${info.position}Different control dependency lengths: ${a.length} vs. ${b.length}`)
}
for(let i = 0; i < a.length; ++i) {
diffControlDependency(a[i], b[i], { ...info, position: `${info.position}Control dependency at index: ${i}: ` })
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function processForLoop<OtherInfo>(
// this should not be able to exit always!

const originalDependency = data.controlDependencies
data = { ...data, controlDependencies: [...data.controlDependencies ?? [], name.info.id] }
data = { ...data, controlDependencies: [...data.controlDependencies ?? [], { id: name.info.id, when: true }] }

let headEnvironments = overwriteEnvironment(vector.environment, variable.environment)
const headGraph = variable.graph.mergeWith(vector.graph)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,22 +79,24 @@ export function processIfThenElse<OtherInfo>(
// if there is no "else" case, we have to recover whatever we had before as it may be not executed
const finalEnvironment = appendEnvironment(thenEnvironment, otherwise ? otherwise.environment : cond.environment)

const cdTrue = { id: rootId, when: true }
const cdFalse = { id: rootId, when: false }
// again within an if-then-else we consider all actives to be read
const ingoing: IdentifierReference[] = [
...cond.in,
...(makeThenMaybe ? makeAllMaybe(then?.in, nextGraph, finalEnvironment, false, rootId) : then?.in ?? []),
...(makeOtherwiseMaybe ? makeAllMaybe(otherwise?.in, nextGraph, finalEnvironment, false, rootId) : otherwise?.in ?? []),
...(makeThenMaybe ? makeAllMaybe(then?.in, nextGraph, finalEnvironment, false, cdTrue) : then?.in ?? []),
...(makeOtherwiseMaybe ? makeAllMaybe(otherwise?.in, nextGraph, finalEnvironment, false, cdFalse) : otherwise?.in ?? []),
...cond.unknownReferences,
...(makeThenMaybe ? makeAllMaybe(then?.unknownReferences, nextGraph, finalEnvironment, false, rootId) : then?.unknownReferences ?? []),
...(makeOtherwiseMaybe ? makeAllMaybe(otherwise?.unknownReferences, nextGraph, finalEnvironment, false, rootId) : otherwise?.unknownReferences ?? []),
...(makeThenMaybe ? makeAllMaybe(then?.unknownReferences, nextGraph, finalEnvironment, false, cdTrue) : then?.unknownReferences ?? []),
...(makeOtherwiseMaybe ? makeAllMaybe(otherwise?.unknownReferences, nextGraph, finalEnvironment, false, cdFalse) : otherwise?.unknownReferences ?? []),
]

// we assign all with a maybe marker
// we do not merge even if they appear in both branches because the maybe links will refer to different ids
const outgoing = [
...cond.out,
...(makeThenMaybe ? makeAllMaybe(then?.out, nextGraph, finalEnvironment, true, rootId) : then?.out ?? []),
...(makeOtherwiseMaybe ? makeAllMaybe(otherwise?.out, nextGraph, finalEnvironment, true, rootId) : otherwise?.out ?? []),
...(makeThenMaybe ? makeAllMaybe(then?.out, nextGraph, finalEnvironment, true, cdTrue) : then?.out ?? []),
...(makeOtherwiseMaybe ? makeAllMaybe(otherwise?.out, nextGraph, finalEnvironment, true, cdFalse) : otherwise?.out ?? []),
]

patchFunctionCall({
Expand All @@ -109,8 +111,8 @@ export function processIfThenElse<OtherInfo>(
nextGraph.addEdge(name.info.id, cond.entryPoint, { type: EdgeType.Reads })

const exitPoints = [
...(then?.exitPoints ?? []).map(e => ({ ...e, controlDependencies: makeThenMaybe ? [...data.controlDependencies ?? []] : e.controlDependencies })),
...(otherwise?.exitPoints ?? []).map(e => ({ ...e, controlDependencies: makeOtherwiseMaybe ? [...data.controlDependencies ?? []] : e.controlDependencies }))
...(then?.exitPoints ?? []).map(e => ({ ...e, controlDependencies: makeThenMaybe ? [...data.controlDependencies ?? [], { id: rootId, when: true }] : e.controlDependencies })),
...(otherwise?.exitPoints ?? []).map(e => ({ ...e, controlDependencies: makeOtherwiseMaybe ? [...data.controlDependencies ?? [], { id: rootId, when: false }] : e.controlDependencies }))
]

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function processSpecialBinOp<OtherInfo>(
args: readonly RFunctionArgument<OtherInfo & ParentInformation>[],
rootId: NodeId,
data: DataflowProcessorInformation<OtherInfo & ParentInformation>,
config: { lazy: boolean }
config: { lazy: boolean, evalRhsWhen: boolean }
): DataflowInformation {
if(!config.lazy) {
return processKnownFunctionCall({ name, args, rootId, data }).information
Expand All @@ -26,8 +26,7 @@ export function processSpecialBinOp<OtherInfo>(
const { information, processedArguments } = processKnownFunctionCall({ name, args, rootId, data,
patchData: (d, i) => {
if(i === 1) {
// the rhs will be overshadowed by the lhs
return { ...d, controlDependencies: [...d.controlDependencies ?? [], name.info.id] }
return { ...d, controlDependencies: [...d.controlDependencies ?? [], { id: name.info.id, when: config.evalRhsWhen }] }
}
return d
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function processWhileLoop<OtherInfo>(
markAsNSE: [1],
patchData: (d, i) => {
if(i === 1) {
return { ...d, controlDependencies: [...d.controlDependencies ?? [], name.info.id] }
return { ...d, controlDependencies: [...d.controlDependencies ?? [], { id: name.info.id, when: true }] }
}
return d
} })
Expand Down
Loading
Loading