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

Basic Structure for the Abstract Interpretation #397

Merged
merged 51 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
8759589
feat: added an abstract interpretation (ai) step
LukasPietzschmann Oct 12, 2023
7c25938
feat-fix: forgot to add processor.ts
LukasPietzschmann Oct 12, 2023
a96f623
feat-fix: pass the correct arguments to the ai and slicing step
LukasPietzschmann Oct 12, 2023
954b547
feat: added the normalized ast as a parameter to the ai step
LukasPietzschmann Oct 12, 2023
87e328d
feat-fix: remove the explicitly included normalzsation step
LukasPietzschmann Oct 13, 2023
c3f0a99
refactor: remove normalized ast parameter from abstract interpretatio…
EagleoutIce Oct 13, 2023
2b1020c
git: Merge tag 'v1.2.0' into 378-basic-structure-for-the-abstract-int…
LukasPietzschmann Oct 16, 2023
91c029f
feat: added back the ast as an input for the ai processor
LukasPietzschmann Oct 16, 2023
2c6a555
Merge branch 'main' into 378-basic-structure-for-the-abstract-interpr…
LukasPietzschmann Oct 16, 2023
38077e8
feat, wip: borderline execution trace visitor for the cfg
EagleoutIce Nov 3, 2023
c75224d
feat, wip: only visit exit nodes after we have visited all of its suc…
EagleoutIce Nov 3, 2023
6739f2e
feat, wip: attributing vertex type in cfg
EagleoutIce Nov 3, 2023
71c4e2d
test-fix: adapting tests to new cfg vertex types
EagleoutIce Nov 3, 2023
5023dba
lint-fix: remove unused reference
EagleoutIce Nov 3, 2023
35b9620
Merge branch 'main' into 378-basic-structure-for-the-abstract-interpr…
EagleoutIce Nov 22, 2023
59561af
feat: Added basic structure for traversing the cfg
LukasPietzschmann Dec 6, 2023
1982f2e
feat: Added an interface specifying an interval
LukasPietzschmann Dec 7, 2023
934499a
feat: Narrowed down the node type of assignments
LukasPietzschmann Dec 7, 2023
14401a9
refactor: Intervals -> Domain
LukasPietzschmann Dec 7, 2023
302d19a
feat: If a dfg node reads from multiple sources, unify their domains
LukasPietzschmann Dec 7, 2023
2aab44a
refactor: fixed function names
LukasPietzschmann Dec 7, 2023
20fe13b
refactor: Refactored the unification of domains
LukasPietzschmann Dec 8, 2023
e397ecf
refactor: Domain is now a real class
LukasPietzschmann Dec 8, 2023
2929cae
refactor: Consistent naming scheme and no more .forEach
LukasPietzschmann Dec 8, 2023
7db9f76
feat-fix: don't use members before setting them lol
LukasPietzschmann Dec 9, 2023
7735735
feat: Exported functions, classes and interfaces
LukasPietzschmann Dec 9, 2023
aec40b9
test: Added AI test
LukasPietzschmann Dec 9, 2023
bfa5dca
feat-fix: Added additional flag for comparing intervals
LukasPietzschmann Dec 10, 2023
cc3bb71
refactor: replaced new Array() by the literal syntax []
LukasPietzschmann Dec 10, 2023
76a8242
test: Added tests for the unification of domains
LukasPietzschmann Dec 10, 2023
c3b19e4
refactor: Don't inherit Domain from a Set, make the intervals a member
LukasPietzschmann Dec 10, 2023
b316241
feat: Added interface AINode and a place to store them
LukasPietzschmann Jan 6, 2024
5002fdd
refactor: More sensible types and names
LukasPietzschmann Jan 6, 2024
6c8dd13
refactor: Merged BinOp and Assignment
LukasPietzschmann Jan 6, 2024
621ffef
feat: Added function for adding two domains
LukasPietzschmann Jan 6, 2024
1f439b0
refactor: Fixed formatting
LukasPietzschmann Jan 6, 2024
25ea6ec
feat: Implemented more handling code for bin ops
LukasPietzschmann Jan 6, 2024
b0db502
fix: Don't create Intervals using the literal syntax
LukasPietzschmann Jan 7, 2024
f187d45
fix: Typo
LukasPietzschmann Jan 8, 2024
3a73901
feat: We can now subtract one domain from another
LukasPietzschmann Jan 8, 2024
9b5e98e
refactor: Split into multiple files
LukasPietzschmann Jan 8, 2024
5ce85d9
feat: Added unifyIntervals as a helper
LukasPietzschmann Jan 8, 2024
5d57eef
feat: Check for interval overlaps, if an interval is added to a domain
LukasPietzschmann Jan 8, 2024
775863f
refactor: Code cleanup
LukasPietzschmann Jan 11, 2024
2d31117
refactor: Use the flowR logger
LukasPietzschmann Jan 11, 2024
e7a4e35
refactor: Add static factory methods to the Domain
LukasPietzschmann Jan 11, 2024
c57732e
refactor: Removed TODOs referencing more tests
LukasPietzschmann Jan 15, 2024
d9462d7
refactor: De-globalized the nodeMap
LukasPietzschmann Jan 15, 2024
af963b7
refactor: Replaced a guard with a warning
LukasPietzschmann Jan 22, 2024
7c68256
fix: Temporarily remove the call to `runAbstractInterpretation`
LukasPietzschmann Jan 23, 2024
dcf9eae
Merge branch 'main' into 378-basic-structure-for-the-abstract-interpr…
EagleoutIce Jan 26, 2024
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
169 changes: 169 additions & 0 deletions src/abstract-interpretation/domain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import {assertUnreachable, guard} from '../util/assert'

interface IntervalBound {
readonly value: number,
readonly inclusive: boolean
}

export class 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 {

Check warning on line 14 in src/abstract-interpretation/domain.ts

View check run for this annotation

Codecov / codecov/patch

src/abstract-interpretation/domain.ts#L14

Added line #L14 was not covered by tests
return `${this.min.inclusive ? '[' : '('}${this.min.value}, ${this.max.value}${this.max.inclusive ? ']' : ')'}`
}
}

export class Domain {
private readonly _intervals: Set<Interval>

private constructor(intervals: Interval[] = []) {
this._intervals = new Set(unifyOverlappingIntervals(intervals))
}

static bottom(): Domain {
return new Domain()
}

static fromIntervals(intervals: Interval[] | Set<Interval>): 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<Interval> {
return this._intervals
}

private set intervals(intervals: Interval[]) {
this._intervals.clear()
for(const interval of intervals) {
this._intervals.add(interval)

Check warning on line 48 in src/abstract-interpretation/domain.ts

View check run for this annotation

Codecov / codecov/patch

src/abstract-interpretation/domain.ts#L45-L48

Added lines #L45 - L48 were not covered by tests
}
}

addInterval(interval: Interval): void {
this.intervals = unifyOverlappingIntervals([...this.intervals, interval])

Check warning on line 53 in src/abstract-interpretation/domain.ts

View check run for this annotation

Codecov / codecov/patch

src/abstract-interpretation/domain.ts#L52-L53

Added lines #L52 - L53 were not covered by tests
}

toString(): string {
return `{${Array.from(this.intervals).join(', ')}}`

Check warning on line 57 in src/abstract-interpretation/domain.ts

View check run for this annotation

Codecov / codecov/patch

src/abstract-interpretation/domain.ts#L56-L57

Added lines #L56 - L57 were not covered by tests
}
}

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)

Check warning on line 81 in src/abstract-interpretation/domain.ts

View check run for this annotation

Codecov / codecov/patch

src/abstract-interpretation/domain.ts#L81

Added line #L81 was not covered by tests
}
}

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)
}

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
}

export function unifyDomains(domains: Domain[]): Domain {
const unifiedIntervals = unifyOverlappingIntervals(domains.flatMap(domain => Array.from(domain.intervals)))
return Domain.fromIntervals(unifiedIntervals)

Check warning on line 114 in src/abstract-interpretation/domain.ts

View check run for this annotation

Codecov / codecov/patch

src/abstract-interpretation/domain.ts#L113-L114

Added lines #L113 - L114 were not covered by tests
}

export function unifyOverlappingIntervals(intervals: 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)) {
LukasPietzschmann marked this conversation as resolved.
Show resolved Hide resolved
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
}

export function addDomains(domain1: Domain, domain2: Domain): Domain {
const intervals = new Set<Interval>()
for(const interval1 of domain1.intervals) {
LukasPietzschmann marked this conversation as resolved.
Show resolved Hide resolved
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)
}

export function subtractDomains(domain1: Domain, domain2: Domain): Domain {
const intervals = new Set<Interval>()
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)
}
42 changes: 42 additions & 0 deletions src/abstract-interpretation/handler/binop/binop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {Handler} from '../handler'
import {aiLogger, AINode} from '../../processor'
import {BinaryOperatorFlavor, ParentInformation, RBinaryOp} from '../../../r-bridge'
import {guard} from '../../../util/assert'
import {operators} from './operators'

export type BinOpOperators = {
LukasPietzschmann marked this conversation as resolved.
Show resolved Hide resolved
[key in BinaryOperatorFlavor]: (lhs: AINode, rhs: AINode, node: RBinaryOp<ParentInformation>) => AINode
}

export class BinOp implements Handler<AINode> {
lhs: AINode | undefined
rhs: AINode | undefined

constructor(readonly node: RBinaryOp<ParentInformation>) {}

getName(): string {
return `Bin Op (${this.node.flavor})`
}

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}`)
return operators[this.node.flavor](this.lhs, this.rhs, this.node)
LukasPietzschmann marked this conversation as resolved.
Show resolved Hide resolved
}

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`)
}
}
}
40 changes: 40 additions & 0 deletions src/abstract-interpretation/handler/binop/operators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {guard} from '../../../util/assert'
import {BinOpOperators} from './binop'
import {addDomains, subtractDomains} from '../../domain'

export const operators: BinOpOperators = {
'assignment': (lhs, rhs, node) => {
return {
id: lhs.id,
domain: rhs.domain,
astNode: node.lhs,
}
},
'arithmetic': (lhs, rhs, node) => {
switch(node.operator) {
case '+':
return {
id: lhs.id,
domain: addDomains(lhs.domain, rhs.domain),
astNode: node,
}
case '-':
return {
id: lhs.id,
domain: subtractDomains(lhs.domain, rhs.domain),
astNode: node,
}
default:
guard(false, `Unknown binary operator ${node.operator}`)
}
},
'logical': () => {
guard(false, 'Not implemented yet')
},
'model formula': () => {
guard(false, 'Not implemented yet')
},
'comparison': () => {
guard(false, 'Not implemented yet')
},
}
6 changes: 6 additions & 0 deletions src/abstract-interpretation/handler/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface Handler<ValueType> {
getName: () => string,
enter: () => void
exit: () => ValueType
next: (value: ValueType) => void
}
83 changes: 83 additions & 0 deletions src/abstract-interpretation/processor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {DataflowInformation} from '../dataflow/internal/info'
import {NodeId, NormalizedAst, ParentInformation, RNodeWithParent, RType} from '../r-bridge'
import {CfgVertexType, extractCFG} from '../util/cfg/cfg'
import {visitCfg} from '../util/cfg/visitor'
import {guard} from '../util/assert'
import {DataflowGraphVertexInfo, EdgeType, OutgoingEdges} from '../dataflow'
import {Handler} from './handler/handler'
import {BinOp} from './handler/binop/binop'
import {Domain, unifyDomains} from './domain'
import {log} from '../util/log'

export const aiLogger = log.getSubLogger({name: 'abstract-interpretation'})

export interface AINode {
readonly id: NodeId
readonly domain: Domain
readonly astNode: RNodeWithParent<ParentInformation>
}

class Stack<ElementType> {
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<NodeId, AINode>): 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]) => edge.types.has(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<Handler<AINode>>()
const nodeMap = new Map<NodeId, AINode>()
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({
LukasPietzschmann marked this conversation as resolved.
Show resolved Hide resolved
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
}
2 changes: 2 additions & 0 deletions src/benchmark/slicer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export class BenchmarkSlicer {
private loadedXml: string | undefined
private tokenMap: Record<string, string> | undefined
private dataflow: DataflowInformation | undefined
private ai: DataflowInformation | undefined
private normalizedAst: NormalizedAst | undefined
private totalStopwatch: IStoppableStopwatch
private finished = false
Expand Down Expand Up @@ -133,6 +134,7 @@ export class BenchmarkSlicer {
this.loadedXml = await this.measureCommonStep('parse', 'retrieve AST from R code')
this.normalizedAst = await this.measureCommonStep('normalize', 'normalize R AST')
this.dataflow = await this.measureCommonStep('dataflow', 'produce dataflow information')
this.ai = await this.measureCommonStep('ai', 'run abstract interpretation')

this.stepper.switchToSliceStage()

Expand Down
2 changes: 1 addition & 1 deletion src/benchmark/stats/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { SingleSlicingCriterion, SlicingCriteria } from '../../slicing'
import { NodeId, RParseRequestFromFile, RParseRequestFromText } from '../../r-bridge'
import { ReconstructionResult } from '../../slicing'

export const CommonSlicerMeasurements = ['initialize R session', 'inject home path', 'ensure installation of xmlparsedata', 'retrieve token map', 'retrieve AST from R code', 'normalize R AST', 'produce dataflow information', 'close R session', 'total'] as const
export const CommonSlicerMeasurements = ['initialize R session', 'inject home path', 'ensure installation of xmlparsedata', 'retrieve token map', 'retrieve AST from R code', 'normalize R AST', 'produce dataflow information', 'run abstract interpretation', 'close R session', 'total'] as const
export type CommonSlicerMeasurements = typeof CommonSlicerMeasurements[number]

export const PerSliceMeasurements = ['static slicing', 'reconstruct code', 'total'] as const
Expand Down
2 changes: 1 addition & 1 deletion src/cli/repl/commands/cfg.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ReplCommand } from './main'
import { SteppingSlicer } from '../../../core'
import { requestFromInput, RShell } from '../../../r-bridge'
import { extractCFG } from '../../../util/cfg'
import { extractCFG } from '../../../util/cfg/cfg'
import { cfgToMermaid, cfgToMermaidUrl } from '../../../util/mermaid'

async function controlflow(shell: RShell, remainingLine: string) {
Expand Down
Loading
Loading