Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/dataflow/environments/default-builtin-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,14 @@ export const DefaultBuiltinConfig: BuiltInDefinitions = [
{ 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: ['body', 'formals', 'environment'], processor: 'builtin:default', config: { hasUnknownSideEffects: true, forceArgs: [true] }, assumePrimitive: true },
{ type: 'function', names: ['.Call', '.External', '.C', '.Fortran'], processor: 'builtin:default', config: { hasUnknownSideEffects: true, forceArgs: [true],
treatAsFnCall: {
'.Call': ['.NAME'],
'.External': ['.NAME'],
'.C': ['.NAME'],
'.Fortran': ['.NAME']
}
}, assumePrimitive: true },
{ type: 'function', names: ['eval'], processor: 'builtin:eval', config: { includeFunctionCall: true }, assumePrimitive: true },
{ 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 },
Expand Down
2 changes: 2 additions & 0 deletions src/dataflow/eval/resolve/alias-tracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ export function resolveIdToValue(id: NodeId | RNodeWithParent | undefined, { env
} else {
return Top;
}
case RType.FunctionDefinition:
return setFrom({ type: 'function-definition' });
case RType.FunctionCall:
case RType.BinaryOp:
case RType.UnaryOp:
Expand Down
2 changes: 2 additions & 0 deletions src/dataflow/eval/resolve/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export function resolveNode(resolve: VariableResolve, a: RNodeWithParent, env?:
return intervalFrom(a.content.num, a.content.num);
} else if(a.type === RType.Logical) {
return a.content.valueOf() ? ValueLogicalTrue : ValueLogicalFalse;
} else if(a.type === RType.FunctionDefinition) {
return { type: 'function-definition' };
} else if((a.type === RType.FunctionCall || a.type === RType.BinaryOp || a.type === RType.UnaryOp) && graph) {
const origin = getOriginInDfg(graph, a.info.id)?.[0];
if(origin === undefined || origin.type !== OriginType.BuiltInFunctionOrigin) {
Expand Down
6 changes: 6 additions & 0 deletions src/dataflow/eval/values/r-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export interface ValueString<Str extends Lift<RStringValue> = Lift<RStringValue>
type: 'string'
value: Str
}
export interface ValueFunctionDefinition {
type: 'function-definition'
}
export interface ValueMissing {
type: 'missing'
}
Expand All @@ -55,6 +58,7 @@ export type Value = Lift<
| ValueString
| ValueLogical
| ValueMissing
| ValueFunctionDefinition
>
export type ValueType<V> = V extends { type: infer T } ? T : never
export type ValueTypes = ValueType<Value>
Expand Down Expand Up @@ -143,6 +147,8 @@ export function stringifyValue(value: Lift<Value>): string {
return tryStringifyBoTop(v.value, l => l === 'maybe' ? 'maybe' : l ? 'TRUE' : 'FALSE', () => '⊤ (logical)', () => '⊥ (logical)');
case 'missing':
return '(missing)';
case 'function-definition':
return 'fn-def';
default:
assertUnreachable(t);
}
Expand Down
80 changes: 80 additions & 0 deletions src/dataflow/fn/higher-order-function.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { NodeId } from '../../r-bridge/lang-4.x/ast/model/processing/node-id';
import type { DataflowGraph } from '../graph/graph';
import type { DataflowGraphVertexArgument, DataflowGraphVertexFunctionDefinition } from '../graph/vertex';
import { isFunctionCallVertex, isFunctionDefinitionVertex } from '../graph/vertex';
import { isNotUndefined } from '../../util/assert';
import { edgeIncludesType, EdgeType } from '../graph/edge';
import { resolveIdToValue } from '../eval/resolve/alias-tracking';
import { VariableResolve } from '../../config';
import { EmptyArgument } from '../../r-bridge/lang-4.x/ast/model/nodes/r-function-call';
import { valueSetGuard } from '../eval/values/general';

function isAnyReturnAFunction(def: DataflowGraphVertexFunctionDefinition, graph: DataflowGraph): boolean {
const workingQueue: DataflowGraphVertexArgument[] = def.exitPoints.map(d => graph.getVertex(d, true)).filter(isNotUndefined);
const seen = new Set<NodeId>();
while(workingQueue.length > 0) {
const current = workingQueue.pop() as DataflowGraphVertexArgument;
if(seen.has(current.id)) {
continue;
}
seen.add(current.id);
if(isFunctionDefinitionVertex(current)) {
return true;
}
const next = graph.outgoingEdges(current.id) ?? [];
for(const [t, { types }] of next) {
if(edgeIncludesType(types, EdgeType.Returns)) {
const v = graph.getVertex(t, true);
if(v) {
workingQueue.push(v);
}
}
}
}
return false;
}

function inspectCallSitesArgumentsFns(def: DataflowGraphVertexFunctionDefinition, graph: DataflowGraph): boolean {
const callSites = graph.ingoingEdges(def.id);

for(const [callerId, { types }] of callSites ?? []) {
if(!edgeIncludesType(types, EdgeType.Calls)) {
continue;
}
const caller = graph.getVertex(callerId, true);
if(!caller || !isFunctionCallVertex(caller)) {
continue;
}
for(const arg of caller.args) {
if(arg === EmptyArgument) {
continue;
}
const value = valueSetGuard(resolveIdToValue(arg.nodeId, { graph, idMap: graph.idMap, resolve: VariableResolve.Alias, full: true }));
if(value?.elements.some(e => e.type === 'function-definition')) {
return true;
}
}
}
return false;
}

/**
* Determines whether the function with the given id is a higher-order function, i.e.,
* either takes a function as an argument or (may) returns a function.
* If the return is an identity, e.g., `function(x) x`, this is not considered higher-order,
* if no function is passed as an argument.
*/
export function isHigherOrder(id: NodeId, graph: DataflowGraph): boolean {
const vert = graph.getVertex(id);
if(!vert || !isFunctionDefinitionVertex(vert)) {
return false;
}

// 1. check whether any of the exit types is a function
if(isAnyReturnAFunction(vert, graph)) {
return true;
}

// 2. check whether any of the callsites passes a function
return inspectCallSitesArgumentsFns(vert, graph);
}
26 changes: 26 additions & 0 deletions src/documentation/print-query-wiki.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ import { printCfgCode } from './doc-util/doc-cfg';
import { executeDfShapeQuery } from '../queries/catalog/df-shape-query/df-shape-query-executor';
import { SliceDirection } from '../core/steps/all/static-slicing/00-slice';
import { documentReplSession } from './doc-util/doc-repl';
import {
executeHigherOrderQuery
} from '../queries/catalog/inspect-higher-order-query/inspect-higher-order-query-executor';


registerQueryDocumentation('call-context', {
Expand Down Expand Up @@ -274,6 +277,29 @@ ${
}
});


registerQueryDocumentation('inspect-higher-order', {
name: 'Inspect Higher-Order Functions Query',
type: 'active',
shortDescription: 'Determine whether functions are higher-order functions',
functionName: executeHigherOrderQuery.name,
functionFile: '../queries/catalog/inspect-higher-order-query/inspect-higher-order-query-executor.ts',
buildExplanation: async(shell: RShell) => {
const exampleCode = 'f <- function() function(x) x; f()';
return `
With this query you can identify which functions in the code are higher-order functions, i.e., either take a function as an argument or return a function.
Please note, that functions that are just identities (e.g., \`function(x) x\`) are not considered higher-order if they do not take a function as an argument.

Using the example code \`${exampleCode}\` the following query returns the information for all identified function definitions whether they are higher-order functions:
${
await showQuery(shell, exampleCode, [{
type: 'inspect-higher-order',
}], { showCode: true })
}
`;
}
});

registerQueryDocumentation('origin', {
name: 'Origin Query',
type: 'active',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type {
InspectHigherOrderQuery, InspectHigherOrderQueryResult
} from './inspect-higher-order-query-format';
import type { BasicQueryData } from '../../base-query-format';
import type { SingleSlicingCriterion } from '../../../slicing/criterion/parse';
import { tryResolveSliceCriterionToId } from '../../../slicing/criterion/parse';
import { isFunctionDefinitionVertex } from '../../../dataflow/graph/vertex';
import type { NodeId } from '../../../r-bridge/lang-4.x/ast/model/processing/node-id';
import { isHigherOrder } from '../../../dataflow/fn/higher-order-function';


export function executeHigherOrderQuery({ dataflow: { graph }, ast }: BasicQueryData, queries: readonly InspectHigherOrderQuery[]): InspectHigherOrderQueryResult {
const start = Date.now();
let filters: SingleSlicingCriterion[] | undefined = undefined;
// filter will remain undefined if at least one of the queries wants all functions
for(const q of queries) {
if(q.filter === undefined) {
filters = undefined;
break;
} else {
filters ??= [];
filters = filters.concat(filters);
}
}

const filterFor = new Set<NodeId>();
if(filters) {
for(const f of filters) {
const i = tryResolveSliceCriterionToId(f, ast.idMap);
if(i !== undefined) {
filterFor.add(i);
}
}
}
const fns = graph.vertices(true)
.filter(([,v]) => isFunctionDefinitionVertex(v) && (filterFor.size === 0 || filterFor.has(v.id)));
const result: Record<NodeId, boolean> = {};
for(const [id,] of fns) {
result[id] = isHigherOrder(id, graph);
}

return {
'.meta': {
timing: Date.now() - start
},
higherOrder: result
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { BaseQueryFormat, BaseQueryResult } from '../../base-query-format';
import { bold } from '../../../util/text/ansi';
import Joi from 'joi';
import type { QueryResults, SupportedQuery } from '../../query';
import { executeHigherOrderQuery } from './inspect-higher-order-query-executor';
import type { NodeId } from '../../../r-bridge/lang-4.x/ast/model/processing/node-id';
import { normalizeIdToNumberIfPossible } from '../../../r-bridge/lang-4.x/ast/model/processing/node-id';
import type { SingleSlicingCriterion } from '../../../slicing/criterion/parse';
import { formatRange } from '../../../util/mermaid/dfg';

/**
* Either returns all function definitions alongside whether they are higher-order functions,
* or just those matching the filters.
*/
export interface InspectHigherOrderQuery extends BaseQueryFormat {
readonly type: 'inspect-higher-order';
readonly filter?: SingleSlicingCriterion[]
}

export interface InspectHigherOrderQueryResult extends BaseQueryResult {
readonly higherOrder: Record<NodeId, boolean>;
}

export const InspectHigherOrderQueryDefinition = {
executor: executeHigherOrderQuery,
asciiSummarizer: (formatter, processed, queryResults, result) => {
const out = queryResults as QueryResults<'inspect-higher-order'>['inspect-higher-order'];
result.push(`Query: ${bold('inspect-higher-order', formatter)} (${out['.meta'].timing.toFixed(0)}ms)`);
for(const [r, v] of Object.entries(out.higherOrder)) {
const loc = processed.normalize.idMap.get(normalizeIdToNumberIfPossible(r))?.location ?? undefined;
result.push(` - Function ${bold(r, formatter)} (${formatRange(loc)}) is ${v ? '' : 'not '}a higher-order function`);
}
return true;
},
schema: Joi.object({
type: Joi.string().valid('inspect-higher-order').required().description('The type of the query.'),
filter: Joi.array().items(Joi.string().required()).optional().description('If given, only function definitions that match one of the given slicing criteria are considered. Each criterion can be either `line:column`, `line@variable-name`, or `$id`, where the latter directly specifies the node id of the function definition to be considered.')
}).description('Either returns all function definitions alongside whether they are higher-order functions, or just those matching the filters.'),
flattenInvolvedNodes: (queryResults: BaseQueryResult): NodeId[] => {
const out = queryResults as QueryResults<'inspect-higher-order'>['inspect-higher-order'];
return Object.keys(out.higherOrder).filter(id => out.higherOrder[id]);
}
} as const satisfies SupportedQuery<'inspect-higher-order'>;
45 changes: 26 additions & 19 deletions src/queries/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ import type { DfShapeQuery } from './catalog/df-shape-query/df-shape-query-forma
import { DfShapeQueryDefinition } from './catalog/df-shape-query/df-shape-query-format';
import type { AsyncOrSync, AsyncOrSyncType, Writable } from 'ts-essentials';
import type { FlowrConfigOptions } from '../config';
import type {
InspectHigherOrderQuery } from './catalog/inspect-higher-order-query/inspect-higher-order-query-format';
import {
InspectHigherOrderQueryDefinition
} from './catalog/inspect-higher-order-query/inspect-higher-order-query-format';

/**
* These are all queries that can be executed from within flowR
Expand All @@ -68,6 +73,7 @@ export type Query = CallContextQuery
| DependenciesQuery
| LocationMapQuery
| HappensBeforeQuery
| InspectHigherOrderQuery
| ResolveValueQuery
| ProjectQuery
| OriginQuery
Expand Down Expand Up @@ -103,25 +109,26 @@ export interface SupportedQuery<QueryType extends BaseQueryFormat['type'] = Base
}

export const SupportedQueries = {
'call-context': CallContextQueryDefinition,
'config': ConfigQueryDefinition,
'control-flow': ControlFlowQueryDefinition,
'dataflow': DataflowQueryDefinition,
'dataflow-lens': DataflowLensQueryDefinition,
'df-shape': DfShapeQueryDefinition,
'id-map': IdMapQueryDefinition,
'normalized-ast': NormalizedAstQueryDefinition,
'dataflow-cluster': ClusterQueryDefinition,
'static-slice': StaticSliceQueryDefinition,
'lineage': LineageQueryDefinition,
'dependencies': DependenciesQueryDefinition,
'location-map': LocationMapQueryDefinition,
'search': SearchQueryDefinition,
'happens-before': HappensBeforeQueryDefinition,
'resolve-value': ResolveValueQueryDefinition,
'project': ProjectQueryDefinition,
'origin': OriginQueryDefinition,
'linter': LinterQueryDefinition
'call-context': CallContextQueryDefinition,
'config': ConfigQueryDefinition,
'control-flow': ControlFlowQueryDefinition,
'dataflow': DataflowQueryDefinition,
'dataflow-lens': DataflowLensQueryDefinition,
'df-shape': DfShapeQueryDefinition,
'id-map': IdMapQueryDefinition,
'normalized-ast': NormalizedAstQueryDefinition,
'dataflow-cluster': ClusterQueryDefinition,
'static-slice': StaticSliceQueryDefinition,
'lineage': LineageQueryDefinition,
'dependencies': DependenciesQueryDefinition,
'location-map': LocationMapQueryDefinition,
'search': SearchQueryDefinition,
'happens-before': HappensBeforeQueryDefinition,
'inspect-higher-order': InspectHigherOrderQueryDefinition,
'resolve-value': ResolveValueQueryDefinition,
'project': ProjectQueryDefinition,
'origin': OriginQueryDefinition,
'linter': LinterQueryDefinition
} as const satisfies SupportedQueries;

export type SupportedQueryTypes = keyof typeof SupportedQueries;
Expand Down
15 changes: 12 additions & 3 deletions src/util/r-value.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import type { Value, ValueInterval, ValueLogical, ValueNumber, ValueString, ValueVector } from '../dataflow/eval/values/r-value';
import type {
Value,
ValueInterval,
ValueLogical,
ValueNumber,
ValueString,
ValueVector
} from '../dataflow/eval/values/r-value';
import { isValue } from '../dataflow/eval/values/r-value';
import type { RLogicalValue } from '../r-bridge/lang-4.x/ast/model/nodes/r-logical';
import { RFalse, RTrue, type RNumberValue, type RStringValue } from '../r-bridge/lang-4.x/convert-values';
Expand Down Expand Up @@ -80,8 +87,8 @@ export function unliftRValue(value: ValueString): RStringValue | undefined;
export function unliftRValue(value: ValueNumber | ValueInterval): RNumberValue | undefined;
export function unliftRValue(value: ValueLogical): RLogicalValue | undefined;
export function unliftRValue(value: ValueVector): (RStringValue | RNumberValue | RLogicalValue)[] | undefined;
export function unliftRValue(value: Value): RStringValue | RNumberValue | boolean | (RStringValue | RNumberValue | RLogicalValue)[] | undefined;
export function unliftRValue(value: Value): RStringValue | RNumberValue | boolean | (RStringValue | RNumberValue | RLogicalValue)[] | undefined {
export function unliftRValue(value: Value): RStringValue | RNumberValue | 'fn-def' | boolean | ('fn-def' | RStringValue | RNumberValue | RLogicalValue)[] | undefined;
export function unliftRValue(value: Value): RStringValue | RNumberValue | 'fn-def' | boolean | ('fn-def' | RStringValue | RNumberValue | RLogicalValue)[] | undefined {
if(!isValue(value)) {
return undefined;
}
Expand Down Expand Up @@ -112,6 +119,8 @@ export function unliftRValue(value: Value): RStringValue | RNumberValue | boolea
case 'missing': {
return undefined;
}
case 'function-definition':
return 'fn-def';
default:
assertUnreachable(type);
}
Expand Down
Loading