x/tools/go/analysis: AllObjectFacts 在作为独立二进制文件运行分析器与通过 unitchecker 运行之间存在不一致性,

brc7rcf0  于 6个月前  发布在  Go
关注(0)|答案(7)|浏览(132)

对象事实
分析器当前可用的对象事实取决于如何运行分析器。当{单,多}检查器作为独立的二进制文件运行时,它可以看到来自AllObjectFacts与当它以增量方式(单元检查器)运行,例如go vetgo vet -vetool=...和nogo bazel规则时的不同对象事实。
关于事实的高级文档。
对象事实可以在分析过程中附加到任何types.Object,对于该*types.Package内的任何Pass。这包括函数内的变量、无法从*types.Package访问的函数(func "_"和"init" funcs)、仅限于函数的类型等。相关文档:

// ExportObjectFact associates a fact of type *T with the obj,
	// replacing any previous fact of that type.
	//
	// ExportObjectFact panics if it is called after the pass is
	// complete, or if obj does not belong to the package being analyzed.
	// ExportObjectFact is not concurrency-safe.
	ExportObjectFact func(obj types.Object, fact Fact)

这允许Pass对放置在同一个包内的对象事实以及可以从当前包访问的不同包中的类型.Objects进行推理。
AllObjectFacts返回当前Analyzer的所有对象事实,包括来自传递依赖项的对象事实。
{单,多}checker是旨在易于使用的 Package 器,支持作为独立二进制文件运行(由库go/analysis/internal/checker支持)和增量运行(由go/analysis/unitchecker支持)。unitchecker通过为每个包序列化Facts来支持增量分析。Facts序列化实现在objectpath之上。objectpath不支持无法从*types.Package访问的类型.Objects。在go/analysis/internal中还有过滤事实的地方:facts.gochecker.go。这两个过滤方法略有不一致。
上述内容的总结如下:

  1. 如果分析器使用AllObjectFacts,我们可以根据分析器是增量还是独立执行来获得不同的事实、结果、诊断等。(使用AllObjectFacts是否是唯一这样的条件尚不清楚。)
  2. AllObjectFacts的文档可能误导了Analyzer设计师。它不是“所有对象事实”。如果我们想要支持所有对象事实,objectpath需要额外的信息来进行序列化。或者,文档可以更新以反映更狭窄的合同。
    CL显示如何通过增量和独立执行产生不同的结果:https://go-review.googlesource.com/c/tools/+/342554
    额外注意事项:
  3. 对于独立检查器,即使不相互传递导入的包(pq),它们也可以通过AllObjectFacts看到彼此的对象事实。这在增量构建中在架构上是不可能的。尚不清楚go/analysis/internal/checker是否正确地强制执行这一点。
  4. ExportObjectFact的“在同一包内”的区别可能很微妙,因为它不包括诸如来自另一个包嵌入接口的功能之类的东西。但这不是这个问题的重点。
ozxc1zmp

ozxc1zmp1#

感谢timothy-king的非常清晰的解释和复现。
我正在进行增量指针分析@go-air(非常不完善...),并对事实分析接口有很多疑问——我觉得这对于该项目来说最容易解决。
(我已经在那里打开了一个跟踪问题)
我认为对象路径限制太多,不适合强加给工具/go/analysis用户(除了你提到的那些点,例如跨包的表达式等)。我还认为工具/go/analysis-library的存在本身就存在跨包对象路径过滤逻辑(嵌入类型等)的问题,尽管拥有对象路径总比没有好,有助于序列化。
无论如何,如果分析器独立运行时能产生与go vet工具相同的结果,那将是非常好的。
关于让分析器作者自己进行跨包过滤,大家有什么想法吗?
@dominikh这样做会影响静态检查吗?

qxsslcnc

qxsslcnc2#

关于是否让分析器作者自己进行跨包过滤,有什么想法吗?

@wsc0 我觉得将x/tools/go/analysis看作是一个编译器框架,它恰好针对静态分析很有帮助。尤其是在考虑事实时。一个传递(Analyzer, Pkg)的输入来自Pkg导入的事实,包括解析后的源代码、类型和无Go文件等。对于Pkg,当分析器直接依赖于A'时,传递(A', Pkg)的结果会产生事实、结果、错误值和诊断序列。这允许按顺序执行检查每个包的传递,每次只精确地检查一次,通过始终序列化有关包的所有事实并让结果在分析器之间流动来实现。这基本上与编译器相同,只是用事实替换了对象文件。这使得它可以简单地集成到go cmd中,例如go vet,以及其他构建系统如bazel可以遮蔽编译图以获得增量x/tools/go/analysis图。所有这些都是为了回答以下问题而展开的长篇大论。

如果不对ExportObjectFact施加相同的*types.Package限制,如何处理事实之间的钻石依赖关系和碰撞?假设我们有以下导入图:

a -> b -> d
  a -> c -> d
  a -> d

其中b和c都在types.Object内为d产生一个ObjectFact。(请记住,c和b是独立处理的[就像可能在不同的子进程中一样]。)当ad导入对象事实时,它会看到什么?

回想一下在这里从编译器+链接器了解到的一些显而易见的候选答案,如果我们允许这种情况发生:

  1. 将输入作为错误拒绝。不允许碰撞。
  2. 任意选择一个输入。
  3. 要求碰撞产生的值相同。否则报错。
  4. 有优先级规则来选择输入。
  5. 有用户指定的折叠函数和应用顺序。
  6. 有用户指定的连接函数,并要求它是可交换的。(最后两个可能还需要幂等性?)

这些都不是完全令人满意的。

(有可能我误解了你的问题,所以请纠正我,如果我理解错了的话。)

j8ag8udp

j8ag8udp3#

另一种看待ObjectFacts的方式是“为什么PackageFacts不够用?”我认为答案是,在AST元素上进行序列化和反序列化很困难,所以框架介入并在后台完成这个操作。考虑通过使用包事实类型map[*types.Func]NoReturn来模拟一个对象事实类型“NoReturn”的函数,看看这为什么会有挑战性。这就是objectpath试图解决的问题。

k2arahey

k2arahey4#

另一种看待ObjectFacts的方式是“为什么PackageFacts不够用”?我认为答案是,在AST元素上进行序列化和反序列化是困难的,因此框架介入并在后台完成这个操作。

我认为将所有内容序列化为go/{types,ast,token,...}的问题是“根本性问题”,因为解决它会给出最有效的长期解决方案。与具有微妙的“框架介入”解决方案(例如objectpath,即使是今天的状态)来解决钻石碰撞问题的复杂框架一起工作,这对于许多分析用例来说也是一个困难的问题,因为它缺乏基本的表达能力,如引用任意表达式的能力。

考虑通过使用包事实类型map[*types.Func]NoReturn在函数上模拟一个对象事实类型“NoReturn”,看看这可能有多具挑战性。这是objectpath试图解决的问题。

如果我理解正确的话,理论上我们可以解决上面提到的“根本性问题”,在map键中使用可序列化的每个包唯一的ID代替*types.Func。否则,在这个情况下,我想一个有纪律的使用token.Pos作为map键可能足以使用包事实。

但这些都是更大的问题。在这个问题中更直接的问题是:
当导入来自d的对象事实时,see看到了什么?
即对象路径事实的一致性。假设objectpath + object facts被使用(它们确实被使用,但我认为它们只适用于某种类型的分析器,并且很复杂),我想每个分析器都可以决定如何解决冲突,考虑到自己的需求。由于对象事实对分析器是私有的,所以在我看来这是可以接受的。依赖于它的分析器将使用结果(也许令人困惑的是,这些结果不是事实,而是不同的,让使用事实的分析器将它们转换为结果。)

就补丁而言,它的形式将是删除所有对象事实过滤,然后修复现有分析器中产生的任何问题。有些人可能会抛弃间接信息,例如。但是事实的意义非常松散或任意。

也许另一个可能的解决方案类型(对于上面@timothy-king提出的6个)是定义d上的由b定义的对象事实与由c定义的对象事实之间的区别,以及由a定义的对象事实与由c定义的对象事实之间的区别。然后分析器只需要根据它提供的功能做正确的事情。这就像是跨包的上下文敏感性而不是调用图,每个分析器都可以抽象出这种上下文敏感性以减少所需的开销(或者不)。

这个想法可以探索是否处于“框架介入”的形式或在每个分析器的肩上。
尽管我个人更感兴趣的是解决序列化问题,至少是以示例的形式。
但我同意对象事实一致性的问题很深,因此目前为止的解决方案或解决方案方向似乎都不令人满意。我们能添加一个“思考”标签吗?

cl25kdpy

cl25kdpy5#

这稍微偏离了主题,但我会注意到,如果这里的“唯一”问题是我们无法完全序列化类型Object等,那么https://pkg.go.dev/gvisor.dev/gvisor/pkg/state可能有用。这个包能够序列化大多数复杂的Go对象图,实际上gVisor就是用它来序列化几乎整个应用程序状态的。

查看types.Object,我发现几乎没有什么不能自动序列化的[1]。话虽如此,看起来你最终会序列化整个包对象图,这意味着最后的分析步骤可能会将整个解析后的程序保存在内存中,从某种程度上来说,这似乎有悖于分段分析器的目的。

[1] go/types.lazyObject.resolve是我注意到的唯一具体的事情。这个包不能处理函数指针(但通常可以用接口值替换)。

fruv7luv

fruv7luv6#

感谢prattmic的参考。gvisor状态看起来不错。关于序列化整个对象图和分段分析器,我认为对于许多目的,序列化+能够引用对象/表达式的唯一ID,而无需加载整个图形,更符合我之前看到的并正在寻找的想法,即根据对包的近似摘要(的概念)进行分析。因此,要分析A导入B和C,以及C导入D,您只需要在内存中存储{A, summary-of(B), summary-of(C)}来分析A;您不需要D,并且只会使用导入的“summary-of”。
例如,https://drops.dagstuhl.de/opus/volltexte/2021/14045/pdf/LIPIcs-ECOOP-2021-2.pdf
这些摘要,如果像论文中那样持久化,并且像go vet中的单独进程那样进行通信,往往需要引用当前内存中的内容,如类型、ast节点和声明等。

42fyovps

42fyovps7#

我正在进行增量指针分析https://github.com/go-air,并对事实分析接口有很多疑问——我觉得它令人困惑,我的go-air/pal#6(评论)认为,为了这个项目的目的,最容易解决这个问题。
大多数指针分析可以表示为两个阶段:(1)将源代码转换为约束系统;(2)求解约束。第一个阶段本质上是编译器对程序语句的一次遍历,第二个阶段通常是计算图可达性等。第一个算法是线性的,第二个是立方的。所以我在想是否有必要优化第一个阶段。
此外,指针分析通常从main开始进行整个程序的分析。它们类似于链接器的死代码消除步骤。相比之下,go/analysis框架更像是一个按拓扑顺序分析每个包的编译器,但在任何时候都无法看到程序的所有派生信息("对象代码"在这个类比中)。
使用一个复杂的框架,其中包含微妙的“框架步骤来解决钻石碰撞”(即对象路径即使在今天也存在),缺乏基本表达能力,如引用任意表达式的能力,这对于许多分析用例来说也是一个难题。
我并不否认objectpath的问题和框架的僵化,但在我看来,go/analysis框架似乎不是指针分析的正确基础。你可能会发现go/packages是一个更好的起点。

相关问题