Skip to content

Commit 7e34507

Browse files
authored
Implement special handling of subapplications when collecting MethodCallInfo (#9677)
Fixes on of the issues in #9354 Stale method call info for inner sub-application was causing additional argument placeholders on the node for certain expressions. Now it is fixed: 1. We only create function widget for the most top-level expression in the prefix application chain. 2. We reuse method call info from inner expressions, assuming it will be always correct for our purposes. https://github.com/enso-org/enso/assets/6566674/91d2b4ba-a789-4c7b-b40c-f09ac45da7f0
1 parent ca9e150 commit 7e34507

File tree

5 files changed

+233
-21
lines changed

5 files changed

+233
-21
lines changed

app/gui2/src/components/GraphEditor/widgets/WidgetFunction.vue

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ import { useProjectStore, type NodeVisualizationConfiguration } from '@/stores/p
1717
import { entryQn } from '@/stores/suggestionDatabase/entry'
1818
import { assert, assertUnreachable } from '@/util/assert'
1919
import { Ast } from '@/util/ast'
20+
import type { AstId } from '@/util/ast/abstract'
2021
import {
2122
ArgumentApplication,
2223
ArgumentApplicationKey,
2324
ArgumentAst,
2425
ArgumentPlaceholder,
2526
getAccessOprSubject,
27+
getMethodCallInfoRecursively,
2628
interpretCall,
2729
} from '@/util/callTree'
2830
import { partitionPoint } from '@/util/data/array'
@@ -37,12 +39,20 @@ const project = useProjectStore()
3739
3840
provideFunctionInfo(
3941
proxyRefs({
40-
callId: computed(() => props.input.value.id),
42+
prefixCalls: computed(() => {
43+
const ids = new Set<AstId>([props.input.value.id])
44+
let ast: any = props.input.value
45+
while (ast instanceof Ast.App) {
46+
ids.add(ast.function.id)
47+
ast = ast.function
48+
}
49+
return ids
50+
}),
4151
}),
4252
)
4353
4454
const methodCallInfo = computed(() => {
45-
return graph.db.getMethodCallInfo(props.input.value.id)
55+
return getMethodCallInfoRecursively(props.input.value, graph.db)
4656
})
4757
4858
const interpreted = computed(() => {
@@ -306,18 +316,12 @@ export const widgetDefinition = defineWidget(
306316
// application with no arguments applied yet, but the application target is also an infix call.
307317
// In that case, the reentrant call method info must be ignored to not create an infinite loop,
308318
// and to resolve the infix call as its own application.
309-
if (prevFunctionState?.callId === ast.id) return Score.Mismatch
319+
// We only render the function widget on the application chain’s top-level.
320+
if (prevFunctionState?.prefixCalls.has(ast.id)) return Score.Mismatch
310321
311322
if (ast instanceof Ast.App || ast instanceof Ast.OprApp) return Score.Perfect
312323
313-
const info = db.getMethodCallInfo(ast.id)
314-
if (
315-
prevFunctionState != null &&
316-
info?.partiallyApplied === true &&
317-
ast instanceof Ast.Ident
318-
) {
319-
return Score.Mismatch
320-
}
324+
const info = getMethodCallInfoRecursively(ast, db)
321325
return info != null ? Score.Perfect : Score.Mismatch
322326
},
323327
},

app/gui2/src/providers/functionInfo.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import type { AstId } from '@/util/ast/abstract.ts'
33
import { identity } from '@vueuse/core'
44

55
interface FunctionInfo {
6-
callId: AstId | undefined
6+
/** Ids of all nested prefix applications inside top-level expression (including the top-level). */
7+
prefixCalls: Set<AstId>
78
}
89

910
export { injectFn as injectFunctionInfo, provideFn as provideFunctionInfo }

app/gui2/src/stores/graph/graphDatabase.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ import type { ExternalId, SourceRange, VisualizationMetadata } from 'shared/yjsM
2020
import { isUuid, sourceRangeKey, visMetadataEquals } from 'shared/yjsModel'
2121
import { reactive, ref, type Ref } from 'vue'
2222

23+
export interface MethodCallInfo {
24+
methodCall: MethodCall
25+
suggestion: SuggestionEntry
26+
}
27+
2328
export interface BindingInfo {
2429
identifier: string
2530
usages: Set<AstId>
@@ -270,11 +275,7 @@ export class GraphDb {
270275
)
271276
}
272277

273-
getMethodCallInfo(
274-
id: AstId,
275-
):
276-
| { methodCall: MethodCall; suggestion: SuggestionEntry; partiallyApplied: boolean }
277-
| undefined {
278+
getMethodCallInfo(id: AstId): MethodCallInfo | undefined {
278279
const info = this.getExpressionInfo(id)
279280
if (info == null) return
280281
const payloadFuncSchema =
@@ -285,8 +286,7 @@ export class GraphDb {
285286
if (suggestionId == null) return
286287
const suggestion = this.suggestionDb.get(suggestionId)
287288
if (suggestion == null) return
288-
const partiallyApplied = mathodCallEquals(methodCall, payloadFuncSchema)
289-
return { methodCall, suggestion, partiallyApplied }
289+
return { methodCall, suggestion }
290290
}
291291

292292
getNodeColorStyle(id: NodeId): string {
@@ -524,7 +524,7 @@ const baseMockNode = {
524524
conditionalPorts: new Set(),
525525
} satisfies Partial<Node>
526526

527-
function mathodCallEquals(a: MethodCall | undefined, b: MethodCall | undefined): boolean {
527+
export function mathodCallEquals(a: MethodCall | undefined, b: MethodCall | undefined): boolean {
528528
return (
529529
a === b ||
530530
(a != null &&

app/gui2/src/util/ast/__tests__/callTree.test.ts

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,28 @@
11
import * as widgetCfg from '@/providers/widgetRegistry/configuration'
2-
import { makeArgument, makeMethod, makeModuleMethod } from '@/stores/suggestionDatabase/entry'
2+
import { GraphDb } from '@/stores/graph/graphDatabase'
3+
import { ComputedValueRegistry, type ExpressionInfo } from '@/stores/project/computedValueRegistry'
4+
import { SuggestionDb } from '@/stores/suggestionDatabase'
5+
import {
6+
makeArgument,
7+
makeConstructor,
8+
makeMethod,
9+
makeModule,
10+
makeModuleMethod,
11+
makeType,
12+
type SuggestionEntry,
13+
} from '@/stores/suggestionDatabase/entry'
314
import { Ast } from '@/util/ast'
415
import {
516
ArgumentApplication,
617
ArgumentAst,
718
ArgumentPlaceholder,
19+
getMethodCallInfoRecursively,
820
interpretCall,
921
} from '@/util/callTree'
1022
import { initializeFFI } from 'shared/ast/ffi'
23+
import type { ExpressionUpdatePayload, MethodCall } from 'shared/languageServerTypes'
1124
import { assert, expect, test } from 'vitest'
25+
import type { AstId } from '../abstract'
1226

1327
await initializeFFI()
1428

@@ -107,6 +121,150 @@ test.each`
107121
},
108122
)
109123

124+
interface TestCase {
125+
description: string
126+
code: string
127+
/** Index of sub-application for which the MethodCallInfo is available. */
128+
subapplicationIndex: number
129+
/** Not applied arguments in available MethodCallInfo. */
130+
notAppliedArguments: number[]
131+
/** Not applied arguments expected for the whole expression. */
132+
expectedNotAppliedArguments: number[]
133+
}
134+
135+
test.each([
136+
{
137+
description: 'Base case',
138+
code: 'Aggregate_Column.Sum',
139+
subapplicationIndex: 0,
140+
notAppliedArguments: [0, 1],
141+
expectedNotAppliedArguments: [0, 1],
142+
},
143+
{
144+
description: '1 arg, info on most inner subapplication.',
145+
code: 'Aggregate_Column.Sum x',
146+
subapplicationIndex: 1,
147+
notAppliedArguments: [0, 1],
148+
expectedNotAppliedArguments: [1],
149+
},
150+
{
151+
description: '2 args, info on most inner subapplication.',
152+
code: 'Aggregate_Column.Sum x y',
153+
subapplicationIndex: 2,
154+
notAppliedArguments: [0, 1],
155+
expectedNotAppliedArguments: [],
156+
},
157+
{
158+
description: '2 args, info on inner subapplication.',
159+
code: 'Aggregate_Column.Sum x y',
160+
subapplicationIndex: 1,
161+
notAppliedArguments: [1],
162+
expectedNotAppliedArguments: [],
163+
},
164+
{
165+
description: '2 args, notAppliedArguments are incorrectly empty.',
166+
code: 'Aggregate_Column.Sum x y',
167+
subapplicationIndex: 2,
168+
notAppliedArguments: [],
169+
expectedNotAppliedArguments: [],
170+
},
171+
{
172+
description: '1 arg, notAppliedArguments unsorted.',
173+
code: 'Aggregate_Column.Sum x',
174+
subapplicationIndex: 1,
175+
notAppliedArguments: [1, 0],
176+
expectedNotAppliedArguments: [1],
177+
},
178+
{
179+
description: '1 named arg.',
180+
code: 'Aggregate_Column.Sum as=x',
181+
subapplicationIndex: 1,
182+
notAppliedArguments: [0, 1],
183+
expectedNotAppliedArguments: [0],
184+
},
185+
{
186+
description: '2 named args.',
187+
code: 'Aggregate_Column.Sum as=x column=y',
188+
subapplicationIndex: 2,
189+
notAppliedArguments: [0, 1],
190+
expectedNotAppliedArguments: [],
191+
},
192+
{
193+
description: '1 wrongly named arg.',
194+
code: 'Aggregate_Column.Sum bla=x',
195+
subapplicationIndex: 1,
196+
notAppliedArguments: [0, 1],
197+
expectedNotAppliedArguments: [0, 1],
198+
},
199+
{
200+
description: '1 named & 1 unnamed args.',
201+
code: 'Aggregate_Column.Sum as=x y',
202+
subapplicationIndex: 2,
203+
notAppliedArguments: [0, 1],
204+
expectedNotAppliedArguments: [],
205+
},
206+
] as TestCase[])(
207+
'Getting MethodCallInfo for sub-applications: $description',
208+
({ code, subapplicationIndex, notAppliedArguments, expectedNotAppliedArguments }: TestCase) => {
209+
const { db, expectedMethodCall, expectedSuggestion, setExpressionInfo } =
210+
prepareMocksForGetMethodCallTest()
211+
const ast = Ast.parse(code)
212+
const subApplication = nthSubapplication(ast, subapplicationIndex)
213+
assert(subApplication)
214+
db.updateExternalIds(ast)
215+
setExpressionInfo(subApplication.id, {
216+
typename: undefined,
217+
methodCall: { ...expectedMethodCall, notAppliedArguments },
218+
payload: { type: 'Pending' } as ExpressionUpdatePayload,
219+
profilingInfo: [],
220+
})
221+
222+
const info = getMethodCallInfoRecursively(ast, db)
223+
expect(info?.methodCall).toEqual({
224+
...expectedMethodCall,
225+
notAppliedArguments: expectedNotAppliedArguments,
226+
})
227+
expect(info?.suggestion).toEqual(expectedSuggestion)
228+
},
229+
)
230+
231+
function prepareMocksForGetMethodCallTest(): {
232+
db: GraphDb
233+
expectedMethodCall: MethodCall
234+
expectedSuggestion: SuggestionEntry
235+
setExpressionInfo: (id: AstId, info: ExpressionInfo) => void
236+
} {
237+
const suggestionDb = new SuggestionDb()
238+
suggestionDb.set(1, makeModule('Standard.Table.Aggregate_Column'))
239+
suggestionDb.set(2, makeType('Standard.Table.Aggregate_Column.Aggregate_Column'))
240+
const con = makeConstructor('Standard.Table.Aggregate_Column.Aggregate_Column.Sum')
241+
con.arguments = [makeArgument('column', 'Any'), makeArgument('as', 'Any')]
242+
suggestionDb.set(3, con)
243+
const expectedSuggestion = suggestionDb.get(3)!
244+
const registry = ComputedValueRegistry.Mock()
245+
const db = GraphDb.Mock(registry, suggestionDb)
246+
const expectedMethodCall = {
247+
methodPointer: {
248+
module: 'Standard.Table.Aggregate_Column',
249+
definedOnType: 'Standard.Table.Aggregate_Column.Aggregate_Column',
250+
name: 'Sum',
251+
},
252+
notAppliedArguments: [0, 1],
253+
}
254+
const setExpressionInfo = (id: AstId, info: ExpressionInfo) =>
255+
registry.db.set(db.idToExternal(id)!, info)
256+
return { db, expectedMethodCall, expectedSuggestion, setExpressionInfo }
257+
}
258+
259+
/** Nth sub-application of the Ast.App call chain. 0th is a `root` itself. */
260+
function nthSubapplication(root: Ast.Ast, n: number): Ast.Ast | undefined {
261+
let current: Ast.Ast | undefined = root
262+
for (let i = 0; i < n; i++) {
263+
current = current instanceof Ast.App ? current.function : undefined
264+
}
265+
return current
266+
}
267+
110268
function printArgPattern(application: ArgumentApplication | Ast.Ast) {
111269
const parts: string[] = []
112270
let current: ArgumentApplication['target'] = application

app/gui2/src/util/callTree.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { WidgetInput } from '@/providers/widgetRegistry'
33
import type { WidgetConfiguration } from '@/providers/widgetRegistry/configuration'
44
import * as widgetCfg from '@/providers/widgetRegistry/configuration'
55
import { DisplayMode } from '@/providers/widgetRegistry/configuration'
6+
import type { GraphDb, MethodCallInfo } from '@/stores/graph/graphDatabase'
67
import type { SuggestionEntry, SuggestionEntryArgument } from '@/stores/suggestionDatabase/entry'
78
import { Ast } from '@/util/ast'
89
import { findLastIndex, tryGetIndex } from '@/util/data/array'
@@ -409,6 +410,54 @@ export function getAccessOprSubject(app: Ast.Ast): Ast.Ast | undefined {
409410
if (app instanceof Ast.PropertyAccess) return app.lhs
410411
}
411412

413+
/** Same as {@link GraphDb.getMethodCallInfo} but with a special handling for nested Applications.
414+
* Sometimes we receive MethodCallInfo for inner sub-applications of the expression,
415+
* and we want to reuse it for outer application expressions when adding new arguments to the call.
416+
* It requires adjusting `notAppliedArguments` array, but otherwise is a straightforward recursive call.
417+
* We stop recursion at any not-application AST. We expect that a subexpression’s info is only valid if it is a part of the prefix application chain.
418+
* We also don’t consider infix applications here, as using them inside a prefix chain would require additional syntax (like parenthesis). */
419+
export function getMethodCallInfoRecursively(
420+
ast: Ast.Ast,
421+
db: GraphDb,
422+
): MethodCallInfo | undefined {
423+
let appliedArgs = 0
424+
const appliedNamedArgs: string[] = []
425+
for (;;) {
426+
const info = db.getMethodCallInfo(ast.id)
427+
if (info) {
428+
// There is an info available! Stop the recursion and adjust `notAppliedArguments`.
429+
// Indices of all named arguments applied so far.
430+
const appliedNamed =
431+
appliedNamedArgs.length > 0 ?
432+
info.suggestion.arguments
433+
.map((arg, index) => (appliedNamedArgs.includes(arg.name) ? index : -1))
434+
.filter((i) => i !== -1)
435+
: []
436+
const withoutNamed = info.methodCall.notAppliedArguments.filter(
437+
(idx) => !appliedNamed.includes(idx),
438+
)
439+
return {
440+
methodCall: {
441+
...info.methodCall,
442+
notAppliedArguments: withoutNamed.sort().slice(appliedArgs),
443+
},
444+
suggestion: info.suggestion,
445+
}
446+
}
447+
// No info, continue recursion to the next sub-application AST.
448+
if (ast instanceof Ast.App) {
449+
if (ast.argumentName) {
450+
appliedNamedArgs.push(ast.argumentName.code())
451+
} else {
452+
appliedArgs += 1
453+
}
454+
ast = ast.function
455+
} else {
456+
break
457+
}
458+
}
459+
}
460+
412461
export const ArgumentApplicationKey: unique symbol = Symbol('ArgumentApplicationKey')
413462
export const ArgumentInfoKey: unique symbol = Symbol('ArgumentInfoKey')
414463
declare module '@/providers/widgetRegistry' {

0 commit comments

Comments
 (0)