From 3efc72eaa8fb0830fe527dea096c0bdb2931ab5d Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Thu, 22 Feb 2024 14:28:06 +0100 Subject: [PATCH 01/70] feat-fix: some AINodes had the wrong id Pls Florian don't hate on me for not creating a separate issue --- src/abstract-interpretation/handler/binop/operators.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/abstract-interpretation/handler/binop/operators.ts b/src/abstract-interpretation/handler/binop/operators.ts index f603808b6b..0812aa18b1 100644 --- a/src/abstract-interpretation/handler/binop/operators.ts +++ b/src/abstract-interpretation/handler/binop/operators.ts @@ -14,13 +14,13 @@ export const operators: BinOpOperators = { switch(node.operator) { case '+': return { - id: lhs.id, + id: node.info.id, domain: addDomains(lhs.domain, rhs.domain), astNode: node, } case '-': return { - id: lhs.id, + id: node.info.id, domain: subtractDomains(lhs.domain, rhs.domain), astNode: node, } From 5b187bdfd854b692021896659bc77dfd7ca344ad Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Thu, 22 Feb 2024 15:55:02 +0100 Subject: [PATCH 02/70] feat: added conditional handler file --- .../handler/conditional/conditional.ts | 46 +++++++++++++++++++ src/abstract-interpretation/processor.ts | 3 ++ 2 files changed, 49 insertions(+) create mode 100644 src/abstract-interpretation/handler/conditional/conditional.ts diff --git a/src/abstract-interpretation/handler/conditional/conditional.ts b/src/abstract-interpretation/handler/conditional/conditional.ts new file mode 100644 index 0000000000..183e9f8cbd --- /dev/null +++ b/src/abstract-interpretation/handler/conditional/conditional.ts @@ -0,0 +1,46 @@ +import { ParentInformation, RIfThenElse } from '../../../r-bridge' +import { guard } from '../../../util/assert' +import { AINode, aiLogger } from '../../processor' +import { Handler } from '../handler' + +export class Conditional implements Handler { + condition: AINode | undefined + then: AINode | undefined + else: AINode | undefined + + constructor(readonly node: RIfThenElse) {} + + getName(): string { + return 'IfThenElse' + } + + enter(): void { + aiLogger.trace(`Entered ${this.getName()}`) + } + + exit(): AINode { + aiLogger.trace(`Exited ${this.getName()}`) + guard(this.condition !== undefined, `No condition found for conditional ${this.node.info.id}`) + guard(this.then !== undefined, `No then-branch found for conditional ${this.node.info.id}`) + guard(this.else !== undefined, `No else-branch found for conditional ${this.node.info.id}`) + return { + id: this.node.info.id, + domain: this.condition.domain, + astNode: this.node, + } + // guard(false, 'Not implemented yet') + } + + next(node: AINode): void { + aiLogger.trace(`${this.getName()} received`) + if(this.condition === undefined) { + this.condition = node + } else if(this.then === undefined) { + this.then = node + } else if(this.else === undefined) { + this.else = node + } else { + guard(false, `Conditional ${this.node.info.id} already has condition, then and else`) + } + } +} diff --git a/src/abstract-interpretation/processor.ts b/src/abstract-interpretation/processor.ts index 50b54a9b14..6f6e18c4bc 100644 --- a/src/abstract-interpretation/processor.ts +++ b/src/abstract-interpretation/processor.ts @@ -6,6 +6,7 @@ import {guard} from '../util/assert' import {DataflowGraphVertexInfo, EdgeType, OutgoingEdges} from '../dataflow' import {Handler} from './handler/handler' import {BinOp} from './handler/binop/binop' +import {Conditional} from './handler/conditional/conditional' import {Domain, unifyDomains} from './domain' import {log} from '../util/log' @@ -53,6 +54,8 @@ export function runAbstractInterpretation(ast: NormalizedAst, dfg: DataflowInfor const astNode = ast.idMap.get(node.id) if(astNode?.type === RType.BinaryOp) { operationStack.push(new BinOp(astNode)).enter() + } else if(astNode?.type === RType.IfThenElse) { + operationStack.push(new Conditional(astNode)).enter() } else if(astNode?.type === RType.Symbol) { operationStack.peek()?.next({ id: astNode.info.id, From ec43e1062f4f715aba73fec3fc49ff43d59458fa Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Fri, 8 Mar 2024 12:56:44 +0100 Subject: [PATCH 03/70] feat: the cfg now contains entry and exit nodes for expr. lists --- src/util/cfg/cfg.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/util/cfg/cfg.ts b/src/util/cfg/cfg.ts index edb585faa0..ada7e0582f 100644 --- a/src/util/cfg/cfg.ts +++ b/src/util/cfg/cfg.ts @@ -524,24 +524,25 @@ function cfgUnaryOp(unary: RNodeWithParent, operand: ControlFlowInformation): Co function cfgExprList(_node: RNodeWithParent, expressions: ControlFlowInformation[]): ControlFlowInformation { - const result: ControlFlowInformation = { graph: new ControlFlowGraph(), breaks: [], nexts: [], returns: [], exitPoints: [], entryPoints: [] } - let first = true + const exitPoint: NodeId = _node.info.id + '-exit' + const result: ControlFlowInformation = { graph: new ControlFlowGraph(), breaks: [], nexts: [], returns: [], exitPoints: [exitPoint], entryPoints: [_node.info.id] } + result.graph.addVertex({ id: _node.info.id, name: _node.type, type: CfgVertexType.Statement}) + result.graph.addVertex({ id: exitPoint, name: 'exprlist-exit', type: CfgVertexType.EndMarker}) + let previousExitPoints: NodeId[] = [_node.info.id] for(const expression of expressions) { - if(first) { - result.entryPoints = expression.entryPoints - first = false - } else { - for(const previousExitPoint of result.exitPoints) { - for(const entryPoint of expression.entryPoints) { - result.graph.addEdge(entryPoint, previousExitPoint, { label: 'FD' }) - } + for(const previousExitPoint of previousExitPoints) { + for(const entryPoint of expression.entryPoints) { + result.graph.addEdge(entryPoint, previousExitPoint, { label: 'FD' }) } } result.graph.merge(expression.graph) result.breaks.push(...expression.breaks) result.nexts.push(...expression.nexts) result.returns.push(...expression.returns) - result.exitPoints = expression.exitPoints + previousExitPoints = expression.exitPoints + } + for(const exitPoint of previousExitPoints) { + result.graph.addEdge(_node.info.id + '-exit', exitPoint, { label: 'FD' }) } return result } From f1c89ef86144e8435994fe77385dcf3d7082fe43 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Fri, 8 Mar 2024 13:07:58 +0100 Subject: [PATCH 04/70] feat: differentiate between expression and node ID in AINode --- .../handler/binop/operators.ts | 21 ++++++++------- .../handler/conditional/conditional.ts | 7 ++--- src/abstract-interpretation/processor.ts | 27 +++++++++++-------- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/abstract-interpretation/handler/binop/operators.ts b/src/abstract-interpretation/handler/binop/operators.ts index 0812aa18b1..96bee9c73b 100644 --- a/src/abstract-interpretation/handler/binop/operators.ts +++ b/src/abstract-interpretation/handler/binop/operators.ts @@ -5,24 +5,27 @@ import {addDomains, subtractDomains} from '../../domain' export const operators: BinOpOperators = { 'assignment': (lhs, rhs, node) => { return { - id: lhs.id, - domain: rhs.domain, - astNode: node.lhs, + nodeId: lhs.nodeId, + expressionId: node.info.id, + domain: rhs.domain, + astNode: node.lhs, } }, 'arithmetic': (lhs, rhs, node) => { switch(node.operator) { case '+': return { - id: node.info.id, - domain: addDomains(lhs.domain, rhs.domain), - astNode: node, + nodeId: node.info.id, + expressionId: node.info.id, + domain: addDomains(lhs.domain, rhs.domain), + astNode: node, } case '-': return { - id: node.info.id, - domain: subtractDomains(lhs.domain, rhs.domain), - astNode: node, + nodeId: node.info.id, + expressionId: node.info.id, + domain: subtractDomains(lhs.domain, rhs.domain), + astNode: node, } default: guard(false, `Unknown binary operator ${node.operator}`) diff --git a/src/abstract-interpretation/handler/conditional/conditional.ts b/src/abstract-interpretation/handler/conditional/conditional.ts index 183e9f8cbd..bc87baa17b 100644 --- a/src/abstract-interpretation/handler/conditional/conditional.ts +++ b/src/abstract-interpretation/handler/conditional/conditional.ts @@ -24,9 +24,10 @@ export class Conditional implements Handler { guard(this.then !== undefined, `No then-branch found for conditional ${this.node.info.id}`) guard(this.else !== undefined, `No else-branch found for conditional ${this.node.info.id}`) return { - id: this.node.info.id, - domain: this.condition.domain, - astNode: this.node, + nodeId: this.node.info.id, + expressionId: this.node.info.id, + domain: this.condition.domain, + astNode: this.node, } // guard(false, 'Not implemented yet') } diff --git a/src/abstract-interpretation/processor.ts b/src/abstract-interpretation/processor.ts index 6f6e18c4bc..8e6cac0636 100644 --- a/src/abstract-interpretation/processor.ts +++ b/src/abstract-interpretation/processor.ts @@ -13,9 +13,12 @@ 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 + // The ID of the node that logically holds the domain + readonly nodeId: NodeId + // The ID of the whole expression that the domain was calculated from + readonly expressionId: NodeId + readonly domain: Domain + readonly astNode: RNodeWithParent } class Stack { @@ -58,16 +61,18 @@ export function runAbstractInterpretation(ast: NormalizedAst, dfg: DataflowInfor operationStack.push(new Conditional(astNode)).enter() } else if(astNode?.type === RType.Symbol) { operationStack.peek()?.next({ - id: astNode.info.id, - domain: getDomainOfDfgChild(node.id, dfg, nodeMap), - astNode: astNode, + nodeId: astNode.info.id, + expressionId: 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, + nodeId: astNode.info.id, + expressionId: astNode.info.id, + domain: Domain.fromScalar(num), + astNode: astNode, }) } else if(node.type === CfgVertexType.EndMarker) { const operation = operationStack.pop() @@ -75,8 +80,8 @@ export function runAbstractInterpretation(ast: NormalizedAst, dfg: DataflowInfor return } const operationResult = operation.exit() - guard(!nodeMap.has(operationResult.id), `Domain for ID ${operationResult.id} already exists`) - nodeMap.set(operationResult.id, operationResult) + guard(!nodeMap.has(operationResult.nodeId), `Domain for ID ${operationResult.nodeId} already exists`) + nodeMap.set(operationResult.nodeId, operationResult) operationStack.peek()?.next(operationResult) } else { aiLogger.warn(`Unknown node type ${node.type}`) From ad57a962d033f3aa1f4c2c6095967d94cba6fb4d Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Thu, 28 Mar 2024 12:39:08 +0100 Subject: [PATCH 05/70] feat: changed how we store a bunch of domains --- .../handler/binop/binop.ts | 13 ++-- .../handler/conditional/conditional.ts | 27 +++++---- .../handler/handler.ts | 8 ++- src/abstract-interpretation/processor.ts | 59 +++++++++++++++---- 4 files changed, 74 insertions(+), 33 deletions(-) diff --git a/src/abstract-interpretation/handler/binop/binop.ts b/src/abstract-interpretation/handler/binop/binop.ts index e395abe6df..6d4805f891 100644 --- a/src/abstract-interpretation/handler/binop/binop.ts +++ b/src/abstract-interpretation/handler/binop/binop.ts @@ -1,5 +1,5 @@ import {Handler} from '../handler' -import {aiLogger, AINode} from '../../processor' +import {aiLogger, AINode, AINodeStore} from '../../processor' import {BinaryOperatorFlavor, ParentInformation, RBinaryOp} from '../../../r-bridge' import {guard} from '../../../util/assert' import {operators} from './operators' @@ -8,7 +8,7 @@ export type BinOpOperators = { [key in BinaryOperatorFlavor]: (lhs: AINode, rhs: AINode, node: RBinaryOp) => AINode } -export class BinOp implements Handler { +export class BinOp implements Handler { lhs: AINode | undefined rhs: AINode | undefined @@ -22,15 +22,18 @@ export class BinOp implements Handler { aiLogger.trace(`Entered ${this.getName()}`) } - exit(): AINode { + exit(): AINodeStore { 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) + const operatorResult = operators[this.node.flavor](this.lhs, this.rhs, this.node) + return new AINodeStore(operatorResult) } - next(node: AINode): void { + next(aiNodes: AINodeStore): void { aiLogger.trace(`${this.getName()} received`) + guard(aiNodes.size === 1, 'Welp, next received more than one AINodes') + const node = aiNodes.values().next().value as AINode if(this.lhs === undefined) { this.lhs = node } else if(this.rhs === undefined) { diff --git a/src/abstract-interpretation/handler/conditional/conditional.ts b/src/abstract-interpretation/handler/conditional/conditional.ts index bc87baa17b..a15f77f594 100644 --- a/src/abstract-interpretation/handler/conditional/conditional.ts +++ b/src/abstract-interpretation/handler/conditional/conditional.ts @@ -1,12 +1,12 @@ import { ParentInformation, RIfThenElse } from '../../../r-bridge' import { guard } from '../../../util/assert' -import { AINode, aiLogger } from '../../processor' +import { AINode, AINodeStore, aiLogger } from '../../processor' import { Handler } from '../handler' -export class Conditional implements Handler { - condition: AINode | undefined - then: AINode | undefined - else: AINode | undefined +export class Conditional implements Handler { + condition: AINode | undefined + then: AINodeStore | undefined + else: AINodeStore | undefined constructor(readonly node: RIfThenElse) {} @@ -18,28 +18,29 @@ export class Conditional implements Handler { aiLogger.trace(`Entered ${this.getName()}`) } - exit(): AINode { + exit(): AINodeStore { aiLogger.trace(`Exited ${this.getName()}`) guard(this.condition !== undefined, `No condition found for conditional ${this.node.info.id}`) guard(this.then !== undefined, `No then-branch found for conditional ${this.node.info.id}`) - guard(this.else !== undefined, `No else-branch found for conditional ${this.node.info.id}`) - return { + // TODO: calculate new domain + return new AINodeStore({ nodeId: this.node.info.id, expressionId: this.node.info.id, domain: this.condition.domain, astNode: this.node, - } - // guard(false, 'Not implemented yet') + }) } - next(node: AINode): void { + next(aiNodes: AINodeStore): void { aiLogger.trace(`${this.getName()} received`) if(this.condition === undefined) { + guard(aiNodes.size === 1, 'Welp, next received more than one AINodes') + const node = aiNodes.values().next().value as AINode this.condition = node } else if(this.then === undefined) { - this.then = node + this.then = aiNodes } else if(this.else === undefined) { - this.else = node + this.else = aiNodes } else { guard(false, `Conditional ${this.node.info.id} already has condition, then and else`) } diff --git a/src/abstract-interpretation/handler/handler.ts b/src/abstract-interpretation/handler/handler.ts index 805613fe03..3f8c1359f7 100644 --- a/src/abstract-interpretation/handler/handler.ts +++ b/src/abstract-interpretation/handler/handler.ts @@ -1,6 +1,8 @@ -export interface Handler { +import {AINodeStore} from '../processor' + +export interface Handler { getName: () => string, enter: () => void - exit: () => ValueType - next: (value: ValueType) => void + exit: () => AINodeStore + next: (aiNodes: AINodeStore) => void } \ No newline at end of file diff --git a/src/abstract-interpretation/processor.ts b/src/abstract-interpretation/processor.ts index 8e6cac0636..afc473fbe0 100644 --- a/src/abstract-interpretation/processor.ts +++ b/src/abstract-interpretation/processor.ts @@ -2,7 +2,7 @@ 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 {assertUnreachable, guard} from '../util/assert' import {DataflowGraphVertexInfo, EdgeType, OutgoingEdges} from '../dataflow' import {Handler} from './handler/handler' import {BinOp} from './handler/binop/binop' @@ -21,6 +21,42 @@ export interface AINode { readonly astNode: RNodeWithParent } + +export class AINodeStore extends Map { + constructor(content: AINode[] | AINode | undefined = undefined) { + if(Array.isArray(content)) { + super(content.map(node => [node.nodeId, node])) + } else if(content !== undefined) { + super([[content.nodeId, content]]) + } else if(content === undefined) { + super() + } else { + assertUnreachable(content) + } + } + + push(node: AINode): void { + this.set(node.nodeId, node) + } +} + +export function mergeDomainStores(...stores: AINodeStore[]): AINodeStore { + const result = new AINodeStore() + for(const store of stores) { + for(const [id, node] of store) { + if(result.has(id)) { + const existing = result.get(id) + guard(existing !== undefined, `Domain for ID ${id} is missing`) + const unified = unifyDomains([existing.domain, node.domain]) + result.set(id, {...node, domain: unified}) + } else { + result.set(id, node) + } + } + } + return result +} + class Stack { private backingStore: ElementType[] = [] @@ -33,7 +69,7 @@ class Stack { } } -function getDomainOfDfgChild(node: NodeId, dfg: DataflowInformation, nodeMap: Map): Domain { +function getDomainOfDfgChild(node: NodeId, dfg: DataflowInformation, domainStore: AINodeStore): Domain { const dfgNode: [DataflowGraphVertexInfo, OutgoingEdges] | undefined = dfg.graph.get(node) guard(dfgNode !== undefined, `No DFG-Node found with ID ${node}`) const [_, children] = dfgNode @@ -42,7 +78,7 @@ function getDomainOfDfgChild(node: NodeId, dfg: DataflowInformation, nodeMap: Ma .map(([id, _]) => id) const domains: Domain[] = [] for(const id of ids) { - const domain = nodeMap.get(id)?.domain + const domain = domainStore.get(id)?.domain guard(domain !== undefined, `No domain found for ID ${id}`) domains.push(domain) } @@ -51,8 +87,8 @@ function getDomainOfDfgChild(node: NodeId, dfg: DataflowInformation, nodeMap: Ma export function runAbstractInterpretation(ast: NormalizedAst, dfg: DataflowInformation): DataflowInformation { const cfg = extractCFG(ast) - const operationStack = new Stack>() - const nodeMap = new Map() + const operationStack = new Stack() + let domainStore = new AINodeStore() visitCfg(cfg, (node, _) => { const astNode = ast.idMap.get(node.id) if(astNode?.type === RType.BinaryOp) { @@ -60,28 +96,27 @@ export function runAbstractInterpretation(ast: NormalizedAst, dfg: DataflowInfor } else if(astNode?.type === RType.IfThenElse) { operationStack.push(new Conditional(astNode)).enter() } else if(astNode?.type === RType.Symbol) { - operationStack.peek()?.next({ + operationStack.peek()?.next(new AINodeStore({ nodeId: astNode.info.id, expressionId: astNode.info.id, - domain: getDomainOfDfgChild(node.id, dfg, nodeMap), + domain: getDomainOfDfgChild(node.id, dfg, domainStore), astNode: astNode, - }) + })) } else if(astNode?.type === RType.Number){ const num = astNode.content.num - operationStack.peek()?.next({ + operationStack.peek()?.next(new AINodeStore({ nodeId: astNode.info.id, expressionId: 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.nodeId), `Domain for ID ${operationResult.nodeId} already exists`) - nodeMap.set(operationResult.nodeId, operationResult) + domainStore = mergeDomainStores(domainStore, operationResult) operationStack.peek()?.next(operationResult) } else { aiLogger.warn(`Unknown node type ${node.type}`) From 08fb17261d0ea1aca05ee04cfff679e3a3efd15e Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Thu, 28 Mar 2024 13:21:15 +0100 Subject: [PATCH 06/70] fixup! feat: changed how we store a bunch of domains --- src/abstract-interpretation/ainode.ts | 48 ++++++++++++++++++ .../handler/binop/binop.ts | 3 +- .../handler/conditional/conditional.ts | 3 +- .../handler/handler.ts | 2 +- src/abstract-interpretation/processor.ts | 50 ++----------------- 5 files changed, 56 insertions(+), 50 deletions(-) create mode 100644 src/abstract-interpretation/ainode.ts diff --git a/src/abstract-interpretation/ainode.ts b/src/abstract-interpretation/ainode.ts new file mode 100644 index 0000000000..36f735292d --- /dev/null +++ b/src/abstract-interpretation/ainode.ts @@ -0,0 +1,48 @@ +import { NodeId, ParentInformation, RNodeWithParent } from '../r-bridge' +import { assertUnreachable, guard } from '../util/assert' +import { Domain, unifyDomains } from './domain' + +export interface AINode { + // The ID of the node that logically holds the domain + readonly nodeId: NodeId + // The ID of the whole expression that the domain was calculated from + readonly expressionId: NodeId + readonly domain: Domain + readonly astNode: RNodeWithParent +} + + +export class AINodeStore extends Map { + constructor(content: AINode[] | AINode | undefined = undefined) { + if(Array.isArray(content)) { + super(content.map(node => [node.nodeId, node])) + } else if(content !== undefined) { + super([[content.nodeId, content]]) + } else if(content === undefined) { + super() + } else { + assertUnreachable(content) + } + } + + push(node: AINode): void { + this.set(node.nodeId, node) + } +} + +export function mergeDomainStores(...stores: AINodeStore[]): AINodeStore { + const result = new AINodeStore() + for(const store of stores) { + for(const [id, node] of store) { + if(result.has(id)) { + const existing = result.get(id) + guard(existing !== undefined, `Domain for ID ${id} is missing`) + const unified = unifyDomains([existing.domain, node.domain]) + result.set(id, {...node, domain: unified}) + } else { + result.set(id, node) + } + } + } + return result +} diff --git a/src/abstract-interpretation/handler/binop/binop.ts b/src/abstract-interpretation/handler/binop/binop.ts index 6d4805f891..0aac91ec63 100644 --- a/src/abstract-interpretation/handler/binop/binop.ts +++ b/src/abstract-interpretation/handler/binop/binop.ts @@ -1,8 +1,9 @@ import {Handler} from '../handler' -import {aiLogger, AINode, AINodeStore} from '../../processor' +import {aiLogger} from '../../processor' import {BinaryOperatorFlavor, ParentInformation, RBinaryOp} from '../../../r-bridge' import {guard} from '../../../util/assert' import {operators} from './operators' +import { AINode, AINodeStore } from '../../ainode' export type BinOpOperators = { [key in BinaryOperatorFlavor]: (lhs: AINode, rhs: AINode, node: RBinaryOp) => AINode diff --git a/src/abstract-interpretation/handler/conditional/conditional.ts b/src/abstract-interpretation/handler/conditional/conditional.ts index a15f77f594..f1817eeebe 100644 --- a/src/abstract-interpretation/handler/conditional/conditional.ts +++ b/src/abstract-interpretation/handler/conditional/conditional.ts @@ -1,6 +1,7 @@ import { ParentInformation, RIfThenElse } from '../../../r-bridge' import { guard } from '../../../util/assert' -import { AINode, AINodeStore, aiLogger } from '../../processor' +import { AINode, AINodeStore } from '../../ainode' +import { aiLogger } from '../../processor' import { Handler } from '../handler' export class Conditional implements Handler { diff --git a/src/abstract-interpretation/handler/handler.ts b/src/abstract-interpretation/handler/handler.ts index 3f8c1359f7..b1855a574c 100644 --- a/src/abstract-interpretation/handler/handler.ts +++ b/src/abstract-interpretation/handler/handler.ts @@ -1,4 +1,4 @@ -import {AINodeStore} from '../processor' +import {AINodeStore} from '../ainode' export interface Handler { getName: () => string, diff --git a/src/abstract-interpretation/processor.ts b/src/abstract-interpretation/processor.ts index afc473fbe0..3685aa98fc 100644 --- a/src/abstract-interpretation/processor.ts +++ b/src/abstract-interpretation/processor.ts @@ -1,62 +1,18 @@ import {DataflowInformation} from '../dataflow/internal/info' -import {NodeId, NormalizedAst, ParentInformation, RNodeWithParent, RType} from '../r-bridge' +import {NodeId, NormalizedAst, RType} from '../r-bridge' import {CfgVertexType, extractCFG} from '../util/cfg/cfg' import {visitCfg} from '../util/cfg/visitor' -import {assertUnreachable, guard} from '../util/assert' +import {guard} from '../util/assert' import {DataflowGraphVertexInfo, EdgeType, OutgoingEdges} from '../dataflow' import {Handler} from './handler/handler' import {BinOp} from './handler/binop/binop' import {Conditional} from './handler/conditional/conditional' import {Domain, unifyDomains} from './domain' import {log} from '../util/log' +import {AINodeStore, mergeDomainStores} from './ainode' export const aiLogger = log.getSubLogger({name: 'abstract-interpretation'}) -export interface AINode { - // The ID of the node that logically holds the domain - readonly nodeId: NodeId - // The ID of the whole expression that the domain was calculated from - readonly expressionId: NodeId - readonly domain: Domain - readonly astNode: RNodeWithParent -} - - -export class AINodeStore extends Map { - constructor(content: AINode[] | AINode | undefined = undefined) { - if(Array.isArray(content)) { - super(content.map(node => [node.nodeId, node])) - } else if(content !== undefined) { - super([[content.nodeId, content]]) - } else if(content === undefined) { - super() - } else { - assertUnreachable(content) - } - } - - push(node: AINode): void { - this.set(node.nodeId, node) - } -} - -export function mergeDomainStores(...stores: AINodeStore[]): AINodeStore { - const result = new AINodeStore() - for(const store of stores) { - for(const [id, node] of store) { - if(result.has(id)) { - const existing = result.get(id) - guard(existing !== undefined, `Domain for ID ${id} is missing`) - const unified = unifyDomains([existing.domain, node.domain]) - result.set(id, {...node, domain: unified}) - } else { - result.set(id, node) - } - } - } - return result -} - class Stack { private backingStore: ElementType[] = [] From 07ed85a339f6bdb619f3535f6758544c20396ac7 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Thu, 28 Mar 2024 12:47:03 +0100 Subject: [PATCH 07/70] feat: added exprlist handler --- .../handler/exprlist/exprlist.ts | 23 +++++++++++++++++++ src/abstract-interpretation/processor.ts | 3 +++ 2 files changed, 26 insertions(+) create mode 100644 src/abstract-interpretation/handler/exprlist/exprlist.ts diff --git a/src/abstract-interpretation/handler/exprlist/exprlist.ts b/src/abstract-interpretation/handler/exprlist/exprlist.ts new file mode 100644 index 0000000000..568aef7534 --- /dev/null +++ b/src/abstract-interpretation/handler/exprlist/exprlist.ts @@ -0,0 +1,23 @@ +import { AINodeStore, mergeDomainStores } from '../../ainode' +import { aiLogger } from '../../processor' +import { Handler } from '../handler' + +export class ExprList implements Handler { + private exprList: AINodeStore = new AINodeStore() + + getName(): string { + return 'ExprList' + } + + enter(): void { + aiLogger.trace(`Entered ${this.getName()}`) + } + + exit(): AINodeStore { + return this.exprList + } + + next(aiNodes: AINodeStore): void { + this.exprList = mergeDomainStores(aiNodes, this.exprList) + } +} diff --git a/src/abstract-interpretation/processor.ts b/src/abstract-interpretation/processor.ts index 3685aa98fc..048237bd39 100644 --- a/src/abstract-interpretation/processor.ts +++ b/src/abstract-interpretation/processor.ts @@ -9,6 +9,7 @@ import {BinOp} from './handler/binop/binop' import {Conditional} from './handler/conditional/conditional' import {Domain, unifyDomains} from './domain' import {log} from '../util/log' +import {ExprList} from './handler/exprlist/exprlist' import {AINodeStore, mergeDomainStores} from './ainode' export const aiLogger = log.getSubLogger({name: 'abstract-interpretation'}) @@ -51,6 +52,8 @@ export function runAbstractInterpretation(ast: NormalizedAst, dfg: DataflowInfor operationStack.push(new BinOp(astNode)).enter() } else if(astNode?.type === RType.IfThenElse) { operationStack.push(new Conditional(astNode)).enter() + } else if(astNode?.type === RType.ExpressionList) { + operationStack.push(new ExprList()).enter() } else if(astNode?.type === RType.Symbol) { operationStack.peek()?.next(new AINodeStore({ nodeId: astNode.info.id, From 59bb2803418c539d48bf0fa17fc152bee256b594 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Thu, 28 Mar 2024 15:13:03 +0100 Subject: [PATCH 08/70] refactor: binOpOperators return a AINodeStore --- src/abstract-interpretation/handler/binop/binop.ts | 5 ++--- .../handler/binop/operators.ts | 13 +++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/abstract-interpretation/handler/binop/binop.ts b/src/abstract-interpretation/handler/binop/binop.ts index 0aac91ec63..a0723b4eb4 100644 --- a/src/abstract-interpretation/handler/binop/binop.ts +++ b/src/abstract-interpretation/handler/binop/binop.ts @@ -6,7 +6,7 @@ import {operators} from './operators' import { AINode, AINodeStore } from '../../ainode' export type BinOpOperators = { - [key in BinaryOperatorFlavor]: (lhs: AINode, rhs: AINode, node: RBinaryOp) => AINode + [key in BinaryOperatorFlavor]: (lhs: AINode, rhs: AINode, node: RBinaryOp) => AINodeStore } export class BinOp implements Handler { @@ -27,8 +27,7 @@ export class BinOp implements Handler { 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 operatorResult = operators[this.node.flavor](this.lhs, this.rhs, this.node) - return new AINodeStore(operatorResult) + return operators[this.node.flavor](this.lhs, this.rhs, this.node) } next(aiNodes: AINodeStore): void { diff --git a/src/abstract-interpretation/handler/binop/operators.ts b/src/abstract-interpretation/handler/binop/operators.ts index 96bee9c73b..a15d918a91 100644 --- a/src/abstract-interpretation/handler/binop/operators.ts +++ b/src/abstract-interpretation/handler/binop/operators.ts @@ -1,32 +1,33 @@ import {guard} from '../../../util/assert' import {BinOpOperators} from './binop' import {addDomains, subtractDomains} from '../../domain' +import { AINodeStore } from '../../ainode' export const operators: BinOpOperators = { 'assignment': (lhs, rhs, node) => { - return { + return new AINodeStore({ nodeId: lhs.nodeId, expressionId: node.info.id, domain: rhs.domain, astNode: node.lhs, - } + }) }, 'arithmetic': (lhs, rhs, node) => { switch(node.operator) { case '+': - return { + return new AINodeStore({ nodeId: node.info.id, expressionId: node.info.id, domain: addDomains(lhs.domain, rhs.domain), astNode: node, - } + }) case '-': - return { + return new AINodeStore({ nodeId: node.info.id, expressionId: node.info.id, domain: subtractDomains(lhs.domain, rhs.domain), astNode: node, - } + }) default: guard(false, `Unknown binary operator ${node.operator}`) } From a6170746f645007be63769d36a3812348a620c3a Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Thu, 28 Mar 2024 18:59:47 +0100 Subject: [PATCH 09/70] wip: added narrowDomain stub --- src/abstract-interpretation/domain.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/abstract-interpretation/domain.ts b/src/abstract-interpretation/domain.ts index 4e7dec0971..f6ae5a815f 100644 --- a/src/abstract-interpretation/domain.ts +++ b/src/abstract-interpretation/domain.ts @@ -166,4 +166,18 @@ export function subtractDomains(domain1: Domain, domain2: Domain): Domain { } } return Domain.fromIntervals(intervals) +} + +export const enum NarrowKind { + Equal, + Smaller, + Greater +} + +export function narrowDomain(baseDomain: Domain, boundDomain: Domain, narrowKind: NarrowKind): Domain { + // TODO: Implement + boundDomain + narrowKind + return baseDomain + } \ No newline at end of file From 9fdd9d080d93ed25efe7dc2b16018a8d568b2fe1 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Thu, 28 Mar 2024 19:01:18 +0100 Subject: [PATCH 10/70] feat: implemented comparison operators --- .../handler/binop/operators.ts | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/abstract-interpretation/handler/binop/operators.ts b/src/abstract-interpretation/handler/binop/operators.ts index a15d918a91..bbb10dc67a 100644 --- a/src/abstract-interpretation/handler/binop/operators.ts +++ b/src/abstract-interpretation/handler/binop/operators.ts @@ -1,6 +1,6 @@ import {guard} from '../../../util/assert' import {BinOpOperators} from './binop' -import {addDomains, subtractDomains} from '../../domain' +import {NarrowKind, addDomains, narrowDomain, subtractDomains} from '../../domain' import { AINodeStore } from '../../ainode' export const operators: BinOpOperators = { @@ -38,7 +38,39 @@ export const operators: BinOpOperators = { 'model formula': () => { guard(false, 'Not implemented yet') }, - 'comparison': () => { - guard(false, 'Not implemented yet') - }, + 'comparison': (lhs, rhs, node) => { + let lhsNarrowedDomain + let rhsNarrowedDomain + switch(node.operator) { + case '<': + lhsNarrowedDomain = narrowDomain(lhs.domain, rhs.domain, NarrowKind.Smaller) + rhsNarrowedDomain = narrowDomain(rhs.domain, lhs.domain, NarrowKind.Greater) + break + case '>': + lhsNarrowedDomain = narrowDomain(lhs.domain, rhs.domain, NarrowKind.Greater) + rhsNarrowedDomain = narrowDomain(rhs.domain, lhs.domain, NarrowKind.Smaller) + break + case '<=': + lhsNarrowedDomain = narrowDomain(lhs.domain, rhs.domain, NarrowKind.Smaller | NarrowKind.Equal) + rhsNarrowedDomain = narrowDomain(rhs.domain, lhs.domain, NarrowKind.Greater | NarrowKind.Equal) + break + case '>=': + lhsNarrowedDomain = narrowDomain(lhs.domain, rhs.domain, NarrowKind.Greater | NarrowKind.Equal) + rhsNarrowedDomain = narrowDomain(rhs.domain, lhs.domain, NarrowKind.Smaller | NarrowKind.Equal) + break + default: + guard(false, `Unknown binary operator ${node.operator}`) + } + return new AINodeStore([{ + nodeId: lhs.nodeId, + expressionId: node.info.id, + domain: lhsNarrowedDomain, + astNode: node, + }, { + nodeId: rhs.nodeId, + expressionId: node.info.id, + domain: rhsNarrowedDomain, + astNode: node, + }]) + } } From cd7171df07696d0b10eb5d79ab8ffbff0eae4576 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Thu, 28 Mar 2024 19:04:02 +0100 Subject: [PATCH 11/70] feat: the conditiona handler's condition has to be an AINodeStore --- .../handler/conditional/conditional.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/abstract-interpretation/handler/conditional/conditional.ts b/src/abstract-interpretation/handler/conditional/conditional.ts index f1817eeebe..8d4b83fc73 100644 --- a/src/abstract-interpretation/handler/conditional/conditional.ts +++ b/src/abstract-interpretation/handler/conditional/conditional.ts @@ -1,11 +1,11 @@ import { ParentInformation, RIfThenElse } from '../../../r-bridge' import { guard } from '../../../util/assert' -import { AINode, AINodeStore } from '../../ainode' +import { AINodeStore } from '../../ainode' import { aiLogger } from '../../processor' import { Handler } from '../handler' export class Conditional implements Handler { - condition: AINode | undefined + condition: AINodeStore | undefined then: AINodeStore | undefined else: AINodeStore | undefined @@ -24,20 +24,13 @@ export class Conditional implements Handler { guard(this.condition !== undefined, `No condition found for conditional ${this.node.info.id}`) guard(this.then !== undefined, `No then-branch found for conditional ${this.node.info.id}`) // TODO: calculate new domain - return new AINodeStore({ - nodeId: this.node.info.id, - expressionId: this.node.info.id, - domain: this.condition.domain, - astNode: this.node, - }) + return this.condition } next(aiNodes: AINodeStore): void { aiLogger.trace(`${this.getName()} received`) if(this.condition === undefined) { - guard(aiNodes.size === 1, 'Welp, next received more than one AINodes') - const node = aiNodes.values().next().value as AINode - this.condition = node + this.condition = aiNodes } else if(this.then === undefined) { this.then = aiNodes } else if(this.else === undefined) { From 2becfea4f4e756ce27119afdd25ab2d5e8e534e1 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Mon, 15 Apr 2024 15:56:43 +0200 Subject: [PATCH 12/70] feat-fix: fixed two bugs related to intervals 1: (2, 2) is not a valid interval 2: [2, 3) and [3, 4] can be merged --- src/abstract-interpretation/domain.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/abstract-interpretation/domain.ts b/src/abstract-interpretation/domain.ts index f6ae5a815f..cd72ad77ea 100644 --- a/src/abstract-interpretation/domain.ts +++ b/src/abstract-interpretation/domain.ts @@ -8,7 +8,7 @@ interface IntervalBound { 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`) + guard(min.value !== max.value || (min.inclusive && max.inclusive), `The interval ${this.toString()} is not possible. Both bounds should be inclusive if they are equal.`) } toString(): string { @@ -98,12 +98,12 @@ export function doIntervalsOverlap(interval1: Interval, interval2: Interval): bo if(diff1 < 0 || diff2 < 0) { return false } - // If their end and start are equal, they only overlap if both are inclusive + // If their start and end are equal, they only overlap (or rather touch) if at least one is inclusive if(diff1 === 0) { - return interval1.max.inclusive && interval2.min.inclusive + return interval1.max.inclusive || interval2.min.inclusive } if(diff2 === 0) { - return interval2.max.inclusive && interval1.min.inclusive + return interval2.max.inclusive || interval1.min.inclusive } return true From fd9df2b6a7df28370a23f9e00c2a4d6df9b4c187 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Mon, 15 Apr 2024 16:29:00 +0200 Subject: [PATCH 13/70] wip: implemented most of narrowDomain --- src/abstract-interpretation/domain.ts | 63 ++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/src/abstract-interpretation/domain.ts b/src/abstract-interpretation/domain.ts index cd72ad77ea..7a5902b6e4 100644 --- a/src/abstract-interpretation/domain.ts +++ b/src/abstract-interpretation/domain.ts @@ -169,15 +169,64 @@ export function subtractDomains(domain1: Domain, domain2: Domain): Domain { } export const enum NarrowKind { - Equal, - Smaller, - Greater + Equal = 1, + Smaller = 2, + Greater = 4 } +interface IntervalOverlap { + smaller: Interval | undefined, + intersection: Interval | undefined, + larger: Interval | undefined +} + +function flipInclusiveness(intervalBound: IntervalBound): IntervalBound { + return {value: intervalBound.value, inclusive: !intervalBound.inclusive} + +} + +export function overlapIntervals(interval1: Interval, interval2: Interval): IntervalOverlap { + const diffMin = compareIntervalsByTheirMinimum(interval1, interval2) + const diffMax = compareIntervalsByTheirMaximum(interval1, interval2) + + const intersectionStart = diffMin > 0 ? interval1.min : interval2.min + const intersectionEnd = diffMax < 0 ? interval1.max : interval2.max + const intersection = new Interval(intersectionStart, intersectionEnd) + + const smallerOverhang = diffMin < 0 ? new Interval(interval1.min, flipInclusiveness(intersectionStart)) : undefined + const greaterOverhang = diffMax > 0 ? new Interval(flipInclusiveness(intersectionEnd), interval1.max) : undefined + + return { + smaller: smallerOverhang, + intersection: intersection, + larger: greaterOverhang + } +} + +// TODO: Handle NarrowKind.Equal cases and in- and exclusive bounds export function narrowDomain(baseDomain: Domain, boundDomain: Domain, narrowKind: NarrowKind): Domain { - // TODO: Implement - boundDomain - narrowKind - return baseDomain + guard(!((narrowKind & NarrowKind.Greater) !== 0 && (narrowKind & NarrowKind.Smaller) !== 0), 'Greater and Smaller cannot be combined') + + const result = Domain.bottom() + const addIntervalPartToResult = (interval: Interval | undefined) => { + if(interval !== undefined) { + result.addInterval(interval) + } + } + let handleOneSideOfTheOverlap: (overlap : IntervalOverlap) => void + if((narrowKind & NarrowKind.Greater) !== 0) { + handleOneSideOfTheOverlap = overlap => addIntervalPartToResult(overlap.larger) + } else if((narrowKind & NarrowKind.Smaller) !== 0) { + handleOneSideOfTheOverlap = overlap => addIntervalPartToResult(overlap.smaller) + } else { guard(false, 'Should not reach') } + + for(const baseInterval of baseDomain.intervals) { + for(const boundInterval of boundDomain.intervals) { + const overlap = overlapIntervals(baseInterval, boundInterval) + addIntervalPartToResult(overlap.intersection) + handleOneSideOfTheOverlap(overlap) + } + } + return result } \ No newline at end of file From 94193c4979c65ae29a8a5458169c8043b0677392 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Tue, 16 Apr 2024 09:12:19 +0200 Subject: [PATCH 14/70] refactor: restructured narrowDomain a bit --- src/abstract-interpretation/domain.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/abstract-interpretation/domain.ts b/src/abstract-interpretation/domain.ts index 7a5902b6e4..1fbba27421 100644 --- a/src/abstract-interpretation/domain.ts +++ b/src/abstract-interpretation/domain.ts @@ -205,25 +205,29 @@ export function overlapIntervals(interval1: Interval, interval2: Interval): Inte // TODO: Handle NarrowKind.Equal cases and in- and exclusive bounds export function narrowDomain(baseDomain: Domain, boundDomain: Domain, narrowKind: NarrowKind): Domain { - guard(!((narrowKind & NarrowKind.Greater) !== 0 && (narrowKind & NarrowKind.Smaller) !== 0), 'Greater and Smaller cannot be combined') + const isSmaller = (narrowKind & NarrowKind.Smaller) !== 0 + const isGreater = (narrowKind & NarrowKind.Greater) !== 0 + const isEqual = (narrowKind & NarrowKind.Equal) !== 0 + + guard(!(isGreater && isSmaller), 'Greater and Smaller cannot be combined') const result = Domain.bottom() - const addIntervalPartToResult = (interval: Interval | undefined) => { + const addIntervalToResult = (interval: Interval | undefined) => { if(interval !== undefined) { result.addInterval(interval) } } let handleOneSideOfTheOverlap: (overlap : IntervalOverlap) => void - if((narrowKind & NarrowKind.Greater) !== 0) { - handleOneSideOfTheOverlap = overlap => addIntervalPartToResult(overlap.larger) - } else if((narrowKind & NarrowKind.Smaller) !== 0) { - handleOneSideOfTheOverlap = overlap => addIntervalPartToResult(overlap.smaller) + if(isGreater) { + handleOneSideOfTheOverlap = overlap => addIntervalToResult(overlap.larger) + } else if(isSmaller) { + handleOneSideOfTheOverlap = overlap => addIntervalToResult(overlap.smaller) } else { guard(false, 'Should not reach') } for(const baseInterval of baseDomain.intervals) { for(const boundInterval of boundDomain.intervals) { const overlap = overlapIntervals(baseInterval, boundInterval) - addIntervalPartToResult(overlap.intersection) + addIntervalToResult(overlap.intersection) handleOneSideOfTheOverlap(overlap) } } From 04a12d85180fbfc8742bf7a6866569598a8bc032 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Tue, 16 Apr 2024 10:57:37 +0200 Subject: [PATCH 15/70] feat-fix: fixed overlapIntervals for non overlapping intervals --- src/abstract-interpretation/domain.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/abstract-interpretation/domain.ts b/src/abstract-interpretation/domain.ts index 1fbba27421..c93853c51d 100644 --- a/src/abstract-interpretation/domain.ts +++ b/src/abstract-interpretation/domain.ts @@ -189,6 +189,14 @@ export function overlapIntervals(interval1: Interval, interval2: Interval): Inte const diffMin = compareIntervalsByTheirMinimum(interval1, interval2) const diffMax = compareIntervalsByTheirMaximum(interval1, interval2) + if(!doIntervalsOverlap(interval1, interval2)) { + if(diffMin < 0) { + return {smaller: interval1, intersection: undefined, larger: undefined} + } else if(diffMin > 0) { + return {smaller: undefined, intersection: undefined, larger: interval1} + } else { guard(false, 'Their lower bounds cannot be the same as they do not overlap') } + } + const intersectionStart = diffMin > 0 ? interval1.min : interval2.min const intersectionEnd = diffMax < 0 ? interval1.max : interval2.max const intersection = new Interval(intersectionStart, intersectionEnd) From 6fbe5ab970e9446ddcee28d2a696f15b592992ec Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Tue, 16 Apr 2024 11:00:41 +0200 Subject: [PATCH 16/70] feat-fix: differentiate between overlapping and touching intervals --- src/abstract-interpretation/domain.ts | 38 ++++++++++++++++++++------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/abstract-interpretation/domain.ts b/src/abstract-interpretation/domain.ts index c93853c51d..0a9f5d390e 100644 --- a/src/abstract-interpretation/domain.ts +++ b/src/abstract-interpretation/domain.ts @@ -90,23 +90,41 @@ function compareIntervalsByTheirMaximum(interval1: Interval, interval2: Interval return compareIntervals(CompareType.Max, interval1.max, interval2.max) } -export function doIntervalsOverlap(interval1: Interval, interval2: Interval): boolean { +export const enum OverlapKind { + Overlap = 0, + Touch = 1, +} + +export function doIntervalsOverlap(interval1: Interval, interval2: Interval, kind: OverlapKind = OverlapKind.Overlap): boolean { const diff1 = compareIntervals(CompareType.IgnoreInclusivity, interval1.max, interval2.min) const diff2 = compareIntervals(CompareType.IgnoreInclusivity, interval2.max, interval1.min) + let doIntervalsOverlap = true + let doIntervalsTouch = true + // If one interval ends before the other starts, they don't overlap if(diff1 < 0 || diff2 < 0) { - return false + doIntervalsOverlap = false + doIntervalsTouch = false } - // If their start and end are equal, they only overlap (or rather touch) if at least one is inclusive - if(diff1 === 0) { - return interval1.max.inclusive || interval2.min.inclusive - } - if(diff2 === 0) { - return interval2.max.inclusive || interval1.min.inclusive + // If their bounds have the same value, they overlap if both are inclusive + // and touch if only one is inclusive + else if(diff1 === 0) { + doIntervalsOverlap = interval1.max.inclusive && interval2.min.inclusive + doIntervalsTouch = interval1.max.inclusive !== interval2.min.inclusive + } else if(diff2 === 0) { + doIntervalsOverlap = interval2.max.inclusive && interval1.min.inclusive + doIntervalsTouch = interval2.max.inclusive !== interval1.min.inclusive } - return true + switch(kind) { + case OverlapKind.Overlap: + return doIntervalsOverlap + case OverlapKind.Touch: + return doIntervalsTouch + default: + return doIntervalsOverlap && doIntervalsTouch + } } export function unifyDomains(domains: Domain[]): Domain { @@ -123,7 +141,7 @@ export function unifyOverlappingIntervals(intervals: Interval[]): Interval[] { const unifiedIntervals: Interval[] = [] let currentInterval = sortedIntervals[0] for(const nextInterval of sortedIntervals) { - if(doIntervalsOverlap(currentInterval, nextInterval)) { + if(doIntervalsOverlap(currentInterval, nextInterval, OverlapKind.Touch | OverlapKind.Overlap)) { const intervalWithEarlierStart = compareIntervalsByTheirMinimum(currentInterval, nextInterval) < 0 ? currentInterval : nextInterval const intervalWithLaterEnd = compareIntervalsByTheirMaximum(currentInterval, nextInterval) > 0 ? currentInterval : nextInterval currentInterval = new Interval(intervalWithEarlierStart.min, intervalWithLaterEnd.max) From 6dd10cbea06482b285a939ba5069669c225196c3 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Tue, 16 Apr 2024 14:35:38 +0200 Subject: [PATCH 17/70] feat: narrowDomain now handles narrowing to exclusive bounds Hopefully xD --- src/abstract-interpretation/domain.ts | 31 +++++++++++++++------------ 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/abstract-interpretation/domain.ts b/src/abstract-interpretation/domain.ts index 0a9f5d390e..b6b7166365 100644 --- a/src/abstract-interpretation/domain.ts +++ b/src/abstract-interpretation/domain.ts @@ -229,7 +229,6 @@ export function overlapIntervals(interval1: Interval, interval2: Interval): Inte } } -// TODO: Handle NarrowKind.Equal cases and in- and exclusive bounds export function narrowDomain(baseDomain: Domain, boundDomain: Domain, narrowKind: NarrowKind): Domain { const isSmaller = (narrowKind & NarrowKind.Smaller) !== 0 const isGreater = (narrowKind & NarrowKind.Greater) !== 0 @@ -237,26 +236,30 @@ export function narrowDomain(baseDomain: Domain, boundDomain: Domain, narrowKind guard(!(isGreater && isSmaller), 'Greater and Smaller cannot be combined') - const result = Domain.bottom() - const addIntervalToResult = (interval: Interval | undefined) => { - if(interval !== undefined) { - result.addInterval(interval) - } - } - let handleOneSideOfTheOverlap: (overlap : IntervalOverlap) => void + let getNarrowedIntervals: (overlap: IntervalOverlap, bound: Interval) => (Interval | undefined)[] if(isGreater) { - handleOneSideOfTheOverlap = overlap => addIntervalToResult(overlap.larger) + getNarrowedIntervals = ({intersection, larger}, bound) => { + if(!isEqual && intersection !== undefined && compareIntervalsByTheirMinimum(intersection, bound) === 0) { + intersection = new Interval({value: intersection.min.value, inclusive: false}, intersection.max) + } + return [intersection, larger] + } } else if(isSmaller) { - handleOneSideOfTheOverlap = overlap => addIntervalToResult(overlap.smaller) - } else { guard(false, 'Should not reach') } + getNarrowedIntervals = ({smaller, intersection}, bound) => { + if(!isEqual && intersection !== undefined && compareIntervalsByTheirMaximum(intersection, bound) === 0) { + intersection = new Interval(intersection.min, {value: intersection.max.value, inclusive: false}) + } + return [intersection, smaller] + } + } else {guard(false, 'Either isGreater or isSmaller must be set')} + const narrowedIntervals: (Interval | undefined)[] = [] for(const baseInterval of baseDomain.intervals) { for(const boundInterval of boundDomain.intervals) { const overlap = overlapIntervals(baseInterval, boundInterval) - addIntervalToResult(overlap.intersection) - handleOneSideOfTheOverlap(overlap) + narrowedIntervals.push(...getNarrowedIntervals(overlap, boundInterval)) } } - return result + return Domain.fromIntervals(narrowedIntervals.filter(interval => interval !== undefined).map(interval => interval as Interval)) } \ No newline at end of file From 3f0849fe8e2a97ce4079afe29fe905213349cd10 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Tue, 16 Apr 2024 15:09:20 +0200 Subject: [PATCH 18/70] refactor: simplify comparison operator handler --- .../handler/binop/operators.ts | 30 +++++-------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/src/abstract-interpretation/handler/binop/operators.ts b/src/abstract-interpretation/handler/binop/operators.ts index bbb10dc67a..9086f62d9e 100644 --- a/src/abstract-interpretation/handler/binop/operators.ts +++ b/src/abstract-interpretation/handler/binop/operators.ts @@ -39,37 +39,23 @@ export const operators: BinOpOperators = { guard(false, 'Not implemented yet') }, 'comparison': (lhs, rhs, node) => { - let lhsNarrowedDomain - let rhsNarrowedDomain + let narrowKind: NarrowKind switch(node.operator) { - case '<': - lhsNarrowedDomain = narrowDomain(lhs.domain, rhs.domain, NarrowKind.Smaller) - rhsNarrowedDomain = narrowDomain(rhs.domain, lhs.domain, NarrowKind.Greater) - break - case '>': - lhsNarrowedDomain = narrowDomain(lhs.domain, rhs.domain, NarrowKind.Greater) - rhsNarrowedDomain = narrowDomain(rhs.domain, lhs.domain, NarrowKind.Smaller) - break - case '<=': - lhsNarrowedDomain = narrowDomain(lhs.domain, rhs.domain, NarrowKind.Smaller | NarrowKind.Equal) - rhsNarrowedDomain = narrowDomain(rhs.domain, lhs.domain, NarrowKind.Greater | NarrowKind.Equal) - break - case '>=': - lhsNarrowedDomain = narrowDomain(lhs.domain, rhs.domain, NarrowKind.Greater | NarrowKind.Equal) - rhsNarrowedDomain = narrowDomain(rhs.domain, lhs.domain, NarrowKind.Smaller | NarrowKind.Equal) - break - default: - guard(false, `Unknown binary operator ${node.operator}`) + case '<': narrowKind = NarrowKind.Smaller; break + case '>': narrowKind = NarrowKind.Greater; break + case '<=': narrowKind = NarrowKind.Smaller | NarrowKind.Equal; break + case '>=': narrowKind = NarrowKind.Greater | NarrowKind.Equal; break + default: guard(false, `Unknown binary operator ${node.operator}`) } return new AINodeStore([{ nodeId: lhs.nodeId, expressionId: node.info.id, - domain: lhsNarrowedDomain, + domain: narrowDomain(lhs.domain, rhs.domain, narrowKind), astNode: node, }, { nodeId: rhs.nodeId, expressionId: node.info.id, - domain: rhsNarrowedDomain, + domain: narrowDomain(rhs.domain, lhs.domain, narrowKind ^ 0b110 /* flip < and > but leave = */), astNode: node, }]) } From 28f4046b4e34609e8c79ab21d82e27de4cc1a52e Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Wed, 24 Apr 2024 10:41:23 +0200 Subject: [PATCH 19/70] refactor: added comments and renamed push to register --- src/abstract-interpretation/ainode.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/abstract-interpretation/ainode.ts b/src/abstract-interpretation/ainode.ts index 36f735292d..db47865df0 100644 --- a/src/abstract-interpretation/ainode.ts +++ b/src/abstract-interpretation/ainode.ts @@ -3,9 +3,9 @@ import { assertUnreachable, guard } from '../util/assert' import { Domain, unifyDomains } from './domain' export interface AINode { - // The ID of the node that logically holds the domain + // The ID of the node that logically holds the domain (e.g. the lhs of an assignment) readonly nodeId: NodeId - // The ID of the whole expression that the domain was calculated from + // The ID of the whole expression that the domain was calculated from (e.g. the whole assignment expression) readonly expressionId: NodeId readonly domain: Domain readonly astNode: RNodeWithParent @@ -25,7 +25,8 @@ export class AINodeStore extends Map { } } - push(node: AINode): void { + register(node: AINode): void { + guard(!this.has(node.nodeId), `Node with ID ${node.nodeId} already exists in the store`) this.set(node.nodeId, node) } } @@ -38,9 +39,9 @@ export function mergeDomainStores(...stores: AINodeStore[]): AINodeStore { const existing = result.get(id) guard(existing !== undefined, `Domain for ID ${id} is missing`) const unified = unifyDomains([existing.domain, node.domain]) - result.set(id, {...node, domain: unified}) + result.register({...node, domain: unified}) } else { - result.set(id, node) + result.register(node) } } } From 8a8fa68ddb2c5dbff1a7c9e5ece0ac0a5bfb12a8 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Wed, 24 Apr 2024 11:12:37 +0200 Subject: [PATCH 20/70] feat-fix: added flag to indicate when register should overwrite --- src/abstract-interpretation/ainode.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/abstract-interpretation/ainode.ts b/src/abstract-interpretation/ainode.ts index db47865df0..5adb10073b 100644 --- a/src/abstract-interpretation/ainode.ts +++ b/src/abstract-interpretation/ainode.ts @@ -25,8 +25,8 @@ export class AINodeStore extends Map { } } - register(node: AINode): void { - guard(!this.has(node.nodeId), `Node with ID ${node.nodeId} already exists in the store`) + register(node: AINode, overwrite = false): void { + guard(!this.has(node.nodeId) || overwrite, `Node with ID ${node.nodeId} already exists in the store`) this.set(node.nodeId, node) } } @@ -39,7 +39,7 @@ export function mergeDomainStores(...stores: AINodeStore[]): AINodeStore { const existing = result.get(id) guard(existing !== undefined, `Domain for ID ${id} is missing`) const unified = unifyDomains([existing.domain, node.domain]) - result.register({...node, domain: unified}) + result.register({...node, domain: unified}, true) } else { result.register(node) } From e1e32a5d4f29909aea56f2bdaf8b0e6a2629391e Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Sun, 5 May 2024 11:01:26 +0200 Subject: [PATCH 21/70] feat: intervals like [T, T) or (T, T] are considered empty --- src/abstract-interpretation/domain.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/abstract-interpretation/domain.ts b/src/abstract-interpretation/domain.ts index b6b7166365..7416788fa5 100644 --- a/src/abstract-interpretation/domain.ts +++ b/src/abstract-interpretation/domain.ts @@ -8,19 +8,26 @@ interface IntervalBound { 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 interval ${this.toString()} is not possible. Both bounds should be inclusive if they are equal.`) } toString(): string { + if(this.isEmpty()) { + return '∅' + } return `${this.min.inclusive ? '[' : '('}${this.min.value}, ${this.max.value}${this.max.inclusive ? ']' : ')'}` } + + // An interval is considered empty if it's of the form [T, T) or (T, T] + isEmpty(): boolean { + return this.min.value === this.max.value && !(this.min.inclusive && this.max.inclusive) + } } export class Domain { private readonly _intervals: Set private constructor(intervals: Interval[] = []) { - this._intervals = new Set(unifyOverlappingIntervals(intervals)) + this._intervals = new Set(unifyOverlappingIntervals(intervals).filter(interval => !interval.isEmpty())) } static bottom(): Domain { From e587d5de5499cfa22a6c8e1186511341ae73b358 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Sun, 5 May 2024 11:02:24 +0200 Subject: [PATCH 22/70] feat: add isBottom method to Domain class --- src/abstract-interpretation/domain.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/abstract-interpretation/domain.ts b/src/abstract-interpretation/domain.ts index 7416788fa5..023b17713d 100644 --- a/src/abstract-interpretation/domain.ts +++ b/src/abstract-interpretation/domain.ts @@ -45,6 +45,10 @@ export class Domain { )]) } + isBottom(): boolean { + return this.intervals.size === 0 + } + get intervals(): Set { return this._intervals } @@ -61,7 +65,11 @@ export class Domain { } toString(): string { - return `{${Array.from(this.intervals).join(', ')}}` + if(this.isBottom()) { + return '⊥' + } else { + return `{${Array.from(this.intervals).join(', ')}}` + } } } From d1fde55a418239492cf69d0373a7684d663f7468 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Sun, 5 May 2024 11:06:26 +0200 Subject: [PATCH 23/70] feat: added isSingleton method to Interval class --- src/abstract-interpretation/domain.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/abstract-interpretation/domain.ts b/src/abstract-interpretation/domain.ts index 023b17713d..4cf2b90752 100644 --- a/src/abstract-interpretation/domain.ts +++ b/src/abstract-interpretation/domain.ts @@ -14,6 +14,9 @@ export class Interval { if(this.isEmpty()) { return '∅' } + if(this.isSingleton()) { + return this.min.value.toString() + } return `${this.min.inclusive ? '[' : '('}${this.min.value}, ${this.max.value}${this.max.inclusive ? ']' : ')'}` } @@ -21,6 +24,11 @@ export class Interval { isEmpty(): boolean { return this.min.value === this.max.value && !(this.min.inclusive && this.max.inclusive) } + + // An interval is considered singleton if it's of the form [T, T] + isSingleton(): boolean { + return this.min.value === this.max.value && this.min.inclusive && this.max.inclusive + } } export class Domain { From f5b821cd86b40f7c5ecb0f8674170b03838f76c2 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Tue, 7 May 2024 22:20:36 +0200 Subject: [PATCH 24/70] feat: added dfg to all handlers --- .../handler/binop/binop.ts | 19 +++++-------- .../handler/conditional/conditional.ts | 27 ++++++++----------- .../handler/exprlist/exprlist.ts | 16 +++++------ .../handler/handler.ts | 15 +++++++---- src/abstract-interpretation/processor.ts | 6 ++--- 5 files changed, 37 insertions(+), 46 deletions(-) diff --git a/src/abstract-interpretation/handler/binop/binop.ts b/src/abstract-interpretation/handler/binop/binop.ts index a0723b4eb4..080a57b638 100644 --- a/src/abstract-interpretation/handler/binop/binop.ts +++ b/src/abstract-interpretation/handler/binop/binop.ts @@ -3,35 +3,30 @@ import {aiLogger} from '../../processor' import {BinaryOperatorFlavor, ParentInformation, RBinaryOp} from '../../../r-bridge' import {guard} from '../../../util/assert' import {operators} from './operators' -import { AINode, AINodeStore } from '../../ainode' +import {AINode, AINodeStore} from '../../ainode' +import {DataflowInformation} from '../../../dataflow/internal/info' export type BinOpOperators = { [key in BinaryOperatorFlavor]: (lhs: AINode, rhs: AINode, node: RBinaryOp) => AINodeStore } -export class BinOp implements Handler { +export class BinOp extends Handler { lhs: AINode | undefined rhs: AINode | undefined - constructor(readonly node: RBinaryOp) {} - - getName(): string { - return `Bin Op (${this.node.flavor})` - } - - enter(): void { - aiLogger.trace(`Entered ${this.getName()}`) + constructor(readonly dfg: DataflowInformation, readonly node: RBinaryOp) { + super(dfg, `Bin Op (${node.flavor})`) } exit(): AINodeStore { - aiLogger.trace(`Exited ${this.getName()}`) + aiLogger.trace(`Exited ${this.name}`) 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) } next(aiNodes: AINodeStore): void { - aiLogger.trace(`${this.getName()} received`) + aiLogger.trace(`${this.name} received`) guard(aiNodes.size === 1, 'Welp, next received more than one AINodes') const node = aiNodes.values().next().value as AINode if(this.lhs === undefined) { diff --git a/src/abstract-interpretation/handler/conditional/conditional.ts b/src/abstract-interpretation/handler/conditional/conditional.ts index 8d4b83fc73..08202abd78 100644 --- a/src/abstract-interpretation/handler/conditional/conditional.ts +++ b/src/abstract-interpretation/handler/conditional/conditional.ts @@ -1,26 +1,21 @@ -import { ParentInformation, RIfThenElse } from '../../../r-bridge' -import { guard } from '../../../util/assert' -import { AINodeStore } from '../../ainode' -import { aiLogger } from '../../processor' -import { Handler } from '../handler' +import {ParentInformation, RIfThenElse} from '../../../r-bridge' +import {guard} from '../../../util/assert' +import {AINodeStore} from '../../ainode' +import {aiLogger} from '../../processor' +import {Handler} from '../handler' +import {DataflowInformation} from '../../../dataflow/internal/info' -export class Conditional implements Handler { +export class Conditional extends Handler { condition: AINodeStore | undefined then: AINodeStore | undefined else: AINodeStore | undefined - constructor(readonly node: RIfThenElse) {} - - getName(): string { - return 'IfThenElse' - } - - enter(): void { - aiLogger.trace(`Entered ${this.getName()}`) + constructor(readonly dfg: DataflowInformation, readonly node: RIfThenElse) { + super(dfg, 'IfThenElse') } exit(): AINodeStore { - aiLogger.trace(`Exited ${this.getName()}`) + aiLogger.trace(`Exited ${this.name}`) guard(this.condition !== undefined, `No condition found for conditional ${this.node.info.id}`) guard(this.then !== undefined, `No then-branch found for conditional ${this.node.info.id}`) // TODO: calculate new domain @@ -28,7 +23,7 @@ export class Conditional implements Handler { } next(aiNodes: AINodeStore): void { - aiLogger.trace(`${this.getName()} received`) + aiLogger.trace(`${this.name} received`) if(this.condition === undefined) { this.condition = aiNodes } else if(this.then === undefined) { diff --git a/src/abstract-interpretation/handler/exprlist/exprlist.ts b/src/abstract-interpretation/handler/exprlist/exprlist.ts index 568aef7534..ad656000fd 100644 --- a/src/abstract-interpretation/handler/exprlist/exprlist.ts +++ b/src/abstract-interpretation/handler/exprlist/exprlist.ts @@ -1,16 +1,12 @@ -import { AINodeStore, mergeDomainStores } from '../../ainode' -import { aiLogger } from '../../processor' -import { Handler } from '../handler' +import {AINodeStore, mergeDomainStores} from '../../ainode' +import {Handler} from '../handler' +import {DataflowInformation} from '../../../dataflow/internal/info' -export class ExprList implements Handler { +export class ExprList extends Handler { private exprList: AINodeStore = new AINodeStore() - getName(): string { - return 'ExprList' - } - - enter(): void { - aiLogger.trace(`Entered ${this.getName()}`) + constructor(readonly dfg: DataflowInformation) { + super(dfg, 'ExprList') } exit(): AINodeStore { diff --git a/src/abstract-interpretation/handler/handler.ts b/src/abstract-interpretation/handler/handler.ts index b1855a574c..9f16234880 100644 --- a/src/abstract-interpretation/handler/handler.ts +++ b/src/abstract-interpretation/handler/handler.ts @@ -1,8 +1,13 @@ import {AINodeStore} from '../ainode' +import {DataflowInformation} from '../../dataflow/internal/info' +import {aiLogger} from '../processor' -export interface Handler { - getName: () => string, - enter: () => void - exit: () => AINodeStore - next: (aiNodes: AINodeStore) => void +export abstract class Handler { + protected constructor(protected readonly dfg: DataflowInformation, public readonly name: string) { } + + enter(): void { + aiLogger.trace(`Entered ${this.name}`) + } + abstract exit(): AINodeStore + abstract next(aiNodes: AINodeStore): void } \ No newline at end of file diff --git a/src/abstract-interpretation/processor.ts b/src/abstract-interpretation/processor.ts index 048237bd39..7d3a40baa9 100644 --- a/src/abstract-interpretation/processor.ts +++ b/src/abstract-interpretation/processor.ts @@ -49,11 +49,11 @@ export function runAbstractInterpretation(ast: NormalizedAst, dfg: DataflowInfor visitCfg(cfg, (node, _) => { const astNode = ast.idMap.get(node.id) if(astNode?.type === RType.BinaryOp) { - operationStack.push(new BinOp(astNode)).enter() + operationStack.push(new BinOp(dfg, astNode)).enter() } else if(astNode?.type === RType.IfThenElse) { - operationStack.push(new Conditional(astNode)).enter() + operationStack.push(new Conditional(dfg, astNode)).enter() } else if(astNode?.type === RType.ExpressionList) { - operationStack.push(new ExprList()).enter() + operationStack.push(new ExprList(dfg)).enter() } else if(astNode?.type === RType.Symbol) { operationStack.peek()?.next(new AINodeStore({ nodeId: astNode.info.id, From c7ec544071ad4f0b2b7a2fffe3af785376684f11 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Tue, 7 May 2024 22:48:19 +0200 Subject: [PATCH 25/70] feat: conditionals assign the narrowed domain to the correct node --- .../handler/conditional/conditional.ts | 14 ++++++++++++-- src/abstract-interpretation/processor.ts | 15 +++++++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/abstract-interpretation/handler/conditional/conditional.ts b/src/abstract-interpretation/handler/conditional/conditional.ts index 08202abd78..b6f2376ad9 100644 --- a/src/abstract-interpretation/handler/conditional/conditional.ts +++ b/src/abstract-interpretation/handler/conditional/conditional.ts @@ -1,9 +1,10 @@ import {ParentInformation, RIfThenElse} from '../../../r-bridge' import {guard} from '../../../util/assert' import {AINodeStore} from '../../ainode' -import {aiLogger} from '../../processor' +import {aiLogger, getDfgChildrenOfType} from '../../processor' import {Handler} from '../handler' import {DataflowInformation} from '../../../dataflow/internal/info' +import {EdgeType} from '../../../dataflow' export class Conditional extends Handler { condition: AINodeStore | undefined @@ -25,7 +26,16 @@ export class Conditional extends Handler { next(aiNodes: AINodeStore): void { aiLogger.trace(`${this.name} received`) if(this.condition === undefined) { - this.condition = aiNodes + this.condition = new AINodeStore() + for(const [_, node] of aiNodes) { + const children = getDfgChildrenOfType(node.nodeId, this.dfg, EdgeType.Reads) + for(const child of children ?? []) { + this.condition.register({ + ...node, + nodeId: child + }) + } + } } else if(this.then === undefined) { this.then = aiNodes } else if(this.else === undefined) { diff --git a/src/abstract-interpretation/processor.ts b/src/abstract-interpretation/processor.ts index 7d3a40baa9..21cbcf0ff0 100644 --- a/src/abstract-interpretation/processor.ts +++ b/src/abstract-interpretation/processor.ts @@ -26,13 +26,20 @@ class Stack { } } -function getDomainOfDfgChild(node: NodeId, dfg: DataflowInformation, domainStore: AINodeStore): Domain { +export function getDfgChildrenOfType(node: NodeId, dfg: DataflowInformation, ...types: EdgeType[]): NodeId[] | undefined { const dfgNode: [DataflowGraphVertexInfo, OutgoingEdges] | undefined = dfg.graph.get(node) - guard(dfgNode !== undefined, `No DFG-Node found with ID ${node}`) + if(dfgNode === undefined) { + return undefined + } const [_, children] = dfgNode - const ids = Array.from(children.entries()) - .filter(([_, edge]) => edge.types.has(EdgeType.Reads)) + return Array.from(children.entries()) + .filter(([_, edge]) => types.some(type => edge.types.has(type))) .map(([id, _]) => id) +} + +function getDomainOfDfgChild(node: NodeId, dfg: DataflowInformation, domainStore: AINodeStore): Domain { + const ids = getDfgChildrenOfType(node, dfg, EdgeType.Reads) + guard(ids !== undefined, `No DFG-Node found with ID ${node}`) const domains: Domain[] = [] for(const id of ids) { const domain = domainStore.get(id)?.domain From 9278390304f670074efa14a82294cf710dc6b094 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Tue, 7 May 2024 23:13:34 +0200 Subject: [PATCH 26/70] feat: handler::next can now also return new domains --- .../handler/binop/binop.ts | 12 ++++++------ .../handler/conditional/conditional.ts | 16 +++++++++------- .../handler/exprlist/exprlist.ts | 4 ++-- src/abstract-interpretation/handler/handler.ts | 2 +- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/abstract-interpretation/handler/binop/binop.ts b/src/abstract-interpretation/handler/binop/binop.ts index 080a57b638..4e56c36600 100644 --- a/src/abstract-interpretation/handler/binop/binop.ts +++ b/src/abstract-interpretation/handler/binop/binop.ts @@ -25,16 +25,16 @@ export class BinOp extends Handler { return operators[this.node.flavor](this.lhs, this.rhs, this.node) } - next(aiNodes: AINodeStore): void { + next(aiNodes: AINodeStore): AINodeStore { aiLogger.trace(`${this.name} received`) guard(aiNodes.size === 1, 'Welp, next received more than one AINodes') const node = aiNodes.values().next().value as AINode 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`) + return new AINodeStore(this.lhs = node) } + if(this.rhs === undefined) { + return new AINodeStore(this.rhs = node) + } + guard(false, `BinOp ${this.node.info.id} already has both LHS and RHS`) } } \ No newline at end of file diff --git a/src/abstract-interpretation/handler/conditional/conditional.ts b/src/abstract-interpretation/handler/conditional/conditional.ts index b6f2376ad9..181b365685 100644 --- a/src/abstract-interpretation/handler/conditional/conditional.ts +++ b/src/abstract-interpretation/handler/conditional/conditional.ts @@ -23,7 +23,7 @@ export class Conditional extends Handler { return this.condition } - next(aiNodes: AINodeStore): void { + next(aiNodes: AINodeStore): AINodeStore { aiLogger.trace(`${this.name} received`) if(this.condition === undefined) { this.condition = new AINodeStore() @@ -36,12 +36,14 @@ export class Conditional extends Handler { }) } } - } else if(this.then === undefined) { - this.then = aiNodes - } else if(this.else === undefined) { - this.else = aiNodes - } else { - guard(false, `Conditional ${this.node.info.id} already has condition, then and else`) + return this.condition } + if(this.then === undefined) { + return this.then = aiNodes + } + if(this.else === undefined) { + return this.else = aiNodes + } + guard(false, `Conditional ${this.node.info.id} already has condition, then and else`) } } diff --git a/src/abstract-interpretation/handler/exprlist/exprlist.ts b/src/abstract-interpretation/handler/exprlist/exprlist.ts index ad656000fd..d61e7a3ce2 100644 --- a/src/abstract-interpretation/handler/exprlist/exprlist.ts +++ b/src/abstract-interpretation/handler/exprlist/exprlist.ts @@ -13,7 +13,7 @@ export class ExprList extends Handler { return this.exprList } - next(aiNodes: AINodeStore): void { - this.exprList = mergeDomainStores(aiNodes, this.exprList) + next(aiNodes: AINodeStore): AINodeStore { + return this.exprList = mergeDomainStores(aiNodes, this.exprList) } } diff --git a/src/abstract-interpretation/handler/handler.ts b/src/abstract-interpretation/handler/handler.ts index 9f16234880..92014a4219 100644 --- a/src/abstract-interpretation/handler/handler.ts +++ b/src/abstract-interpretation/handler/handler.ts @@ -9,5 +9,5 @@ export abstract class Handler { aiLogger.trace(`Entered ${this.name}`) } abstract exit(): AINodeStore - abstract next(aiNodes: AINodeStore): void + abstract next(aiNodes: AINodeStore): AINodeStore } \ No newline at end of file From 2ff9e4cf23754803ea5cbdaaa37ba6181b112b38 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Tue, 7 May 2024 23:14:52 +0200 Subject: [PATCH 27/70] feat: add domains to the operationStack --- src/abstract-interpretation/processor.ts | 26 ++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/abstract-interpretation/processor.ts b/src/abstract-interpretation/processor.ts index 21cbcf0ff0..ff7ab9b857 100644 --- a/src/abstract-interpretation/processor.ts +++ b/src/abstract-interpretation/processor.ts @@ -51,18 +51,27 @@ function getDomainOfDfgChild(node: NodeId, dfg: DataflowInformation, domainStore export function runAbstractInterpretation(ast: NormalizedAst, dfg: DataflowInformation): DataflowInformation { const cfg = extractCFG(ast) - const operationStack = new Stack() + const operationStack = new Stack<{ handler: Handler, domains: AINodeStore }>() let domainStore = new AINodeStore() visitCfg(cfg, (node, _) => { const astNode = ast.idMap.get(node.id) if(astNode?.type === RType.BinaryOp) { - operationStack.push(new BinOp(dfg, astNode)).enter() + operationStack.push({ + domains: domainStore, + handler: new BinOp(dfg, astNode) + }).handler.enter() } else if(astNode?.type === RType.IfThenElse) { - operationStack.push(new Conditional(dfg, astNode)).enter() + operationStack.push({ + domains: domainStore, + handler: new Conditional(dfg, astNode) + }).handler.enter() } else if(astNode?.type === RType.ExpressionList) { - operationStack.push(new ExprList(dfg)).enter() + operationStack.push({ + domains: domainStore, + handler: new ExprList(dfg) + }).handler.enter() } else if(astNode?.type === RType.Symbol) { - operationStack.peek()?.next(new AINodeStore({ + operationStack.peek()?.handler.next(new AINodeStore({ nodeId: astNode.info.id, expressionId: astNode.info.id, domain: getDomainOfDfgChild(node.id, dfg, domainStore), @@ -70,7 +79,7 @@ export function runAbstractInterpretation(ast: NormalizedAst, dfg: DataflowInfor })) } else if(astNode?.type === RType.Number){ const num = astNode.content.num - operationStack.peek()?.next(new AINodeStore({ + operationStack.peek()?.handler.next(new AINodeStore({ nodeId: astNode.info.id, expressionId: astNode.info.id, domain: Domain.fromScalar(num), @@ -81,9 +90,10 @@ export function runAbstractInterpretation(ast: NormalizedAst, dfg: DataflowInfor if(operation === undefined) { return } - const operationResult = operation.exit() + const operationResult = operation.handler.exit() domainStore = mergeDomainStores(domainStore, operationResult) - operationStack.peek()?.next(operationResult) + // TODO: use the result from next as a base for the next operation + operationStack.peek()?.handler.next(operationResult) } else { aiLogger.warn(`Unknown node type ${node.type}`) } From 6b719109fa3a73a66cd835696ad766dd091ab4bd Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Sat, 11 May 2024 14:48:46 +0200 Subject: [PATCH 28/70] feat: added better control on how to insert AINodes into the store --- src/abstract-interpretation/ainode.ts | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/abstract-interpretation/ainode.ts b/src/abstract-interpretation/ainode.ts index 5adb10073b..e0d6914a9a 100644 --- a/src/abstract-interpretation/ainode.ts +++ b/src/abstract-interpretation/ainode.ts @@ -11,6 +11,11 @@ export interface AINode { readonly astNode: RNodeWithParent } +export const enum RegisterBehavior { + Overwrite, + Ignore, + Fail +} export class AINodeStore extends Map { constructor(content: AINode[] | AINode | undefined = undefined) { @@ -25,9 +30,21 @@ export class AINodeStore extends Map { } } - register(node: AINode, overwrite = false): void { - guard(!this.has(node.nodeId) || overwrite, `Node with ID ${node.nodeId} already exists in the store`) - this.set(node.nodeId, node) + register(node: AINode, behavior: RegisterBehavior = RegisterBehavior.Fail): void { + if(this.has(node.nodeId)) { + switch(behavior) { + case RegisterBehavior.Overwrite: + this.set(node.nodeId, node) + break + case RegisterBehavior.Ignore: + break + case RegisterBehavior.Fail: + return guard(false, `Node with ID ${node.nodeId} already exists in the store`) + default: assertUnreachable(behavior) + } + } else { + this.set(node.nodeId, node) + } } } @@ -39,7 +56,7 @@ export function mergeDomainStores(...stores: AINodeStore[]): AINodeStore { const existing = result.get(id) guard(existing !== undefined, `Domain for ID ${id} is missing`) const unified = unifyDomains([existing.domain, node.domain]) - result.register({...node, domain: unified}, true) + result.register({...node, domain: unified}, RegisterBehavior.Overwrite) } else { result.register(node) } From 09568022ed6c686860c33c7fa4a9f1461a898b51 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Sat, 11 May 2024 14:50:52 +0200 Subject: [PATCH 29/70] feat: every handler now owns its domains --- .../handler/binop/binop.ts | 8 ++- .../handler/conditional/conditional.ts | 56 +++++++++++++------ .../handler/exprlist/exprlist.ts | 4 +- .../handler/handler.ts | 12 +++- .../handler/nop/nop.ts | 17 ++++++ src/abstract-interpretation/processor.ts | 47 +++++++--------- 6 files changed, 93 insertions(+), 51 deletions(-) create mode 100644 src/abstract-interpretation/handler/nop/nop.ts diff --git a/src/abstract-interpretation/handler/binop/binop.ts b/src/abstract-interpretation/handler/binop/binop.ts index 4e56c36600..f186036e9f 100644 --- a/src/abstract-interpretation/handler/binop/binop.ts +++ b/src/abstract-interpretation/handler/binop/binop.ts @@ -14,8 +14,12 @@ export class BinOp extends Handler { lhs: AINode | undefined rhs: AINode | undefined - constructor(readonly dfg: DataflowInformation, readonly node: RBinaryOp) { - super(dfg, `Bin Op (${node.flavor})`) + constructor( + readonly dfg: DataflowInformation, + readonly domains: AINodeStore, + readonly node: RBinaryOp + ) { + super(dfg, domains, `Bin Op (${node.flavor})`) } exit(): AINodeStore { diff --git a/src/abstract-interpretation/handler/conditional/conditional.ts b/src/abstract-interpretation/handler/conditional/conditional.ts index 181b365685..afb39ec991 100644 --- a/src/abstract-interpretation/handler/conditional/conditional.ts +++ b/src/abstract-interpretation/handler/conditional/conditional.ts @@ -1,6 +1,6 @@ import {ParentInformation, RIfThenElse} from '../../../r-bridge' import {guard} from '../../../util/assert' -import {AINodeStore} from '../../ainode' +import {AINodeStore, RegisterBehavior} from '../../ainode' import {aiLogger, getDfgChildrenOfType} from '../../processor' import {Handler} from '../handler' import {DataflowInformation} from '../../../dataflow/internal/info' @@ -11,39 +11,61 @@ export class Conditional extends Handler { then: AINodeStore | undefined else: AINodeStore | undefined - constructor(readonly dfg: DataflowInformation, readonly node: RIfThenElse) { - super(dfg, 'IfThenElse') + constructor( + readonly dfg: DataflowInformation, + readonly domains: AINodeStore, + readonly node: RIfThenElse + ) { + super(dfg, domains, 'IfThenElse') } exit(): AINodeStore { aiLogger.trace(`Exited ${this.name}`) guard(this.condition !== undefined, `No condition found for conditional ${this.node.info.id}`) guard(this.then !== undefined, `No then-branch found for conditional ${this.node.info.id}`) - // TODO: calculate new domain - return this.condition + + const result = new AINodeStore() + for(const [_, thenNode] of this.then) { + result.register(thenNode) + } + for(const [_, elseNode] of this.else ?? []) { + result.register(elseNode) + } + + return result } - next(aiNodes: AINodeStore): AINodeStore { + next(aiNodes: AINodeStore): void { aiLogger.trace(`${this.name} received`) if(this.condition === undefined) { - this.condition = new AINodeStore() + this.condition = aiNodes for(const [_, node] of aiNodes) { const children = getDfgChildrenOfType(node.nodeId, this.dfg, EdgeType.Reads) for(const child of children ?? []) { - this.condition.register({ + this.domains.register({ ...node, nodeId: child - }) + }, RegisterBehavior.Overwrite) } } - return this.condition - } - if(this.then === undefined) { - return this.then = aiNodes + } else if(this.then === undefined) { + this.then = aiNodes + } else if(this.else === undefined) { + this.else = aiNodes + } else { + guard(false, `Conditional ${this.node.info.id} already has condition, then and else`) } - if(this.else === undefined) { - return this.else = aiNodes - } - guard(false, `Conditional ${this.node.info.id} already has condition, then and else`) } } + +/* const result = new AINodeStore() +for(const [_, conditionNode] of this.condition) { + console.log(`cond: ${conditionNode.astNode.lexeme}`) + for(const [_, thenNode] of this.then) { + const intersection = intersectDomains(conditionNode.domain, thenNode.domain) + } + for(const [_, elseNode] of this.else ?? []) { + console.log(`else: ${elseNode.astNode.lexeme}`) + } +} +return result */ diff --git a/src/abstract-interpretation/handler/exprlist/exprlist.ts b/src/abstract-interpretation/handler/exprlist/exprlist.ts index d61e7a3ce2..50dbb6bbd7 100644 --- a/src/abstract-interpretation/handler/exprlist/exprlist.ts +++ b/src/abstract-interpretation/handler/exprlist/exprlist.ts @@ -5,8 +5,8 @@ import {DataflowInformation} from '../../../dataflow/internal/info' export class ExprList extends Handler { private exprList: AINodeStore = new AINodeStore() - constructor(readonly dfg: DataflowInformation) { - super(dfg, 'ExprList') + constructor(readonly dfg: DataflowInformation, readonly domains: AINodeStore) { + super(dfg, domains, 'ExprList') } exit(): AINodeStore { diff --git a/src/abstract-interpretation/handler/handler.ts b/src/abstract-interpretation/handler/handler.ts index 92014a4219..3518fd893a 100644 --- a/src/abstract-interpretation/handler/handler.ts +++ b/src/abstract-interpretation/handler/handler.ts @@ -3,11 +3,19 @@ import {DataflowInformation} from '../../dataflow/internal/info' import {aiLogger} from '../processor' export abstract class Handler { - protected constructor(protected readonly dfg: DataflowInformation, public readonly name: string) { } + protected constructor( + protected readonly dfg: DataflowInformation, + public domains: AINodeStore, + public readonly name: string + ) { } enter(): void { aiLogger.trace(`Entered ${this.name}`) } + abstract exit(): AINodeStore - abstract next(aiNodes: AINodeStore): AINodeStore + + next(aiNodes: AINodeStore): void { + aiLogger.trace(`${this.name} received`) + } } \ No newline at end of file diff --git a/src/abstract-interpretation/handler/nop/nop.ts b/src/abstract-interpretation/handler/nop/nop.ts new file mode 100644 index 0000000000..f372ce5ff0 --- /dev/null +++ b/src/abstract-interpretation/handler/nop/nop.ts @@ -0,0 +1,17 @@ +import {AINodeStore} from '../../ainode' +import {Handler} from '../handler' +import {DataflowInformation} from '../../../dataflow/internal/info' + +export class Nop extends Handler { + constructor(readonly dfg: DataflowInformation, readonly domains: AINodeStore) { + super(dfg, domains, 'Base') + } + + exit(): AINodeStore { + return new AINodeStore() + } + + next(aiNodes: AINodeStore): AINodeStore { + return aiNodes + } +} diff --git a/src/abstract-interpretation/processor.ts b/src/abstract-interpretation/processor.ts index ff7ab9b857..62d20dca07 100644 --- a/src/abstract-interpretation/processor.ts +++ b/src/abstract-interpretation/processor.ts @@ -11,6 +11,7 @@ import {Domain, unifyDomains} from './domain' import {log} from '../util/log' import {ExprList} from './handler/exprlist/exprlist' import {AINodeStore, mergeDomainStores} from './ainode' +import {Nop} from './handler/nop/nop' export const aiLogger = log.getSubLogger({name: 'abstract-interpretation'}) @@ -51,49 +52,39 @@ function getDomainOfDfgChild(node: NodeId, dfg: DataflowInformation, domainStore export function runAbstractInterpretation(ast: NormalizedAst, dfg: DataflowInformation): DataflowInformation { const cfg = extractCFG(ast) - const operationStack = new Stack<{ handler: Handler, domains: AINodeStore }>() - let domainStore = new AINodeStore() + const operationStack = new Stack() + operationStack.push(new Nop(dfg, new AINodeStore())).enter() visitCfg(cfg, (node, _) => { const astNode = ast.idMap.get(node.id) + const top = operationStack.peek() + guard(top !== undefined, 'No operation on the stack') if(astNode?.type === RType.BinaryOp) { - operationStack.push({ - domains: domainStore, - handler: new BinOp(dfg, astNode) - }).handler.enter() + operationStack.push(new BinOp(dfg, top.domains, astNode)).enter() } else if(astNode?.type === RType.IfThenElse) { - operationStack.push({ - domains: domainStore, - handler: new Conditional(dfg, astNode) - }).handler.enter() + operationStack.push(new Conditional(dfg, top.domains, astNode)).enter() } else if(astNode?.type === RType.ExpressionList) { - operationStack.push({ - domains: domainStore, - handler: new ExprList(dfg) - }).handler.enter() + operationStack.push(new ExprList(dfg, top.domains)).enter() } else if(astNode?.type === RType.Symbol) { - operationStack.peek()?.handler.next(new AINodeStore({ + top.next(new AINodeStore({ nodeId: astNode.info.id, expressionId: astNode.info.id, - domain: getDomainOfDfgChild(node.id, dfg, domainStore), + domain: getDomainOfDfgChild(node.id, dfg, top.domains), astNode: astNode, })) - } else if(astNode?.type === RType.Number){ - const num = astNode.content.num - operationStack.peek()?.handler.next(new AINodeStore({ + } else if(astNode?.type === RType.Number) { + top.next(new AINodeStore({ nodeId: astNode.info.id, expressionId: astNode.info.id, - domain: Domain.fromScalar(num), + domain: Domain.fromScalar(astNode.content.num), astNode: astNode, })) } else if(node.type === CfgVertexType.EndMarker) { - const operation = operationStack.pop() - if(operation === undefined) { - return - } - const operationResult = operation.handler.exit() - domainStore = mergeDomainStores(domainStore, operationResult) - // TODO: use the result from next as a base for the next operation - operationStack.peek()?.handler.next(operationResult) + const operationResult = operationStack.pop()?.exit() + guard(operationResult !== undefined, 'No operation result') + const newTop = operationStack.peek() + guard(newTop !== undefined, 'No operation on the stack') + newTop.domains = mergeDomainStores(newTop.domains, operationResult) + newTop.next(operationResult) } else { aiLogger.warn(`Unknown node type ${node.type}`) } From e4da4c31805a1ad6a77e1d3a30063977e456fefd Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Sat, 11 May 2024 14:51:40 +0200 Subject: [PATCH 30/70] refactor: formatting --- src/abstract-interpretation/ainode.ts | 6 +++--- src/abstract-interpretation/domain.ts | 1 - src/abstract-interpretation/handler/binop/operators.ts | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/abstract-interpretation/ainode.ts b/src/abstract-interpretation/ainode.ts index e0d6914a9a..1e6d67bef2 100644 --- a/src/abstract-interpretation/ainode.ts +++ b/src/abstract-interpretation/ainode.ts @@ -1,6 +1,6 @@ -import { NodeId, ParentInformation, RNodeWithParent } from '../r-bridge' -import { assertUnreachable, guard } from '../util/assert' -import { Domain, unifyDomains } from './domain' +import {NodeId, ParentInformation, RNodeWithParent} from '../r-bridge' +import {assertUnreachable, guard} from '../util/assert' +import {Domain, unifyDomains} from './domain' export interface AINode { // The ID of the node that logically holds the domain (e.g. the lhs of an assignment) diff --git a/src/abstract-interpretation/domain.ts b/src/abstract-interpretation/domain.ts index 4cf2b90752..b346c88a97 100644 --- a/src/abstract-interpretation/domain.ts +++ b/src/abstract-interpretation/domain.ts @@ -223,7 +223,6 @@ interface IntervalOverlap { function flipInclusiveness(intervalBound: IntervalBound): IntervalBound { return {value: intervalBound.value, inclusive: !intervalBound.inclusive} - } export function overlapIntervals(interval1: Interval, interval2: Interval): IntervalOverlap { diff --git a/src/abstract-interpretation/handler/binop/operators.ts b/src/abstract-interpretation/handler/binop/operators.ts index 9086f62d9e..f9125a8c63 100644 --- a/src/abstract-interpretation/handler/binop/operators.ts +++ b/src/abstract-interpretation/handler/binop/operators.ts @@ -1,7 +1,7 @@ import {guard} from '../../../util/assert' import {BinOpOperators} from './binop' -import {NarrowKind, addDomains, narrowDomain, subtractDomains} from '../../domain' -import { AINodeStore } from '../../ainode' +import {addDomains, narrowDomain, NarrowKind, subtractDomains} from '../../domain' +import {AINodeStore} from '../../ainode' export const operators: BinOpOperators = { 'assignment': (lhs, rhs, node) => { From 9bfc404f5e1a660ea1ffa31f35a8eec79670c4c3 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Wed, 15 May 2024 11:03:23 +0200 Subject: [PATCH 31/70] feat: multiple changes; I was too lazy to separate them Here are the changes I did: - Introduced the notion of "scopes" to the AINodeStore When pushing new layers to the operationStack, we would need to copy the AINodeScore of the current top, so we don't modify the domains from outer scopes. This new way removes the need for copies :) - More sensible default implementations of handler methods - Fixed implementations of BinOp handler - Other smaller code changes --- src/abstract-interpretation/ainode.ts | 102 +++++++++++++----- .../handler/binop/binop.ts | 39 ++++--- .../handler/binop/operators.ts | 10 +- .../handler/conditional/conditional.ts | 28 ++--- .../handler/exprlist/exprlist.ts | 14 +-- .../handler/handler.ts | 11 +- .../handler/nop/nop.ts | 10 +- src/abstract-interpretation/processor.ts | 15 ++- 8 files changed, 130 insertions(+), 99 deletions(-) diff --git a/src/abstract-interpretation/ainode.ts b/src/abstract-interpretation/ainode.ts index 1e6d67bef2..79453722c9 100644 --- a/src/abstract-interpretation/ainode.ts +++ b/src/abstract-interpretation/ainode.ts @@ -14,53 +14,107 @@ export interface AINode { export const enum RegisterBehavior { Overwrite, Ignore, - Fail + Fail, + Merge } -export class AINodeStore extends Map { - constructor(content: AINode[] | AINode | undefined = undefined) { +export class AINodeStore implements Iterable { + private readonly map: Map + + private constructor(content: AINode[] | AINode | undefined = undefined, private readonly parent: AINodeStore | undefined = undefined) { if(Array.isArray(content)) { - super(content.map(node => [node.nodeId, node])) + this.map = new Map(content.map(node => [node.nodeId, node])) } else if(content !== undefined) { - super([[content.nodeId, content]]) + this.map = new Map([[content.nodeId, content]]) } else if(content === undefined) { - super() + this.map = new Map() } else { assertUnreachable(content) } } + static empty(): AINodeStore { + return new AINodeStore() + } + + static from(content: AINode[] | AINode): AINodeStore { + return new AINodeStore(content) + } + + static withParent(parent: AINodeStore): AINodeStore { + return new AINodeStore(undefined, parent) + } + + has(id: NodeId): boolean { + return this.map.has(id) || (this.parent?.has(id) ?? false) + } + + get(id: NodeId | undefined): AINode | undefined { + if(id === undefined) { + return undefined + } + return this.map.get(id) ?? this.parent?.get(id) + } + + get size(): number { + return this.map.size + (this.parent?.size ?? 0) + } + + get top(): AINodeStore { + return new AINodeStore(Array.from(this.map.values()), undefined) + } + register(node: AINode, behavior: RegisterBehavior = RegisterBehavior.Fail): void { - if(this.has(node.nodeId)) { + const existing = this.get(node.nodeId) + if(existing !== undefined) { switch(behavior) { case RegisterBehavior.Overwrite: - this.set(node.nodeId, node) + // Even if a parent contains the node, we will set it in the top store, so + // outer scopes are not affected by inner scopes + this.map.set(node.nodeId, node) break case RegisterBehavior.Ignore: break case RegisterBehavior.Fail: - return guard(false, `Node with ID ${node.nodeId} already exists in the store`) + return guard(existing === node, `Node with ID ${node.nodeId} already exists in the store`) + case RegisterBehavior.Merge: { + const existing = this.map.get(node.nodeId) + guard(existing !== undefined, `Node with ID ${node.nodeId} should exist`) + this.register({...node, domain: unifyDomains([existing.domain, node.domain])}, RegisterBehavior.Overwrite) + break + } default: assertUnreachable(behavior) } } else { - this.set(node.nodeId, node) + this.map.set(node.nodeId, node) } } -} -export function mergeDomainStores(...stores: AINodeStore[]): AINodeStore { - const result = new AINodeStore() - for(const store of stores) { - for(const [id, node] of store) { - if(result.has(id)) { - const existing = result.get(id) - guard(existing !== undefined, `Domain for ID ${id} is missing`) - const unified = unifyDomains([existing.domain, node.domain]) - result.register({...node, domain: unified}, RegisterBehavior.Overwrite) - } else { - result.register(node) - } + [Symbol.iterator](): Iterator { + return composeIterators(this.map.values(), this.parent?.[Symbol.iterator]()) + } + + updateWith(domains: AINodeStore): void { + for(const node of domains) { + this.register(node, RegisterBehavior.Overwrite) } } - return result +} + +function* composeIterators(first: Iterator, second: Iterator | undefined): Iterator { + let result = first.next() + while(!result.done) { + yield result.value + result = first.next() + } + + if(second === undefined) { + return + } + + result = second.next() + while(!result.done) { + yield result.value + result = second.next() + } } diff --git a/src/abstract-interpretation/handler/binop/binop.ts b/src/abstract-interpretation/handler/binop/binop.ts index f186036e9f..d633c1b238 100644 --- a/src/abstract-interpretation/handler/binop/binop.ts +++ b/src/abstract-interpretation/handler/binop/binop.ts @@ -1,6 +1,5 @@ import {Handler} from '../handler' -import {aiLogger} from '../../processor' -import {BinaryOperatorFlavor, ParentInformation, RBinaryOp} from '../../../r-bridge' +import {BinaryOperatorFlavor, NodeId, ParentInformation, RBinaryOp} from '../../../r-bridge' import {guard} from '../../../util/assert' import {operators} from './operators' import {AINode, AINodeStore} from '../../ainode' @@ -11,34 +10,34 @@ export type BinOpOperators = { } export class BinOp extends Handler { - lhs: AINode | undefined - rhs: AINode | undefined + lhs: NodeId | undefined + rhs: NodeId | undefined constructor( - readonly dfg: DataflowInformation, - readonly domains: AINodeStore, - readonly node: RBinaryOp + dfg: DataflowInformation, + domains: AINodeStore, + private readonly node: RBinaryOp ) { super(dfg, domains, `Bin Op (${node.flavor})`) } exit(): AINodeStore { - aiLogger.trace(`Exited ${this.name}`) - 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) + const lhs = this.domains.get(this.lhs) + const rhs = this.domains.get(this.rhs) + guard(lhs !== undefined, `No LHS found for assignment ${this.node.info.id}`) + guard(rhs !== undefined, `No RHS found for assignment ${this.node.info.id}`) + this.domains.updateWith(operators[this.node.flavor](lhs, rhs, this.node)) + return super.exit() } - next(aiNodes: AINodeStore): AINodeStore { - aiLogger.trace(`${this.name} received`) - guard(aiNodes.size === 1, 'Welp, next received more than one AINodes') - const node = aiNodes.values().next().value as AINode + next(aiNodes: AINodeStore): void { + super.next(aiNodes) if(this.lhs === undefined) { - return new AINodeStore(this.lhs = node) + this.lhs = this.node.lhs.info.id + } else if(this.rhs === undefined) { + this.rhs = this.node.rhs.info.id + } else { + guard(false, `BinOp ${this.node.info.id} already has both LHS and RHS`) } - if(this.rhs === undefined) { - return new AINodeStore(this.rhs = node) - } - guard(false, `BinOp ${this.node.info.id} already has both LHS and RHS`) } } \ No newline at end of file diff --git a/src/abstract-interpretation/handler/binop/operators.ts b/src/abstract-interpretation/handler/binop/operators.ts index f9125a8c63..8963aaf2db 100644 --- a/src/abstract-interpretation/handler/binop/operators.ts +++ b/src/abstract-interpretation/handler/binop/operators.ts @@ -5,7 +5,7 @@ import {AINodeStore} from '../../ainode' export const operators: BinOpOperators = { 'assignment': (lhs, rhs, node) => { - return new AINodeStore({ + return AINodeStore.from({ nodeId: lhs.nodeId, expressionId: node.info.id, domain: rhs.domain, @@ -15,14 +15,14 @@ export const operators: BinOpOperators = { 'arithmetic': (lhs, rhs, node) => { switch(node.operator) { case '+': - return new AINodeStore({ + return AINodeStore.from({ nodeId: node.info.id, expressionId: node.info.id, domain: addDomains(lhs.domain, rhs.domain), astNode: node, }) case '-': - return new AINodeStore({ + return AINodeStore.from({ nodeId: node.info.id, expressionId: node.info.id, domain: subtractDomains(lhs.domain, rhs.domain), @@ -47,7 +47,9 @@ export const operators: BinOpOperators = { case '>=': narrowKind = NarrowKind.Greater | NarrowKind.Equal; break default: guard(false, `Unknown binary operator ${node.operator}`) } - return new AINodeStore([{ + // FIXME: We should not set the domain of the operands! + // But if we would only set the domain of the whole expression, we could only narrow one operand. + return AINodeStore.from([{ nodeId: lhs.nodeId, expressionId: node.info.id, domain: narrowDomain(lhs.domain, rhs.domain, narrowKind), diff --git a/src/abstract-interpretation/handler/conditional/conditional.ts b/src/abstract-interpretation/handler/conditional/conditional.ts index afb39ec991..79e3026ada 100644 --- a/src/abstract-interpretation/handler/conditional/conditional.ts +++ b/src/abstract-interpretation/handler/conditional/conditional.ts @@ -12,9 +12,9 @@ export class Conditional extends Handler { else: AINodeStore | undefined constructor( - readonly dfg: DataflowInformation, - readonly domains: AINodeStore, - readonly node: RIfThenElse + dfg: DataflowInformation, + domains: AINodeStore, + private readonly node: RIfThenElse ) { super(dfg, domains, 'IfThenElse') } @@ -24,11 +24,11 @@ export class Conditional extends Handler { guard(this.condition !== undefined, `No condition found for conditional ${this.node.info.id}`) guard(this.then !== undefined, `No then-branch found for conditional ${this.node.info.id}`) - const result = new AINodeStore() - for(const [_, thenNode] of this.then) { + const result = AINodeStore.empty() + for(const thenNode of this.then) { result.register(thenNode) } - for(const [_, elseNode] of this.else ?? []) { + for(const elseNode of this.else ?? []) { result.register(elseNode) } @@ -39,7 +39,7 @@ export class Conditional extends Handler { aiLogger.trace(`${this.name} received`) if(this.condition === undefined) { this.condition = aiNodes - for(const [_, node] of aiNodes) { + for(const node of aiNodes) { const children = getDfgChildrenOfType(node.nodeId, this.dfg, EdgeType.Reads) for(const child of children ?? []) { this.domains.register({ @@ -56,16 +56,4 @@ export class Conditional extends Handler { guard(false, `Conditional ${this.node.info.id} already has condition, then and else`) } } -} - -/* const result = new AINodeStore() -for(const [_, conditionNode] of this.condition) { - console.log(`cond: ${conditionNode.astNode.lexeme}`) - for(const [_, thenNode] of this.then) { - const intersection = intersectDomains(conditionNode.domain, thenNode.domain) - } - for(const [_, elseNode] of this.else ?? []) { - console.log(`else: ${elseNode.astNode.lexeme}`) - } -} -return result */ +} \ No newline at end of file diff --git a/src/abstract-interpretation/handler/exprlist/exprlist.ts b/src/abstract-interpretation/handler/exprlist/exprlist.ts index 50dbb6bbd7..6d4680b6ee 100644 --- a/src/abstract-interpretation/handler/exprlist/exprlist.ts +++ b/src/abstract-interpretation/handler/exprlist/exprlist.ts @@ -1,19 +1,9 @@ -import {AINodeStore, mergeDomainStores} from '../../ainode' +import {AINodeStore} from '../../ainode' import {Handler} from '../handler' import {DataflowInformation} from '../../../dataflow/internal/info' export class ExprList extends Handler { - private exprList: AINodeStore = new AINodeStore() - - constructor(readonly dfg: DataflowInformation, readonly domains: AINodeStore) { + constructor(dfg: DataflowInformation, domains: AINodeStore) { super(dfg, domains, 'ExprList') } - - exit(): AINodeStore { - return this.exprList - } - - next(aiNodes: AINodeStore): AINodeStore { - return this.exprList = mergeDomainStores(aiNodes, this.exprList) - } } diff --git a/src/abstract-interpretation/handler/handler.ts b/src/abstract-interpretation/handler/handler.ts index 3518fd893a..a2b244e2e9 100644 --- a/src/abstract-interpretation/handler/handler.ts +++ b/src/abstract-interpretation/handler/handler.ts @@ -5,7 +5,7 @@ import {aiLogger} from '../processor' export abstract class Handler { protected constructor( protected readonly dfg: DataflowInformation, - public domains: AINodeStore, + private _domains: AINodeStore, public readonly name: string ) { } @@ -13,9 +13,16 @@ export abstract class Handler { aiLogger.trace(`Entered ${this.name}`) } - abstract exit(): AINodeStore + exit(): AINodeStore { + return this.domains.top + } next(aiNodes: AINodeStore): void { aiLogger.trace(`${this.name} received`) + this.domains.updateWith(aiNodes) + } + + get domains(): AINodeStore { + return this._domains } } \ No newline at end of file diff --git a/src/abstract-interpretation/handler/nop/nop.ts b/src/abstract-interpretation/handler/nop/nop.ts index f372ce5ff0..2bfe54e0b7 100644 --- a/src/abstract-interpretation/handler/nop/nop.ts +++ b/src/abstract-interpretation/handler/nop/nop.ts @@ -3,15 +3,7 @@ import {Handler} from '../handler' import {DataflowInformation} from '../../../dataflow/internal/info' export class Nop extends Handler { - constructor(readonly dfg: DataflowInformation, readonly domains: AINodeStore) { + constructor(dfg: DataflowInformation, domains: AINodeStore) { super(dfg, domains, 'Base') } - - exit(): AINodeStore { - return new AINodeStore() - } - - next(aiNodes: AINodeStore): AINodeStore { - return aiNodes - } } diff --git a/src/abstract-interpretation/processor.ts b/src/abstract-interpretation/processor.ts index 62d20dca07..283fcea48b 100644 --- a/src/abstract-interpretation/processor.ts +++ b/src/abstract-interpretation/processor.ts @@ -10,7 +10,7 @@ import {Conditional} from './handler/conditional/conditional' import {Domain, unifyDomains} from './domain' import {log} from '../util/log' import {ExprList} from './handler/exprlist/exprlist' -import {AINodeStore, mergeDomainStores} from './ainode' +import {AINodeStore} from './ainode' import {Nop} from './handler/nop/nop' export const aiLogger = log.getSubLogger({name: 'abstract-interpretation'}) @@ -53,26 +53,26 @@ function getDomainOfDfgChild(node: NodeId, dfg: DataflowInformation, domainStore export function runAbstractInterpretation(ast: NormalizedAst, dfg: DataflowInformation): DataflowInformation { const cfg = extractCFG(ast) const operationStack = new Stack() - operationStack.push(new Nop(dfg, new AINodeStore())).enter() + operationStack.push(new Nop(dfg, AINodeStore.empty())).enter() visitCfg(cfg, (node, _) => { const astNode = ast.idMap.get(node.id) const top = operationStack.peek() guard(top !== undefined, 'No operation on the stack') if(astNode?.type === RType.BinaryOp) { - operationStack.push(new BinOp(dfg, top.domains, astNode)).enter() + operationStack.push(new BinOp(dfg, AINodeStore.withParent(top.domains), astNode)).enter() } else if(astNode?.type === RType.IfThenElse) { - operationStack.push(new Conditional(dfg, top.domains, astNode)).enter() + operationStack.push(new Conditional(dfg, AINodeStore.withParent(top.domains), astNode)).enter() } else if(astNode?.type === RType.ExpressionList) { - operationStack.push(new ExprList(dfg, top.domains)).enter() + operationStack.push(new ExprList(dfg, AINodeStore.withParent(top.domains))).enter() } else if(astNode?.type === RType.Symbol) { - top.next(new AINodeStore({ + top.next(AINodeStore.from({ nodeId: astNode.info.id, expressionId: astNode.info.id, domain: getDomainOfDfgChild(node.id, dfg, top.domains), astNode: astNode, })) } else if(astNode?.type === RType.Number) { - top.next(new AINodeStore({ + top.next(AINodeStore.from({ nodeId: astNode.info.id, expressionId: astNode.info.id, domain: Domain.fromScalar(astNode.content.num), @@ -83,7 +83,6 @@ export function runAbstractInterpretation(ast: NormalizedAst, dfg: DataflowInfor guard(operationResult !== undefined, 'No operation result') const newTop = operationStack.peek() guard(newTop !== undefined, 'No operation on the stack') - newTop.domains = mergeDomainStores(newTop.domains, operationResult) newTop.next(operationResult) } else { aiLogger.warn(`Unknown node type ${node.type}`) From 4dd6450d195356d71a593cc0a4be372bb9bd2246 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Wed, 15 May 2024 11:06:13 +0200 Subject: [PATCH 32/70] refactor: swapped Nop with ExprList since they behave identically --- src/abstract-interpretation/handler/nop/nop.ts | 9 --------- src/abstract-interpretation/processor.ts | 3 +-- 2 files changed, 1 insertion(+), 11 deletions(-) delete mode 100644 src/abstract-interpretation/handler/nop/nop.ts diff --git a/src/abstract-interpretation/handler/nop/nop.ts b/src/abstract-interpretation/handler/nop/nop.ts deleted file mode 100644 index 2bfe54e0b7..0000000000 --- a/src/abstract-interpretation/handler/nop/nop.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {AINodeStore} from '../../ainode' -import {Handler} from '../handler' -import {DataflowInformation} from '../../../dataflow/internal/info' - -export class Nop extends Handler { - constructor(dfg: DataflowInformation, domains: AINodeStore) { - super(dfg, domains, 'Base') - } -} diff --git a/src/abstract-interpretation/processor.ts b/src/abstract-interpretation/processor.ts index 283fcea48b..a906d0296c 100644 --- a/src/abstract-interpretation/processor.ts +++ b/src/abstract-interpretation/processor.ts @@ -11,7 +11,6 @@ import {Domain, unifyDomains} from './domain' import {log} from '../util/log' import {ExprList} from './handler/exprlist/exprlist' import {AINodeStore} from './ainode' -import {Nop} from './handler/nop/nop' export const aiLogger = log.getSubLogger({name: 'abstract-interpretation'}) @@ -53,7 +52,7 @@ function getDomainOfDfgChild(node: NodeId, dfg: DataflowInformation, domainStore export function runAbstractInterpretation(ast: NormalizedAst, dfg: DataflowInformation): DataflowInformation { const cfg = extractCFG(ast) const operationStack = new Stack() - operationStack.push(new Nop(dfg, AINodeStore.empty())).enter() + operationStack.push(new ExprList(dfg, AINodeStore.empty())).enter() visitCfg(cfg, (node, _) => { const astNode = ast.idMap.get(node.id) const top = operationStack.peek() From 06d704dd07e9d4fc4b1f9432dce92a2015ce8de1 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Wed, 15 May 2024 15:11:29 +0200 Subject: [PATCH 33/70] feat: don't throw, if we haven't yet calculated a domain for a dfg child --- src/abstract-interpretation/processor.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/abstract-interpretation/processor.ts b/src/abstract-interpretation/processor.ts index a906d0296c..ebbabce5e4 100644 --- a/src/abstract-interpretation/processor.ts +++ b/src/abstract-interpretation/processor.ts @@ -38,15 +38,12 @@ export function getDfgChildrenOfType(node: NodeId, dfg: DataflowInformation, ... } function getDomainOfDfgChild(node: NodeId, dfg: DataflowInformation, domainStore: AINodeStore): Domain { - const ids = getDfgChildrenOfType(node, dfg, EdgeType.Reads) - guard(ids !== undefined, `No DFG-Node found with ID ${node}`) - const domains: Domain[] = [] - for(const id of ids) { - const domain = domainStore.get(id)?.domain - guard(domain !== undefined, `No domain found for ID ${id}`) - domains.push(domain) - } - return unifyDomains(domains) + guard(dfg.graph.hasNode(node, true), `No DFG-Node found with ID ${node}`) + const domains = getDfgChildrenOfType(node, dfg, EdgeType.Reads) + ?.map(id => domainStore.get(id)?.domain) + .filter(domain => domain !== undefined) + .map(domain => domain as Domain) + return unifyDomains(domains ?? []) } export function runAbstractInterpretation(ast: NormalizedAst, dfg: DataflowInformation): DataflowInformation { From 1f6c2ab3a888e43dc72f19c56350a926784ff4d9 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Wed, 15 May 2024 15:13:55 +0200 Subject: [PATCH 34/70] feat: comparisons generate additional Domains for the else-case --- .../handler/binop/operators.ts | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/abstract-interpretation/handler/binop/operators.ts b/src/abstract-interpretation/handler/binop/operators.ts index 8963aaf2db..42e8a219e7 100644 --- a/src/abstract-interpretation/handler/binop/operators.ts +++ b/src/abstract-interpretation/handler/binop/operators.ts @@ -1,7 +1,7 @@ import {guard} from '../../../util/assert' import {BinOpOperators} from './binop' import {addDomains, narrowDomain, NarrowKind, subtractDomains} from '../../domain' -import {AINodeStore} from '../../ainode' +import {AINode, AINodeStore} from '../../ainode' export const operators: BinOpOperators = { 'assignment': (lhs, rhs, node) => { @@ -47,18 +47,23 @@ export const operators: BinOpOperators = { case '>=': narrowKind = NarrowKind.Greater | NarrowKind.Equal; break default: guard(false, `Unknown binary operator ${node.operator}`) } + const calculateDomains = (lhs: AINode, rhs: AINode, narrowKind: NarrowKind, idSuffix = ''): AINode[] => { + return [{ + nodeId: lhs.nodeId + idSuffix, + expressionId: node.info.id, + domain: narrowDomain(lhs.domain, rhs.domain, narrowKind), + astNode: node, + }, { + nodeId: rhs.nodeId + idSuffix, + expressionId: node.info.id, + domain: narrowDomain(rhs.domain, lhs.domain, narrowKind ^ 0b110 /* flip < and > but leave = */), + astNode: node, + }] + } // FIXME: We should not set the domain of the operands! // But if we would only set the domain of the whole expression, we could only narrow one operand. - return AINodeStore.from([{ - nodeId: lhs.nodeId, - expressionId: node.info.id, - domain: narrowDomain(lhs.domain, rhs.domain, narrowKind), - astNode: node, - }, { - nodeId: rhs.nodeId, - expressionId: node.info.id, - domain: narrowDomain(rhs.domain, lhs.domain, narrowKind ^ 0b110 /* flip < and > but leave = */), - astNode: node, - }]) + const thenDomains = calculateDomains(lhs, rhs, narrowKind) + const elseDomains = calculateDomains(lhs, rhs, narrowKind ^ 0b111 /* flip everything */, '-else') + return AINodeStore.from(thenDomains.concat(elseDomains)) } } From bad230e8d736de896a651156d39817606b8ace2c Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Wed, 15 May 2024 15:15:20 +0200 Subject: [PATCH 35/70] feat: the conditional handler now uses the -else domains --- .../handler/conditional/conditional.ts | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/abstract-interpretation/handler/conditional/conditional.ts b/src/abstract-interpretation/handler/conditional/conditional.ts index 79e3026ada..9b6dec02eb 100644 --- a/src/abstract-interpretation/handler/conditional/conditional.ts +++ b/src/abstract-interpretation/handler/conditional/conditional.ts @@ -10,6 +10,8 @@ export class Conditional extends Handler { condition: AINodeStore | undefined then: AINodeStore | undefined else: AINodeStore | undefined + thenDomains = AINodeStore.empty() + elseDomains = AINodeStore.empty() constructor( dfg: DataflowInformation, @@ -40,16 +42,26 @@ export class Conditional extends Handler { if(this.condition === undefined) { this.condition = aiNodes for(const node of aiNodes) { - const children = getDfgChildrenOfType(node.nodeId, this.dfg, EdgeType.Reads) - for(const child of children ?? []) { - this.domains.register({ - ...node, - nodeId: child - }, RegisterBehavior.Overwrite) + const isElseNode = node.nodeId.endsWith('-else') + const cleanedId = isElseNode ? node.nodeId.slice(0, -5) : node.nodeId + for(const child of getDfgChildrenOfType(cleanedId, this.dfg, EdgeType.Reads) ?? []) { + if(isElseNode) { + this.elseDomains.register({ + ...node, + nodeId: child + }, RegisterBehavior.Overwrite) + } else { + this.thenDomains.register({ + ...node, + nodeId: child + }, RegisterBehavior.Overwrite) + } } } + this.domains.updateWith(this.thenDomains) } else if(this.then === undefined) { this.then = aiNodes + this.domains.updateWith(this.elseDomains) } else if(this.else === undefined) { this.else = aiNodes } else { From 4e46327139fcc7ff8b9cc6021cd720f24f35168b Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Wed, 15 May 2024 16:23:17 +0200 Subject: [PATCH 36/70] refactor: intervals are now bottom instead of empty --- src/abstract-interpretation/domain.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/abstract-interpretation/domain.ts b/src/abstract-interpretation/domain.ts index b346c88a97..551dc61930 100644 --- a/src/abstract-interpretation/domain.ts +++ b/src/abstract-interpretation/domain.ts @@ -11,7 +11,7 @@ export class Interval { } toString(): string { - if(this.isEmpty()) { + if(this.isBottom()) { return '∅' } if(this.isSingleton()) { @@ -21,7 +21,7 @@ export class Interval { } // An interval is considered empty if it's of the form [T, T) or (T, T] - isEmpty(): boolean { + isBottom(): boolean { return this.min.value === this.max.value && !(this.min.inclusive && this.max.inclusive) } @@ -35,7 +35,7 @@ export class Domain { private readonly _intervals: Set private constructor(intervals: Interval[] = []) { - this._intervals = new Set(unifyOverlappingIntervals(intervals).filter(interval => !interval.isEmpty())) + this._intervals = new Set(unifyOverlappingIntervals(intervals).filter(interval => !interval.isBottom())) } static bottom(): Domain { From 7096ff166ccdeefa64d791f19bad642227214b2a Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Wed, 15 May 2024 16:24:20 +0200 Subject: [PATCH 37/70] feat: added the notion of top domains --- src/abstract-interpretation/domain.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/abstract-interpretation/domain.ts b/src/abstract-interpretation/domain.ts index 551dc61930..3f07876491 100644 --- a/src/abstract-interpretation/domain.ts +++ b/src/abstract-interpretation/domain.ts @@ -10,6 +10,13 @@ export class Interval { guard(min.value <= max.value, () => `The interval ${this.toString()} has a minimum that is greater than its maximum`) } + static top(): Interval { + return new Interval( + {value: Number.NEGATIVE_INFINITY, inclusive: false}, + {value: Number.POSITIVE_INFINITY, inclusive: false} + ) + } + toString(): string { if(this.isBottom()) { return '∅' @@ -29,6 +36,11 @@ export class Interval { isSingleton(): boolean { return this.min.value === this.max.value && this.min.inclusive && this.max.inclusive } + + // An interval is considered top if it's of the form (-∞, ∞) + isTop(): boolean { + return this.min.value === Number.NEGATIVE_INFINITY && this.max.value === Number.POSITIVE_INFINITY + } } export class Domain { @@ -42,6 +54,10 @@ export class Domain { return new Domain() } + static top(): Domain { + return new Domain([Interval.top()]) + } + static fromIntervals(intervals: Interval[] | Set): Domain { return new Domain(Array.from(intervals)) } @@ -57,6 +73,10 @@ export class Domain { return this.intervals.size === 0 } + isTop(): boolean { + return this.intervals.size === 1 && Array.from(this.intervals)[0].isTop() + } + get intervals(): Set { return this._intervals } @@ -75,6 +95,8 @@ export class Domain { toString(): string { if(this.isBottom()) { return '⊥' + } else if(this.isTop()) { + return '⊤' } else { return `{${Array.from(this.intervals).join(', ')}}` } From 0cbb41a290fd75e2ef870c937d08b3a1be1c695d Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Wed, 15 May 2024 16:24:54 +0200 Subject: [PATCH 38/70] feat: comparisons now generate a third domain for the whole expr --- .../handler/binop/operators.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/abstract-interpretation/handler/binop/operators.ts b/src/abstract-interpretation/handler/binop/operators.ts index 42e8a219e7..1ccc94e1e4 100644 --- a/src/abstract-interpretation/handler/binop/operators.ts +++ b/src/abstract-interpretation/handler/binop/operators.ts @@ -1,6 +1,6 @@ import {guard} from '../../../util/assert' import {BinOpOperators} from './binop' -import {addDomains, narrowDomain, NarrowKind, subtractDomains} from '../../domain' +import {addDomains, Domain, narrowDomain, NarrowKind, subtractDomains} from '../../domain' import {AINode, AINodeStore} from '../../ainode' export const operators: BinOpOperators = { @@ -48,15 +48,23 @@ export const operators: BinOpOperators = { default: guard(false, `Unknown binary operator ${node.operator}`) } const calculateDomains = (lhs: AINode, rhs: AINode, narrowKind: NarrowKind, idSuffix = ''): AINode[] => { + const lhsNarrowed = narrowDomain(lhs.domain, rhs.domain, narrowKind) + const rhsNarrowed = narrowDomain(rhs.domain, lhs.domain, narrowKind ^ 0b111 /* flip everything */) return [{ + nodeId: node.info.id + idSuffix, + expressionId: node.info.id, + domain: lhsNarrowed.isBottom() && rhsNarrowed.isBottom() ? Domain.bottom() : Domain.top(), + astNode: node, + }, + { nodeId: lhs.nodeId + idSuffix, expressionId: node.info.id, - domain: narrowDomain(lhs.domain, rhs.domain, narrowKind), + domain: lhsNarrowed, astNode: node, }, { nodeId: rhs.nodeId + idSuffix, expressionId: node.info.id, - domain: narrowDomain(rhs.domain, lhs.domain, narrowKind ^ 0b110 /* flip < and > but leave = */), + domain: rhsNarrowed, astNode: node, }] } From 21348b06a964e102136790e765aef6a33d10c29d Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Wed, 15 May 2024 16:25:34 +0200 Subject: [PATCH 39/70] feat: recognize paths that cannot be taken in conditionals --- .../handler/conditional/conditional.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/abstract-interpretation/handler/conditional/conditional.ts b/src/abstract-interpretation/handler/conditional/conditional.ts index 9b6dec02eb..a5bc8211a5 100644 --- a/src/abstract-interpretation/handler/conditional/conditional.ts +++ b/src/abstract-interpretation/handler/conditional/conditional.ts @@ -27,6 +27,9 @@ export class Conditional extends Handler { guard(this.then !== undefined, `No then-branch found for conditional ${this.node.info.id}`) const result = AINodeStore.empty() + for(const conditionNode of this.condition) { + result.register(conditionNode) + } for(const thenNode of this.then) { result.register(thenNode) } @@ -61,9 +64,21 @@ export class Conditional extends Handler { this.domains.updateWith(this.thenDomains) } else if(this.then === undefined) { this.then = aiNodes + const conditionDomain = this.thenDomains.get(this.node.condition.info.id)?.domain + guard(conditionDomain !== undefined, `No domain found for condition ${this.node.condition.info.id}`) + if(conditionDomain.isBottom()) { + // TODO: How can I indicate that this path is not possible and all Domains in it should be bottom? + // Do I just iterate over all AINodes and replace the domains. Sounds dumb :D + } this.domains.updateWith(this.elseDomains) } else if(this.else === undefined) { this.else = aiNodes + const conditionDomain = this.thenDomains.get(this.node.condition.info.id)?.domain + guard(conditionDomain !== undefined, `No domain found for condition ${this.node.condition.info.id}`) + if(conditionDomain.isBottom()) { + // TODO: How can I indicate that this path is not possible and all Domains in it should be bottom? + // Do I just iterate over all AINodes and replace the domains. Sounds dumb :D + } } else { guard(false, `Conditional ${this.node.info.id} already has condition, then and else`) } From de339e40779de2c51884a5c977cee8f9188e40a8 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Thu, 16 May 2024 09:15:01 +0200 Subject: [PATCH 40/70] feat-fix: add comparison domains to conditional domains --- .../handler/conditional/conditional.ts | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/abstract-interpretation/handler/conditional/conditional.ts b/src/abstract-interpretation/handler/conditional/conditional.ts index a5bc8211a5..7351d2d0e4 100644 --- a/src/abstract-interpretation/handler/conditional/conditional.ts +++ b/src/abstract-interpretation/handler/conditional/conditional.ts @@ -47,17 +47,26 @@ export class Conditional extends Handler { for(const node of aiNodes) { const isElseNode = node.nodeId.endsWith('-else') const cleanedId = isElseNode ? node.nodeId.slice(0, -5) : node.nodeId - for(const child of getDfgChildrenOfType(cleanedId, this.dfg, EdgeType.Reads) ?? []) { + const dfChildren = getDfgChildrenOfType(cleanedId, this.dfg, EdgeType.Reads) + if(dfChildren === undefined) { if(isElseNode) { - this.elseDomains.register({ - ...node, - nodeId: child - }, RegisterBehavior.Overwrite) + this.elseDomains.register(node, RegisterBehavior.Overwrite) } else { - this.thenDomains.register({ - ...node, - nodeId: child - }, RegisterBehavior.Overwrite) + this.thenDomains.register(node, RegisterBehavior.Overwrite) + } + } else { + for(const child of dfChildren) { + if(isElseNode) { + this.elseDomains.register({ + ...node, + nodeId: child + }, RegisterBehavior.Overwrite) + } else { + this.thenDomains.register({ + ...node, + nodeId: child + }, RegisterBehavior.Overwrite) + } } } } @@ -73,7 +82,7 @@ export class Conditional extends Handler { this.domains.updateWith(this.elseDomains) } else if(this.else === undefined) { this.else = aiNodes - const conditionDomain = this.thenDomains.get(this.node.condition.info.id)?.domain + const conditionDomain = this.elseDomains.get(this.node.condition.info.id)?.domain guard(conditionDomain !== undefined, `No domain found for condition ${this.node.condition.info.id}`) if(conditionDomain.isBottom()) { // TODO: How can I indicate that this path is not possible and all Domains in it should be bottom? From 4c93b5570fd05403ab932fdeda76481078fd7606 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Thu, 16 May 2024 10:09:31 +0200 Subject: [PATCH 41/70] feat-fix: store comparison domain with the cleaned id --- src/abstract-interpretation/handler/conditional/conditional.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/abstract-interpretation/handler/conditional/conditional.ts b/src/abstract-interpretation/handler/conditional/conditional.ts index 7351d2d0e4..8f1f4bdfdf 100644 --- a/src/abstract-interpretation/handler/conditional/conditional.ts +++ b/src/abstract-interpretation/handler/conditional/conditional.ts @@ -50,7 +50,7 @@ export class Conditional extends Handler { const dfChildren = getDfgChildrenOfType(cleanedId, this.dfg, EdgeType.Reads) if(dfChildren === undefined) { if(isElseNode) { - this.elseDomains.register(node, RegisterBehavior.Overwrite) + this.elseDomains.register({...node, nodeId: cleanedId}, RegisterBehavior.Overwrite) } else { this.thenDomains.register(node, RegisterBehavior.Overwrite) } From ddcd60a1464067f19c2ba0768d02e56be2302cc1 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Thu, 16 May 2024 10:36:42 +0200 Subject: [PATCH 42/70] feat-fix: wrong narrowing of the right hand side of a comparison --- src/abstract-interpretation/handler/binop/operators.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/abstract-interpretation/handler/binop/operators.ts b/src/abstract-interpretation/handler/binop/operators.ts index 1ccc94e1e4..f5af6bbc8d 100644 --- a/src/abstract-interpretation/handler/binop/operators.ts +++ b/src/abstract-interpretation/handler/binop/operators.ts @@ -49,7 +49,7 @@ export const operators: BinOpOperators = { } const calculateDomains = (lhs: AINode, rhs: AINode, narrowKind: NarrowKind, idSuffix = ''): AINode[] => { const lhsNarrowed = narrowDomain(lhs.domain, rhs.domain, narrowKind) - const rhsNarrowed = narrowDomain(rhs.domain, lhs.domain, narrowKind ^ 0b111 /* flip everything */) + const rhsNarrowed = narrowDomain(rhs.domain, lhs.domain, narrowKind ^ 0b110 /* flip < and > but not = */) return [{ nodeId: node.info.id + idSuffix, expressionId: node.info.id, From 3f92648a7274718e187c8c36dbbba8a83d2f3cf0 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Thu, 16 May 2024 10:39:26 +0200 Subject: [PATCH 43/70] refactor: removed fixme 'cause I fixed it :^) --- src/abstract-interpretation/handler/binop/operators.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/abstract-interpretation/handler/binop/operators.ts b/src/abstract-interpretation/handler/binop/operators.ts index f5af6bbc8d..02e529e5ca 100644 --- a/src/abstract-interpretation/handler/binop/operators.ts +++ b/src/abstract-interpretation/handler/binop/operators.ts @@ -68,8 +68,6 @@ export const operators: BinOpOperators = { astNode: node, }] } - // FIXME: We should not set the domain of the operands! - // But if we would only set the domain of the whole expression, we could only narrow one operand. const thenDomains = calculateDomains(lhs, rhs, narrowKind) const elseDomains = calculateDomains(lhs, rhs, narrowKind ^ 0b111 /* flip everything */, '-else') return AINodeStore.from(thenDomains.concat(elseDomains)) From 710255f93c67ac52bd5f6d10377bdeae28e7e3f3 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Thu, 16 May 2024 11:25:42 +0200 Subject: [PATCH 44/70] refactor: added tsdoc comments and removed unneeded method --- src/abstract-interpretation/ainode.ts | 4 ++-- src/abstract-interpretation/domain.ts | 10 +++------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/abstract-interpretation/ainode.ts b/src/abstract-interpretation/ainode.ts index 79453722c9..6f8b43f687 100644 --- a/src/abstract-interpretation/ainode.ts +++ b/src/abstract-interpretation/ainode.ts @@ -3,9 +3,9 @@ import {assertUnreachable, guard} from '../util/assert' import {Domain, unifyDomains} from './domain' export interface AINode { - // The ID of the node that logically holds the domain (e.g. the lhs of an assignment) + /** The ID of the node that logically holds the domain (e.g. the lhs of an assignment) */ readonly nodeId: NodeId - // The ID of the whole expression that the domain was calculated from (e.g. the whole assignment expression) + /** The ID of the whole expression that the domain was calculated from (e.g. the whole assignment expression) */ readonly expressionId: NodeId readonly domain: Domain readonly astNode: RNodeWithParent diff --git a/src/abstract-interpretation/domain.ts b/src/abstract-interpretation/domain.ts index 3f07876491..fb226f06e2 100644 --- a/src/abstract-interpretation/domain.ts +++ b/src/abstract-interpretation/domain.ts @@ -27,17 +27,17 @@ export class Interval { return `${this.min.inclusive ? '[' : '('}${this.min.value}, ${this.max.value}${this.max.inclusive ? ']' : ')'}` } - // An interval is considered empty if it's of the form [T, T) or (T, T] + /** An interval is considered empty if it's of the form [T, T) or (T, T] */ isBottom(): boolean { return this.min.value === this.max.value && !(this.min.inclusive && this.max.inclusive) } - // An interval is considered singleton if it's of the form [T, T] + /** An interval is considered singleton if it's of the form [T, T] */ isSingleton(): boolean { return this.min.value === this.max.value && this.min.inclusive && this.max.inclusive } - // An interval is considered top if it's of the form (-∞, ∞) + /** An interval is considered top if it's of the form (-∞, ∞) */ isTop(): boolean { return this.min.value === Number.NEGATIVE_INFINITY && this.max.value === Number.POSITIVE_INFINITY } @@ -88,10 +88,6 @@ export class Domain { } } - addInterval(interval: Interval): void { - this.intervals = unifyOverlappingIntervals([...this.intervals, interval]) - } - toString(): string { if(this.isBottom()) { return '⊥' From 2ea665f9d99b4092f55225cdd9fbf4f3177110f5 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Sun, 19 May 2024 17:29:22 +0200 Subject: [PATCH 45/70] feat: conditionals now work (at least for simple examples) --- .../handler/conditional/conditional.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/abstract-interpretation/handler/conditional/conditional.ts b/src/abstract-interpretation/handler/conditional/conditional.ts index 8f1f4bdfdf..719ffebc25 100644 --- a/src/abstract-interpretation/handler/conditional/conditional.ts +++ b/src/abstract-interpretation/handler/conditional/conditional.ts @@ -72,21 +72,19 @@ export class Conditional extends Handler { } this.domains.updateWith(this.thenDomains) } else if(this.then === undefined) { - this.then = aiNodes + this.then = AINodeStore.empty() const conditionDomain = this.thenDomains.get(this.node.condition.info.id)?.domain guard(conditionDomain !== undefined, `No domain found for condition ${this.node.condition.info.id}`) - if(conditionDomain.isBottom()) { - // TODO: How can I indicate that this path is not possible and all Domains in it should be bottom? - // Do I just iterate over all AINodes and replace the domains. Sounds dumb :D + if(!conditionDomain.isBottom()) { + this.then.updateWith(aiNodes) } this.domains.updateWith(this.elseDomains) } else if(this.else === undefined) { - this.else = aiNodes + this.else = AINodeStore.empty() const conditionDomain = this.elseDomains.get(this.node.condition.info.id)?.domain guard(conditionDomain !== undefined, `No domain found for condition ${this.node.condition.info.id}`) - if(conditionDomain.isBottom()) { - // TODO: How can I indicate that this path is not possible and all Domains in it should be bottom? - // Do I just iterate over all AINodes and replace the domains. Sounds dumb :D + if(!conditionDomain.isBottom()) { + this.else.updateWith(aiNodes) } } else { guard(false, `Conditional ${this.node.info.id} already has condition, then and else`) From a82c7d2d22142473ac4787df918b57e20f473ecf Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Sun, 19 May 2024 17:29:51 +0200 Subject: [PATCH 46/70] refactor: removed unused setter --- src/abstract-interpretation/domain.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/abstract-interpretation/domain.ts b/src/abstract-interpretation/domain.ts index fb226f06e2..6be6e40b70 100644 --- a/src/abstract-interpretation/domain.ts +++ b/src/abstract-interpretation/domain.ts @@ -81,13 +81,6 @@ export class Domain { return this._intervals } - private set intervals(intervals: Interval[]) { - this._intervals.clear() - for(const interval of intervals) { - this._intervals.add(interval) - } - } - toString(): string { if(this.isBottom()) { return '⊥' From e6d3add0cf0f9c6bce9bbf6ff5b9ddaa9eb1038b Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Tue, 21 May 2024 11:30:25 +0200 Subject: [PATCH 47/70] refactor: promote AINode to class --- src/abstract-interpretation/ainode.ts | 18 ++++++++------- .../handler/binop/operators.ts | 23 ++++--------------- .../handler/conditional/conditional.ts | 14 ++++------- src/abstract-interpretation/processor.ts | 16 +++---------- 4 files changed, 21 insertions(+), 50 deletions(-) diff --git a/src/abstract-interpretation/ainode.ts b/src/abstract-interpretation/ainode.ts index 6f8b43f687..3fc687476a 100644 --- a/src/abstract-interpretation/ainode.ts +++ b/src/abstract-interpretation/ainode.ts @@ -2,13 +2,15 @@ import {NodeId, ParentInformation, RNodeWithParent} from '../r-bridge' import {assertUnreachable, guard} from '../util/assert' import {Domain, unifyDomains} from './domain' -export interface AINode { - /** The ID of the node that logically holds the domain (e.g. the lhs of an assignment) */ - readonly nodeId: NodeId - /** The ID of the whole expression that the domain was calculated from (e.g. the whole assignment expression) */ - readonly expressionId: NodeId - readonly domain: Domain - readonly astNode: RNodeWithParent +export class AINode { + constructor( + public readonly domain: Domain, + public readonly astNode: RNodeWithParent, + /** The ID of the whole expression that the domain was calculated from (e.g. the whole assignment expression) */ + public readonly expressionId: NodeId = astNode.info.id, + /** The ID of the node that logically holds the domain (e.g. the lhs of an assignment) */ + public readonly nodeId: NodeId = astNode.info.id + ) {} } export const enum RegisterBehavior { @@ -80,7 +82,7 @@ export class AINodeStore implements Iterable { case RegisterBehavior.Merge: { const existing = this.map.get(node.nodeId) guard(existing !== undefined, `Node with ID ${node.nodeId} should exist`) - this.register({...node, domain: unifyDomains([existing.domain, node.domain])}, RegisterBehavior.Overwrite) + this.register(new AINode(unifyDomains([existing.domain, node.domain]), node.astNode, node.expressionId), RegisterBehavior.Overwrite) break } default: assertUnreachable(behavior) diff --git a/src/abstract-interpretation/handler/binop/operators.ts b/src/abstract-interpretation/handler/binop/operators.ts index 02e529e5ca..c3e883635c 100644 --- a/src/abstract-interpretation/handler/binop/operators.ts +++ b/src/abstract-interpretation/handler/binop/operators.ts @@ -4,30 +4,15 @@ import {addDomains, Domain, narrowDomain, NarrowKind, subtractDomains} from '../ import {AINode, AINodeStore} from '../../ainode' export const operators: BinOpOperators = { - 'assignment': (lhs, rhs, node) => { - return AINodeStore.from({ - nodeId: lhs.nodeId, - expressionId: node.info.id, - domain: rhs.domain, - astNode: node.lhs, - }) + 'assignment': (_, rhs, node) => { + return AINodeStore.from(new AINode(rhs.domain, node.lhs, node.info.id)) }, 'arithmetic': (lhs, rhs, node) => { switch(node.operator) { case '+': - return AINodeStore.from({ - nodeId: node.info.id, - expressionId: node.info.id, - domain: addDomains(lhs.domain, rhs.domain), - astNode: node, - }) + return AINodeStore.from(new AINode(addDomains(lhs.domain, rhs.domain), node)) case '-': - return AINodeStore.from({ - nodeId: node.info.id, - expressionId: node.info.id, - domain: subtractDomains(lhs.domain, rhs.domain), - astNode: node, - }) + return AINodeStore.from(new AINode(subtractDomains(lhs.domain, rhs.domain), node)) default: guard(false, `Unknown binary operator ${node.operator}`) } diff --git a/src/abstract-interpretation/handler/conditional/conditional.ts b/src/abstract-interpretation/handler/conditional/conditional.ts index 719ffebc25..23510a5fec 100644 --- a/src/abstract-interpretation/handler/conditional/conditional.ts +++ b/src/abstract-interpretation/handler/conditional/conditional.ts @@ -1,6 +1,6 @@ import {ParentInformation, RIfThenElse} from '../../../r-bridge' import {guard} from '../../../util/assert' -import {AINodeStore, RegisterBehavior} from '../../ainode' +import {AINode, AINodeStore, RegisterBehavior} from '../../ainode' import {aiLogger, getDfgChildrenOfType} from '../../processor' import {Handler} from '../handler' import {DataflowInformation} from '../../../dataflow/internal/info' @@ -50,22 +50,16 @@ export class Conditional extends Handler { const dfChildren = getDfgChildrenOfType(cleanedId, this.dfg, EdgeType.Reads) if(dfChildren === undefined) { if(isElseNode) { - this.elseDomains.register({...node, nodeId: cleanedId}, RegisterBehavior.Overwrite) + this.elseDomains.register(new AINode(node.domain, node.astNode, node.expressionId, cleanedId), RegisterBehavior.Overwrite) } else { this.thenDomains.register(node, RegisterBehavior.Overwrite) } } else { for(const child of dfChildren) { if(isElseNode) { - this.elseDomains.register({ - ...node, - nodeId: child - }, RegisterBehavior.Overwrite) + this.elseDomains.register(new AINode(node.domain, node.astNode, node.expressionId, child), RegisterBehavior.Overwrite) } else { - this.thenDomains.register({ - ...node, - nodeId: child - }, RegisterBehavior.Overwrite) + this.thenDomains.register(new AINode(node.domain, node.astNode, node.expressionId, child), RegisterBehavior.Overwrite) } } } diff --git a/src/abstract-interpretation/processor.ts b/src/abstract-interpretation/processor.ts index ebbabce5e4..0feb9f815f 100644 --- a/src/abstract-interpretation/processor.ts +++ b/src/abstract-interpretation/processor.ts @@ -10,7 +10,7 @@ import {Conditional} from './handler/conditional/conditional' import {Domain, unifyDomains} from './domain' import {log} from '../util/log' import {ExprList} from './handler/exprlist/exprlist' -import {AINodeStore} from './ainode' +import {AINode, AINodeStore} from './ainode' export const aiLogger = log.getSubLogger({name: 'abstract-interpretation'}) @@ -61,19 +61,9 @@ export function runAbstractInterpretation(ast: NormalizedAst, dfg: DataflowInfor } else if(astNode?.type === RType.ExpressionList) { operationStack.push(new ExprList(dfg, AINodeStore.withParent(top.domains))).enter() } else if(astNode?.type === RType.Symbol) { - top.next(AINodeStore.from({ - nodeId: astNode.info.id, - expressionId: astNode.info.id, - domain: getDomainOfDfgChild(node.id, dfg, top.domains), - astNode: astNode, - })) + top.next(AINodeStore.from(new AINode(getDomainOfDfgChild(node.id, dfg, top.domains), astNode))) } else if(astNode?.type === RType.Number) { - top.next(AINodeStore.from({ - nodeId: astNode.info.id, - expressionId: astNode.info.id, - domain: Domain.fromScalar(astNode.content.num), - astNode: astNode, - })) + top.next(AINodeStore.from(new AINode(Domain.fromScalar(astNode.content.num), astNode))) } else if(node.type === CfgVertexType.EndMarker) { const operationResult = operationStack.pop()?.exit() guard(operationResult !== undefined, 'No operation result') From 67871b64b93207b17a5b8ec99862dd5eb89d2b0f Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Tue, 21 May 2024 11:36:05 +0200 Subject: [PATCH 48/70] refactor: easier way to partially modify an AINode --- src/abstract-interpretation/ainode.ts | 11 ++++++++++- .../handler/conditional/conditional.ts | 6 +++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/abstract-interpretation/ainode.ts b/src/abstract-interpretation/ainode.ts index 3fc687476a..fe6d01b39d 100644 --- a/src/abstract-interpretation/ainode.ts +++ b/src/abstract-interpretation/ainode.ts @@ -11,6 +11,15 @@ export class AINode { /** The ID of the node that logically holds the domain (e.g. the lhs of an assignment) */ public readonly nodeId: NodeId = astNode.info.id ) {} + + static copy(node: AINode, changes: Partial): AINode { + return new AINode( + changes.domain ?? node.domain, + changes.astNode ?? node.astNode, + changes.expressionId ?? node.expressionId, + changes.nodeId ?? node.nodeId + ) + } } export const enum RegisterBehavior { @@ -82,7 +91,7 @@ export class AINodeStore implements Iterable { case RegisterBehavior.Merge: { const existing = this.map.get(node.nodeId) guard(existing !== undefined, `Node with ID ${node.nodeId} should exist`) - this.register(new AINode(unifyDomains([existing.domain, node.domain]), node.astNode, node.expressionId), RegisterBehavior.Overwrite) + this.register(AINode.copy(node, {domain: unifyDomains([existing.domain, node.domain])}), RegisterBehavior.Overwrite) break } default: assertUnreachable(behavior) diff --git a/src/abstract-interpretation/handler/conditional/conditional.ts b/src/abstract-interpretation/handler/conditional/conditional.ts index 23510a5fec..98f9cb0c7a 100644 --- a/src/abstract-interpretation/handler/conditional/conditional.ts +++ b/src/abstract-interpretation/handler/conditional/conditional.ts @@ -50,16 +50,16 @@ export class Conditional extends Handler { const dfChildren = getDfgChildrenOfType(cleanedId, this.dfg, EdgeType.Reads) if(dfChildren === undefined) { if(isElseNode) { - this.elseDomains.register(new AINode(node.domain, node.astNode, node.expressionId, cleanedId), RegisterBehavior.Overwrite) + this.elseDomains.register(AINode.copy(node, {nodeId: cleanedId}), RegisterBehavior.Overwrite) } else { this.thenDomains.register(node, RegisterBehavior.Overwrite) } } else { for(const child of dfChildren) { if(isElseNode) { - this.elseDomains.register(new AINode(node.domain, node.astNode, node.expressionId, child), RegisterBehavior.Overwrite) + this.elseDomains.register(AINode.copy(node, {nodeId: child}), RegisterBehavior.Overwrite) } else { - this.thenDomains.register(new AINode(node.domain, node.astNode, node.expressionId, child), RegisterBehavior.Overwrite) + this.thenDomains.register(AINode.copy(node, {nodeId: child}), RegisterBehavior.Overwrite) } } } From 36a121865ce02e3e8a43f9bd2a197b77dcc0f7a5 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Tue, 21 May 2024 14:33:50 +0200 Subject: [PATCH 49/70] feat: create domain for RLogical --- src/abstract-interpretation/processor.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/abstract-interpretation/processor.ts b/src/abstract-interpretation/processor.ts index 0feb9f815f..13f08c30d6 100644 --- a/src/abstract-interpretation/processor.ts +++ b/src/abstract-interpretation/processor.ts @@ -64,6 +64,8 @@ export function runAbstractInterpretation(ast: NormalizedAst, dfg: DataflowInfor top.next(AINodeStore.from(new AINode(getDomainOfDfgChild(node.id, dfg, top.domains), astNode))) } else if(astNode?.type === RType.Number) { top.next(AINodeStore.from(new AINode(Domain.fromScalar(astNode.content.num), astNode))) + } else if(astNode?.type === RType.Logical) { + top.next(AINodeStore.from(new AINode(astNode.content ? Domain.top() : Domain.bottom(), astNode))) } else if(node.type === CfgVertexType.EndMarker) { const operationResult = operationStack.pop()?.exit() guard(operationResult !== undefined, 'No operation result') From e2714bb8b9204e3bc8eafb273bf224c838622a72 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Sun, 26 May 2024 15:04:20 +0200 Subject: [PATCH 50/70] refactor: use AINode constructor --- .../handler/binop/operators.ts | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/abstract-interpretation/handler/binop/operators.ts b/src/abstract-interpretation/handler/binop/operators.ts index c3e883635c..f0005d257b 100644 --- a/src/abstract-interpretation/handler/binop/operators.ts +++ b/src/abstract-interpretation/handler/binop/operators.ts @@ -35,23 +35,12 @@ export const operators: BinOpOperators = { const calculateDomains = (lhs: AINode, rhs: AINode, narrowKind: NarrowKind, idSuffix = ''): AINode[] => { const lhsNarrowed = narrowDomain(lhs.domain, rhs.domain, narrowKind) const rhsNarrowed = narrowDomain(rhs.domain, lhs.domain, narrowKind ^ 0b110 /* flip < and > but not = */) - return [{ - nodeId: node.info.id + idSuffix, - expressionId: node.info.id, - domain: lhsNarrowed.isBottom() && rhsNarrowed.isBottom() ? Domain.bottom() : Domain.top(), - astNode: node, - }, - { - nodeId: lhs.nodeId + idSuffix, - expressionId: node.info.id, - domain: lhsNarrowed, - astNode: node, - }, { - nodeId: rhs.nodeId + idSuffix, - expressionId: node.info.id, - domain: rhsNarrowed, - astNode: node, - }] + const isConditionTrue = lhsNarrowed.isBottom() && rhsNarrowed.isBottom() + return [ + new AINode(isConditionTrue ? Domain.bottom() : Domain.top(), node, node.info.id, node.info.id + idSuffix), + new AINode(lhsNarrowed, node, node.info.id, lhs.nodeId + idSuffix), + new AINode(rhsNarrowed, node, node.info.id, rhs.nodeId + idSuffix), + ] } const thenDomains = calculateDomains(lhs, rhs, narrowKind) const elseDomains = calculateDomains(lhs, rhs, narrowKind ^ 0b111 /* flip everything */, '-else') From a4023d0a679f06afc3f312d4d9e0d24a4b9bb57d Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Tue, 28 May 2024 14:37:08 +0200 Subject: [PATCH 51/70] feat-fix: adapted AI impl to v2.0.1 --- src/abstract-interpretation/ainode.ts | 10 +++--- src/abstract-interpretation/domain.ts | 35 +++++++++++-------- .../handler/binop/binop.ts | 4 +-- .../handler/binop/operators.ts | 18 +++++----- .../handler/conditional/conditional.ts | 26 +++++++------- .../handler/exprlist/exprlist.ts | 6 ++-- .../handler/handler.ts | 6 ++-- src/abstract-interpretation/processor.ts | 2 ++ src/util/cfg/cfg.ts | 4 +-- .../abstract-interpretation.spec.ts | 20 +++++++++++ 10 files changed, 82 insertions(+), 49 deletions(-) diff --git a/src/abstract-interpretation/ainode.ts b/src/abstract-interpretation/ainode.ts index fe6d01b39d..9802395f02 100644 --- a/src/abstract-interpretation/ainode.ts +++ b/src/abstract-interpretation/ainode.ts @@ -1,6 +1,8 @@ -import {NodeId, ParentInformation, RNodeWithParent} from '../r-bridge' -import {assertUnreachable, guard} from '../util/assert' -import {Domain, unifyDomains} from './domain' +import { assertUnreachable, guard } from '../util/assert' +import type { Domain } from './domain' +import { unifyDomains } from './domain' +import type { ParentInformation, RNodeWithParent } from '../r-bridge/lang-4.x/ast/model/processing/decorate' +import type { NodeId } from '../r-bridge/lang-4.x/ast/model/processing/node-id' export class AINode { constructor( @@ -91,7 +93,7 @@ export class AINodeStore implements Iterable { case RegisterBehavior.Merge: { const existing = this.map.get(node.nodeId) guard(existing !== undefined, `Node with ID ${node.nodeId} should exist`) - this.register(AINode.copy(node, {domain: unifyDomains([existing.domain, node.domain])}), RegisterBehavior.Overwrite) + this.register(AINode.copy(node, { domain: unifyDomains([existing.domain, node.domain]) }), RegisterBehavior.Overwrite) break } default: assertUnreachable(behavior) diff --git a/src/abstract-interpretation/domain.ts b/src/abstract-interpretation/domain.ts index 6f9fe1558d..4fdfcfa28b 100644 --- a/src/abstract-interpretation/domain.ts +++ b/src/abstract-interpretation/domain.ts @@ -12,8 +12,8 @@ export class Interval { static top(): Interval { return new Interval( - {value: Number.NEGATIVE_INFINITY, inclusive: false}, - {value: Number.POSITIVE_INFINITY, inclusive: false} + { value: Number.NEGATIVE_INFINITY, inclusive: false }, + { value: Number.POSITIVE_INFINITY, inclusive: false } ) } @@ -140,10 +140,9 @@ export function doIntervalsOverlap(interval1: Interval, interval2: Interval, kin if(diff1 < 0 || diff2 < 0) { doIntervalsOverlap = false doIntervalsTouch = false - } // If their bounds have the same value, they overlap if both are inclusive // and touch if only one is inclusive - else if(diff1 === 0) { + } else if(diff1 === 0) { doIntervalsOverlap = interval1.max.inclusive && interval2.min.inclusive doIntervalsTouch = interval1.max.inclusive !== interval2.min.inclusive } else if(diff2 === 0) { @@ -233,7 +232,7 @@ interface IntervalOverlap { } function flipInclusiveness(intervalBound: IntervalBound): IntervalBound { - return {value: intervalBound.value, inclusive: !intervalBound.inclusive} + return { value: intervalBound.value, inclusive: !intervalBound.inclusive } } export function overlapIntervals(interval1: Interval, interval2: Interval): IntervalOverlap { @@ -242,10 +241,12 @@ export function overlapIntervals(interval1: Interval, interval2: Interval): Inte if(!doIntervalsOverlap(interval1, interval2)) { if(diffMin < 0) { - return {smaller: interval1, intersection: undefined, larger: undefined} + return { smaller: interval1, intersection: undefined, larger: undefined } } else if(diffMin > 0) { - return {smaller: undefined, intersection: undefined, larger: interval1} - } else { guard(false, 'Their lower bounds cannot be the same as they do not overlap') } + return { smaller: undefined, intersection: undefined, larger: interval1 } + } else { + guard(false, 'Their lower bounds cannot be the same as they do not overlap') + } } const intersectionStart = diffMin > 0 ? interval1.min : interval2.min @@ -271,28 +272,34 @@ export function narrowDomain(baseDomain: Domain, boundDomain: Domain, narrowKind let getNarrowedIntervals: (overlap: IntervalOverlap, bound: Interval) => (Interval | undefined)[] if(isGreater) { - getNarrowedIntervals = ({intersection, larger}, bound) => { + getNarrowedIntervals = ({ intersection, larger }, bound) => { if(!isEqual && intersection !== undefined && compareIntervalsByTheirMinimum(intersection, bound) === 0) { - intersection = new Interval({value: intersection.min.value, inclusive: false}, intersection.max) + intersection = new Interval({ value: intersection.min.value, inclusive: false }, intersection.max) } return [intersection, larger] } } else if(isSmaller) { - getNarrowedIntervals = ({smaller, intersection}, bound) => { + getNarrowedIntervals = ({ smaller, intersection }, bound) => { if(!isEqual && intersection !== undefined && compareIntervalsByTheirMaximum(intersection, bound) === 0) { - intersection = new Interval(intersection.min, {value: intersection.max.value, inclusive: false}) + intersection = new Interval(intersection.min, { value: intersection.max.value, inclusive: false }) } return [intersection, smaller] } - } else {guard(false, 'Either isGreater or isSmaller must be set')} + } else { + guard(false, 'Either isGreater or isSmaller must be set') + } const narrowedIntervals: (Interval | undefined)[] = [] for(const baseInterval of baseDomain.intervals) { for(const boundInterval of boundDomain.intervals) { const overlap = overlapIntervals(baseInterval, boundInterval) + console.log(`Prompt: ${baseInterval.toString()} ${isSmaller ? '<' : ''}${isGreater ? '>' : ''}${isEqual ? '=' : ''} ${boundInterval.toString()}`) + console.log(`Overlap: ${(overlap.smaller ?? 'none').toString()} ${(overlap.intersection ?? 'none').toString()} ${(overlap.larger ?? 'none').toString()}`) narrowedIntervals.push(...getNarrowedIntervals(overlap, boundInterval)) } } - return Domain.fromIntervals(narrowedIntervals.filter(interval => interval !== undefined).map(interval => interval as Interval)) + const newDomain = Domain.fromIntervals(narrowedIntervals.filter(interval => interval !== undefined).map(interval => interval as Interval)) + console.log(`Domain: ${newDomain.toString()}`) + return newDomain } \ No newline at end of file diff --git a/src/abstract-interpretation/handler/binop/binop.ts b/src/abstract-interpretation/handler/binop/binop.ts index 08dc6b277d..17c382d21d 100644 --- a/src/abstract-interpretation/handler/binop/binop.ts +++ b/src/abstract-interpretation/handler/binop/binop.ts @@ -7,7 +7,7 @@ import { operators } from './operators' import type { NodeId } from '../../../r-bridge/lang-4.x/ast/model/processing/node-id' import type { DataflowInformation } from '../../../dataflow/info' -export type BinOpOperators = (lhs: AINode, rhs: AINode, node: RBinaryOp) => AINode +export type BinOpOperators = Record) => AINodeStore> export class BinOp extends Handler { lhs: NodeId | undefined @@ -18,7 +18,7 @@ export class BinOp extends Handler { domains: AINodeStore, private readonly node: RBinaryOp ) { - super(dfg, domains, `Bin Op (${node.flavor})`) + super(dfg, domains, `Bin Op (${node.operator})`) } exit(): AINodeStore { diff --git a/src/abstract-interpretation/handler/binop/operators.ts b/src/abstract-interpretation/handler/binop/operators.ts index b02fa9bb50..b211b9cb49 100644 --- a/src/abstract-interpretation/handler/binop/operators.ts +++ b/src/abstract-interpretation/handler/binop/operators.ts @@ -17,7 +17,7 @@ const calculateComparisonDomains = (lhs: AINode, rhs: AINode, node: RBinaryOp { - return AINodeStore.from(new AINode(rhs.domain, node.lhs, node.info.if)) + return AINodeStore.from(new AINode(rhs.domain, node.lhs, node.info.id)) }, '+': (lhs, rhs, node) => { return AINodeStore.from(new AINode(addDomains(lhs.domain, rhs.domain), node)) @@ -27,22 +27,22 @@ export const operators: BinOpOperators = { }, '<': (lhs, rhs, node) => { const narrowKind = NarrowKind.Smaller - return calculateComparisonDomains(lhs, rhs, node, narrowKind) - .concat(calculateComparisonDomains(lhs, rhs, node, narrowKind ^0b111, '-else')) + return AINodeStore.from(calculateComparisonDomains(lhs, rhs, node, narrowKind) + .concat(calculateComparisonDomains(lhs, rhs, node, narrowKind ^0b111, '-else'))) }, '>': (lhs, rhs, node) => { const narrowKind = NarrowKind.Greater - return calculateComparisonDomains(lhs, rhs, node, narrowKind) - .concat(calculateComparisonDomains(lhs, rhs, node, narrowKind ^0b111, '-else')) + return AINodeStore.from(calculateComparisonDomains(lhs, rhs, node, narrowKind) + .concat(calculateComparisonDomains(lhs, rhs, node, narrowKind ^0b111, '-else'))) }, '<=': (lhs, rhs, node) => { const narrowKind = NarrowKind.Smaller | NarrowKind.Equal - return calculateComparisonDomains(lhs, rhs, node, narrowKind) - .concat(calculateComparisonDomains(lhs, rhs, node, narrowKind ^0b111, '-else')) + return AINodeStore.from(calculateComparisonDomains(lhs, rhs, node, narrowKind) + .concat(calculateComparisonDomains(lhs, rhs, node, narrowKind ^0b111, '-else'))) }, '>=': (lhs, rhs, node) => { const narrowKind = NarrowKind.Greater | NarrowKind.Equal - return calculateComparisonDomains(lhs, rhs, node, narrowKind) - .concat(calculateComparisonDomains(lhs, rhs, node, narrowKind ^0b111, '-else')) + return AINodeStore.from(calculateComparisonDomains(lhs, rhs, node, narrowKind) + .concat(calculateComparisonDomains(lhs, rhs, node, narrowKind ^0b111, '-else'))) }, } diff --git a/src/abstract-interpretation/handler/conditional/conditional.ts b/src/abstract-interpretation/handler/conditional/conditional.ts index 98f9cb0c7a..27b9843cb3 100644 --- a/src/abstract-interpretation/handler/conditional/conditional.ts +++ b/src/abstract-interpretation/handler/conditional/conditional.ts @@ -1,15 +1,17 @@ -import {ParentInformation, RIfThenElse} from '../../../r-bridge' -import {guard} from '../../../util/assert' -import {AINode, AINodeStore, RegisterBehavior} from '../../ainode' -import {aiLogger, getDfgChildrenOfType} from '../../processor' -import {Handler} from '../handler' -import {DataflowInformation} from '../../../dataflow/internal/info' -import {EdgeType} from '../../../dataflow' +import { guard } from '../../../util/assert' +import { AINode, AINodeStore, RegisterBehavior } from '../../ainode' +import { aiLogger, getDfgChildrenOfType } from '../../processor' +import { Handler } from '../handler' +import type { DataflowInformation } from '../../../dataflow/info' +import type { RIfThenElse } from '../../../r-bridge/lang-4.x/ast/model/nodes/r-if-then-else' +import type { ParentInformation } from '../../../r-bridge/lang-4.x/ast/model/processing/decorate' +import { EdgeType } from '../../../dataflow/graph/edge' export class Conditional extends Handler { condition: AINodeStore | undefined then: AINodeStore | undefined else: AINodeStore | undefined + // TODO: They are pretty unnecessary, and only here 'cause we use then and else to indicate which we already handled thenDomains = AINodeStore.empty() elseDomains = AINodeStore.empty() @@ -45,21 +47,21 @@ export class Conditional extends Handler { if(this.condition === undefined) { this.condition = aiNodes for(const node of aiNodes) { - const isElseNode = node.nodeId.endsWith('-else') - const cleanedId = isElseNode ? node.nodeId.slice(0, -5) : node.nodeId + const isElseNode = node.nodeId.toString().endsWith('-else') + const cleanedId = isElseNode ? node.nodeId.toString().slice(0, -5) : node.nodeId const dfChildren = getDfgChildrenOfType(cleanedId, this.dfg, EdgeType.Reads) if(dfChildren === undefined) { if(isElseNode) { - this.elseDomains.register(AINode.copy(node, {nodeId: cleanedId}), RegisterBehavior.Overwrite) + this.elseDomains.register(AINode.copy(node, { nodeId: cleanedId }), RegisterBehavior.Overwrite) } else { this.thenDomains.register(node, RegisterBehavior.Overwrite) } } else { for(const child of dfChildren) { if(isElseNode) { - this.elseDomains.register(AINode.copy(node, {nodeId: child}), RegisterBehavior.Overwrite) + this.elseDomains.register(AINode.copy(node, { nodeId: child }), RegisterBehavior.Overwrite) } else { - this.thenDomains.register(AINode.copy(node, {nodeId: child}), RegisterBehavior.Overwrite) + this.thenDomains.register(AINode.copy(node, { nodeId: child }), RegisterBehavior.Overwrite) } } } diff --git a/src/abstract-interpretation/handler/exprlist/exprlist.ts b/src/abstract-interpretation/handler/exprlist/exprlist.ts index 6d4680b6ee..94e35bbbb6 100644 --- a/src/abstract-interpretation/handler/exprlist/exprlist.ts +++ b/src/abstract-interpretation/handler/exprlist/exprlist.ts @@ -1,6 +1,6 @@ -import {AINodeStore} from '../../ainode' -import {Handler} from '../handler' -import {DataflowInformation} from '../../../dataflow/internal/info' +import { Handler } from '../handler' +import type { DataflowInformation } from '../../../dataflow/info' +import type { AINodeStore } from '../../ainode' export class ExprList extends Handler { constructor(dfg: DataflowInformation, domains: AINodeStore) { diff --git a/src/abstract-interpretation/handler/handler.ts b/src/abstract-interpretation/handler/handler.ts index a2b244e2e9..9f829d3008 100644 --- a/src/abstract-interpretation/handler/handler.ts +++ b/src/abstract-interpretation/handler/handler.ts @@ -1,6 +1,6 @@ -import {AINodeStore} from '../ainode' -import {DataflowInformation} from '../../dataflow/internal/info' -import {aiLogger} from '../processor' +import type { AINodeStore } from '../ainode' +import { aiLogger } from '../processor' +import type { DataflowInformation } from '../../dataflow/info' export abstract class Handler { protected constructor( diff --git a/src/abstract-interpretation/processor.ts b/src/abstract-interpretation/processor.ts index f27684c1d4..d411e75ae1 100644 --- a/src/abstract-interpretation/processor.ts +++ b/src/abstract-interpretation/processor.ts @@ -87,5 +87,7 @@ export function runAbstractInterpretation(ast: NormalizedAst, dfg: DataflowInfor aiLogger.warn(`Unknown node type ${node.type}`) } }) + const result = operationStack.pop()?.exit() + guard(result !== undefined, 'Empty operationStack') return dfg } diff --git a/src/util/cfg/cfg.ts b/src/util/cfg/cfg.ts index 7c7a17d1cd..495ceae60f 100644 --- a/src/util/cfg/cfg.ts +++ b/src/util/cfg/cfg.ts @@ -514,9 +514,9 @@ function cfgUnaryOp(unary: RNodeWithParent, operand: ControlFlowInformation): Co function cfgExprList(node: RNodeWithParent, _grouping: unknown, expressions: ControlFlowInformation[]): ControlFlowInformation { - const exitPoint: NodeId = node.info.id + '-exit' + const exitPoint: NodeId = node.info.id + '-exit' const result: ControlFlowInformation = { graph: new ControlFlowGraph(), breaks: [], nexts: [], returns: [], exitPoints: [exitPoint], entryPoints: [node.info.id] } - let previousExitPoints: NodeId[] = [node.info.id] + let previousExitPoints: NodeId[] = [node.info.id] let first = true for(const expression of expressions) { if(first) { diff --git a/test/functionality/abstract-interpretation/abstract-interpretation.spec.ts b/test/functionality/abstract-interpretation/abstract-interpretation.spec.ts index aa6008123d..e639a1e488 100644 --- a/test/functionality/abstract-interpretation/abstract-interpretation.spec.ts +++ b/test/functionality/abstract-interpretation/abstract-interpretation.spec.ts @@ -3,6 +3,8 @@ import { doIntervalsOverlap, Domain, Interval, + narrowDomain, + NarrowKind, subtractDomains, unifyOverlappingIntervals } from '../../../src/abstract-interpretation/domain' @@ -126,4 +128,22 @@ describe('Abstract Interpretation', () => { 'Subtracting two domains with overlapping intervals, subtracts the intervals' ) }) + + it('Domain narrowing', () => { + // narrowDomain(Domain.fromIntervals([new Interval({value: 0, inclusive: true}, {value: 3, inclusive: false})]), + // Domain.fromIntervals([new Interval({value: 3, inclusive: true}, {value: 7, inclusive: true})]), + // NarrowKind.Greater).toString() + narrowDomain(Domain.fromIntervals([new Interval({ value: 2, inclusive: true }, { value: 5, inclusive: true })]), + Domain.fromIntervals([new Interval({ value: 3, inclusive: true }, { value: 7, inclusive: true })]), + NarrowKind.Greater).toString() + console.log('') + narrowDomain(Domain.fromIntervals([new Interval({ value: 2, inclusive: true }, { value: 5, inclusive: true })]), + Domain.fromIntervals([new Interval({ value: 3, inclusive: true }, { value: 7, inclusive: true })]), + NarrowKind.Smaller).toString() + /* // const overlap = overlapIntervals(new Interval({value: 2, inclusive: true}, {value: 6, inclusive: true}), new Interval({value: 2, inclusive: false}, {value: 8, inclusive: true})) + const overlap = overlapIntervals(new Interval({value: 2, inclusive: true}, {value: 5, inclusive: true}), new Interval({value: 0, inclusive: true}, {value: 4, inclusive: true})) + console.log(`left: ${overlap.smaller?.toString() ?? 'none'}`) + console.log(`overlap: ${overlap.intersection?.toString() ?? 'none'}`) + console.log(`right: ${overlap.larger?.toString() ?? 'none'}`) */ + }) }) From 52437c88748d3d5bd37fa1a389a9dd5a7c7975bc Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Wed, 29 May 2024 15:09:18 +0200 Subject: [PATCH 52/70] feat: handle string and number node-ids correctly --- src/abstract-interpretation/ainode.ts | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/abstract-interpretation/ainode.ts b/src/abstract-interpretation/ainode.ts index 9802395f02..9838b08491 100644 --- a/src/abstract-interpretation/ainode.ts +++ b/src/abstract-interpretation/ainode.ts @@ -31,14 +31,21 @@ export const enum RegisterBehavior { Merge } +function nodeIdToString(id: NodeId): NodeId { + if(typeof id === 'string') { + return id + } + return String(id) +} + export class AINodeStore implements Iterable { - private readonly map: Map + private readonly map: Map, AINode> private constructor(content: AINode[] | AINode | undefined = undefined, private readonly parent: AINodeStore | undefined = undefined) { if(Array.isArray(content)) { - this.map = new Map(content.map(node => [node.nodeId, node])) + this.map = new Map(content.map(node => [nodeIdToString(node.nodeId), node])) } else if(content !== undefined) { - this.map = new Map([[content.nodeId, content]]) + this.map = new Map([[nodeIdToString(content.nodeId), content]]) } else if(content === undefined) { this.map = new Map() } else { @@ -59,14 +66,16 @@ export class AINodeStore implements Iterable { } has(id: NodeId): boolean { - return this.map.has(id) || (this.parent?.has(id) ?? false) + const stringId = nodeIdToString(id) + return this.map.has(stringId) || (this.parent?.has(stringId) ?? false) } get(id: NodeId | undefined): AINode | undefined { if(id === undefined) { return undefined } - return this.map.get(id) ?? this.parent?.get(id) + const stringId = nodeIdToString(id) + return this.map.get(stringId) ?? this.parent?.get(stringId) } get size(): number { @@ -84,14 +93,14 @@ export class AINodeStore implements Iterable { case RegisterBehavior.Overwrite: // Even if a parent contains the node, we will set it in the top store, so // outer scopes are not affected by inner scopes - this.map.set(node.nodeId, node) + this.map.set(nodeIdToString(node.nodeId), node) break case RegisterBehavior.Ignore: break case RegisterBehavior.Fail: return guard(existing === node, `Node with ID ${node.nodeId} already exists in the store`) case RegisterBehavior.Merge: { - const existing = this.map.get(node.nodeId) + const existing = this.map.get(nodeIdToString(node.nodeId)) guard(existing !== undefined, `Node with ID ${node.nodeId} should exist`) this.register(AINode.copy(node, { domain: unifyDomains([existing.domain, node.domain]) }), RegisterBehavior.Overwrite) break @@ -99,7 +108,7 @@ export class AINodeStore implements Iterable { default: assertUnreachable(behavior) } } else { - this.map.set(node.nodeId, node) + this.map.set(nodeIdToString(node.nodeId), node) } } From 92d5c5a3fd923a89686a591863e689c8ef181a1b Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Thu, 6 Jun 2024 17:34:56 +0200 Subject: [PATCH 53/70] feat: added domain generation to the dataflow step --- src/abstract-interpretation/ainode.ts | 142 ------------------ src/abstract-interpretation/domain.ts | 13 ++ .../handler/binop/binop.ts | 43 ------ .../handler/binop/operators.ts | 48 ------ .../handler/conditional/conditional.ts | 89 ----------- .../handler/exprlist/exprlist.ts | 9 -- .../handler/handler.ts | 28 ---- src/abstract-interpretation/processor.ts | 93 ------------ src/dataflow/environments/clone.ts | 11 +- src/dataflow/environments/identifier.ts | 5 +- src/dataflow/graph/vertex.ts | 2 + src/dataflow/info.ts | 3 + src/util/mermaid/dfg.ts | 2 +- 13 files changed, 33 insertions(+), 455 deletions(-) delete mode 100644 src/abstract-interpretation/ainode.ts delete mode 100644 src/abstract-interpretation/handler/binop/binop.ts delete mode 100644 src/abstract-interpretation/handler/binop/operators.ts delete mode 100644 src/abstract-interpretation/handler/conditional/conditional.ts delete mode 100644 src/abstract-interpretation/handler/exprlist/exprlist.ts delete mode 100644 src/abstract-interpretation/handler/handler.ts delete mode 100644 src/abstract-interpretation/processor.ts diff --git a/src/abstract-interpretation/ainode.ts b/src/abstract-interpretation/ainode.ts deleted file mode 100644 index 9838b08491..0000000000 --- a/src/abstract-interpretation/ainode.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { assertUnreachable, guard } from '../util/assert' -import type { Domain } from './domain' -import { unifyDomains } from './domain' -import type { ParentInformation, RNodeWithParent } from '../r-bridge/lang-4.x/ast/model/processing/decorate' -import type { NodeId } from '../r-bridge/lang-4.x/ast/model/processing/node-id' - -export class AINode { - constructor( - public readonly domain: Domain, - public readonly astNode: RNodeWithParent, - /** The ID of the whole expression that the domain was calculated from (e.g. the whole assignment expression) */ - public readonly expressionId: NodeId = astNode.info.id, - /** The ID of the node that logically holds the domain (e.g. the lhs of an assignment) */ - public readonly nodeId: NodeId = astNode.info.id - ) {} - - static copy(node: AINode, changes: Partial): AINode { - return new AINode( - changes.domain ?? node.domain, - changes.astNode ?? node.astNode, - changes.expressionId ?? node.expressionId, - changes.nodeId ?? node.nodeId - ) - } -} - -export const enum RegisterBehavior { - Overwrite, - Ignore, - Fail, - Merge -} - -function nodeIdToString(id: NodeId): NodeId { - if(typeof id === 'string') { - return id - } - return String(id) -} - -export class AINodeStore implements Iterable { - private readonly map: Map, AINode> - - private constructor(content: AINode[] | AINode | undefined = undefined, private readonly parent: AINodeStore | undefined = undefined) { - if(Array.isArray(content)) { - this.map = new Map(content.map(node => [nodeIdToString(node.nodeId), node])) - } else if(content !== undefined) { - this.map = new Map([[nodeIdToString(content.nodeId), content]]) - } else if(content === undefined) { - this.map = new Map() - } else { - assertUnreachable(content) - } - } - - static empty(): AINodeStore { - return new AINodeStore() - } - - static from(content: AINode[] | AINode): AINodeStore { - return new AINodeStore(content) - } - - static withParent(parent: AINodeStore): AINodeStore { - return new AINodeStore(undefined, parent) - } - - has(id: NodeId): boolean { - const stringId = nodeIdToString(id) - return this.map.has(stringId) || (this.parent?.has(stringId) ?? false) - } - - get(id: NodeId | undefined): AINode | undefined { - if(id === undefined) { - return undefined - } - const stringId = nodeIdToString(id) - return this.map.get(stringId) ?? this.parent?.get(stringId) - } - - get size(): number { - return this.map.size + (this.parent?.size ?? 0) - } - - get top(): AINodeStore { - return new AINodeStore(Array.from(this.map.values()), undefined) - } - - register(node: AINode, behavior: RegisterBehavior = RegisterBehavior.Fail): void { - const existing = this.get(node.nodeId) - if(existing !== undefined) { - switch(behavior) { - case RegisterBehavior.Overwrite: - // Even if a parent contains the node, we will set it in the top store, so - // outer scopes are not affected by inner scopes - this.map.set(nodeIdToString(node.nodeId), node) - break - case RegisterBehavior.Ignore: - break - case RegisterBehavior.Fail: - return guard(existing === node, `Node with ID ${node.nodeId} already exists in the store`) - case RegisterBehavior.Merge: { - const existing = this.map.get(nodeIdToString(node.nodeId)) - guard(existing !== undefined, `Node with ID ${node.nodeId} should exist`) - this.register(AINode.copy(node, { domain: unifyDomains([existing.domain, node.domain]) }), RegisterBehavior.Overwrite) - break - } - default: assertUnreachable(behavior) - } - } else { - this.map.set(nodeIdToString(node.nodeId), node) - } - } - - [Symbol.iterator](): Iterator { - return composeIterators(this.map.values(), this.parent?.[Symbol.iterator]()) - } - - updateWith(domains: AINodeStore): void { - for(const node of domains) { - this.register(node, RegisterBehavior.Overwrite) - } - } -} - -function* composeIterators(first: Iterator, second: Iterator | undefined): Iterator { - let result = first.next() - while(!result.done) { - yield result.value - result = first.next() - } - - if(second === undefined) { - return - } - - result = second.next() - while(!result.done) { - yield result.value - result = second.next() - } -} diff --git a/src/abstract-interpretation/domain.ts b/src/abstract-interpretation/domain.ts index 4fdfcfa28b..ffaf8f5753 100644 --- a/src/abstract-interpretation/domain.ts +++ b/src/abstract-interpretation/domain.ts @@ -90,6 +90,19 @@ export class Domain { return `{${Array.from(this.intervals).join(', ')}}` } } + + toJSON(): object { + return { + __type: 'Domain', + intervals: this.intervals + } + } + + static revive(data: object): Domain { + // @ts-expect-error I'm too lazy to define the proper type for the JSON object + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return Domain.fromIntervals(data.intervals) + } } const enum CompareType { diff --git a/src/abstract-interpretation/handler/binop/binop.ts b/src/abstract-interpretation/handler/binop/binop.ts deleted file mode 100644 index 17c382d21d..0000000000 --- a/src/abstract-interpretation/handler/binop/binop.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Handler } from '../handler' -import type { AINode, AINodeStore } from '../../ainode' -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' -import { guard } from '../../../util/assert' -import { operators } from './operators' -import type { NodeId } from '../../../r-bridge/lang-4.x/ast/model/processing/node-id' -import type { DataflowInformation } from '../../../dataflow/info' - -export type BinOpOperators = Record) => AINodeStore> - -export class BinOp extends Handler { - lhs: NodeId | undefined - rhs: NodeId | undefined - - constructor( - dfg: DataflowInformation, - domains: AINodeStore, - private readonly node: RBinaryOp - ) { - super(dfg, domains, `Bin Op (${node.operator})`) - } - - exit(): AINodeStore { - const lhs = this.domains.get(this.lhs) - const rhs = this.domains.get(this.rhs) - guard(lhs !== undefined, `No LHS found for assignment ${this.node.info.id}`) - guard(rhs !== undefined, `No RHS found for assignment ${this.node.info.id}`) - this.domains.updateWith(operators[this.node.operator](lhs, rhs, this.node)) - return super.exit() - } - - next(aiNodes: AINodeStore): void { - super.next(aiNodes) - if(this.lhs === undefined) { - this.lhs = this.node.lhs.info.id - } else if(this.rhs === undefined) { - this.rhs = this.node.rhs.info.id - } else { - guard(false, `BinOp ${this.node.info.id} already has both LHS and RHS`) - } - } -} \ No newline at end of file diff --git a/src/abstract-interpretation/handler/binop/operators.ts b/src/abstract-interpretation/handler/binop/operators.ts deleted file mode 100644 index b211b9cb49..0000000000 --- a/src/abstract-interpretation/handler/binop/operators.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { addDomains, Domain, narrowDomain, NarrowKind, subtractDomains } from '../../domain' -import type { BinOpOperators } from './binop' -import { AINode, AINodeStore } from '../../ainode' -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' - -const calculateComparisonDomains = (lhs: AINode, rhs: AINode, node: RBinaryOp, narrowKind: NarrowKind, idSuffix = ''): AINode[] => { - const lhsNarrowed = narrowDomain(lhs.domain, rhs.domain, narrowKind) - const rhsNarrowed = narrowDomain(rhs.domain, lhs.domain, narrowKind ^ 0b110 /* flip < and > but not = */) - const isConditionTrue = lhsNarrowed.isBottom() && rhsNarrowed.isBottom() - return [ - new AINode(isConditionTrue ? Domain.bottom() : Domain.top(), node, node.info.id, node.info.id + idSuffix), - new AINode(lhsNarrowed, node, node.info.id, lhs.nodeId + idSuffix), - new AINode(rhsNarrowed, node, node.info.id, rhs.nodeId + idSuffix), - ] -} - -export const operators: BinOpOperators = { - '<-': (lhs, rhs, node) => { - return AINodeStore.from(new AINode(rhs.domain, node.lhs, node.info.id)) - }, - '+': (lhs, rhs, node) => { - return AINodeStore.from(new AINode(addDomains(lhs.domain, rhs.domain), node)) - }, - '-': (lhs, rhs, node) => { - return AINodeStore.from(new AINode(subtractDomains(lhs.domain, rhs.domain), node)) - }, - '<': (lhs, rhs, node) => { - const narrowKind = NarrowKind.Smaller - return AINodeStore.from(calculateComparisonDomains(lhs, rhs, node, narrowKind) - .concat(calculateComparisonDomains(lhs, rhs, node, narrowKind ^0b111, '-else'))) - }, - '>': (lhs, rhs, node) => { - const narrowKind = NarrowKind.Greater - return AINodeStore.from(calculateComparisonDomains(lhs, rhs, node, narrowKind) - .concat(calculateComparisonDomains(lhs, rhs, node, narrowKind ^0b111, '-else'))) - }, - '<=': (lhs, rhs, node) => { - const narrowKind = NarrowKind.Smaller | NarrowKind.Equal - return AINodeStore.from(calculateComparisonDomains(lhs, rhs, node, narrowKind) - .concat(calculateComparisonDomains(lhs, rhs, node, narrowKind ^0b111, '-else'))) - }, - '>=': (lhs, rhs, node) => { - const narrowKind = NarrowKind.Greater | NarrowKind.Equal - return AINodeStore.from(calculateComparisonDomains(lhs, rhs, node, narrowKind) - .concat(calculateComparisonDomains(lhs, rhs, node, narrowKind ^0b111, '-else'))) - }, -} diff --git a/src/abstract-interpretation/handler/conditional/conditional.ts b/src/abstract-interpretation/handler/conditional/conditional.ts deleted file mode 100644 index 27b9843cb3..0000000000 --- a/src/abstract-interpretation/handler/conditional/conditional.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { guard } from '../../../util/assert' -import { AINode, AINodeStore, RegisterBehavior } from '../../ainode' -import { aiLogger, getDfgChildrenOfType } from '../../processor' -import { Handler } from '../handler' -import type { DataflowInformation } from '../../../dataflow/info' -import type { RIfThenElse } from '../../../r-bridge/lang-4.x/ast/model/nodes/r-if-then-else' -import type { ParentInformation } from '../../../r-bridge/lang-4.x/ast/model/processing/decorate' -import { EdgeType } from '../../../dataflow/graph/edge' - -export class Conditional extends Handler { - condition: AINodeStore | undefined - then: AINodeStore | undefined - else: AINodeStore | undefined - // TODO: They are pretty unnecessary, and only here 'cause we use then and else to indicate which we already handled - thenDomains = AINodeStore.empty() - elseDomains = AINodeStore.empty() - - constructor( - dfg: DataflowInformation, - domains: AINodeStore, - private readonly node: RIfThenElse - ) { - super(dfg, domains, 'IfThenElse') - } - - exit(): AINodeStore { - aiLogger.trace(`Exited ${this.name}`) - guard(this.condition !== undefined, `No condition found for conditional ${this.node.info.id}`) - guard(this.then !== undefined, `No then-branch found for conditional ${this.node.info.id}`) - - const result = AINodeStore.empty() - for(const conditionNode of this.condition) { - result.register(conditionNode) - } - for(const thenNode of this.then) { - result.register(thenNode) - } - for(const elseNode of this.else ?? []) { - result.register(elseNode) - } - - return result - } - - next(aiNodes: AINodeStore): void { - aiLogger.trace(`${this.name} received`) - if(this.condition === undefined) { - this.condition = aiNodes - for(const node of aiNodes) { - const isElseNode = node.nodeId.toString().endsWith('-else') - const cleanedId = isElseNode ? node.nodeId.toString().slice(0, -5) : node.nodeId - const dfChildren = getDfgChildrenOfType(cleanedId, this.dfg, EdgeType.Reads) - if(dfChildren === undefined) { - if(isElseNode) { - this.elseDomains.register(AINode.copy(node, { nodeId: cleanedId }), RegisterBehavior.Overwrite) - } else { - this.thenDomains.register(node, RegisterBehavior.Overwrite) - } - } else { - for(const child of dfChildren) { - if(isElseNode) { - this.elseDomains.register(AINode.copy(node, { nodeId: child }), RegisterBehavior.Overwrite) - } else { - this.thenDomains.register(AINode.copy(node, { nodeId: child }), RegisterBehavior.Overwrite) - } - } - } - } - this.domains.updateWith(this.thenDomains) - } else if(this.then === undefined) { - this.then = AINodeStore.empty() - const conditionDomain = this.thenDomains.get(this.node.condition.info.id)?.domain - guard(conditionDomain !== undefined, `No domain found for condition ${this.node.condition.info.id}`) - if(!conditionDomain.isBottom()) { - this.then.updateWith(aiNodes) - } - this.domains.updateWith(this.elseDomains) - } else if(this.else === undefined) { - this.else = AINodeStore.empty() - const conditionDomain = this.elseDomains.get(this.node.condition.info.id)?.domain - guard(conditionDomain !== undefined, `No domain found for condition ${this.node.condition.info.id}`) - if(!conditionDomain.isBottom()) { - this.else.updateWith(aiNodes) - } - } else { - guard(false, `Conditional ${this.node.info.id} already has condition, then and else`) - } - } -} \ No newline at end of file diff --git a/src/abstract-interpretation/handler/exprlist/exprlist.ts b/src/abstract-interpretation/handler/exprlist/exprlist.ts deleted file mode 100644 index 94e35bbbb6..0000000000 --- a/src/abstract-interpretation/handler/exprlist/exprlist.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Handler } from '../handler' -import type { DataflowInformation } from '../../../dataflow/info' -import type { AINodeStore } from '../../ainode' - -export class ExprList extends Handler { - constructor(dfg: DataflowInformation, domains: AINodeStore) { - super(dfg, domains, 'ExprList') - } -} diff --git a/src/abstract-interpretation/handler/handler.ts b/src/abstract-interpretation/handler/handler.ts deleted file mode 100644 index 9f829d3008..0000000000 --- a/src/abstract-interpretation/handler/handler.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { AINodeStore } from '../ainode' -import { aiLogger } from '../processor' -import type { DataflowInformation } from '../../dataflow/info' - -export abstract class Handler { - protected constructor( - protected readonly dfg: DataflowInformation, - private _domains: AINodeStore, - public readonly name: string - ) { } - - enter(): void { - aiLogger.trace(`Entered ${this.name}`) - } - - exit(): AINodeStore { - return this.domains.top - } - - next(aiNodes: AINodeStore): void { - aiLogger.trace(`${this.name} received`) - this.domains.updateWith(aiNodes) - } - - get domains(): AINodeStore { - return this._domains - } -} \ 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 d411e75ae1..0000000000 --- a/src/abstract-interpretation/processor.ts +++ /dev/null @@ -1,93 +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 } 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' -import { ExprList } from './handler/exprlist/exprlist' -import { AINode, AINodeStore } from './ainode' -import { Conditional } from './handler/conditional/conditional' - -export const aiLogger = log.getSubLogger({ name: 'abstract-interpretation' }) - -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 - } -} - -export function getDfgChildrenOfType(node: NodeId, dfg: DataflowInformation, ...types: EdgeType[]): NodeId[] | undefined { - const dfgNode: [DataflowGraphVertexInfo, OutgoingEdges] | undefined = dfg.graph.get(node) - if(dfgNode === undefined) { - return undefined - } - const [_, children] = dfgNode - return Array.from(children.entries()) - .filter(([_, edge]) => types.some(type => edgeIncludesType(edge.types, type))) - .map(([id, _]) => id) -} - -function getDomainOfDfgChild(node: NodeId, dfg: DataflowInformation, domainStore: AINodeStore): Domain { - guard(dfg.graph.hasVertex(node, true), `No DFG-Node found with ID ${node}`) - const domains = getDfgChildrenOfType(node, dfg, EdgeType.Reads) - ?.map(id => domainStore.get(id)?.domain) - .filter(domain => domain !== undefined) - .map(domain => domain as Domain) - return unifyDomains(domains ?? []) -} - -export function runAbstractInterpretation(ast: NormalizedAst, dfg: DataflowInformation): DataflowInformation { - const cfg = extractCFG(ast) - const operationStack = new Stack() - operationStack.push(new ExprList(dfg, AINodeStore.empty())).enter() - visitCfg(cfg, (node, _) => { - const astNode = ast.idMap.get(node.id) - const top = operationStack.peek() - guard(top !== undefined, 'No operation on the stack') - if(astNode?.type === RType.BinaryOp) { - operationStack.push(new BinOp(dfg, AINodeStore.withParent(top.domains), astNode)).enter() - } else if(astNode?.type === RType.IfThenElse) { - operationStack.push(new Conditional(dfg, AINodeStore.withParent(top.domains), astNode)).enter() - } else if(astNode?.type === RType.ExpressionList) { - operationStack.push(new ExprList(dfg, AINodeStore.withParent(top.domains))).enter() - } else if(astNode?.type === RType.Symbol) { - top.next(AINodeStore.from(new AINode(getDomainOfDfgChild(node.id, dfg, top.domains), astNode))) - } else if(astNode?.type === RType.Number) { - top.next(AINodeStore.from(new AINode(Domain.fromScalar(astNode.content.num), astNode))) - } else if(astNode?.type === RType.Logical) { - top.next(AINodeStore.from(new AINode(astNode.content ? Domain.top() : Domain.bottom(), astNode))) - } else if(node.type === CfgVertexType.EndMarker) { - const operationResult = operationStack.pop()?.exit() - guard(operationResult !== undefined, 'No operation result') - const newTop = operationStack.peek() - guard(newTop !== undefined, 'No operation on the stack') - newTop.next(operationResult) - } else { - aiLogger.warn(`Unknown node type ${node.type}`) - } - }) - const result = operationStack.pop()?.exit() - guard(result !== undefined, 'Empty operationStack') - return dfg -} diff --git a/src/dataflow/environments/clone.ts b/src/dataflow/environments/clone.ts index eaffcd112f..67283487a2 100644 --- a/src/dataflow/environments/clone.ts +++ b/src/dataflow/environments/clone.ts @@ -7,6 +7,7 @@ import { BuiltInEnvironment } from './environment' import type { Identifier, IdentifierDefinition } from './identifier' +import { Domain } from '../../abstract-interpretation/domain' function cloneEnvironment(environment: IEnvironment, recurseParents: boolean): IEnvironment function cloneEnvironment(environment: IEnvironment | undefined, recurseParents: boolean): IEnvironment | undefined { @@ -16,7 +17,15 @@ function cloneEnvironment(environment: IEnvironment | undefined, recurseParents: return BuiltInEnvironment } const clone = new Environment(environment.name, recurseParents ? cloneEnvironment(environment.parent, recurseParents) : environment.parent) - clone.memory = new Map(JSON.parse(JSON.stringify([...environment.memory])) as [Identifier, IdentifierDefinition[]][]) + clone.memory = new Map(JSON.parse(JSON.stringify([...environment.memory]), (k, v) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if(k === '' && v?.__type === 'Domain') { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return Domain.revive(v) + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return v + }) as [Identifier, IdentifierDefinition[]][]) return clone } diff --git a/src/dataflow/environments/identifier.ts b/src/dataflow/environments/identifier.ts index 42d4a7128c..6807376430 100644 --- a/src/dataflow/environments/identifier.ts +++ b/src/dataflow/environments/identifier.ts @@ -1,12 +1,15 @@ import type { BuiltInIdentifierConstant, BuiltInIdentifierDefinition } from './built-in' import type { NodeId } from '../../r-bridge/lang-4.x/ast/model/processing/node-id' +import type { Domain } from '../../abstract-interpretation/domain' export type Identifier = string & { __brand?: 'identifier' } -interface InGraphIdentifierDefinition extends IdentifierReference { +export interface InGraphIdentifierDefinition extends IdentifierReference { kind: 'function' | 'variable' | 'parameter' | 'argument' /** The assignment (or whatever, like `assign` function call) node which ultimately defined this identifier */ definedAt: NodeId + /** If one could be determined, the domain of the identifier */ + domain?: Domain } /** diff --git a/src/dataflow/graph/vertex.ts b/src/dataflow/graph/vertex.ts index 16ed2bcbd2..e27af05f33 100644 --- a/src/dataflow/graph/vertex.ts +++ b/src/dataflow/graph/vertex.ts @@ -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 { Domain } from '../../abstract-interpretation/domain' export type DataflowGraphVertices = Map @@ -38,6 +39,7 @@ interface DataflowGraphVertexBase extends MergeableRecord { * See {@link IdentifierReference} */ controlDependencies: NodeId[] | undefined + domain?: Domain } export interface DataflowGraphValue extends DataflowGraphVertexBase { diff --git a/src/dataflow/info.ts b/src/dataflow/info.ts index 341bc8d294..a8f98396d2 100644 --- a/src/dataflow/info.ts +++ b/src/dataflow/info.ts @@ -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 { Domain } from '../abstract-interpretation/domain' export const enum ExitPointType { Default = 0, @@ -50,6 +51,8 @@ export interface DataflowInformation extends DataflowCfgInformation { environment: REnvironmentInformation /** The current constructed dataflow graph */ graph: DataflowGraph + /** Domain of the thing that was most recently added */ + domain?: Domain } export function initializeCleanDataflowInformation(entryPoint: NodeId, data: Pick, 'environment' | 'completeAst'>): DataflowInformation { diff --git a/src/util/mermaid/dfg.ts b/src/util/mermaid/dfg.ts index 56504c157a..a60906d060 100644 --- a/src/util/mermaid/dfg.ts +++ b/src/util/mermaid/dfg.ts @@ -156,7 +156,7 @@ function vertexToMermaid(info: DataflowGraphVertexInfo, mermaid: MermaidGraph, i } const node = mermaid.rootGraph.idMap?.get(info.id) - const escapedName = escapeMarkdown(node ? `[${node.type}] ${node.lexeme ?? '??'}` : '??') + const escapedName = escapeMarkdown(node ? `${info.domain?.toString() ?? ''} [${node.type}] ${node.lexeme ?? '??'}` : '??') const deps = info.controlDependencies ? ', :maybe:' + info.controlDependencies.join(',') : '' mermaid.nodeLines.push(` ${idPrefix}${id}${open}"\`${escapedName}${escapedName.length > 10 ? '\n ' : ' '}(${id}${deps})\n *${formatRange(mermaid.rootGraph.idMap?.get(id)?.location)}*${ From 1c76879b6a58cbfba456529ff32df6fb08310069 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Thu, 6 Jun 2024 17:37:29 +0200 Subject: [PATCH 54/70] feat: propagate domain of rhs to lhs in assignements --- .../call/built-in/built-in-assignment.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/dataflow/internal/process/functions/call/built-in/built-in-assignment.ts b/src/dataflow/internal/process/functions/call/built-in/built-in-assignment.ts index 2606b8ed44..eb36f7a3a0 100644 --- a/src/dataflow/internal/process/functions/call/built-in/built-in-assignment.ts +++ b/src/dataflow/internal/process/functions/call/built-in/built-in-assignment.ts @@ -6,7 +6,10 @@ import { log, LogLevel } from '../../../../../../util/log' import { unpackArgument } from '../argument/unpack-argument' import { processAsNamedCall } from '../../../process-named-call' import { toUnnamedArgument } from '../argument/make-argument' -import type { ParentInformation, RNodeWithParent } from '../../../../../../r-bridge/lang-4.x/ast/model/processing/decorate' +import type { + ParentInformation, + RNodeWithParent +} from '../../../../../../r-bridge/lang-4.x/ast/model/processing/decorate' import type { Base, Location, RNode } from '../../../../../../r-bridge/lang-4.x/ast/model/model' import type { RSymbol } from '../../../../../../r-bridge/lang-4.x/ast/model/nodes/r-symbol' import { RType } from '../../../../../../r-bridge/lang-4.x/ast/model/type' @@ -110,12 +113,13 @@ function extractSourceAndTarget(args: readonly RFunctionArgument(rootId: NodeId, target: DataflowInformation, isFunctionDef: boolean, data: DataflowProcessorInformation, makeMaybe: boolean): IdentifierDefinition[] { +function produceWrittenNodes(rootId: NodeId, target: DataflowInformation, source: DataflowInformation, isFunctionDef: boolean, data: DataflowProcessorInformation, makeMaybe: boolean): IdentifierDefinition[] { return target.in.map(ref => ({ ...ref, kind: isFunctionDef ? 'function' : 'variable', definedAt: rootId, - controlDependencies: data.controlDependencies ?? (makeMaybe ? [] : undefined) + controlDependencies: data.controlDependencies ?? (makeMaybe ? [] : undefined), + domain: source.domain })) } @@ -192,7 +196,7 @@ function processAssignmentToSymbol({ }: AssignmentToSymbolParameters): DataflowInformation { const isFunctionDef = checkFunctionDef(source, sourceArg) - const writeNodes = produceWrittenNodes(rootId, targetArg, isFunctionDef, data, makeMaybe ?? false) + const writeNodes = produceWrittenNodes(rootId, targetArg, sourceArg, isFunctionDef, data, makeMaybe ?? false) if(writeNodes.length !== 1 && log.settings.minLevel <= LogLevel.Warn) { log.warn(`Unexpected write number in assignment: ${JSON.stringify(writeNodes)}`) @@ -234,6 +238,7 @@ function processAssignmentToSymbol({ unknownReferences: [], entryPoint: name.info.id, in: readTargets, - out: writeTargets + out: writeTargets, + domain: sourceArg.domain } } From 8892c75d834db8c4f4c456dcc617ff9fdcb0a261 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Thu, 6 Jun 2024 17:38:00 +0200 Subject: [PATCH 55/70] feat: generate domain for symbols --- src/dataflow/internal/process/process-symbol.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/dataflow/internal/process/process-symbol.ts b/src/dataflow/internal/process/process-symbol.ts index ce8bb063a3..3840b28d5a 100644 --- a/src/dataflow/internal/process/process-symbol.ts +++ b/src/dataflow/internal/process/process-symbol.ts @@ -6,12 +6,22 @@ import type { ParentInformation } from '../../../r-bridge/lang-4.x/ast/model/pro import { RNa, RNull } from '../../../r-bridge/lang-4.x/convert-values' import { DataflowGraph } from '../../graph/graph' import { VertexType } from '../../graph/vertex' +import type { Domain } from '../../../abstract-interpretation/domain' +import { unifyDomains } from '../../../abstract-interpretation/domain' +import type { InGraphIdentifierDefinition } from '../../environments/identifier' export function processSymbol(symbol: RSymbol, data: DataflowProcessorInformation): DataflowInformation { if(symbol.content === RNull || symbol.content === RNa) { return processValue(symbol, data) } + const domains = data.environment.current.memory.get(symbol.content) + ?.filter(id => id.kind === 'function' || id.kind === 'variable' || id.kind === 'parameter' || id.kind === 'argument') + .map(id => (id as InGraphIdentifierDefinition).domain) + .filter(domain => domain !== undefined) + .map(domain => domain as Domain) + const domain = domains && unifyDomains(domains) + return { unknownReferences: [ { nodeId: symbol.info.id, name: symbol.content, controlDependencies: data.controlDependencies } ], in: [], @@ -21,9 +31,11 @@ export function processSymbol(symbol: RSymbol Date: Thu, 6 Jun 2024 17:38:27 +0200 Subject: [PATCH 56/70] feat: generate domains for numbers and logicals --- src/dataflow/internal/process/process-value.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/dataflow/internal/process/process-value.ts b/src/dataflow/internal/process/process-value.ts index f96e51693f..a018f08cfc 100644 --- a/src/dataflow/internal/process/process-value.ts +++ b/src/dataflow/internal/process/process-value.ts @@ -3,8 +3,15 @@ import type { DataflowProcessorInformation } from '../../processor' import type { RNodeWithParent } from '../../../r-bridge/lang-4.x/ast/model/processing/decorate' import { DataflowGraph } from '../../graph/graph' import { VertexType } from '../../graph/vertex' +import { Domain } from '../../../abstract-interpretation/domain' +import { RType } from '../../../r-bridge/lang-4.x/ast/model/type' export function processValue(value: RNodeWithParent, data: DataflowProcessorInformation): DataflowInformation { + let domain: Domain | undefined + switch(value.type) { + case RType.Number: domain = Domain.fromScalar(value.content.num); break + case RType.Logical: domain = value.content ? Domain.top() : Domain.bottom(); break + } return { unknownReferences: [], in: [{ nodeId: value.info.id, name: undefined, controlDependencies: data.controlDependencies }], @@ -14,9 +21,11 @@ export function processValue(value: RNodeWithParent, data: DataflowPr tag: VertexType.Value, id: value.info.id, value: value.lexeme, - controlDependencies: data.controlDependencies + controlDependencies: data.controlDependencies, + domain: domain }), exitPoints: [{ nodeId: value.info.id, type: ExitPointType.Default, controlDependencies: data.controlDependencies }], - entryPoint: value.info.id + entryPoint: value.info.id, + domain: domain } } From 09f7f500a822f3f48d24eb087501a689c287353d Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Thu, 6 Jun 2024 17:38:53 +0200 Subject: [PATCH 57/70] feat: propagate domains through arguments --- src/dataflow/internal/process/functions/process-argument.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/dataflow/internal/process/functions/process-argument.ts b/src/dataflow/internal/process/functions/process-argument.ts index 2468d67be7..563689d207 100644 --- a/src/dataflow/internal/process/functions/process-argument.ts +++ b/src/dataflow/internal/process/functions/process-argument.ts @@ -38,7 +38,8 @@ export function processFunctionArgument( graph.addVertex({ tag: VertexType.Use, id: argument.info.id, - controlDependencies: data.controlDependencies + controlDependencies: data.controlDependencies, + domain: value?.domain }) entryPoint = argument.info.id } @@ -60,6 +61,7 @@ export function processFunctionArgument( graph: graph, environment: value?.environment ?? data.environment, entryPoint: entryPoint ?? argument.info.id, - exitPoints: value?.exitPoints ?? name?.exitPoints ?? [{ nodeId: argument.info.id, type: ExitPointType.Default, controlDependencies: data.controlDependencies }] + exitPoints: value?.exitPoints ?? name?.exitPoints ?? [{ nodeId: argument.info.id, type: ExitPointType.Default, controlDependencies: data.controlDependencies }], + domain: value?.domain } } From 5f24188c2da9a6e10d3885024c73111f53b636d4 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Thu, 6 Jun 2024 17:39:54 +0200 Subject: [PATCH 58/70] refactor: fixed imports and removed logs --- src/abstract-interpretation/domain.ts | 4 +--- src/dataflow/environments/built-in.ts | 2 +- src/dataflow/environments/clone.ts | 10 ++-------- src/util/mermaid/dfg.ts | 12 +++--------- 4 files changed, 7 insertions(+), 21 deletions(-) diff --git a/src/abstract-interpretation/domain.ts b/src/abstract-interpretation/domain.ts index ffaf8f5753..975f58e090 100644 --- a/src/abstract-interpretation/domain.ts +++ b/src/abstract-interpretation/domain.ts @@ -312,7 +312,5 @@ export function narrowDomain(baseDomain: Domain, boundDomain: Domain, narrowKind } } - const newDomain = Domain.fromIntervals(narrowedIntervals.filter(interval => interval !== undefined).map(interval => interval as Interval)) - console.log(`Domain: ${newDomain.toString()}`) - return newDomain + return Domain.fromIntervals(narrowedIntervals.filter(interval => interval !== undefined).map(interval => interval as Interval)) } \ No newline at end of file diff --git a/src/dataflow/environments/built-in.ts b/src/dataflow/environments/built-in.ts index 41a08ae9f4..d0ebf4cbb3 100644 --- a/src/dataflow/environments/built-in.ts +++ b/src/dataflow/environments/built-in.ts @@ -1,6 +1,6 @@ import type { DataflowProcessorInformation } from '../processor' +import type { DataflowInformation } from '../info' 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' diff --git a/src/dataflow/environments/clone.ts b/src/dataflow/environments/clone.ts index 67283487a2..890f5bb1a0 100644 --- a/src/dataflow/environments/clone.ts +++ b/src/dataflow/environments/clone.ts @@ -1,11 +1,5 @@ -import type { - IEnvironment, - REnvironmentInformation -} from './environment' -import { - Environment, - BuiltInEnvironment -} from './environment' +import type { IEnvironment, REnvironmentInformation } from './environment' +import { BuiltInEnvironment, Environment } from './environment' import type { Identifier, IdentifierDefinition } from './identifier' import { Domain } from '../../abstract-interpretation/domain' diff --git a/src/util/mermaid/dfg.ts b/src/util/mermaid/dfg.ts index a60906d060..0cd76ea877 100644 --- a/src/util/mermaid/dfg.ts +++ b/src/util/mermaid/dfg.ts @@ -3,19 +3,13 @@ import type { SourceRange } from '../range' import { guard } from '../assert' import { escapeMarkdown, mermaidCodeToUrl } from './mermaid' -import type { - DataflowFunctionFlowInformation, - DataflowGraph, - FunctionArgument } from '../../dataflow/graph/graph' -import { isNamedArgument - , - isPositionalArgument -} from '../../dataflow/graph/graph' +import type { DataflowFunctionFlowInformation, DataflowGraph, FunctionArgument } from '../../dataflow/graph/graph' +import { isNamedArgument, isPositionalArgument } from '../../dataflow/graph/graph' import type { NodeId } from '../../r-bridge/lang-4.x/ast/model/processing/node-id' import type { IdentifierDefinition, IdentifierReference } from '../../dataflow/environments/identifier' import { EmptyArgument } from '../../r-bridge/lang-4.x/ast/model/nodes/r-function-call' import type { EdgeType } from '../../dataflow/graph/edge' -import { edgeTypeToName , splitEdgeTypes } from '../../dataflow/graph/edge' +import { edgeTypeToName, splitEdgeTypes } from '../../dataflow/graph/edge' import type { DataflowGraphVertexInfo } from '../../dataflow/graph/vertex' import { VertexType } from '../../dataflow/graph/vertex' import type { IEnvironment } from '../../dataflow/environments/environment' From e80b1f712f56a9c02aa96076f42ec0a9dd4c49d7 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Thu, 6 Jun 2024 17:46:45 +0200 Subject: [PATCH 59/70] feat: added domain calculation for + and - --- src/dataflow/environments/built-in.ts | 4 ++- .../call/built-in/built-in-arithmetic.ts | 32 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/dataflow/internal/process/functions/call/built-in/built-in-arithmetic.ts diff --git a/src/dataflow/environments/built-in.ts b/src/dataflow/environments/built-in.ts index d0ebf4cbb3..4da7970e02 100644 --- a/src/dataflow/environments/built-in.ts +++ b/src/dataflow/environments/built-in.ts @@ -24,6 +24,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 { processArithmetic } from '../internal/process/functions/call/built-in/built-in-arithmetic' export const BuiltIn = 'built-in' @@ -147,7 +148,8 @@ registerBuiltInConstant('TRUE', true) registerBuiltInConstant('T', true) registerBuiltInConstant('FALSE', false) registerBuiltInConstant('F', false) -registerSimpleFunctions('~', '+', '-', '*', '/', '^', '!', '?', '**', '==', '!=', '>', '<', '>=', '<=', '%%', '%/%', '%*%', ':', 'list') +registerSimpleFunctions('~', '*', '/', '^', '!', '?', '**', '==', '!=', '>', '<', '>=', '<=', '%%', '%/%', '%*%', ':', 'list') +registerBuiltInFunctions(['+', '-'], processArithmetic, {} ) registerBuiltInFunctions(['cat', 'switch'], defaultBuiltInProcessor, {}) /* returns null */ registerBuiltInFunctions(['print', '('], defaultBuiltInProcessor, { returnsNthArgument: 0 }, ) registerBuiltInFunctions(['return'], defaultBuiltInProcessor, { returnsNthArgument: 0, cfg: ExitPointType.Return }, ) diff --git a/src/dataflow/internal/process/functions/call/built-in/built-in-arithmetic.ts b/src/dataflow/internal/process/functions/call/built-in/built-in-arithmetic.ts new file mode 100644 index 0000000000..4a0243d0d2 --- /dev/null +++ b/src/dataflow/internal/process/functions/call/built-in/built-in-arithmetic.ts @@ -0,0 +1,32 @@ +import type { DataflowProcessorInformation } from '../../../../../processor' +import type { DataflowInformation } from '../../../../../info' +import type { ParentInformation } 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' +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 { processKnownFunctionCall } from '../known-call-handling' +import type { Domain } from '../../../../../../abstract-interpretation/domain' +import { addDomains, subtractDomains } from '../../../../../../abstract-interpretation/domain' +import { guard } from '../../../../../../util/assert' + +export function processArithmetic( + name: RSymbol, + args: readonly RFunctionArgument[], + rootId: NodeId, + data: DataflowProcessorInformation +): DataflowInformation { + const { information: dfgInfo, processedArguments } = processKnownFunctionCall({ name, args, rootId, data }) + const [lhsDomain, rhsDomain] = processedArguments.map(arg => arg?.domain) + let domain: Domain | undefined + if(lhsDomain !== undefined && rhsDomain !== undefined) { + switch(name.content) { + case '+': domain = addDomains(lhsDomain, rhsDomain); break + case '-': domain = subtractDomains(lhsDomain, rhsDomain); break + default: guard(false, `Unknown arithmetic operator ${name.content}`) + } + } + return { + ...dfgInfo, + domain: domain + } +} From 8e529b93dd00b7d9c531ce08c121207107a8efed Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Thu, 6 Jun 2024 17:48:14 +0200 Subject: [PATCH 60/70] feat: added domain calculation for comparisons There's some stuff missing though --- src/dataflow/environments/built-in.ts | 4 +- .../call/built-in/built-in-comparison.ts | 60 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/dataflow/internal/process/functions/call/built-in/built-in-comparison.ts diff --git a/src/dataflow/environments/built-in.ts b/src/dataflow/environments/built-in.ts index 4da7970e02..d3ecbbcebb 100644 --- a/src/dataflow/environments/built-in.ts +++ b/src/dataflow/environments/built-in.ts @@ -25,6 +25,7 @@ import type { NodeId } from '../../r-bridge/lang-4.x/ast/model/processing/node-i import { EdgeType } from '../graph/edge' import { processLibrary } from '../internal/process/functions/call/built-in/built-in-library' import { processArithmetic } from '../internal/process/functions/call/built-in/built-in-arithmetic' +import { processComparison } from '../internal/process/functions/call/built-in/built-in-comparison' export const BuiltIn = 'built-in' @@ -148,8 +149,9 @@ registerBuiltInConstant('TRUE', true) registerBuiltInConstant('T', true) registerBuiltInConstant('FALSE', false) registerBuiltInConstant('F', false) -registerSimpleFunctions('~', '*', '/', '^', '!', '?', '**', '==', '!=', '>', '<', '>=', '<=', '%%', '%/%', '%*%', ':', 'list') +registerSimpleFunctions('~', '*', '/', '^', '!', '?', '**', '%%', '%/%', '%*%', ':', 'list') registerBuiltInFunctions(['+', '-'], processArithmetic, {} ) +registerBuiltInFunctions(['<', '<=', '>', '>=', '==', '!='], processComparison, {} ) registerBuiltInFunctions(['cat', 'switch'], defaultBuiltInProcessor, {}) /* returns null */ registerBuiltInFunctions(['print', '('], defaultBuiltInProcessor, { returnsNthArgument: 0 }, ) registerBuiltInFunctions(['return'], defaultBuiltInProcessor, { returnsNthArgument: 0, cfg: ExitPointType.Return }, ) diff --git a/src/dataflow/internal/process/functions/call/built-in/built-in-comparison.ts b/src/dataflow/internal/process/functions/call/built-in/built-in-comparison.ts new file mode 100644 index 0000000000..a88c480739 --- /dev/null +++ b/src/dataflow/internal/process/functions/call/built-in/built-in-comparison.ts @@ -0,0 +1,60 @@ +import type { DataflowProcessorInformation } from '../../../../../processor' +import type { DataflowInformation } from '../../../../../info' +import type { ParentInformation } 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' +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 { processKnownFunctionCall } from '../known-call-handling' +import { Domain, narrowDomain, NarrowKind } from '../../../../../../abstract-interpretation/domain' +import { guard } from '../../../../../../util/assert' + +function calculateResult(lhs: Domain, rhs: Domain, narrowKind: NarrowKind): Domain { + const lhsNarrowed = narrowDomain(lhs, rhs, narrowKind) + const rhsNarrowed = narrowDomain(rhs, lhs, narrowKind ^ 0b110) + const isConditionTrue = lhsNarrowed.isBottom() && rhsNarrowed.isBottom() + + // TODO: kann ich durch kluges Schneiden der Domains in then und else fall das Generieren von beiden Fällen im + // Vergleich vermeiden? + // then: mit top schneiden -> kommt top raus? + // else: + return isConditionTrue ? Domain.bottom() : Domain.top() + // TODO: narrowings + /*return new AINode(isConditionTrue ? Domain.bottom() : Domain.top(), node, node.info.id, node.info.id, [ + { + positive: AINodeStore.from([ + new AINode(lhsNarrowed, node, node.info.id, lhs.nodeId), + new AINode(rhsNarrowed, node, node.info.id, rhs.nodeId), + ]), + negative: AINodeStore.from([ + new AINode(narrowDomain(lhs.domain, rhs.domain, narrowKind ^ 0b111), node, node.info.id, lhs.nodeId), + new AINode(narrowDomain(rhs.domain, lhs.domain, narrowKind ^ 0b111 ^ 0b110), node, node.info.id, rhs.nodeId), + ]) + } + ])*/ +} + +export function processComparison( + name: RSymbol, + args: readonly RFunctionArgument[], + rootId: NodeId, + data: DataflowProcessorInformation +): DataflowInformation { + const { information: dfgInfo, processedArguments } = processKnownFunctionCall({ name, args, rootId, data }) + const [lhsDomain, rhsDomain] = processedArguments.map(arg => arg?.domain) + let domain: Domain | undefined + if(lhsDomain !== undefined && rhsDomain !== undefined) { + switch(name.content) { + case '<': domain = calculateResult(lhsDomain, rhsDomain, NarrowKind.Smaller); break + case '<=': domain = calculateResult(lhsDomain, rhsDomain, NarrowKind.Smaller | NarrowKind.Equal); break + case '>': domain = calculateResult(lhsDomain, rhsDomain, NarrowKind.Greater); break + case '>=': domain = calculateResult(lhsDomain, rhsDomain, NarrowKind.Greater | NarrowKind.Equal); break + case '==': domain = calculateResult(lhsDomain, rhsDomain, NarrowKind.Equal); break + case '!=': /* TODO: implement */ break + default: guard(false, 'Unknown comparison operator') + } + } + return { + ...dfgInfo, + domain: domain + } +} From d3a9a505972c3990267c0871950690978a3a5186 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Thu, 6 Jun 2024 20:06:51 +0200 Subject: [PATCH 61/70] feat: use ai to determine if a path in an if-else is not taken --- .../functions/call/built-in/built-in-if-then-else.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/dataflow/internal/process/functions/call/built-in/built-in-if-then-else.ts b/src/dataflow/internal/process/functions/call/built-in/built-in-if-then-else.ts index 14fb709a15..5ff2ae58aa 100644 --- a/src/dataflow/internal/process/functions/call/built-in/built-in-if-then-else.ts +++ b/src/dataflow/internal/process/functions/call/built-in/built-in-if-then-else.ts @@ -10,11 +10,11 @@ import type { ParentInformation } from '../../../../../../r-bridge/lang-4.x/ast/ import type { RFunctionArgument } from '../../../../../../r-bridge/lang-4.x/ast/model/nodes/r-function-call' import type { NodeId } from '../../../../../../r-bridge/lang-4.x/ast/model/processing/node-id' import { dataflowLogger } from '../../../../../logger' -import { resolvesToBuiltInConstant } from '../../../../../environments/resolve-by-name' import { EdgeType } from '../../../../../graph/edge' import { appendEnvironment } from '../../../../../environments/append' import type { IdentifierReference } from '../../../../../environments/identifier' import { makeAllMaybe } from '../../../../../environments/environment' +import type { Ternary } from '../../../../../../util/logic' export function processIfThenElse( name: RSymbol, @@ -48,9 +48,9 @@ export function processIfThenElse( let then: DataflowInformation | undefined let makeThenMaybe = false - // we should defer this to the abstract interpretation - const conditionIsFalse = resolvesToBuiltInConstant(condArg?.lexeme, data.environment, false) - const conditionIsTrue = resolvesToBuiltInConstant(condArg?.lexeme, data.environment, true) + // FIXME: better notion of true/false in a domain + const conditionIsFalse: Ternary = !cond.domain?.isTop() ? 'always' : 'never' + const conditionIsTrue: Ternary = cond.domain?.isTop() ? 'always' : 'never' if(conditionIsFalse !== 'always') { then = processDataflowFor(thenArg, data) if(then.entryPoint) { From 33120a3f12dbc612b5e9695416b9a6197af5aff1 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Fri, 7 Jun 2024 21:46:31 +0200 Subject: [PATCH 62/70] feat: hacked together narrowing propagation --- src/abstract-interpretation/domain.ts | 10 +++ src/dataflow/environments/built-in.ts | 4 +- src/dataflow/environments/identifier.ts | 6 +- src/dataflow/info.ts | 6 +- .../call/built-in/built-in-arithmetic.ts | 6 +- .../call/built-in/built-in-assignment.ts | 11 ++- .../call/built-in/built-in-comparison.ts | 83 +++++++++++-------- .../call/built-in/built-in-if-then-else.ts | 35 +++++++- .../process/functions/process-argument.ts | 4 +- .../internal/process/process-symbol.ts | 18 ++-- .../internal/process/process-value.ts | 4 +- 11 files changed, 121 insertions(+), 66 deletions(-) diff --git a/src/abstract-interpretation/domain.ts b/src/abstract-interpretation/domain.ts index 975f58e090..ba6364128f 100644 --- a/src/abstract-interpretation/domain.ts +++ b/src/abstract-interpretation/domain.ts @@ -1,4 +1,5 @@ import { assertUnreachable, guard } from '../util/assert' +import type { Identifier } from '../dataflow/environments/identifier' interface IntervalBound { readonly value: number, @@ -105,6 +106,15 @@ export class Domain { } } +export interface Narrowing { + readonly positive: Omit, + readonly negative: Omit +} + +export class AiInfo { + constructor(readonly id: Identifier, public domain: Domain, readonly narrowings: Narrowing[] = []) { } +} + const enum CompareType { /** If qual, the bound that's inclusive is the smaller one */ Min, diff --git a/src/dataflow/environments/built-in.ts b/src/dataflow/environments/built-in.ts index d3ecbbcebb..030be1ed0a 100644 --- a/src/dataflow/environments/built-in.ts +++ b/src/dataflow/environments/built-in.ts @@ -149,9 +149,9 @@ registerBuiltInConstant('TRUE', true) registerBuiltInConstant('T', true) registerBuiltInConstant('FALSE', false) registerBuiltInConstant('F', false) -registerSimpleFunctions('~', '*', '/', '^', '!', '?', '**', '%%', '%/%', '%*%', ':', 'list') +registerSimpleFunctions('~', '*', '/', '^', '!', '?', '==', '!=', '**', '%%', '%/%', '%*%', ':', 'list') registerBuiltInFunctions(['+', '-'], processArithmetic, {} ) -registerBuiltInFunctions(['<', '<=', '>', '>=', '==', '!='], processComparison, {} ) +registerBuiltInFunctions(['<', '<=', '>', '>='], processComparison, {} ) registerBuiltInFunctions(['cat', 'switch'], defaultBuiltInProcessor, {}) /* returns null */ registerBuiltInFunctions(['print', '('], defaultBuiltInProcessor, { returnsNthArgument: 0 }, ) registerBuiltInFunctions(['return'], defaultBuiltInProcessor, { returnsNthArgument: 0, cfg: ExitPointType.Return }, ) diff --git a/src/dataflow/environments/identifier.ts b/src/dataflow/environments/identifier.ts index 6807376430..0b58b264d0 100644 --- a/src/dataflow/environments/identifier.ts +++ b/src/dataflow/environments/identifier.ts @@ -1,6 +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 { Domain } from '../../abstract-interpretation/domain' +import type { AiInfo } from '../../abstract-interpretation/domain' export type Identifier = string & { __brand?: 'identifier' } @@ -8,8 +8,8 @@ export interface InGraphIdentifierDefinition extends IdentifierReference { kind: 'function' | 'variable' | 'parameter' | 'argument' /** The assignment (or whatever, like `assign` function call) node which ultimately defined this identifier */ definedAt: NodeId - /** If one could be determined, the domain of the identifier */ - domain?: Domain + /** If it could be determined, ai infos of the identifier */ + aiInfo?: AiInfo } /** diff --git a/src/dataflow/info.ts b/src/dataflow/info.ts index a8f98396d2..745aa0e79a 100644 --- a/src/dataflow/info.ts +++ b/src/dataflow/info.ts @@ -3,7 +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 { Domain } from '../abstract-interpretation/domain' +import type { AiInfo } from '../abstract-interpretation/domain' export const enum ExitPointType { Default = 0, @@ -51,8 +51,8 @@ export interface DataflowInformation extends DataflowCfgInformation { environment: REnvironmentInformation /** The current constructed dataflow graph */ graph: DataflowGraph - /** Domain of the thing that was most recently added */ - domain?: Domain + /** AI info of the thing that was most recently added */ + aiInfo?: AiInfo, } export function initializeCleanDataflowInformation(entryPoint: NodeId, data: Pick, 'environment' | 'completeAst'>): DataflowInformation { diff --git a/src/dataflow/internal/process/functions/call/built-in/built-in-arithmetic.ts b/src/dataflow/internal/process/functions/call/built-in/built-in-arithmetic.ts index 4a0243d0d2..9080f2cad1 100644 --- a/src/dataflow/internal/process/functions/call/built-in/built-in-arithmetic.ts +++ b/src/dataflow/internal/process/functions/call/built-in/built-in-arithmetic.ts @@ -6,7 +6,7 @@ import type { RSymbol } from '../../../../../../r-bridge/lang-4.x/ast/model/node import type { NodeId } from '../../../../../../r-bridge/lang-4.x/ast/model/processing/node-id' import { processKnownFunctionCall } from '../known-call-handling' import type { Domain } from '../../../../../../abstract-interpretation/domain' -import { addDomains, subtractDomains } from '../../../../../../abstract-interpretation/domain' +import { addDomains, AiInfo, subtractDomains } from '../../../../../../abstract-interpretation/domain' import { guard } from '../../../../../../util/assert' export function processArithmetic( @@ -16,7 +16,7 @@ export function processArithmetic( data: DataflowProcessorInformation ): DataflowInformation { const { information: dfgInfo, processedArguments } = processKnownFunctionCall({ name, args, rootId, data }) - const [lhsDomain, rhsDomain] = processedArguments.map(arg => arg?.domain) + const [lhsDomain, rhsDomain] = processedArguments.map(arg => arg?.aiInfo?.domain) let domain: Domain | undefined if(lhsDomain !== undefined && rhsDomain !== undefined) { switch(name.content) { @@ -27,6 +27,6 @@ export function processArithmetic( } return { ...dfgInfo, - domain: domain + aiInfo: domain && new AiInfo('', domain, []) } } diff --git a/src/dataflow/internal/process/functions/call/built-in/built-in-assignment.ts b/src/dataflow/internal/process/functions/call/built-in/built-in-assignment.ts index eb36f7a3a0..7edc79d236 100644 --- a/src/dataflow/internal/process/functions/call/built-in/built-in-assignment.ts +++ b/src/dataflow/internal/process/functions/call/built-in/built-in-assignment.ts @@ -24,6 +24,7 @@ import type { RUnnamedArgument } from '../../../../../../r-bridge/lang-4.x/ast/m import { VertexType } from '../../../../../graph/vertex' import { define } from '../../../../../environments/define' import { EdgeType } from '../../../../../graph/edge' +import { AiInfo } from '../../../../../../abstract-interpretation/domain' function toReplacementSymbol(target: RNodeWithParent & Base & Location, prefix: string, superAssignment: boolean): RSymbol { return { @@ -113,13 +114,14 @@ function extractSourceAndTarget(args: readonly RFunctionArgument(rootId: NodeId, target: DataflowInformation, source: DataflowInformation, isFunctionDef: boolean, data: DataflowProcessorInformation, makeMaybe: boolean): IdentifierDefinition[] { +function produceWrittenNodes(rootId: NodeId, target: DataflowInformation & RSymbol, source: DataflowInformation, isFunctionDef: boolean, data: DataflowProcessorInformation, makeMaybe: boolean): IdentifierDefinition[] { + const sourceAiInfo = source.aiInfo return target.in.map(ref => ({ ...ref, kind: isFunctionDef ? 'function' : 'variable', definedAt: rootId, controlDependencies: data.controlDependencies ?? (makeMaybe ? [] : undefined), - domain: source.domain + aiInfo: sourceAiInfo && new AiInfo(target.content, sourceAiInfo.domain, sourceAiInfo.narrowings) })) } @@ -196,7 +198,7 @@ function processAssignmentToSymbol({ }: AssignmentToSymbolParameters): DataflowInformation { const isFunctionDef = checkFunctionDef(source, sourceArg) - const writeNodes = produceWrittenNodes(rootId, targetArg, sourceArg, isFunctionDef, data, makeMaybe ?? false) + const writeNodes = produceWrittenNodes(rootId, { ...target, ...targetArg }, sourceArg, isFunctionDef, data, makeMaybe ?? false) if(writeNodes.length !== 1 && log.settings.minLevel <= LogLevel.Warn) { log.warn(`Unexpected write number in assignment: ${JSON.stringify(writeNodes)}`) @@ -233,12 +235,13 @@ function processAssignmentToSymbol({ information.graph.addEdge(rootId, source.info.id, { type: EdgeType.NonStandardEvaluation }) } + const sourceAiInfo = sourceArg.aiInfo return { ...information, unknownReferences: [], entryPoint: name.info.id, in: readTargets, out: writeTargets, - domain: sourceArg.domain + aiInfo: sourceAiInfo && new AiInfo(name.content, sourceAiInfo.domain, sourceAiInfo.narrowings) } } diff --git a/src/dataflow/internal/process/functions/call/built-in/built-in-comparison.ts b/src/dataflow/internal/process/functions/call/built-in/built-in-comparison.ts index a88c480739..7eb1e361f5 100644 --- a/src/dataflow/internal/process/functions/call/built-in/built-in-comparison.ts +++ b/src/dataflow/internal/process/functions/call/built-in/built-in-comparison.ts @@ -2,35 +2,44 @@ import type { DataflowProcessorInformation } from '../../../../../processor' import type { DataflowInformation } from '../../../../../info' import type { ParentInformation } 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' +import { EmptyArgument } from '../../../../../../r-bridge/lang-4.x/ast/model/nodes/r-function-call' 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 { processKnownFunctionCall } from '../known-call-handling' -import { Domain, narrowDomain, NarrowKind } from '../../../../../../abstract-interpretation/domain' +import type { Narrowing } from '../../../../../../abstract-interpretation/domain' +import { AiInfo, Domain, narrowDomain, NarrowKind } from '../../../../../../abstract-interpretation/domain' import { guard } from '../../../../../../util/assert' -function calculateResult(lhs: Domain, rhs: Domain, narrowKind: NarrowKind): Domain { - const lhsNarrowed = narrowDomain(lhs, rhs, narrowKind) - const rhsNarrowed = narrowDomain(rhs, lhs, narrowKind ^ 0b110) +function calculateResult(lhs: AiInfo, rhs: AiInfo, narrowKind: NarrowKind): { + domain: Domain, + narrowings: Narrowing[] +} { + const lhsNarrowed = narrowDomain(lhs.domain, rhs.domain, narrowKind) + const rhsNarrowed = narrowDomain(rhs.domain, lhs.domain, narrowKind ^ 0b110) const isConditionTrue = lhsNarrowed.isBottom() && rhsNarrowed.isBottom() - // TODO: kann ich durch kluges Schneiden der Domains in then und else fall das Generieren von beiden Fällen im - // Vergleich vermeiden? - // then: mit top schneiden -> kommt top raus? - // else: - return isConditionTrue ? Domain.bottom() : Domain.top() - // TODO: narrowings - /*return new AINode(isConditionTrue ? Domain.bottom() : Domain.top(), node, node.info.id, node.info.id, [ - { - positive: AINodeStore.from([ - new AINode(lhsNarrowed, node, node.info.id, lhs.nodeId), - new AINode(rhsNarrowed, node, node.info.id, rhs.nodeId), - ]), - negative: AINodeStore.from([ - new AINode(narrowDomain(lhs.domain, rhs.domain, narrowKind ^ 0b111), node, node.info.id, lhs.nodeId), - new AINode(narrowDomain(rhs.domain, lhs.domain, narrowKind ^ 0b111 ^ 0b110), node, node.info.id, rhs.nodeId), - ]) - } - ])*/ + return { + domain: isConditionTrue ? Domain.bottom() : Domain.top(), + narrowings: [{ + positive: { + id: lhs.id, + domain: lhsNarrowed + }, + negative: { + id: lhs.id, + domain: narrowDomain(lhs.domain, rhs.domain, narrowKind ^ 0b111) + } + }, { + positive: { + id: rhs.id, + domain: rhsNarrowed + }, + negative: { + id: rhs.id, + domain: narrowDomain(rhs.domain, lhs.domain, narrowKind ^ 0b111) + } + }] + } } export function processComparison( @@ -40,21 +49,25 @@ export function processComparison( data: DataflowProcessorInformation ): DataflowInformation { const { information: dfgInfo, processedArguments } = processKnownFunctionCall({ name, args, rootId, data }) - const [lhsDomain, rhsDomain] = processedArguments.map(arg => arg?.domain) - let domain: Domain | undefined - if(lhsDomain !== undefined && rhsDomain !== undefined) { - switch(name.content) { - case '<': domain = calculateResult(lhsDomain, rhsDomain, NarrowKind.Smaller); break - case '<=': domain = calculateResult(lhsDomain, rhsDomain, NarrowKind.Smaller | NarrowKind.Equal); break - case '>': domain = calculateResult(lhsDomain, rhsDomain, NarrowKind.Greater); break - case '>=': domain = calculateResult(lhsDomain, rhsDomain, NarrowKind.Greater | NarrowKind.Equal); break - case '==': domain = calculateResult(lhsDomain, rhsDomain, NarrowKind.Equal); break - case '!=': /* TODO: implement */ break - default: guard(false, 'Unknown comparison operator') - } + const [lhs, rhs] = processedArguments.map(arg => arg?.aiInfo) + if(lhs === undefined || rhs === undefined) { + return dfgInfo + } + + let narrowKind: NarrowKind + switch(name.content) { + case '<': narrowKind = NarrowKind.Smaller; break + case '<=': narrowKind = NarrowKind.Smaller | NarrowKind.Equal; break + case '>': narrowKind = NarrowKind.Greater; break + case '>=': narrowKind = NarrowKind.Greater | NarrowKind.Equal; break + default: guard(false, 'Unknown comparison operator') } + const lhsArg = args[0] + const rhsArg = args[1] + guard(lhsArg !== EmptyArgument && rhsArg !== EmptyArgument, 'Comparison operator with empty argument') + const { domain, narrowings } = calculateResult(lhs, rhs, narrowKind) return { ...dfgInfo, - domain: domain + aiInfo: new AiInfo(name.content, domain, narrowings) } } diff --git a/src/dataflow/internal/process/functions/call/built-in/built-in-if-then-else.ts b/src/dataflow/internal/process/functions/call/built-in/built-in-if-then-else.ts index 5ff2ae58aa..8da1549a74 100644 --- a/src/dataflow/internal/process/functions/call/built-in/built-in-if-then-else.ts +++ b/src/dataflow/internal/process/functions/call/built-in/built-in-if-then-else.ts @@ -13,8 +13,29 @@ import { dataflowLogger } from '../../../../../logger' import { EdgeType } from '../../../../../graph/edge' import { appendEnvironment } from '../../../../../environments/append' import type { IdentifierReference } from '../../../../../environments/identifier' +import type { REnvironmentInformation } from '../../../../../environments/environment' import { makeAllMaybe } from '../../../../../environments/environment' import type { Ternary } from '../../../../../../util/logic' +import { cloneEnvironmentInformation } from '../../../../../environments/clone' +import { AiInfo } from '../../../../../../abstract-interpretation/domain' + +function applyNarrowing(environment: REnvironmentInformation, narrowings: Omit[]): REnvironmentInformation { + const envClone = cloneEnvironmentInformation(environment) + for(const narrowing of narrowings) { + const possibleDefinitions = envClone.current.memory.get(narrowing.id) + for(const definition of possibleDefinitions ?? []) { + if(definition.kind !== 'variable' && definition.kind !== 'parameter' && definition.kind !== 'argument') { + continue + } + if(definition.aiInfo === undefined) { + definition.aiInfo = new AiInfo(narrowing.id, narrowing.domain, []) + } else { + definition.aiInfo.domain = narrowing.domain + } + } + } + return envClone +} export function processIfThenElse( name: RSymbol, @@ -49,10 +70,13 @@ export function processIfThenElse( let makeThenMaybe = false // FIXME: better notion of true/false in a domain - const conditionIsFalse: Ternary = !cond.domain?.isTop() ? 'always' : 'never' - const conditionIsTrue: Ternary = cond.domain?.isTop() ? 'always' : 'never' + const conditionIsFalse: Ternary = !cond.aiInfo?.domain?.isTop() ? 'always' : 'never' + const conditionIsTrue: Ternary = cond.aiInfo?.domain?.isTop() ? 'always' : 'never' if(conditionIsFalse !== 'always') { - then = processDataflowFor(thenArg, data) + then = processDataflowFor(thenArg, { + ...data, + environment: applyNarrowing(data.environment, cond.aiInfo?.narrowings?.map(n => n.positive) ?? []) + }) if(then.entryPoint) { then.graph.addEdge(name.info.id, then.entryPoint, { type: EdgeType.Returns }) } @@ -64,7 +88,10 @@ export function processIfThenElse( let otherwise: DataflowInformation | undefined let makeOtherwiseMaybe = false if(otherwiseArg !== undefined && conditionIsTrue !== 'always') { - otherwise = processDataflowFor(otherwiseArg, data) + otherwise = processDataflowFor(otherwiseArg, { + ...data, + environment: applyNarrowing(data.environment, cond.aiInfo?.narrowings?.map(n => n.negative) ?? []) + }) if(otherwise.entryPoint) { otherwise.graph.addEdge(name.info.id, otherwise.entryPoint, { type: EdgeType.Returns }) } diff --git a/src/dataflow/internal/process/functions/process-argument.ts b/src/dataflow/internal/process/functions/process-argument.ts index 563689d207..fafe32c189 100644 --- a/src/dataflow/internal/process/functions/process-argument.ts +++ b/src/dataflow/internal/process/functions/process-argument.ts @@ -39,7 +39,7 @@ export function processFunctionArgument( tag: VertexType.Use, id: argument.info.id, controlDependencies: data.controlDependencies, - domain: value?.domain + aiInfo: value?.aiInfo }) entryPoint = argument.info.id } @@ -62,6 +62,6 @@ export function processFunctionArgument( environment: value?.environment ?? data.environment, entryPoint: entryPoint ?? argument.info.id, exitPoints: value?.exitPoints ?? name?.exitPoints ?? [{ nodeId: argument.info.id, type: ExitPointType.Default, controlDependencies: data.controlDependencies }], - domain: value?.domain + aiInfo: value?.aiInfo } } diff --git a/src/dataflow/internal/process/process-symbol.ts b/src/dataflow/internal/process/process-symbol.ts index 3840b28d5a..51b908c6a4 100644 --- a/src/dataflow/internal/process/process-symbol.ts +++ b/src/dataflow/internal/process/process-symbol.ts @@ -6,8 +6,7 @@ import type { ParentInformation } from '../../../r-bridge/lang-4.x/ast/model/pro import { RNa, RNull } from '../../../r-bridge/lang-4.x/convert-values' import { DataflowGraph } from '../../graph/graph' import { VertexType } from '../../graph/vertex' -import type { Domain } from '../../../abstract-interpretation/domain' -import { unifyDomains } from '../../../abstract-interpretation/domain' +import { AiInfo, unifyDomains } from '../../../abstract-interpretation/domain' import type { InGraphIdentifierDefinition } from '../../environments/identifier' export function processSymbol(symbol: RSymbol, data: DataflowProcessorInformation): DataflowInformation { @@ -15,12 +14,15 @@ export function processSymbol(symbol: RSymbol id.kind === 'function' || id.kind === 'variable' || id.kind === 'parameter' || id.kind === 'argument') - .map(id => (id as InGraphIdentifierDefinition).domain) - .filter(domain => domain !== undefined) - .map(domain => domain as Domain) - const domain = domains && unifyDomains(domains) + .map(id => (id as InGraphIdentifierDefinition).aiInfo) + .filter(aiInfo => aiInfo !== undefined) + .map(aiInfo => aiInfo as AiInfo) + + //FIXME: aggregate AiInfos (especially narrowings) + const domain = aiInfos && unifyDomains(aiInfos.map(info => info.domain)) + const narrowings = aiInfos && aiInfos.map(info => info.narrowings).flat() return { unknownReferences: [ { nodeId: symbol.info.id, name: symbol.content, controlDependencies: data.controlDependencies } ], @@ -36,6 +38,6 @@ export function processSymbol(symbol: RSymbol(value: RNodeWithParent, data: DataflowProcessorInformation): DataflowInformation { @@ -26,6 +26,6 @@ export function processValue(value: RNodeWithParent, data: DataflowPr }), exitPoints: [{ nodeId: value.info.id, type: ExitPointType.Default, controlDependencies: data.controlDependencies }], entryPoint: value.info.id, - domain: domain + aiInfo: domain && new AiInfo('', domain, []) } } From f8fc7535275e1bda64b70d207420374481226711 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Fri, 7 Jun 2024 21:46:52 +0200 Subject: [PATCH 63/70] feat-fix: fixed interval toJSON --- src/abstract-interpretation/domain.ts | 16 +++++++++++++++- src/dataflow/environments/clone.ts | 9 +++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/abstract-interpretation/domain.ts b/src/abstract-interpretation/domain.ts index ba6364128f..32423a5abd 100644 --- a/src/abstract-interpretation/domain.ts +++ b/src/abstract-interpretation/domain.ts @@ -42,6 +42,20 @@ export class Interval { isTop(): boolean { return this.min.value === Number.NEGATIVE_INFINITY && this.max.value === Number.POSITIVE_INFINITY } + + toJSON(): object { + return { + __type: 'Interval', + min: this.min, + max: this.max + } + } + + static revive(data: object): Interval { + // @ts-expect-error I'm too lazy to define the proper type for the JSON object + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return new Interval(data.min, data.max) + } } export class Domain { @@ -95,7 +109,7 @@ export class Domain { toJSON(): object { return { __type: 'Domain', - intervals: this.intervals + intervals: Array.from(this.intervals) } } diff --git a/src/dataflow/environments/clone.ts b/src/dataflow/environments/clone.ts index 890f5bb1a0..362e88dbe6 100644 --- a/src/dataflow/environments/clone.ts +++ b/src/dataflow/environments/clone.ts @@ -1,7 +1,7 @@ import type { IEnvironment, REnvironmentInformation } from './environment' import { BuiltInEnvironment, Environment } from './environment' import type { Identifier, IdentifierDefinition } from './identifier' -import { Domain } from '../../abstract-interpretation/domain' +import { Domain, Interval } from '../../abstract-interpretation/domain' function cloneEnvironment(environment: IEnvironment, recurseParents: boolean): IEnvironment function cloneEnvironment(environment: IEnvironment | undefined, recurseParents: boolean): IEnvironment | undefined { @@ -13,10 +13,15 @@ function cloneEnvironment(environment: IEnvironment | undefined, recurseParents: const clone = new Environment(environment.name, recurseParents ? cloneEnvironment(environment.parent, recurseParents) : environment.parent) clone.memory = new Map(JSON.parse(JSON.stringify([...environment.memory]), (k, v) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if(k === '' && v?.__type === 'Domain') { + if(v?.__type === 'Domain') { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument return Domain.revive(v) } + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if(v?.__type === 'Interval') { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return Interval.revive(v) + } // eslint-disable-next-line @typescript-eslint/no-unsafe-return return v }) as [Identifier, IdentifierDefinition[]][]) From cebd411dfe12e6830910fe4a2b5573c528ba2b0c Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Fri, 7 Jun 2024 23:35:48 +0200 Subject: [PATCH 64/70] feat: added ability to update a vertex inside the dfg --- src/dataflow/graph/graph.ts | 7 +++++++ .../process/functions/call/built-in/built-in-assignment.ts | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/src/dataflow/graph/graph.ts b/src/dataflow/graph/graph.ts index 1654dd0b16..9882bd9e09 100644 --- a/src/dataflow/graph/graph.ts +++ b/src/dataflow/graph/graph.ts @@ -119,6 +119,13 @@ export class DataflowGraph): this { + const vertex = this.vertexInformation.get(id) + guard(vertex !== undefined, () => `vertex with id ${id} does not exist`) + this.vertexInformation.set(id, { ...vertex, ...updated } as Vertex) + return this + } + public outgoingEdges(id: NodeId): OutgoingEdges | undefined { return this.edgeInformation.get(id) } diff --git a/src/dataflow/internal/process/functions/call/built-in/built-in-assignment.ts b/src/dataflow/internal/process/functions/call/built-in/built-in-assignment.ts index 7edc79d236..ef3805c276 100644 --- a/src/dataflow/internal/process/functions/call/built-in/built-in-assignment.ts +++ b/src/dataflow/internal/process/functions/call/built-in/built-in-assignment.ts @@ -213,6 +213,11 @@ function processAssignmentToSymbol({ // install assigned variables in environment for(const write of writeNodes) { + if(write.kind === 'function' || write.kind === 'variable' || write.kind === 'parameter' || write.kind === 'argument') { + information.graph.updateVertex(write.nodeId, { + domain: write.aiInfo?.domain + }) + } information.environment = define(write, superAssignment, information.environment) information.graph.setDefinitionOfVertex(write) if(!quoteSource) { From a6229dd8a4a6ce2acbb469bcd606a5de52258139 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Fri, 7 Jun 2024 23:36:15 +0200 Subject: [PATCH 65/70] feat-fix: fixed type --- src/dataflow/graph/vertex.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dataflow/graph/vertex.ts b/src/dataflow/graph/vertex.ts index e27af05f33..fbd6e61f7e 100644 --- a/src/dataflow/graph/vertex.ts +++ b/src/dataflow/graph/vertex.ts @@ -39,7 +39,7 @@ interface DataflowGraphVertexBase extends MergeableRecord { * See {@link IdentifierReference} */ controlDependencies: NodeId[] | undefined - domain?: Domain + domain?: Domain | undefined } export interface DataflowGraphValue extends DataflowGraphVertexBase { From e0e05518d0debad2d2d5c4697380b2b36350a637 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Fri, 7 Jun 2024 23:36:50 +0200 Subject: [PATCH 66/70] test: compare vertex domains in tests --- src/abstract-interpretation/domain.ts | 17 +++++++++++++++++ src/dataflow/graph/diff.ts | 6 +++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/abstract-interpretation/domain.ts b/src/abstract-interpretation/domain.ts index 32423a5abd..e9591f8f10 100644 --- a/src/abstract-interpretation/domain.ts +++ b/src/abstract-interpretation/domain.ts @@ -28,6 +28,12 @@ export class Interval { return `${this.min.inclusive ? '[' : '('}${this.min.value}, ${this.max.value}${this.max.inclusive ? ']' : ')'}` } + equals(other: Interval): boolean { + return this.min.value === other.min.value && this.min.inclusive === other.min.inclusive && + this.max.value === other.max.value && this.max.inclusive === other.max.inclusive + + } + /** An interval is considered empty if it's of the form [T, T) or (T, T] */ isBottom(): boolean { return this.min.value === this.max.value && !(this.min.inclusive && this.max.inclusive) @@ -106,6 +112,17 @@ export class Domain { } } + equals(other: Domain): boolean { + if(this.isBottom()) { + return other.isBottom() + } + if(this.isTop()) { + return other.isTop() + } + return this.intervals.size === other.intervals.size && + Array.from(this.intervals).every(interval => Array.from(other.intervals).some(otherInterval => interval.equals(otherInterval))) + } + toJSON(): object { return { __type: 'Domain', diff --git a/src/dataflow/graph/diff.ts b/src/dataflow/graph/diff.ts index efef9b8637..9c5c0df0f7 100644 --- a/src/dataflow/graph/diff.ts +++ b/src/dataflow/graph/diff.ts @@ -6,7 +6,7 @@ import { jsonReplacer } from '../../util/json' import { arrayEqual } from '../../util/arrays' import { VertexType } from './vertex' import type { DataflowGraphEdge } from './edge' -import { edgeTypesToNames , splitEdgeTypes } from './edge' +import { edgeTypesToNames, splitEdgeTypes } from './edge' import type { NodeId } from '../../r-bridge/lang-4.x/ast/model/processing/node-id' import { recoverName } from '../../r-bridge/lang-4.x/ast/model/processing/node-id' import type { IdentifierReference } from '../environments/identifier' @@ -223,6 +223,10 @@ export function diffVertices(ctx: DataflowDiffContext): void { ) } + if(lInfo.domain && rInfo.domain && !lInfo.domain.equals(rInfo.domain)) { + ctx.report.addComment(`Vertex ${id} differs in domain. ${ctx.leftname}: ${lInfo.domain.toString()} vs ${ctx.rightname}: ${rInfo.domain.toString()}`, { tag: 'vertex', id }) + } + diffEnvironmentInformation(lInfo.environment, rInfo.environment, { ...ctx, position: `${ctx.position}Vertex ${id} differs in environment. ` }) if(lInfo.tag === VertexType.FunctionCall) { From cd2198ec7354d052b8c13f3274e54264ea8ba340 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Fri, 7 Jun 2024 23:56:14 +0200 Subject: [PATCH 67/70] test: added two simple tests --- .../_helper/dataflow/dataflowgraph-builder.ts | 30 +++++++----- .../abstract-interpretation.spec.ts | 48 +++++++++++++++++++ 2 files changed, 67 insertions(+), 11 deletions(-) diff --git a/test/functionality/_helper/dataflow/dataflowgraph-builder.ts b/test/functionality/_helper/dataflow/dataflowgraph-builder.ts index 82fa28083f..67d534fb15 100644 --- a/test/functionality/_helper/dataflow/dataflowgraph-builder.ts +++ b/test/functionality/_helper/dataflow/dataflowgraph-builder.ts @@ -2,12 +2,8 @@ import { deepMergeObject } from '../../../../src/util/objects' import type { NodeId } from '../../../../src/r-bridge/lang-4.x/ast/model/processing/node-id' import { normalizeIdToNumberIfPossible } from '../../../../src/r-bridge/lang-4.x/ast/model/processing/node-id' import type { AstIdMap } from '../../../../src/r-bridge/lang-4.x/ast/model/processing/decorate' -import type { - DataflowFunctionFlowInformation, - FunctionArgument } from '../../../../src/dataflow/graph/graph' -import { - isPositionalArgument - , DataflowGraph } from '../../../../src/dataflow/graph/graph' +import type { DataflowFunctionFlowInformation, FunctionArgument } from '../../../../src/dataflow/graph/graph' +import { DataflowGraph, isPositionalArgument } from '../../../../src/dataflow/graph/graph' import type { REnvironmentInformation } from '../../../../src/dataflow/environments/environment' import { initializeCleanEnvironments } from '../../../../src/dataflow/environments/environment' import type { DataflowGraphVertexUse } from '../../../../src/dataflow/graph/vertex' @@ -15,6 +11,7 @@ import { VertexType } from '../../../../src/dataflow/graph/vertex' import { EmptyArgument } from '../../../../src/r-bridge/lang-4.x/ast/model/nodes/r-function-call' import { BuiltIn } from '../../../../src/dataflow/environments/built-in' import { EdgeType } from '../../../../src/dataflow/graph/edge' +import type { Domain } from '../../../../src/abstract-interpretation/domain' export function emptyGraph(idMap?: AstIdMap) { return new DataflowGraphBuilder(idMap) @@ -75,7 +72,8 @@ export class DataflowGraphBuilder extends DataflowGraph { reads?: readonly NodeId[], onlyBuiltIn?: boolean, environment?: REnvironmentInformation, - controlDependency?: NodeId[] + controlDependency?: NodeId[], + domain?: Domain }, asRoot: boolean = true) { const onlyBuiltInAuto = info?.reads?.length === 1 && info?.reads[0] === BuiltIn @@ -86,7 +84,8 @@ export class DataflowGraphBuilder extends DataflowGraph { args: args.map(a => a === EmptyArgument ? EmptyArgument : { ...a, nodeId: normalizeIdToNumberIfPossible(a.nodeId), controlDependency: undefined }), environment: info?.environment ?? initializeCleanEnvironments(), controlDependencies: info?.controlDependency?.map(normalizeIdToNumberIfPossible), - onlyBuiltin: info?.onlyBuiltIn ?? onlyBuiltInAuto ?? false + onlyBuiltin: info?.onlyBuiltIn ?? onlyBuiltInAuto ?? false, + domain: info?.domain }, asRoot) this.addArgumentLinks(id, args) if(info?.returns) { @@ -131,12 +130,17 @@ export class DataflowGraphBuilder extends DataflowGraph { * (i.e., be a valid entry point), or is it nested (e.g., as part of a function definition) */ public defineVariable(id: NodeId, name: string, - info?: { controlDependency?: NodeId[], definedBy?: NodeId[]}, asRoot: boolean = true) { + info?: { + controlDependency?: NodeId[], + definedBy?: NodeId[], + domain?: Domain + }, asRoot: boolean = true) { this.addVertex({ tag: VertexType.VariableDefinition, id: normalizeIdToNumberIfPossible(id), name, controlDependencies: info?.controlDependency?.map(normalizeIdToNumberIfPossible), + domain: info?.domain }, asRoot) if(info?.definedBy) { for(const def of info.definedBy) { @@ -177,12 +181,16 @@ export class DataflowGraphBuilder extends DataflowGraph { * @param asRoot - should the vertex be part of the root vertex set of the graph * (i.e., be a valid entry point), or is it nested (e.g., as part of a function definition) */ - public constant(id: NodeId, options?: { controlDependency?: NodeId[] }, asRoot: boolean = true) { + public constant(id: NodeId, options?: { + controlDependency?: NodeId[], + domain?: Domain + }, asRoot: boolean = true) { return this.addVertex({ tag: VertexType.Value, id: normalizeIdToNumberIfPossible(id), controlDependencies: options?.controlDependency?.map(normalizeIdToNumberIfPossible), - environment: undefined + environment: undefined, + domain: options?.domain }, asRoot) } diff --git a/test/functionality/abstract-interpretation/abstract-interpretation.spec.ts b/test/functionality/abstract-interpretation/abstract-interpretation.spec.ts index e639a1e488..60eb0afe78 100644 --- a/test/functionality/abstract-interpretation/abstract-interpretation.spec.ts +++ b/test/functionality/abstract-interpretation/abstract-interpretation.spec.ts @@ -9,6 +9,10 @@ import { unifyOverlappingIntervals } from '../../../src/abstract-interpretation/domain' import { assert } from 'chai' +import { assertDataflow, withShell } from '../_helper/shell' +import { emptyGraph } from '../_helper/dataflow/dataflowgraph-builder' +import { argumentInCall } from '../_helper/dataflow/environment-builder' +import { BuiltIn } from '../../../src/dataflow/environments/built-in' describe('Abstract Interpretation', () => { it('Interval overlapping', () => { @@ -146,4 +150,48 @@ describe('Abstract Interpretation', () => { console.log(`overlap: ${overlap.intersection?.toString() ?? 'none'}`) console.log(`right: ${overlap.larger?.toString() ?? 'none'}`) */ }) + + describe('Dataflow graph', withShell(shell => { + describe('simple stuff', () => { + it('simple assignment', () => { + assertDataflow('Unsure whats supposed to be here', shell, 'x <- 42', emptyGraph() + .call(2, '<-', [argumentInCall(0), argumentInCall(1)], { + domain: Domain.fromScalar(42), + reads: [BuiltIn], + returns: [0] + }) + .defineVariable(0, 'x', { + domain: Domain.fromScalar(43), + definedBy: [1, 2] + }) + .constant(1, { domain: Domain.fromScalar(42) }) + ) + }) + it('Two assignments', () => { + assertDataflow('Unsure whats supposed to be here', shell, 'x <- 42; a <- x', emptyGraph() + .call(2, '<-', [argumentInCall(0), argumentInCall(1)], { + domain: Domain.fromScalar(42), + reads: [BuiltIn], + returns: [0] + }) + .defineVariable(0, 'x', { + domain: Domain.fromScalar(42), + definedBy: [1, 2] + }) + .constant(1, { domain: Domain.fromScalar(42) }) + + .call(4, '<-', [argumentInCall(3), argumentInCall(4)], { + domain: Domain.fromScalar(42), + reads: [BuiltIn], + returns: [3] + }) + .defineVariable(3, 'a', { + domain: Domain.fromScalar(45), + definedBy: [4, 5] + }) + ) + }) + }) + })) + }) From 11fec388cd5e939c50c63ec396f84834763ee72e Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Fri, 7 Jun 2024 23:59:10 +0200 Subject: [PATCH 68/70] refactor: removed implemented todo --- src/dataflow/internal/process/process-symbol.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/dataflow/internal/process/process-symbol.ts b/src/dataflow/internal/process/process-symbol.ts index 51b908c6a4..e16015bec7 100644 --- a/src/dataflow/internal/process/process-symbol.ts +++ b/src/dataflow/internal/process/process-symbol.ts @@ -20,7 +20,6 @@ export function processSymbol(symbol: RSymbol aiInfo !== undefined) .map(aiInfo => aiInfo as AiInfo) - //FIXME: aggregate AiInfos (especially narrowings) const domain = aiInfos && unifyDomains(aiInfos.map(info => info.domain)) const narrowings = aiInfos && aiInfos.map(info => info.narrowings).flat() From 1c867d987010e4732f13585336696f90e7ccea03 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Sat, 8 Jun 2024 00:00:57 +0200 Subject: [PATCH 69/70] feat: "abstracted" away truthy and falsy domains --- src/abstract-interpretation/domain.ts | 17 +++++++++++++++++ .../call/built-in/built-in-comparison.ts | 2 +- .../call/built-in/built-in-if-then-else.ts | 5 ++--- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/abstract-interpretation/domain.ts b/src/abstract-interpretation/domain.ts index e9591f8f10..55a143b045 100644 --- a/src/abstract-interpretation/domain.ts +++ b/src/abstract-interpretation/domain.ts @@ -90,14 +90,31 @@ export class Domain { )]) } + static truthy(): Domain { + return Domain.fromScalar(1) + } + + static falsy(): Domain { + return this.bottom() + } + isBottom(): boolean { return this.intervals.size === 0 } + // FIXME: better notion of true/false in a domain isTop(): boolean { return this.intervals.size === 1 && Array.from(this.intervals)[0].isTop() } + isTruthy(): boolean { + return !this.isBottom() + } + + isFalsy(): boolean { + return this.isBottom() + } + get intervals(): Set { return this._intervals } diff --git a/src/dataflow/internal/process/functions/call/built-in/built-in-comparison.ts b/src/dataflow/internal/process/functions/call/built-in/built-in-comparison.ts index 7eb1e361f5..4de9bd26c3 100644 --- a/src/dataflow/internal/process/functions/call/built-in/built-in-comparison.ts +++ b/src/dataflow/internal/process/functions/call/built-in/built-in-comparison.ts @@ -19,7 +19,7 @@ function calculateResult(lhs: AiInfo, rhs: AiInfo, narrowKind: NarrowKind): { const isConditionTrue = lhsNarrowed.isBottom() && rhsNarrowed.isBottom() return { - domain: isConditionTrue ? Domain.bottom() : Domain.top(), + domain: isConditionTrue ? Domain.truthy() : Domain.falsy(), narrowings: [{ positive: { id: lhs.id, diff --git a/src/dataflow/internal/process/functions/call/built-in/built-in-if-then-else.ts b/src/dataflow/internal/process/functions/call/built-in/built-in-if-then-else.ts index 8da1549a74..0beaa894ee 100644 --- a/src/dataflow/internal/process/functions/call/built-in/built-in-if-then-else.ts +++ b/src/dataflow/internal/process/functions/call/built-in/built-in-if-then-else.ts @@ -69,9 +69,8 @@ export function processIfThenElse( let then: DataflowInformation | undefined let makeThenMaybe = false - // FIXME: better notion of true/false in a domain - const conditionIsFalse: Ternary = !cond.aiInfo?.domain?.isTop() ? 'always' : 'never' - const conditionIsTrue: Ternary = cond.aiInfo?.domain?.isTop() ? 'always' : 'never' + const conditionIsFalse: Ternary = cond.aiInfo?.domain?.isFalsy() ? 'always' : 'never' + const conditionIsTrue: Ternary = cond.aiInfo?.domain?.isTruthy() ? 'always' : 'never' if(conditionIsFalse !== 'always') { then = processDataflowFor(thenArg, { ...data, From 4b9c13420920797c1ea8a75ea95a1ea69e2861b9 Mon Sep 17 00:00:00 2001 From: Lukas Pietzschmann Date: Sat, 8 Jun 2024 00:25:14 +0200 Subject: [PATCH 70/70] feat: added ai for && and || --- .../call/built-in/built-in-logical-bin-op.ts | 23 ++++++++++++++++++- .../internal/process/process-value.ts | 2 +- 2 files changed, 23 insertions(+), 2 deletions(-) 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-logical-bin-op.ts index 8292d9dd46..bf7e0c4350 100644 --- 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-logical-bin-op.ts @@ -7,6 +7,8 @@ import type { RSymbol } from '../../../../../../r-bridge/lang-4.x/ast/model/node import type { NodeId } from '../../../../../../r-bridge/lang-4.x/ast/model/processing/node-id' import { dataflowLogger } from '../../../../../logger' import { EdgeType } from '../../../../../graph/edge' +import type { Narrowing } from '../../../../../../abstract-interpretation/domain' +import { AiInfo, Domain } from '../../../../../../abstract-interpretation/domain' export function processSpecialBinOp( @@ -43,5 +45,24 @@ export function processSpecialBinOp( } } - return information + const [lhs, rhs] = processedArguments.map(arg => arg?.aiInfo) + let domain: Domain | undefined + let narrowings: Narrowing[] | undefined + if(lhs !== undefined && rhs !== undefined) { + switch(name.content) { + case '&&': + domain = lhs.domain.isTruthy() && rhs.domain.isTruthy() ? Domain.truthy() : Domain.falsy() + narrowings = [...lhs.narrowings, ...rhs.narrowings] + break + case '||': + domain = lhs.domain.isTruthy() || rhs.domain.isTruthy() ? Domain.truthy() : Domain.falsy() + narrowings = [...lhs.narrowings, ...rhs.narrowings] + break + } + } + + return { + ...information, + aiInfo: domain && narrowings && new AiInfo('', domain, narrowings) + } } diff --git a/src/dataflow/internal/process/process-value.ts b/src/dataflow/internal/process/process-value.ts index 4b173022b2..fe8ce25296 100644 --- a/src/dataflow/internal/process/process-value.ts +++ b/src/dataflow/internal/process/process-value.ts @@ -10,7 +10,7 @@ export function processValue(value: RNodeWithParent, data: DataflowPr let domain: Domain | undefined switch(value.type) { case RType.Number: domain = Domain.fromScalar(value.content.num); break - case RType.Logical: domain = value.content ? Domain.top() : Domain.bottom(); break + case RType.Logical: domain = value.content ? Domain.truthy() : Domain.falsy(); break } return { unknownReferences: [],