Skip to content

Commit f72c1a7

Browse files
authored
[DFG] support recall (#2287)
* feat: basic recall support #totalrecall * test: ensure argument linkage! * refactor: drop debug trace * doc: env closure * refactor: drop outdated todo
1 parent f59bbc2 commit f72c1a7

File tree

14 files changed

+233
-37
lines changed

14 files changed

+233
-37
lines changed

src/control-flow/semantic-cfg-guided-visitor.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,8 @@ export class SemanticCfgGuidedVisitor<
308308
return this.onReturnCall({ call });
309309
case BuiltInProcName.Unnamed:
310310
return this.onUnnamedCall({ call });
311+
case BuiltInProcName.Recall:
312+
return this.onRecallCall({ call });
311313
case BuiltInProcName.Default:
312314
case BuiltInProcName.Function:
313315
case BuiltInProcName.FunctionDefinition:
@@ -687,4 +689,10 @@ export class SemanticCfgGuidedVisitor<
687689
* @protected
688690
*/
689691
protected onReturnCall(_data: { call: DataflowGraphVertexFunctionCall }) {}
692+
693+
/**
694+
* This event triggers for every call to `Recall`, which is used to recall the function closure (usually in recursive functions).
695+
* @protected
696+
*/
697+
protected onRecallCall(_data: { call: DataflowGraphVertexFunctionCall }) {}
690698
}

src/dataflow/environments/built-in.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import { processTryCatch } from '../internal/process/functions/call/built-in/bui
5353
import { processRegisterHook } from '../internal/process/functions/call/built-in/built-in-register-hook';
5454
import { processLocal } from '../internal/process/functions/call/built-in/built-in-local';
5555
import { processS3Dispatch } from '../internal/process/functions/call/built-in/built-in-s-three-dispatch';
56+
import { processRecall } from '../internal/process/functions/call/built-in/built-in-recall';
5657

5758
export type BuiltIn = `built-in:${string}`;
5859

@@ -230,6 +231,10 @@ export enum BuiltInProcName {
230231
Pipe = 'builtin:pipe',
231232
/** for `quote`, and other substituting calls, see {@link processQuote} */
232233
Quote = 'builtin:quote',
234+
/**
235+
* for `recall` calls, see {@link processRecall}
236+
*/
237+
Recall = 'builtin:recall',
233238
/** for `on.exìt` and other hooks, see {@link processRegisterHook} */
234239
RegisterHook = 'builtin:register-hook',
235240
/** for `repeat` loops, see {@link processRepeatLoop} */
@@ -280,6 +285,7 @@ export const BuiltInProcessorMapper = {
280285
[BuiltInProcName.Local]: processLocal,
281286
[BuiltInProcName.Pipe]: processPipe,
282287
[BuiltInProcName.Quote]: processQuote,
288+
[BuiltInProcName.Recall]: processRecall,
283289
[BuiltInProcName.RegisterHook]: processRegisterHook,
284290
[BuiltInProcName.RepeatLoop]: processRepeatLoop,
285291
[BuiltInProcName.Replacement]: processReplacementFunction,

src/dataflow/environments/default-builtin-config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ export const DefaultBuiltinConfig = [
282282
{ type: 'function', names: ['interference'], processor: BuiltInProcName.Apply, config: { unquoteFunction: true, nameOfFunctionArgument: 'propensity_integrand', libFn: true }, assumePrimitive: false },
283283
{ type: 'function', names: ['ddply'], processor: BuiltInProcName.Apply, config: { unquoteFunction: true, indexOfFunction: 2, nameOfFunctionArgument: '.fun', libFn: true }, assumePrimitive: false },
284284
{ type: 'function', names: ['list'], processor: BuiltInProcName.List, config: {}, assumePrimitive: true },
285+
{ type: 'function', names: ['Recall'], processor: BuiltInProcName.Recall, config: { libFn: true }, assumePrimitive: false },
285286
{ type: 'function', names: ['c'], processor: BuiltInProcName.Vector, config: {}, assumePrimitive: true, evalHandler: 'built-in:c' },
286287
{
287288
type: 'function',

src/dataflow/environments/environment.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ let environmentIdCounter = 1; // Zero is reserved for built-in environment
4242

4343
/** @see REnvironmentInformation */
4444
export class Environment implements IEnvironment {
45-
readonly id;
45+
readonly id: number;
46+
/** Optional name for namespaced/non-anonymous environments, please only set if you know what you are doing */
47+
n?: string;
48+
/** if created by a closure, the node id of that closure */
49+
private c?: NodeId;
4650
parent: Environment;
4751
memory: BuiltInMemory;
4852
cache?: BuiltInMemory;
@@ -58,6 +62,19 @@ export class Environment implements IEnvironment {
5862
}
5963
}
6064

65+
/** please only use if you know what you are doing */
66+
public setClosureNodeId(nodeId: NodeId) {
67+
this.c = nodeId;
68+
}
69+
70+
/**
71+
* Provides the closure linked to this environment.
72+
* This is of importance if, for example, if you want to know the function definition associated with this environment.
73+
*/
74+
public get closure(): NodeId | undefined {
75+
return this.c;
76+
}
77+
6178
/**
6279
* Create a deep clone of this environment.
6380
* @param recurseParents - Whether to also clone parent environments
@@ -69,6 +86,8 @@ export class Environment implements IEnvironment {
6986

7087
const parent = recurseParents ? this.parent.clone(recurseParents) : this.parent;
7188
const clone = new Environment(parent, this.builtInEnv);
89+
clone.c = this.c;
90+
clone.n = this.n;
7291
clone.memory = new Map(
7392
this.memory.entries()
7493
.map(([k, v]) => [k,
@@ -176,6 +195,8 @@ export class Environment implements IEnvironment {
176195
}
177196

178197
const out = new Environment(this.parent.overwrite(other.parent, applyCds));
198+
out.c = this.c;
199+
out.n = this.n;
179200
out.memory = map;
180201
return out;
181202
}

src/dataflow/eval/resolve/resolve.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type { ReadOnlyFlowrAnalyzerContext } from '../../../project/context/flow
2525
* aliases and vectors (in case of a vector).
2626
* @param a - Ast node to resolve
2727
* @param resolve - Variable resolve mode
28+
* @param ctx - Analyzer context
2829
* @param env - Environment to use
2930
* @param graph - Dataflow Graph to use
3031
* @param map - Idmap of Dataflow Graph

src/dataflow/graph/call-graph.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export function getSubCallGraph(graph: CallGraph, entryPoints: Set<NodeId>): Cal
6161

6262

6363
/**
64-
*
64+
* Determines whether there is a path from `from` to `to` in the given graph (via any edge type, only respecting direction)
6565
*/
6666
export function reaches(from: NodeId, to: NodeId, graph: DataflowGraph, knownReachability: DefaultMap<NodeId, Set<NodeId>> = new DefaultMap(() => new Set())): boolean {
6767
const visited: Set<NodeId> = new Set();
@@ -209,10 +209,12 @@ function processCall(vtx: Required<DataflowGraphVertexFunctionCall>, from: NodeI
209209
// for each call, resolve the targets
210210
const tars = getAllFunctionCallTargets(vid, graph, vtx.environment);
211211
let addedTarget = false;
212+
let addedBiTarget = false;
212213
for(const tar of tars) {
213214
if(isBuiltIn(tar)) {
214215
result.addEdge(vid, tar, EdgeType.Calls);
215216
addedTarget = true;
217+
addedBiTarget = true;
216218
continue;
217219
}
218220
const targetVtx = graph.getVertex(tar);
@@ -222,7 +224,7 @@ function processCall(vtx: Required<DataflowGraphVertexFunctionCall>, from: NodeI
222224
addedTarget = true;
223225
processFunctionDefinition(targetVtx, vid, graph, result, state);
224226
}
225-
if(vtx.origin !== 'unnamed') {
227+
if(!addedBiTarget && vtx.origin !== 'unnamed') {
226228
for(const origs of vtx.origin) {
227229
if(origs.startsWith('builtin:')) {
228230
addedTarget = true;

src/dataflow/internal/linker.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type { REnvironmentInformation } from '../environments/environment';
2222
import { findByPrefixIfUnique } from '../../util/prefix';
2323
import type { ExitPoint } from '../info';
2424
import { doesExitPointPropagateCalls } from '../info';
25+
import { UnnamedFunctionCallPrefix } from './process/functions/call/unnamed-call-handling';
2526

2627
export type NameIdMap = DefaultMap<string, IdentifierReference[]>
2728

@@ -361,7 +362,7 @@ export function linkFunctionCalls(
361362
* convenience function returning all known call targets, as well as the name source which defines them
362363
*/
363364
export function getAllFunctionCallTargets(call: NodeId, graph: DataflowGraph, environment?: REnvironmentInformation): NodeId[] {
364-
let found: NodeId[] = [];
365+
const found: Set<NodeId> = new Set();
365366
const callVertex = graph.get(call, true);
366367
if(callVertex === undefined) {
367368
return [];
@@ -373,23 +374,31 @@ export function getAllFunctionCallTargets(call: NodeId, graph: DataflowGraph, en
373374
return [];
374375
}
375376

376-
if(info.name !== undefined && (environment !== undefined || info.environment !== undefined)) {
377-
const functionCallDefs = resolveByName(
378-
info.name, environment ?? info.environment as REnvironmentInformation, info.origin.includes(BuiltInProcName.S3Dispatch) ? ReferenceType.S3MethodPrefix : ReferenceType.Function
379-
)?.map(d => d.nodeId) ?? [];
377+
if(environment !== undefined || info.environment !== undefined) {
378+
let functionCallDefs: NodeId[] = [];
379+
if(info.name !== undefined && !info.name.startsWith(UnnamedFunctionCallPrefix)) {
380+
functionCallDefs = resolveByName(
381+
info.name, environment ?? info.environment as REnvironmentInformation, info.origin.includes(BuiltInProcName.S3Dispatch) ? ReferenceType.S3MethodPrefix : ReferenceType.Function
382+
)?.map(d => d.nodeId) ?? [];
383+
}
380384
for(const [target, outgoingEdge] of outgoingEdges.entries()) {
381385
if(edgeIncludesType(outgoingEdge.types, EdgeType.Calls)) {
382386
functionCallDefs.push(target);
383387
}
384388
}
389+
385390
const [functionCallTargets, builtInTargets] = getAllLinkedFunctionDefinitions(new Set(functionCallDefs), graph);
386391
for(const target of functionCallTargets) {
387-
found.push(target.id);
392+
found.add(target.id);
393+
}
394+
for(const arr of [builtInTargets, functionCallDefs]) {
395+
for(const target of arr) {
396+
found.add(target);
397+
}
388398
}
389-
found = found.concat(Array.from(builtInTargets), functionCallDefs);
390399
}
391400

392-
return found;
401+
return Array.from(found);
393402
}
394403

395404
const LinkedFnFollowBits = EdgeType.Reads | EdgeType.DefinedBy | EdgeType.DefinedByOnCall;

src/dataflow/internal/process/functions/call/built-in/built-in-function-definition.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export function processFunctionDefinition<OtherInfo>(
5757

5858
const originalEnvironment = data.environment;
5959
// within a function def we do not pass on the outer binds as they could be overwritten when called
60-
data = prepareFunctionEnvironment(data);
60+
data = prepareFunctionEnvironment(data, rootId);
6161

6262
const subgraph = new DataflowGraph(data.completeAst.idMap);
6363

@@ -342,10 +342,13 @@ export function updateNestedFunctionCalls(
342342
}
343343
}
344344

345-
function prepareFunctionEnvironment<OtherInfo>(data: DataflowProcessorInformation<OtherInfo & ParentInformation>) {
345+
function prepareFunctionEnvironment<OtherInfo>(data: DataflowProcessorInformation<OtherInfo & ParentInformation>, rootId: NodeId) {
346346
let env = data.ctx.env.makeCleanEnv();
347347
for(let i = 0; i < data.environment.level + 1 /* add another env */; i++) {
348348
env = pushLocalEnvironment(env);
349+
if(i === data.environment.level) {
350+
env.current.setClosureNodeId(rootId);
351+
}
349352
}
350353
return { ...data, environment: env };
351354
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { DataflowProcessorInformation } from '../../../../../processor';
2+
import type { DataflowInformation } from '../../../../../info';
3+
import { processKnownFunctionCall } from '../known-call-handling';
4+
import type { ParentInformation } from '../../../../../../r-bridge/lang-4.x/ast/model/processing/decorate';
5+
import type { RFunctionArgument } from '../../../../../../r-bridge/lang-4.x/ast/model/nodes/r-function-call';
6+
import type { RSymbol } from '../../../../../../r-bridge/lang-4.x/ast/model/nodes/r-symbol';
7+
import type { NodeId } from '../../../../../../r-bridge/lang-4.x/ast/model/processing/node-id';
8+
import { BuiltInProcName } from '../../../../../environments/built-in';
9+
import { log } from '../../../../../../util/log';
10+
import { EdgeType } from '../../../../../graph/edge';
11+
import { isFunctionCallVertex } from '../../../../../graph/vertex';
12+
import { UnnamedFunctionCallPrefix } from '../unnamed-call-handling';
13+
import type { REnvironmentInformation } from '../../../../../environments/environment';
14+
15+
16+
/**
17+
* Processes a built-in 'Recall' function call, linking
18+
* the recall to the enveloping function closure.
19+
*/
20+
export function processRecall<OtherInfo>(
21+
name: RSymbol<OtherInfo & ParentInformation>,
22+
args: readonly RFunctionArgument<OtherInfo & ParentInformation>[],
23+
rootId: NodeId,
24+
data: DataflowProcessorInformation<OtherInfo & ParentInformation>,
25+
): DataflowInformation {
26+
const { information } = processKnownFunctionCall({
27+
name,
28+
args,
29+
rootId,
30+
data,
31+
origin: BuiltInProcName.Recall
32+
});
33+
34+
let cur = data.environment.current;
35+
let closure: NodeId | undefined;
36+
while(cur) {
37+
if(cur.closure) {
38+
closure = cur.closure;
39+
break;
40+
}
41+
cur = cur.parent;
42+
}
43+
44+
if(closure) {
45+
information.graph.addEdge(rootId, closure, EdgeType.Calls);
46+
// also kill the name of the recall function
47+
const r = information.graph.getVertex(rootId);
48+
if(isFunctionCallVertex(r)){
49+
(r as { name: string }).name = UnnamedFunctionCallPrefix + rootId + '-' + r.name;
50+
(r as { environment: REnvironmentInformation }).environment = information.environment;
51+
}
52+
} else {
53+
log.warn('No enclosing function closure found for recall at node', rootId);
54+
}
55+
56+
return information;
57+
}

src/dataflow/internal/process/functions/call/known-call-handling.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@ import { handleUnknownSideEffect } from '../../../../graph/unknown-side-effect';
1818
import { BuiltInProcName } from '../../../../environments/built-in';
1919

2020
export interface ProcessKnownFunctionCallInput<OtherInfo> extends ForceArguments {
21+
/** The name of the function being called. */
2122
readonly name: RSymbol<OtherInfo & ParentInformation>
23+
/** The arguments to the function call. */
2224
readonly args: readonly (RNode<OtherInfo & ParentInformation> | RFunctionArgument<OtherInfo & ParentInformation>)[]
25+
/** The node ID to use for the function call vertex. */
2326
readonly rootId: NodeId
27+
/** The dataflow processor information at the point of the function call. */
2428
readonly data: DataflowProcessorInformation<OtherInfo & ParentInformation>
2529
/** should arguments be processed from right to left? This does not affect the order recorded in the call but of the environments */
2630
readonly reverseOrder?: boolean

0 commit comments

Comments
 (0)