diff --git a/src/dataflow/environments/built-in.ts b/src/dataflow/environments/built-in.ts index af1f1f64c1..84b069fcfc 100644 --- a/src/dataflow/environments/built-in.ts +++ b/src/dataflow/environments/built-in.ts @@ -29,6 +29,7 @@ import { processApply } from '../internal/process/functions/call/built-in/built- 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'; +import { processList } from '../internal/process/functions/call/built-in/built-in-list'; @@ -76,6 +77,7 @@ function defaultBuiltInProcessor( data: DataflowProcessorInformation, config: DefaultBuiltInProcessorConfiguration ): DataflowInformation { + console.log('processing default:', name.content); const { information: res, processedArguments } = processKnownFunctionCall({ name, args, rootId, data, forceArgs: config.forceArgs }); if(config.returnsNthArgument !== undefined) { const arg = config.returnsNthArgument === 'last' ? processedArguments[args.length - 1] : processedArguments[config.returnsNthArgument]; @@ -147,6 +149,7 @@ export const BuiltInProcessorMapper = { 'builtin:repeat-loop': processRepeatLoop, 'builtin:while-loop': processWhileLoop, 'builtin:replacement': processReplacementFunction, + 'builtin:list': processList, } as const satisfies Record<`builtin:${string}`, BuiltInIdentifierProcessorWithConfig>; export type BuiltInMappingName = keyof typeof BuiltInProcessorMapper; diff --git a/src/dataflow/environments/default-builtin-config.ts b/src/dataflow/environments/default-builtin-config.ts index 64851e6c9c..87c8a45d23 100644 --- a/src/dataflow/environments/default-builtin-config.ts +++ b/src/dataflow/environments/default-builtin-config.ts @@ -11,7 +11,7 @@ export const DefaultBuiltinConfig: BuiltInDefinitions = [ { type: 'function', names: [ - '~', '+', '-', '*', '/', '^', '!', '?', '**', '==', '!=', '>', '<', '>=', '<=', '%%', '%/%', '%*%', '%in%', ':', 'list', + '~', '+', '-', '*', '/', '^', '!', '?', '**', '==', '!=', '>', '<', '>=', '<=', '%%', '%/%', '%*%', '%in%', ':', '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', 'numeric', 'as.character', 'as.integer', 'as.logical', 'as.numeric', 'as.matrix', @@ -74,6 +74,7 @@ export const DefaultBuiltinConfig: BuiltInDefinitions = [ { type: 'function', names: ['repeat'], processor: 'builtin:repeat-loop', config: {}, assumePrimitive: true }, { type: 'function', names: ['while'], processor: 'builtin:while-loop', config: {}, assumePrimitive: true }, { type: 'function', names: ['do.call'], processor: 'builtin:apply', config: { indexOfFunction: 0, unquoteFunction: true }, assumePrimitive: true }, + { type: 'function', names: ['list'], processor: 'builtin:list', config: {}, assumePrimitive: true }, { type: 'function', names: [ diff --git a/src/dataflow/environments/define.ts b/src/dataflow/environments/define.ts index 70bd539ba4..908f4e0ef0 100644 --- a/src/dataflow/environments/define.ts +++ b/src/dataflow/environments/define.ts @@ -8,11 +8,14 @@ import type { IdentifierDefinition } from './identifier'; function defInEnv(newEnvironments: IEnvironment, name: string, definition: IdentifierDefinition) { const existing = newEnvironments.memory.get(name); // check if it is maybe or not + // console.log('existing:', existing, 'definition:', definition); + // TODO: check if indices are already defined regarding overwrite if(existing === undefined || definition.controlDependencies === undefined) { newEnvironments.memory.set(name, [definition]); } else { existing.push(definition); } + // console.log('after:', newEnvironments.memory.get(name)); } /** @@ -21,6 +24,8 @@ function defInEnv(newEnvironments: IEnvironment, name: string, definition: Ident */ export function define(definition: IdentifierDefinition, superAssign: boolean | undefined, environment: REnvironmentInformation): REnvironmentInformation { const name = definition.name; + // console.log('defining:', name); + // console.log('definition:', definition); guard(name !== undefined, () => `Name must be defined, but isn't for ${JSON.stringify(definition)}`); let newEnvironment; if(superAssign) { diff --git a/src/dataflow/environments/identifier.ts b/src/dataflow/environments/identifier.ts index 20945fe812..11ae1eaff0 100644 --- a/src/dataflow/environments/identifier.ts +++ b/src/dataflow/environments/identifier.ts @@ -1,6 +1,7 @@ import type { BuiltInIdentifierConstant, BuiltInIdentifierDefinition } from './built-in'; import type { NodeId } from '../../r-bridge/lang-4.x/ast/model/processing/node-id'; import type { ControlDependency } from '../info'; +import type { ContainerIndices } from '../graph/vertex'; export type Identifier = string & { __brand?: 'identifier' } @@ -59,10 +60,12 @@ export interface IdentifierReference { } -interface InGraphIdentifierDefinition extends IdentifierReference { +export interface InGraphIdentifierDefinition extends IdentifierReference { readonly type: InGraphReferenceType /** The assignment (or whatever, like `assign` function call) node which ultimately defined this identifier */ readonly definedAt: NodeId + + indices?: ContainerIndices | undefined } /** diff --git a/src/dataflow/environments/resolve-by-name.ts b/src/dataflow/environments/resolve-by-name.ts index a53a3cfe27..81cc9d8b24 100644 --- a/src/dataflow/environments/resolve-by-name.ts +++ b/src/dataflow/environments/resolve-by-name.ts @@ -33,6 +33,7 @@ const TargetTypePredicate = { * @returns A list of possible definitions of the identifier (one if the definition location is exactly and always known), or `undefined` if the identifier is undefined in the current scope/with the current environment information. */ export function resolveByName(name: Identifier, environment: REnvironmentInformation, target: ReferenceType = ReferenceType.Unknown): IdentifierDefinition[] | undefined { + // console.log('resolving:', name); let current: IEnvironment = environment.current; let definitions: IdentifierDefinition[] | undefined = undefined; const wantedType = TargetTypePredicate[target]; diff --git a/src/dataflow/graph/vertex.ts b/src/dataflow/graph/vertex.ts index 396eebd342..7c7ef12879 100644 --- a/src/dataflow/graph/vertex.ts +++ b/src/dataflow/graph/vertex.ts @@ -15,6 +15,12 @@ export enum VertexType { FunctionDefinition = 'function-definition' } +export interface ContainerIndex{ + readonly lexeme: string, + readonly nodeId: NodeId, +} +export type ContainerIndices = ContainerIndex[] | undefined + /** * Arguments required to construct a vertex in the dataflow graph. * @@ -39,6 +45,8 @@ interface DataflowGraphVertexBase extends MergeableRecord { * See {@link IdentifierReference} */ controlDependencies: ControlDependency[] | undefined + + indices?: ContainerIndices } /** diff --git a/src/dataflow/internal/process/functions/call/built-in/built-in-access.ts b/src/dataflow/internal/process/functions/call/built-in/built-in-access.ts index 695689629a..73c4f7f047 100644 --- a/src/dataflow/internal/process/functions/call/built-in/built-in-access.ts +++ b/src/dataflow/internal/process/functions/call/built-in/built-in-access.ts @@ -16,6 +16,9 @@ import type { ForceArguments } from '../common'; import { BuiltIn } from '../../../../../environments/built-in'; import { markAsAssignment } from './built-in-assignment'; import { ReferenceType } from '../../../../../environments/identifier'; +import type { InGraphIdentifierDefinition } from '../../../../../environments/identifier'; +import { resolveByName } from '../../../../../environments/resolve-by-name'; +import type { ContainerIndex, ContainerIndices } from '../../../../../graph/vertex'; interface TableAssignmentProcessorMarker { definitionRootNodes: NodeId[] @@ -32,6 +35,17 @@ function tableAssignmentProcessor( return processKnownFunctionCall({ name, args, rootId, data }).information; } +/** + * Processes different types of access operations. + * + * Example: + * ```r + * a[i] + * a$foo + * a[[i]] + * a@foo + * ``` + */ export function processAccess( name: RSymbol, args: readonly RFunctionArgument[], @@ -43,6 +57,7 @@ export function processAccess( dataflowLogger.warn(`Access ${name.content} has less than 2 arguments, skipping`); return processKnownFunctionCall({ name, args, rootId, data, forceArgs: config.forceArgs }).information; } + console.log('processing access:', name.content); const head = args[0]; guard(head !== EmptyArgument, () => `Access ${name.content} has no source, impossible!`); @@ -93,7 +108,39 @@ export function processAccess( }; } } - fnCall = processKnownFunctionCall({ name, args: newArgs, rootId, data, forceArgs: config.forceArgs }); + // a$foo a@foo + let accessedArgument: ContainerIndex | undefined; + let resolvedFirstParameterIndices: ContainerIndices; + if(newArgs[0] !== EmptyArgument) { + const accessArg = newArgs[1] === EmptyArgument ? 'all' : newArgs[1].lexeme; + const resolvedFirstParameter = resolveByName(newArgs[0].lexeme ?? '', data.environment); + // console.log('requesting to access', accessArg); + // resolvedFirstParameterIndices = resolvedFirstParameter?.flatMap(param => (param as InGraphIdentifierDefinition)?.indices ?? []); + resolvedFirstParameter?.forEach(param => { + const definition = param as InGraphIdentifierDefinition; + if(definition.indices) { + if(resolvedFirstParameterIndices) { + resolvedFirstParameterIndices = resolvedFirstParameterIndices.concat(definition.indices); + } else { + resolvedFirstParameterIndices = definition.indices; + } + // console.log('pushed indices', definition.indices); + } else { + // console.log('no indices found for', definition); + } + }); + console.log('resolved', newArgs[0].lexeme, 'to', resolvedFirstParameterIndices); + accessedArgument = resolvedFirstParameterIndices?.find(index => index.lexeme === accessArg); + } + + const indices = accessedArgument === undefined ? undefined : [accessedArgument]; + fnCall = processKnownFunctionCall({ name, args: newArgs, rootId, data, forceArgs: config.forceArgs }, indices); + if(accessedArgument !== undefined) { + // console.log('Accessing known index'); + fnCall.information.graph.addEdge(name.info.id, accessedArgument.nodeId, EdgeType.Reads); + } else { + // console.log('Accessing unknown index'); + } } const info = fnCall.information; @@ -111,16 +158,16 @@ export function processAccess( return { ...info, /* - * Keep active nodes in case of assignments etc. - * We make them maybe as a kind of hack. - * This way when using - * ```ts - * a[[1]] <- 3 - * a[[2]] <- 4 - * a - * ``` - * the read for a will use both accesses as potential definitions and not just the last one! - */ + * Keep active nodes in case of assignments etc. + * We make them maybe as a kind of hack. + * This way when using + * ```ts + * a[[1]] <- 3 + * a[[2]] <- 4 + * a + * ``` + * the read for a will use both accesses as potential definitions and not just the last one! + */ unknownReferences: makeAllMaybe(info.unknownReferences, info.graph, info.environment, false), entryPoint: rootId, /** it is, to be precise, the accessed element we want to map to maybe */ 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 1d759d516b..6d9f91ca85 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 @@ -13,12 +13,12 @@ import type { 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'; -import type { RFunctionArgument } from '../../../../../../r-bridge/lang-4.x/ast/model/nodes/r-function-call'; +import { EmptyArgument, 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 type { - IdentifierDefinition, IdentifierReference, + InGraphIdentifierDefinition, InGraphReferenceType } from '../../../../../environments/identifier'; import { ReferenceType } from '../../../../../environments/identifier'; @@ -27,6 +27,7 @@ import type { RString } from '../../../../../../r-bridge/lang-4.x/ast/model/node import { removeRQuotes } from '../../../../../../r-bridge/retriever'; import type { RUnnamedArgument } from '../../../../../../r-bridge/lang-4.x/ast/model/nodes/r-argument'; import { VertexType } from '../../../../../graph/vertex'; +import type { ContainerIndices } from '../../../../../graph/vertex'; import { define } from '../../../../../environments/define'; import { EdgeType } from '../../../../../graph/edge'; import type { ForceArguments } from '../common'; @@ -85,6 +86,7 @@ export function processAssignment( data: DataflowProcessorInformation, config: AssignmentConfiguration ): DataflowInformation { + console.log('processing assignment:', name.content, 'with id', name.info.id); if(args.length != 2) { dataflowLogger.warn(`Assignment ${name.content} has something else than 2 arguments, skipping`); return processKnownFunctionCall({ name, args, rootId, data, forceArgs: config.forceArgs }).information; @@ -93,8 +95,18 @@ export function processAssignment( const effectiveArgs = getEffectiveOrder(config, args as [RFunctionArgument, RFunctionArgument]); const { target, source } = extractSourceAndTarget(effectiveArgs, name); const { type, named } = target; + // console.log('target', target); + // console.log('source:', source); + // for(const arg of effectiveArgs) { + // if(arg === EmptyArgument) { + // continue; + // } + // const resolved = resolveByName(arg.lexeme ?? '', data.environment); + // console.log('arg:', arg.lexeme, 'resolved: ', resolved?.map((r) => `{ ${r.name}, [${(r as InGraphIdentifierDefinition).indices?.map((r1 => `{ ${r1.lexeme}, ${r1.nodeId} }`)).join('; ')}]}`).join(',')); + // } if(type === RType.Symbol) { + console.log('Symbol type'); const res = processKnownFunctionCall({ name, args, rootId, data, reverseOrder: !config.swapSourceAndTarget, forceArgs: config.forceArgs }); return processAssignmentToSymbol({ ...config, @@ -107,15 +119,20 @@ export function processAssignment( information: res.information, }); } else if(config.canBeReplacement && type === RType.FunctionCall && named) { + console.log('Function call type'); /* as replacement functions take precedence over the lhs fn-call (i.e., `names(x) <- ...` is independent from the definition of `names`), we do not have to process the call */ dataflowLogger.debug(`Assignment ${name.content} has a function call as target => replacement function ${target.lexeme}`); const replacement = toReplacementSymbol(target, target.functionName.content, config.superAssignment ?? false); return processAsNamedCall(replacement, data, replacement.content, [...target.arguments, source]); } else if(config.canBeReplacement && type === RType.Access) { + console.log('Access type, canbeReplacement'); + const acces = target.access?.map((a) => a === EmptyArgument ? '' : a.lexeme).join(' '); + console.log(`'${acces}' of '${target.accessed.lexeme}' is accessed and replaced by '${source.lexeme}'`); dataflowLogger.debug(`Assignment ${name.content} has an access as target => replacement function ${target.lexeme}`); const replacement = toReplacementSymbol(target, target.operator, config.superAssignment ?? false); return processAsNamedCall(replacement, data, replacement.content, [toUnnamedArgument(target.accessed, data.completeAst.idMap), ...target.access, source]); } else if(type === RType.Access) { + console.log('Access type'); const rootArg = findRootAccess(target); if(rootArg) { const res = processKnownFunctionCall({ @@ -156,7 +173,7 @@ function extractSourceAndTarget(args: readonly RFunctionArgument(rootId: NodeId, target: DataflowInformation, referenceType: InGraphReferenceType, data: DataflowProcessorInformation, makeMaybe: boolean): IdentifierDefinition[] { +function produceWrittenNodes(rootId: NodeId, target: DataflowInformation, referenceType: InGraphReferenceType, data: DataflowProcessorInformation, makeMaybe: boolean): InGraphIdentifierDefinition[] { return [...target.in, ...target.unknownReferences].map(ref => ({ ...ref, type: referenceType, @@ -246,12 +263,22 @@ export function markAsAssignment( environment: REnvironmentInformation, graph: DataflowGraph }, - nodeToDefine: IdentifierDefinition, + nodeToDefine: InGraphIdentifierDefinition, sourceIds: readonly NodeId[], rootIdOfAssignment: NodeId, quoteSource?: boolean, superAssignment?: boolean, ) { + let indices: ContainerIndices; + if(sourceIds.length === 1) { + // support for tracking indices + indices = information.graph.getVertex(sourceIds[0])?.indices; + if(indices) { + console.log(`Defining indices ${indices.map((index) => `{ lexeme: ${index.lexeme}, nodeId: ${index.nodeId} }`).join(',')} for ${nodeToDefine.name}`); + } + } + nodeToDefine.indices ??= indices; + information.environment = define(nodeToDefine, superAssignment, information.environment); information.graph.setDefinitionOfVertex(nodeToDefine); if(!quoteSource) { diff --git a/src/dataflow/internal/process/functions/call/built-in/built-in-list.ts b/src/dataflow/internal/process/functions/call/built-in/built-in-list.ts new file mode 100644 index 0000000000..fcfd4d54a2 --- /dev/null +++ b/src/dataflow/internal/process/functions/call/built-in/built-in-list.ts @@ -0,0 +1,37 @@ +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 { ParentInformation } from '../../../../../../r-bridge/lang-4.x/ast/model/processing/decorate'; +import type { NodeId } from '../../../../../../r-bridge/lang-4.x/ast/model/processing/node-id'; +import type { ContainerIndex } from '../../../../../graph/vertex'; +import type { DataflowInformation } from '../../../../../info'; +import type { DataflowProcessorInformation } from '../../../../../processor'; +import { processKnownFunctionCall } from '../known-call-handling'; + +/** + * Process a list call. + * + * Example: + * ```r + * list(a = 1, b = 2) + * ``` + */ +export function processList( + name: RSymbol, + args: readonly RFunctionArgument[], + rootId: NodeId, + data: DataflowProcessorInformation, +): DataflowInformation { + const namedArguments: ContainerIndex[] = []; + for(const arg of args) { + // Skip non named arguments + if(arg === EmptyArgument || arg.type !== 'RArgument' || arg.name === undefined) { + continue; + } + + namedArguments.push({ lexeme: arg.name.content, nodeId: arg.info.id }); + } + console.log('namedArguments:', namedArguments); + + return processKnownFunctionCall({ name, args, rootId, data }, namedArguments).information; +} diff --git a/src/dataflow/internal/process/functions/call/common.ts b/src/dataflow/internal/process/functions/call/common.ts index fa145f639a..2efa60f90e 100644 --- a/src/dataflow/internal/process/functions/call/common.ts +++ b/src/dataflow/internal/process/functions/call/common.ts @@ -77,7 +77,8 @@ function forceVertexArgumentValueReferences(rootId: NodeId, value: DataflowInfor export function processAllArguments( - { functionName, args, data, finalGraph, functionRootId, forceArgs = [], patchData = d => d }: ProcessAllArgumentInput + { functionName, args, data, finalGraph, functionRootId, forceArgs = [], patchData = d => d }: ProcessAllArgumentInput, + // indices: ContainerIndices = undefined, ): ProcessAllArgumentResult { let finalEnv = functionName.environment; // arg env contains the environments with other args defined diff --git a/src/dataflow/internal/process/functions/call/known-call-handling.ts b/src/dataflow/internal/process/functions/call/known-call-handling.ts index a021c2d633..794cbbef1b 100644 --- a/src/dataflow/internal/process/functions/call/known-call-handling.ts +++ b/src/dataflow/internal/process/functions/call/known-call-handling.ts @@ -15,6 +15,7 @@ import { DataflowGraph } from '../../../../graph/graph'; import { EdgeType } from '../../../../graph/edge'; import { dataflowLogger } from '../../../../logger'; import { VertexType } from '../../../../graph/vertex'; +import type { ContainerIndices } from '../../../../graph/vertex'; import { expensiveTrace } from '../../../../../util/log'; export interface ProcessKnownFunctionCallInput extends ForceArguments { @@ -57,7 +58,7 @@ export function markNonStandardEvaluationEdges( } export function processKnownFunctionCall( - { name,args, rootId,data, reverseOrder = false, markAsNSE = undefined, forceArgs, patchData = d => d, hasUnknownSideEffect }: ProcessKnownFunctionCallInput + { name,args, rootId,data, reverseOrder = false, markAsNSE = undefined, forceArgs, patchData = d => d, hasUnknownSideEffect }: ProcessKnownFunctionCallInput, indices: ContainerIndices = undefined, ): ProcessKnownFunctionCallResult { const functionName = processDataflowFor(name, data); @@ -72,7 +73,7 @@ export function processKnownFunctionCall( callArgs, remainingReadInArgs, processedArguments - } = processAllArguments({ functionName, args: processArgs, data, finalGraph, functionRootId: rootId, patchData, forceArgs }); + } = processAllArguments({ functionName, args: processArgs, data, finalGraph, functionRootId: rootId, patchData, forceArgs }, indices); if(markAsNSE) { markNonStandardEvaluationEdges(markAsNSE, processedArguments, finalGraph, rootId); } @@ -85,7 +86,8 @@ export function processKnownFunctionCall( /* will be overwritten accordingly */ onlyBuiltin: false, controlDependencies: data.controlDependencies, - args: reverseOrder ? [...callArgs].reverse() : callArgs + args: reverseOrder ? [...callArgs].reverse() : callArgs, + indices: indices, }); if(hasUnknownSideEffect) { diff --git a/src/dataflow/internal/process/functions/call/named-call-handling.ts b/src/dataflow/internal/process/functions/call/named-call-handling.ts index ee09eed8f8..3586993901 100644 --- a/src/dataflow/internal/process/functions/call/named-call-handling.ts +++ b/src/dataflow/internal/process/functions/call/named-call-handling.ts @@ -48,6 +48,7 @@ export function processNamedCall( data: DataflowProcessorInformation ): DataflowInformation { const resolved = resolveByName(name.content, data.environment, ReferenceType.Function) ?? []; + console.log('processing named call:', name.content, 'resolved:', resolved); let defaultProcessor = resolved.length === 0; let information: DataflowInformation | undefined = undefined; diff --git a/test/functionality/dataflow/pointer-analysis/dataflow-list-access.test.ts b/test/functionality/dataflow/pointer-analysis/dataflow-list-access.test.ts new file mode 100644 index 0000000000..77ee3622cf --- /dev/null +++ b/test/functionality/dataflow/pointer-analysis/dataflow-list-access.test.ts @@ -0,0 +1,19 @@ +import { describe } from 'vitest'; +import { assertDataflow, withShell } from '../../_helper/shell'; +import { emptyGraph } from '../../../../src/dataflow/graph/dataflowgraph-builder'; +import { label } from '../../_helper/label'; + +describe.sequential('List Access', withShell(shell => { + describe('Access named argument', () => { + assertDataflow(label('Assert reads edge to named argument', ['name-normal', 'function-calls', 'named-arguments', 'dollar-access', 'subsetting']), shell, `person <- list(age = 24, name = "John") +person$name`, + emptyGraph() + .defineVariable('1@person') + .reads('2@person', '1@person') + .reads('2@$', '7'), + { + expectIsSubgraph: true, + resolveIdsAsCriterion: true, + }); + }); +})); diff --git a/test/functionality/slicing/pointer-analysis/list-access.test.ts b/test/functionality/slicing/pointer-analysis/list-access.test.ts new file mode 100644 index 0000000000..533127f416 --- /dev/null +++ b/test/functionality/slicing/pointer-analysis/list-access.test.ts @@ -0,0 +1,146 @@ +import { describe, test } from 'vitest'; +import { assertSliced, withShell } from '../../_helper/shell'; +import { label } from '../../_helper/label'; + +describe.sequential('List access', withShell(shell => { + describe('Simple access slice', () => { + const slice = `person <- list(age = 24, name = "John") +a <- person$name`; + + assertSliced( + label('Slice on list object', []), shell, slice, ['2@person'], + `person <- list(age = 24, name = "John") +person`, + ); + assertSliced( + label('Slice on access operator', []), shell, slice, ['2@$'], + `person <- list(age = 24, name = "John") +person$name`, + ); + assertSliced( + label('Slice on accessed index', []), shell, slice, ['2@name'], + 'person$name', + ); + assertSliced( + label('Slice on result of access', []), shell, slice, ['2@a'], + `person <- list(age = 24, name = "John") +a <- person$name`, + ); + }); + + describe('Access slice with noise', () => { + const slice = `person <- list(age = 24, name = "John") +other_person <- list(age = 24, name = "John") +b <- person$age +a <- person$name`; + + assertSliced( + label('Slice on list object', []), shell, slice, ['4@person'], + `person <- list(age = 24, name = "John") +person`, + ); + assertSliced( + label('Slice on access operator', []), shell, slice, ['4@$'], + `person <- list(age = 24, name = "John") +person$name`, + ); + assertSliced( + label('Slice on accessed index', []), shell, slice, ['4@name'], + 'person$name', + ); + assertSliced( + label('Slice on result of access', []), shell, slice, ['4@a'], + `person <- list(age = 24, name = "John") +a <- person$name`, + ); + }); + + describe('Access slice with noise and multiple accesses', () => { + const slice = `person <- list(age = 24, name = "John") +b <- person$name +a <- person$age +c <- person$name`; + + assertSliced( + label('Slice on list object', []), shell, slice, ['4@person'], + `person <- list(age = 24, name = "John") +person`, + + ); + assertSliced( + label('Slice on access operator', []), shell, slice, ['4@$'], + `person <- list(age = 24, name = "John") +person$name`, + ); + assertSliced( + label('Slice on accessed index', []), shell, slice, ['4@name'], + 'person$name', + ); + assertSliced( + label('Slice on result of access', []), shell, slice, ['4@c'], + `person <- list(age = 24, name = "John") +c <- person$name`, + ); + }); + + describe('Access slice with previous assignment', () => { + const slice = `person <- list(age = 24, name = "John") +person$name <- "Bob" +a <- person$name`; + + assertSliced( + label('Slice on list object', []), shell, slice, ['3@person'], + `person <- list(age = 24, name = "John") +person$name <- "Bob" +person`, + + ); + assertSliced( + label('Slice on access operator', []), shell, slice, ['3@$'], + `person <- list(age = 24, name = "John") +person$name <- "Bob" +person$name`, + ); + assertSliced( + label('Slice on accessed index', []), shell, slice, ['3@name'], + 'person$name', + ); + assertSliced( + label('Slice on result of access', []), shell, slice, ['3@a'], + `person <- list(age = 24, name = "John") +person$name <- "Bob" +a <- person$name`, + ); + }); + + test.fails('Access slice with previous assignment to other index', () => { + const slice = `person <- list(age = 24, name = "John") +person$name <- "Bob" +person$age <- 25 +a <- person$name`; + + assertSliced( + label('Slice on list object', []), shell, slice, ['4@person'], + `person <- list(age = 24, name = "John") +person$name <- "Bob" +person`, + + ); + assertSliced( + label('Slice on access operator', []), shell, slice, ['4@$'], + `person <- list(age = 24, name = "John") +person$name <- "Bob" +person$name`, + ); + assertSliced( + label('Slice on accessed index', []), shell, slice, ['4@name'], + 'person$name', + ); + assertSliced( + label('Slice on result of access', []), shell, slice, ['4@a'], + `person <- list(age = 24, name = "John") +person$name <- "Bob" +a <- person$name`, + ); + }); +}));