Skip to content

Commit

Permalink
Support graphics functions with side effects! (#1111)
Browse files Browse the repository at this point in the history
* feat: graphics output options within dependencies query

* feat: basic link to dependency resolution

* feat(r-graphics-pkg): rudimentary side effect support
  • Loading branch information
EagleoutIce authored Oct 27, 2024
1 parent 6580164 commit dfb2e2a
Show file tree
Hide file tree
Showing 26 changed files with 335 additions and 135 deletions.
6 changes: 3 additions & 3 deletions src/core/steps/all/core/20-dataflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ const staticDataflowCommon = {
dependencies: [ 'normalize' ],
} as const;

function legacyProcessor(results: { normalize?: NormalizedAst }, input: { request?: RParseRequests }) {
function processor(results: { normalize?: NormalizedAst }, input: { request?: RParseRequests }) {
return produceDataFlowGraph(input.request as RParseRequests, results.normalize as NormalizedAst);
}

export const STATIC_DATAFLOW = {
...staticDataflowCommon,
humanReadableName: 'dataflow',
processor: legacyProcessor,
processor,
requiredInput: {
}
} as const satisfies DeepReadonly<IPipelineStep<'dataflow', typeof legacyProcessor>>;
} as const satisfies DeepReadonly<IPipelineStep<'dataflow', typeof processor>>;

20 changes: 18 additions & 2 deletions src/dataflow/environments/built-in.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ import type { ForceArguments } from '../internal/process/functions/call/common';
import { processApply } from '../internal/process/functions/call/built-in/built-in-apply';
import { registerBuiltInDefinitions } from './built-in-config';
import { DefaultBuiltinConfig } from './default-builtin-config';
import type { LinkTo } from '../../queries/catalog/call-context-query/call-context-query-format';




export const BuiltIn = 'built-in';

Expand Down Expand Up @@ -58,12 +62,19 @@ export interface BuiltInIdentifierConstant<T = unknown> extends IdentifierRefere
value: T
}

export interface DefaultBuiltInProcessorConfiguration extends ForceArguments {
readonly returnsNthArgument?: number | 'last',
readonly cfg?: ExitPointType,
readonly readAllArguments?: boolean,
readonly hasUnknownSideEffects?: boolean | LinkTo<RegExp | string>
}

function defaultBuiltInProcessor<OtherInfo>(
name: RSymbol<OtherInfo & ParentInformation>,
args: readonly RFunctionArgument<OtherInfo & ParentInformation>[],
rootId: NodeId,
data: DataflowProcessorInformation<OtherInfo & ParentInformation>,
config: { returnsNthArgument?: number | 'last', cfg?: ExitPointType, readAllArguments?: boolean, hasUnknownSideEffects?: boolean } & ForceArguments
config: DefaultBuiltInProcessorConfiguration
): DataflowInformation {
const { information: res, processedArguments } = processKnownFunctionCall({ name, args, rootId, data, forceArgs: config.forceArgs });
if(config.returnsNthArgument !== undefined) {
Expand All @@ -81,12 +92,17 @@ function defaultBuiltInProcessor<OtherInfo>(
}

if(config.hasUnknownSideEffects) {
res.graph.markIdForUnknownSideEffects(rootId);
if(typeof config.hasUnknownSideEffects !== 'boolean') {
res.graph.markIdForUnknownSideEffects(rootId, config.hasUnknownSideEffects);
} else {
res.graph.markIdForUnknownSideEffects(rootId);
}
}

if(config.cfg !== undefined) {
res.exitPoints = [...res.exitPoints, { type: config.cfg, nodeId: rootId, controlDependencies: data.controlDependencies }];
}

return res;
}

Expand Down
26 changes: 21 additions & 5 deletions src/dataflow/environments/default-builtin-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,22 @@ export const DefaultBuiltinConfig: BuiltInDefinitions = [
{
type: 'function',
names: [
'~', '+', '-', '*', '/', '^', '!', '?', '**', '==', '!=', '>', '<', '>=', '<=', '%%', '%/%', '%*%', '%in%', ':', 'list', 'c',
'~', '+', '-', '*', '/', '^', '!', '?', '**', '==', '!=', '>', '<', '>=', '<=', '%%', '%/%', '%*%', '%in%', ':', 'list',
'rep', 'seq', 'seq_len', 'seq_along', 'seq.int', 'gsub', 'which', 'class', 'dimnames', 'min', 'max',
'intersect', 'subset', 'match', 'sqrt', 'abs', 'round', 'floor', 'ceiling', 'signif', 'trunc', 'log', 'log10', 'log2', 'sum', 'mean',
'unique', 'paste', 'paste0', 'read.csv', 'stop', 'is.null', 'plot', 'numeric', 'as.character', 'as.integer', 'as.logical', 'as.numeric', 'as.matrix',
'unique', 'paste', 'paste0', 'read.csv', 'stop', 'is.null', 'numeric', 'as.character', 'as.integer', 'as.logical', 'as.numeric', 'as.matrix',
'rbind', 'nrow', 'ncol', 'tryCatch', 'expression', 'factor',
'missing', 'as.data.frame', 'data.frame', 'na.omit', 'rownames', 'names', 'order', 'length', 'any', 'dim', 'matrix', 'cbind', 'nchar', 't'
'missing', 'as.data.frame', 'data.frame', 'na.omit', 'rownames', 'names', 'order', 'length', 'any', 'dim', 'matrix', 'cbind', 'nchar',
'pdf', 'jpeg', 'png', 'windows', 'postscript', 'xfig', 'bitmap', 'pictex', 'cairo_pdf', 'svg', 'bmp', 'tiff', 'X11', 'quartz'
],
processor: 'builtin:default',
config: { readAllArguments: true },
assumePrimitive: true
},
{
type: 'function',
names: [
'c', 't'
],
processor: 'builtin:default',
config: { readAllArguments: true },
Expand All @@ -27,11 +37,17 @@ export const DefaultBuiltinConfig: BuiltInDefinitions = [
{ type: 'function', names: ['lapply', 'sapply', 'vapply'], processor: 'builtin:apply', config: { indexOfFunction: 1, nameOfFunctionArgument: 'FUN' }, assumePrimitive: false },
{ type: 'function', names: ['Lapply', 'Sapply', 'Vapply'], processor: 'builtin:apply', config: { indexOfFunction: 1, nameOfFunctionArgument: 'FUN' }, assumePrimitive: false }, /* functool wrappers */
{ type: 'function', names: ['apply', 'tapply', 'Tapply'], processor: 'builtin:apply', config: { indexOfFunction: 2, nameOfFunctionArgument: 'FUN' }, assumePrimitive: false },
{ type: 'function', names: ['print'], processor: 'builtin:default', config: { returnsNthArgument: 0, forceArgs: 'all' }, assumePrimitive: false },
{ type: 'function', names: ['print', 'message', 'warning'], processor: 'builtin:default', config: { returnsNthArgument: 0, forceArgs: 'all', hasUnknownSideEffects: { type: 'link-to-last-call', callName: /^sink$/ } }, assumePrimitive: false },
// graphics base
{ type: 'function', names: ['plot', 'map', 'image', 'boxplot', 'barplot', 'matplot', 'hist', 'stem', 'density', 'smoothScatter', 'contour', 'persp'],
processor: 'builtin:default', config: { forceArgs: 'all', hasUnknownSideEffects: { type: 'link-to-last-call', callName: /^pdf|jpeg|png|windows|postscript|xfig|bitmap|pictex|cairo_pdf|svg|bmp|tiff|X11|quartz$/ } }, assumePrimitive: true },
// graphics addons
{ type: 'function', names: ['points', 'abline', 'lines', 'text', 'legend', 'title', 'axis', 'polygon', 'polypath', 'pie', 'rect', 'segments', 'arrows', 'symbols', 'tiplabels'],
processor: 'builtin:default', config: { forceArgs: 'all', hasUnknownSideEffects: { type: 'link-to-last-call', callName: /^dev\.new|dev\.copy|plot|map|image|boxplot|barplot|matplot|hist|stem|density|smoothScatter|contour|persp$/ } }, assumePrimitive: true },
{ type: 'function', names: ['('], processor: 'builtin:default', config: { returnsNthArgument: 0 }, assumePrimitive: true },
{ type: 'function', names: ['load', 'load_all', 'setwd', 'set.seed'], processor: 'builtin:default', config: { hasUnknownSideEffects: true, forceArgs: [true] }, assumePrimitive: false },
{ type: 'function', names: ['eval', 'body', 'formals', 'environment'], processor: 'builtin:default', config: { hasUnknownSideEffects: true, forceArgs: [true] }, assumePrimitive: false },
{ type: 'function', names: ['cat'], processor: 'builtin:default', config: { forceArgs: 'all' }, assumePrimitive: false },
{ type: 'function', names: ['cat'], processor: 'builtin:default', config: { forceArgs: 'all', hasUnknownSideEffects: { type: 'link-to-last-call', callName: /^sink$/ } }, assumePrimitive: false },
{ type: 'function', names: ['switch'], processor: 'builtin:default', config: { forceArgs: [true] }, assumePrimitive: false },
{ type: 'function', names: ['return'], processor: 'builtin:default', config: { returnsNthArgument: 0, cfg: ExitPointType.Return }, assumePrimitive: false },
{ type: 'function', names: ['break'], processor: 'builtin:default', config: { cfg: ExitPointType.Break }, assumePrimitive: false },
Expand Down
29 changes: 29 additions & 0 deletions src/dataflow/extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ import type { RParseRequest, RParseRequests } from '../r-bridge/retriever';
import { requestFingerprint } from '../r-bridge/retriever';
import { initializeCleanEnvironments } from './environments/environment';
import { standaloneSourceFile } from './internal/process/functions/call/built-in/built-in-source';
import type { DataflowGraph } from './graph/graph';
import type { ControlFlowGraph } from '../util/cfg/cfg';
import { extractCFG } from '../util/cfg/cfg';
import { EdgeType } from './graph/edge';
import {
identifyLinkToLastCallRelation
} from '../queries/catalog/call-context-query/identify-link-to-last-call-relation';

export const processors: DataflowProcessors<ParentInformation> = {
[RType.Number]: processValue,
Expand Down Expand Up @@ -49,6 +56,27 @@ export const processors: DataflowProcessors<ParentInformation> = {
}, wrapArgumentsUnnamed(n.children, d.completeAst.idMap), n.info.id, d)
};


function resolveLinkToSideEffects(ast: NormalizedAst, graph: DataflowGraph) {
let cfg: ControlFlowGraph | undefined = undefined;
for(const s of graph.unknownSideEffects) {
if(typeof s !== 'object') {
continue;
}
if(!cfg) {
cfg = extractCFG(ast).graph;
}
/* this has to change whenever we add a new link to relations because we currently offer no abstraction for the type */
const potentials = identifyLinkToLastCallRelation(s.id, cfg, graph, s.linkTo.callName);
for(const pot of potentials) {
graph.addEdge(s.id, pot, EdgeType.Reads);
}
if(potentials.length > 0) {
graph.unknownSideEffects.delete(s);
}
}
}

export function produceDataFlowGraph<OtherInfo>(
request: RParseRequests,
ast: NormalizedAst<OtherInfo & ParentInformation>
Expand Down Expand Up @@ -76,5 +104,6 @@ export function produceDataFlowGraph<OtherInfo>(
}
}

resolveLinkToSideEffects(ast, df.graph);
return df;
}
10 changes: 10 additions & 0 deletions src/dataflow/graph/dataflowgraph-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { EmptyArgument } from '../../r-bridge/lang-4.x/ast/model/nodes/r-functio
import { BuiltIn } from '../environments/built-in';
import { EdgeType } from './edge';
import type { ControlDependency } from '../info';
import type { LinkTo } from '../../queries/catalog/call-context-query/call-context-query-format';
import { DefaultBuiltinConfig } from '../environments/default-builtin-config';

export function emptyGraph(idMap?: AstIdMap) {
return new DataflowGraphBuilder(idMap);
Expand Down Expand Up @@ -277,3 +279,11 @@ export class DataflowGraphBuilder extends DataflowGraph {
return this;
}
}

export function getBuiltInSideEffect(name: string): LinkTo<RegExp> | undefined {
const got = DefaultBuiltinConfig.find(e => e.names.includes(name));
if(got?.type !== 'function') {
return undefined;
}
return (got?.config as { hasUnknownSideEffects: LinkTo<RegExp> | undefined }).hasUnknownSideEffects;
}
5 changes: 4 additions & 1 deletion src/dataflow/graph/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,10 @@ function diffOutgoingEdges(ctx: DataflowDiffContext): void {

function diffRootVertices(ctx: DataflowDiffContext): void {
setDifference(ctx.left.rootIds(), ctx.right.rootIds(), { ...ctx, position: `${ctx.position}Root vertices differ in graphs. ` });
setDifference(ctx.left.unknownSideEffects, ctx.right.unknownSideEffects, { ...ctx, position: `${ctx.position}Unknown side effects differ in graphs. ` });
setDifference(
new Set([...ctx.left.unknownSideEffects].map(n => typeof n === 'object' ? n.id : n)),
new Set([...ctx.right.unknownSideEffects].map(n => typeof n === 'object' ? n.id : n)),
{ ...ctx, position: `${ctx.position}Unknown side effects differ in graphs. ` });
}


Expand Down
19 changes: 15 additions & 4 deletions src/dataflow/graph/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { cloneEnvironmentInformation } from '../environments/clone';
import { jsonReplacer } from '../../util/json';
import { BuiltIn } from '../environments/built-in';
import { dataflowLogger } from '../logger';
import type { LinkTo } from '../../queries/catalog/call-context-query/call-context-query-format';

export type DataflowFunctionFlowInformation = Omit<DataflowInformation, 'graph' | 'exitPoints'> & { graph: Set<NodeId> }

Expand Down Expand Up @@ -89,6 +90,8 @@ export interface DataflowGraphJson {
readonly edgeInformation: [NodeId, [NodeId, DataflowGraphEdge][]][]
}

export type UnknownSidEffect = NodeId | { id: NodeId, linkTo: LinkTo<RegExp> }

/**
* The dataflow graph holds the dataflow information found within the given AST.
* We differentiate the directed edges in {@link EdgeType} and the vertices indicated by {@link DataflowGraphVertexArgument}
Expand All @@ -106,8 +109,12 @@ export class DataflowGraph<
> {
private static DEFAULT_ENVIRONMENT: REnvironmentInformation | undefined = undefined;
private _idMap: AstIdMap | undefined;
/* Set of vertices which have sideEffects that we do not know anything about */
private readonly _unknownSideEffects = new Set<NodeId>();
/*
* Set of vertices which have sideEffects that we do not know anything about.
* As a (temporary) solution until we have FD edges, a side effect may also store known target links
* that have to be/should be resolved (as globals) as a separate pass before the df analysis ends.
*/
private readonly _unknownSideEffects = new Set<UnknownSidEffect>();

constructor(idMap: AstIdMap | undefined) {
DataflowGraph.DEFAULT_ENVIRONMENT ??= initializeCleanEnvironments();
Expand Down Expand Up @@ -173,7 +180,7 @@ export class DataflowGraph<
/**
* Retrieves the set of vertices which have side effects that we do not know anything about.
*/
public get unknownSideEffects(): ReadonlySet<NodeId> {
public get unknownSideEffects(): Set<UnknownSidEffect> {
return this._unknownSideEffects;
}

Expand Down Expand Up @@ -401,7 +408,11 @@ export class DataflowGraph<
}

/** Marks the given node as having unknown side effects */
public markIdForUnknownSideEffects(id: NodeId): this {
public markIdForUnknownSideEffects(id: NodeId, target?: LinkTo<RegExp | string>): this {
if(target) {
this._unknownSideEffects.add({ id: normalizeIdToNumberIfPossible(id), linkTo: typeof target.callName === 'string' ? { ...target, callName: new RegExp(target.callName) } : target as LinkTo<RegExp> });
return this;
}
this._unknownSideEffects.add(normalizeIdToNumberIfPossible(id));
return this;
}
Expand Down
4 changes: 2 additions & 2 deletions src/dataflow/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { RParseRequest } from '../r-bridge/retriever';
import type { RNode } from '../r-bridge/lang-4.x/ast/model/model';

export interface DataflowProcessorInformation<OtherInfo> {
/**
/**
* Initial and frozen ast-information
*/
readonly completeAst: NormalizedAst<OtherInfo>
Expand All @@ -35,7 +35,7 @@ export interface DataflowProcessorInformation<OtherInfo> {
*/
readonly referenceChain: string[]
/**
* The chain of control-flow {@link NodeId}s that lead to the current node (e.g. of known ifs).
* The chain of control-flow {@link NodeId}s that lead to the current node (e.g., of known ifs).
*/
readonly controlDependencies: ControlDependency[] | undefined
}
Expand Down
4 changes: 2 additions & 2 deletions src/documentation/data/server/doc-data-server-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ import {
responseQueryMessage
} from '../../../cli/repl/server/messages/message-query';
import { exampleQueryCode } from '../query/example-query-code';
import { CallTargets } from '../../../queries/catalog/call-context-query/call-context-query-format';
import { requestLineageMessage, responseLineageMessage } from '../../../cli/repl/server/messages/message-lineage';
import { CallTargets } from '../../../queries/catalog/call-context-query/identify-link-to-last-call-relation';

export function documentAllServerMessages() {

Expand Down Expand Up @@ -229,7 +229,7 @@ While the context is derived from the \`filename\`, we currently offer no way to
});

const deprecatedByQuery = `(<a href="${FlowrWikiBaseRef}/Query%20API">deprecated</a>)`;

documentServerMessage({
title: 'Slice',
type: 'request',
Expand Down
2 changes: 1 addition & 1 deletion src/documentation/print-query-wiki.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
showQuery,
tocForQueryType
} from './doc-util/doc-query';
import { CallTargets } from '../queries/catalog/call-context-query/call-context-query-format';
import { describeSchema } from '../util/schema';
import { markdownFormatter } from '../util/ansi';
import { executeCallContextQueries } from '../queries/catalog/call-context-query/call-context-query-executor';
Expand All @@ -30,6 +29,7 @@ import { executeDependenciesQuery } from '../queries/catalog/dependencies-query/
import { getReplCommand } from './doc-util/doc-cli-option';
import { NewIssueUrl } from './doc-util/doc-issue';
import { executeLocationMapQuery } from '../queries/catalog/location-map-query/location-map-query-executor';
import { CallTargets } from '../queries/catalog/call-context-query/identify-link-to-last-call-relation';


registerQueryDocumentation('call-context', {
Expand Down
Loading

0 comments on commit dfb2e2a

Please sign in to comment.