Skip to content

Commit

Permalink
New alias analysis API (#11621)
Browse files Browse the repository at this point in the history
Higher level API will be compatible with a future implementation that doesn't depend on `RawAst` (see: #10753).
  • Loading branch information
kazcw authored Nov 21, 2024
1 parent 92bf61c commit d611939
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 208 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { performCollapseImpl, prepareCollapsedInfo } from '@/components/GraphEditor/collapsing'
import { GraphDb, type NodeId } from '@/stores/graph/graphDatabase'
import { assert } from '@/util/assert'
import { Ast, RawAst } from '@/util/ast'
import { Ast } from '@/util/ast'
import { findExpressions } from '@/util/ast/__tests__/testCase'
import { unwrap } from '@/util/data/result'
import { tryIdentifier } from '@/util/qualifiedName'
import * as iter from 'enso-common/src/utilities/data/iter'
import { expect, test } from 'vitest'
import { watchEffect } from 'vue'
import { Identifier } from 'ydoc-shared/ast'
Expand All @@ -15,15 +16,12 @@ import { nodeIdFromOuterAst } from '../../../stores/graph/graphDatabase'
// ===============================

function setupGraphDb(code: string, graphDb: GraphDb) {
const { root, toRaw, getSpan } = Ast.parseUpdatingIdMap(code)
const expressions = Array.from(root.statements())
const func = expressions[0]
const { root, getSpan } = Ast.parseUpdatingIdMap(code)
const func = iter.first(root.statements())
assert(func instanceof Ast.FunctionDef)
const rawFunc = toRaw.get(func.id)
assert(rawFunc?.type === RawAst.Tree.Type.Function)
graphDb.updateExternalIds(root)
graphDb.updateNodes(func, { watchEffect })
graphDb.updateBindings(func, rawFunc, code, getSpan)
graphDb.updateBindings(func, { text: code, getSpan })
}

interface TestCase {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { asNodeId, GraphDb } from '@/stores/graph/graphDatabase'
import { Ast, RawAst } from '@/util/ast'
import { Ast } from '@/util/ast'
import assert from 'assert'
import { expect, test } from 'vitest'
import { watchEffect } from 'vue'
Expand All @@ -23,14 +23,14 @@ export function parseWithSpans<T extends Record<string, SourceRange>>(code: stri
idMap.insertKnownId(span, eid)
}

const { root: ast, toRaw, getSpan } = Ast.parseUpdatingIdMap(code, idMap)
const { root: ast, getSpan } = Ast.parseUpdatingIdMap(code, idMap)
const idFromExternal = new Map<ExternalId, AstId>()
ast.visitRecursive((ast) => {
idFromExternal.set(ast.externalId, ast.id)
})
const id = (name: keyof T) => idFromExternal.get(eid(name))!

return { ast, id, eid, toRaw, getSpan }
return { ast, id, eid, getSpan }
}

test('Reading graph from definition', () => {
Expand All @@ -53,17 +53,15 @@ test('Reading graph from definition', () => {
node3Content: [65, 74] as [number, number],
}

const { ast, id, eid, toRaw, getSpan } = parseWithSpans(code, spans)
const { ast, id, eid, getSpan } = parseWithSpans(code, spans)

const db = GraphDb.Mock()
const expressions = Array.from(ast.statements())
const func = expressions[0]
assert(func instanceof Ast.FunctionDef)
const rawFunc = toRaw.get(func.id)
assert(rawFunc?.type === RawAst.Tree.Type.Function)
db.updateExternalIds(ast)
db.updateNodes(func, { watchEffect })
db.updateBindings(func, rawFunc, code, getSpan)
db.updateBindings(func, { text: code, getSpan })

expect(Array.from(db.nodeIdToNode.keys())).toEqual([
eid('parameter'),
Expand Down
154 changes: 30 additions & 124 deletions app/gui/src/project-view/stores/graph/graphDatabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ import { computeNodeColor } from '@/composables/nodeColors'
import { ComputedValueRegistry, type ExpressionInfo } from '@/stores/project/computedValueRegistry'
import { SuggestionDb, type Group } from '@/stores/suggestionDatabase'
import type { SuggestionEntry } from '@/stores/suggestionDatabase/entry'
import { assert } from '@/util/assert'
import { Ast, RawAst } from '@/util/ast'
import { Ast } from '@/util/ast'
import type { AstId, NodeMetadata } from '@/util/ast/abstract'
import { MutableModule } from '@/util/ast/abstract'
import { AliasAnalyzer } from '@/util/ast/aliasAnalysis'
import { analyzeBindings, type BindingInfo } from '@/util/ast/bindings'
import { inputNodeFromAst, nodeFromAst, nodeRootExpr } from '@/util/ast/node'
import { MappedKeyMap, MappedSet } from '@/util/containers'
import { tryGetIndex } from '@/util/data/array'
import { recordEqual } from '@/util/data/object'
import { unwrap } from '@/util/data/result'
Expand All @@ -24,129 +22,29 @@ import {
import * as objects from 'enso-common/src/utilities/data/object'
import * as set from 'lib0/set'
import { reactive, ref, shallowReactive, type Ref, type WatchStopHandle } from 'vue'
import { type SourceDocument } from 'ydoc-shared/ast/sourceDocument'
import type { MethodCall, StackItem } from 'ydoc-shared/languageServerTypes'
import type { Opt } from 'ydoc-shared/util/data/opt'
import type { ExternalId, SourceRange, VisualizationMetadata } from 'ydoc-shared/yjsModel'
import { isUuid, sourceRangeKey, visMetadataEquals } from 'ydoc-shared/yjsModel'
import type { ExternalId, VisualizationMetadata } from 'ydoc-shared/yjsModel'
import { isUuid, visMetadataEquals } from 'ydoc-shared/yjsModel'

export interface MethodCallInfo {
methodCall: MethodCall
methodCallSource: Ast.AstId
suggestion: SuggestionEntry
}

export interface BindingInfo {
identifier: string
usages: Set<AstId>
}

/** TODO: Add docs */
export class BindingsDb {
bindings = new ReactiveDb<AstId, BindingInfo>()
identifierToBindingId = new ReactiveIndex(this.bindings, (id, info) => [[info.identifier, id]])

/** TODO: Add docs */
readFunctionAst(
func: Ast.FunctionDef,
rawFunc: RawAst.Tree.Function | undefined,
moduleCode: string,
getSpan: (id: AstId) => SourceRange | undefined,
) {
// TODO[ao]: Rename 'alias' to 'binding' in AliasAnalyzer and it's more accurate term.
const analyzer = new AliasAnalyzer(moduleCode, rawFunc)
analyzer.process()

const [bindingRangeToTree, bindingIdToRange] = BindingsDb.rangeMappings(func, analyzer, getSpan)

// Remove old keys.
for (const key of this.bindings.keys()) {
const range = bindingIdToRange.get(key)
if (range == null || !analyzer.aliases.has(range)) {
this.bindings.delete(key)
}
}

// Add or update bindings.
for (const [bindingRange, usagesRanges] of analyzer.aliases) {
const aliasAst = bindingRangeToTree.get(bindingRange)
if (aliasAst == null) {
console.warn(`Binding not found`, bindingRange)
continue
}
const aliasAstId = aliasAst.id
const info = this.bindings.get(aliasAstId)
if (info == null) {
function* usageIds() {
for (const usageRange of usagesRanges) {
const usageAst = bindingRangeToTree.get(usageRange)
assert(usageAst != null)
if (usageAst != null) yield usageAst.id
}
}
this.bindings.set(aliasAstId, {
identifier: aliasAst.code(),
usages: new Set(usageIds()),
})
} else {
const newIdentifier = aliasAst.code()
if (info.identifier != newIdentifier) info.identifier = newIdentifier
// Remove old usages.
for (const usage of info.usages) {
const range = bindingIdToRange.get(usage)
if (range == null || !usagesRanges.has(range)) info.usages.delete(usage)
}
// Add or update usages.
for (const usageRange of usagesRanges) {
const usageAst = bindingRangeToTree.get(usageRange)
if (usageAst != null && !info.usages.has(usageAst.id)) {
info.usages.add(usageAst.id)
}
}
}
}
}

/**
* Create mappings between bindings' ranges and AST
*
* The AliasAnalyzer is general and returns ranges, but we're interested in AST nodes. This
* method creates mappings in both ways. For given range, only the shallowest AST node will be
* assigned (RawAst.Tree.Identifier, not RawAst.Token.Identifier).
*/
private static rangeMappings(
ast: Ast.Ast,
analyzer: AliasAnalyzer,
getSpan: (id: AstId) => SourceRange | undefined,
): [MappedKeyMap<SourceRange, Ast.Ast>, Map<AstId, SourceRange>] {
const bindingRangeToTree = new MappedKeyMap<SourceRange, Ast.Ast>(sourceRangeKey)
const bindingIdToRange = new Map<AstId, SourceRange>()
const bindingRanges = new MappedSet(sourceRangeKey)
for (const [binding, usages] of analyzer.aliases) {
bindingRanges.add(binding)
for (const usage of usages) bindingRanges.add(usage)
}
ast.visitRecursive((ast) => {
const span = getSpan(ast.id)
assert(span != null)
if (bindingRanges.has(span)) {
bindingRangeToTree.set(span, ast)
bindingIdToRange.set(ast.id, span)
return false
}
return true
})
return [bindingRangeToTree, bindingIdToRange]
}
}

/** TODO: Add docs */
export class GraphDb {
nodeIdToNode = new ReactiveDb<NodeId, Node>()
private readonly nodeSources = new Map<NodeId, { data: NodeSource; stop: WatchStopHandle }>()
private highestZIndex = 0
private readonly idToExternalMap = reactive(new Map<Ast.AstId, ExternalId>())
private readonly idFromExternalMap = reactive(new Map<ExternalId, Ast.AstId>())
private bindings = new BindingsDb()
private readonly bindings = new ReactiveDb<AstId, BindingInfo>()
private readonly identifierToBindingId = new ReactiveIndex(this.bindings, (id, info) => [
[info.identifier, id],
])

/** TODO: Add docs */
constructor(
Expand All @@ -167,7 +65,7 @@ export class GraphDb {
return Array.from(exprs, (expr) => [id, expr])
})

connections = new ReactiveIndex(this.bindings.bindings, (alias, info) => {
connections = new ReactiveIndex(this.bindings, (alias, info) => {
const srcNode = this.getPatternExpressionNodeId(alias) ?? this.getExpressionNodeId(alias)
if (srcNode == null) return []
return Array.from(this.connectionsFromBindings(info, alias, srcNode))
Expand Down Expand Up @@ -200,7 +98,7 @@ export class GraphDb {
if (entry.pattern == null) return []
const ports = new Set<AstId>()
entry.pattern.visitRecursive((ast) => {
if (this.bindings.bindings.has(ast.id)) {
if (this.bindings.has(ast.id)) {
ports.add(ast.id)
return false
}
Expand Down Expand Up @@ -257,7 +155,7 @@ export class GraphDb {

/** TODO: Add docs */
getIdentDefiningNode(ident: string): NodeId | undefined {
const binding = set.first(this.bindings.identifierToBindingId.lookup(ident))
const binding = set.first(this.identifierToBindingId.lookup(ident))
return this.getPatternExpressionNodeId(binding)
}

Expand All @@ -269,12 +167,12 @@ export class GraphDb {

/** TODO: Add docs */
getOutputPortIdentifier(source: AstId | undefined): string | undefined {
return source ? this.bindings.bindings.get(source)?.identifier : undefined
return source ? this.bindings.get(source)?.identifier : undefined
}

/** TODO: Add docs */
identifierUsed(ident: string): boolean {
return this.bindings.identifierToBindingId.hasKey(ident)
return this.identifierToBindingId.hasKey(ident)
}

/** TODO: Add docs */
Expand Down Expand Up @@ -467,13 +365,21 @@ export class GraphDb {
}

/** Deeply scan the function to perform alias-analysis. */
updateBindings(
functionAst_: Ast.FunctionDef,
rawFunction: RawAst.Tree.Function | undefined,
moduleCode: string,
getSpan: (id: AstId) => SourceRange | undefined,
) {
this.bindings.readFunctionAst(functionAst_, rawFunction, moduleCode, getSpan)
updateBindings(func: Ast.FunctionDef, moduleSource: Pick<SourceDocument, 'text' | 'getSpan'>) {
const newBindings = analyzeBindings(func, moduleSource)
for (const id of this.bindings.keys()) {
if (!newBindings.has(id)) this.bindings.delete(id)
}
for (const [id, newInfo] of newBindings) {
const oldInfo = this.bindings.getUntracked(id)
if (oldInfo == null) {
this.bindings.set(id, newInfo)
} else {
const info = resumeReactivity(oldInfo)
if (oldInfo.identifier !== newInfo.identifier) info.identifier = newInfo.identifier
syncSetDiff(info.usages, oldInfo.usages, newInfo.usages)
}
}
}

/** TODO: Add docs */
Expand Down Expand Up @@ -567,7 +473,7 @@ export class GraphDb {
}
const bindingId = pattern.id
this.nodeIdToNode.set(id, node)
this.bindings.bindings.set(bindingId, { identifier: binding, usages: new Set() })
this.bindings.set(bindingId, { identifier: binding, usages: new Set() })
return node
}
}
Expand Down
31 changes: 5 additions & 26 deletions app/gui/src/project-view/stores/graph/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { assert, bail } from '@/util/assert'
import { Ast } from '@/util/ast'
import type { AstId, Identifier, MutableModule } from '@/util/ast/abstract'
import { isAstId, isIdentifier } from '@/util/ast/abstract'
import { RawAst, visitRecursive } from '@/util/ast/raw'
import { reactiveModule } from '@/util/ast/reactive'
import { partition } from '@/util/data/array'
import { stringUnionToArray, type Events } from '@/util/data/observable'
Expand Down Expand Up @@ -52,13 +51,8 @@ import type {
MethodPointer,
} from 'ydoc-shared/languageServerTypes'
import { reachable } from 'ydoc-shared/util/data/graph'
import type {
LocalUserActionOrigin,
Origin,
SourceRangeKey,
VisualizationMetadata,
} from 'ydoc-shared/yjsModel'
import { defaultLocalOrigin, sourceRangeKey, visMetadataEquals } from 'ydoc-shared/yjsModel'
import type { LocalUserActionOrigin, Origin, VisualizationMetadata } from 'ydoc-shared/yjsModel'
import { defaultLocalOrigin, visMetadataEquals } from 'ydoc-shared/yjsModel'
import { UndoManager } from 'yjs'

const FALLBACK_BINDING_PREFIX = 'node'
Expand Down Expand Up @@ -114,7 +108,7 @@ export const { injectFn: useGraphStore, provideFn: provideGraphStore } = createC
const portInstances = shallowReactive(new Map<PortId, Set<PortViewInstance>>())
const editedNodeInfo = ref<NodeEditInfo>()

const moduleSource = reactive(SourceDocument.Empty())
const moduleSource = SourceDocument.Empty(reactive)
const moduleRoot = ref<Ast.BodyBlock>()
const syncModule = computed(() => moduleRoot.value?.module as Ast.MutableModule | undefined)

Expand Down Expand Up @@ -169,23 +163,8 @@ export const { injectFn: useGraphStore, provideFn: provideGraphStore } = createC
})

watchEffect(() => {
if (!methodAst.value.ok || !moduleSource.text) return
const method = methodAst.value.value
const toRaw = new Map<SourceRangeKey, RawAst.Tree.Function>()
visitRecursive(Ast.rawParseModule(moduleSource.text), (node) => {
if (node.type === RawAst.Tree.Type.Function) {
const start = node.whitespaceStartInCodeParsed + node.whitespaceLengthInCodeParsed
const end = start + node.childrenLengthInCodeParsed
toRaw.set(sourceRangeKey([start, end]), node)
return false
}
return true
})
const methodSpan = moduleSource.getSpan(method.id)
assert(methodSpan != null)
const rawFunc = toRaw.get(sourceRangeKey(methodSpan))
const getSpan = (id: AstId) => moduleSource.getSpan(id)
db.updateBindings(method, rawFunc, moduleSource.text, getSpan)
if (methodAst.value.ok && moduleSource.text)
db.updateBindings(methodAst.value.value, moduleSource)
})

function getExecutedMethodAst(module?: Ast.Module): Result<Ast.FunctionDef> {
Expand Down
8 changes: 4 additions & 4 deletions app/gui/src/project-view/util/ast/abstract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,15 +342,15 @@ export function parseUpdatingIdMap(
) {
const rawRoot = rawParseModule(code)
const module = inModule ?? MutableModule.Transient()
const { root, spans, toRaw } = module.transact(() => {
const { root, spans, toRaw } = abstract(module, rawRoot, code)
const { root, spans } = module.transact(() => {
const { root, spans } = abstract(module, rawRoot, code)
root.module.setRoot(root)
if (idMap) setExternalIds(root.module, spans, idMap)
return { root, spans, toRaw }
return { root, spans }
})
const getSpan = spanMapToSpanGetter(spans)
const idMapOut = spanMapToIdMap(spans)
return { root, idMap: idMapOut, getSpan, toRaw }
return { root, idMap: idMapOut, getSpan }
}

declare const tokenKey: unique symbol
Expand Down
Loading

0 comments on commit d611939

Please sign in to comment.