diff --git a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md new file mode 100644 index 00000000..c5a32911 --- /dev/null +++ b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md @@ -0,0 +1,357 @@ +# Frontend Segmented Type Resolution Pipeline Execution Summary + +本文总结 `doc/module_impl/frontend/frontend_resolution_pipeline_implementation.md` 所述前端分析流水线的目标架构形态。内容只描述目标架构、执行顺序与不变量,不展开旧 whole-module 流水线或过渡实现资产。 + +## 1. 总体形态 + +计划完成后,frontend shared semantic pipeline 分为四个层次: + +1. 基础结构层:建立 module skeleton、scope graph 与 baseline inventory。 +2. Interface 层:建立 body 解析所需的 declaration index、typed lexical baseline 与 suite entry roots,并通过不可变 structural policy 描述支持面。 +3. Body 层:通过 `SuiteResolver` 按源码顺序解析 supported body suite,并使用 typed overlay 表达前缀事实可见性。 +4. Diagnostics-only 层:在 body facts 完全收敛并导出为 stable facts 后运行 annotation usage、virtual override、type check、loop control 与 compile-only final gate。 + +核心目标是把 body typed resolution 改为 source-order suite 解析:当前 statement 产生的 typed facts 可以被同一 statement 的后续 owner 子过程和后续 statement 读取,但在 suite export 之前不会污染 `FrontendAnalysisData` stable side tables 或 `BlockScope` stable slot。 + +## 2. 基础结构层 + +基础结构层负责建立后续所有语义阶段共享的不可或缺结构: + +- `FrontendModuleSkeleton`。 +- `scopesByAst()`。 +- callable parameter inventory。 +- supported parameter、iterator 与 ordinary local inventory。 +- skipped / deferred subtree 的硬边界。 + +这一层不做 body expression typing,也不发布 `resolvedMembers()`、`resolvedCalls()`、`expressionTypes()` 或最终 `slotTypes()`。它只保证后续 resolver 能基于完整 lexical inventory 做 declaration-order 与 self-reference 过滤。 + +完整 inventory 是 resolver filtered-hit 模型的前提。即使 body typed resolution 按 source order 运行,普通 local declaration 的 inventory 仍必须先完整发布,不能让 resolver 在 use-site 时临时扫描 AST 补找声明。 + +## 3. Interface 层 + +Interface 层在基础结构层之后运行,负责准备 body `SuiteResolver` 需要的 interface surface。 + +输入包括: + +- `FrontendModuleSkeleton`。 +- `scopesByAst()`。 +- baseline parameter / ordinary local inventory。 +- 当前 diagnostics snapshot。 +- `ClassRegistry`。 + +输出包括: + +- `FrontendBodyDeclarationIndex`:记录每个 supported block 的完整 body-local declaration 列表与 source order;ordinary `var` 使用 `VariableDeclaration` identity,for iterator 使用 owning `ForStatement` identity。对 `FOR_BODY`,inventory 契约是恰好一个 iterator entry 位于列表头部且 `sourceOrder == 0`,ordinary body local 仅能使用连续 `sourceOrder >= 1`。生产 resolver 用 declaration identity 验证 scope 命中的 local 属于已发布 inventory,但不替代 declaration-order / self-reference filtered-hit。 +- `FrontendTypedLexicalBaseline`:记录参数、显式 typed local 与 interface 层可静态确定的 source-facing slot baseline;`TypedLexicalEnvironment` 在 overlay、已发布 slot fact 与 parent environment 都没有事实时读取该冻结 fallback。 +- `FrontendSuiteEntryRoots`:列出 body layer 可进入的 callable、property initializer 与 supported block roots。 + +`FrontendBodySemanticSupportPolicy` 是 AST/scope kind 支持面的唯一事实源;它不读取 typed fact、diagnostic 或 compile state。`FrontendBodyStructuralCompleteness` 在 SuiteResolver 进入 root/child body 前验证 scope identity、suite entry、declaration index 与 typed baseline 完整;完整性对 published body inventory 是双向的(index entry ↔ body `BlockScope` 中每条 `LOCAL`),`sourceOrder` 必须与 AST start-byte 序一致,且 `FOR_BODY` inventory 必须恰好包含一个位于列表头部、`sourceOrder == 0` 的 iterator entry(Interface 层先发布 iterator 再 walk body 的形状)。这两个合同分别表达“该结构受支持”和“本次 interface surface 确实完整”,不能合并为 typed-dependent readiness。 + +Interface 层不得发布 body typed facts。特别是不得发布 `expressionTypes()`、`resolvedMembers()` 或 `resolvedCalls()`,也不得把 `GdCompilerType` 写入 source-facing lexical baseline。 + +## 4. Body `SuiteResolver` + +Body 层由 `FrontendSuiteResolver` 或等价 coordinator 驱动。它按源码顺序进入 supported suite,形态如下: + +```text +resolveCallableOwner(context, callableOwner): + exportBatch = new FrontendCallableExportBatch() + context = newSuiteContext(callableOwner, exportBatch) + requireStructurallyCompleteBody(callableOwner.body) + runCallableEntryVarTypePost(context, callableOwner) + resolveSuite(context, callableOwner.body) + exportBatch.applyTo(context.analysisData()) + +runCallableEntryVarTypePost(context, callableOwner): + for parameter in callableOwner.parameters(): + putSlotType(VAR_TYPE_POST, parameter, baselineType) + context.typedEnvironment().flushPendingFacts() + +resolveSuite(context, block): + for statement in block.statements(): + resolveStatement(context, statement) + context.exportBatch().accumulate(context.typedEnvironment().exportPatchTransaction()) +``` + +参数是 callable-entry `VAR_TYPE_POST` facts,不是 statement root。因此预发布是与 +statement-local var type post procedure 互补的正规步骤,而不是绕过或例外:两条路径都使用 +同一 owner stage、typed overlay 与 callable-scoped export batch。参数 facts 在进入 statement +循环前 flush 到 committed overlay,使第一个 statement 可读取它们,同时 stable side table +仍保持不变。 + +每个 nested suite 收敛时只把自己的 per-owner patch transaction 追加到同一 callable-scoped +export batch,不得直接 apply stable side table。根 callable suite 及其所有 nested suite 完成后, +batch 才按追加顺序 apply;child pending/committed overlay 不合并到 parent overlay。 + +这里的 transaction / batch 只保证顺序与 apply 时机,不提供原子提交。Batch 不在 apply 前预检 queued transaction 之间的冲突;隔离的 child / sibling overlay 也看不到彼此已经导出但尚未进入 stable state 的 fact。因此跨 transaction 的重复、不兼容 publication 可能直到后一个 transaction apply 时才 fail-fast,此前已提交的 patch / transaction 不回滚。当前实现明确保留这一 corner case;apply 异常后必须丢弃整个 `FrontendAnalysisData`,不能继续消费其中的 stable facts。 + +`resolveStatement(...)` 不是新的 semantic owner。它只按 statement 结构编排已有 owner 的 body-aware 子过程:top binding、local stabilization、chain binding、expr typing 与 var type post。 + +Body procedure 必须是 root-bounded、statement-local 的实现。每个 owner 子过程只处理 `SuiteResolver` 传入的 statement、header 或 expression root 及其允许的子表达式。实现可以复用纯语义 helper,但 owner 子过程不能依赖 whole-module traversal 建立隐式上下文。 + +生产 `SuiteResolver` 将 interface surface 的 `FrontendBodyDeclarationIndex` 传给 `FrontendVisibleValueResolver`,以确认 scope 选中的 ordinary local 已在 interface phase inventory 中发布;名称查找仍由 scope 完成,source byte range 仍负责 declaration-order 与 initializer self-reference filter。它同时将 `FrontendTypedLexicalBaseline` 传给每个 `FrontendTypedLexicalEnvironment`,使尚未被 body overlay 或 stable publication 覆盖的 source-facing slot 能读取 interface-level 初始类型。 + +第一版 body statement 支持面包括: + +- ordinary local `VariableDeclaration`。 +- supported property initializer。 +- `ExpressionStatement`。 +- `ReturnStatement`。 +- `AssertStatement`。 +- `IfStatement` / `ElifClause` / `else`。 +- `WhileStatement`。 +- `ForStatement`:header-only statement boundary 完成后,通过普通 `resolveChildSuite(...)` 进入结构完整的 `FOR_BODY`。 + +`MatchStatement`、`LambdaExpression` 与 block-local `const` 仍保持结构性 deferred / unsupported;它们不创建 lifecycle state,也不能因 typed result 改变 body entry。 + +## 5. Statement 内 Owner 顺序 + +每个 statement 内的 owner 子过程顺序是硬不变量: + +1. Top binding runner。 +2. Local stabilization runner。 +3. Chain binding runner。 +4. Expr typing runner。 +5. Var type post procedure。 +6. Pending fact flush。 + +在上述 statement-local 顺序之前,callable-entry var type post 会为每个参数发布 +`VAR_TYPE_POST` slot fact 并 flush 到 committed overlay。它不属于第 1-6 步的 statement +procedure,但使用相同的 owner stage 和发布协议。 + +Top binding runner 为当前 statement 内的 bare identifier 与 chain head use-site 写入 binding overlay。 + +Local stabilization runner 在 top binding overlay、current-suite committed typed facts 与 stable lexical inventory 之上解析 eligible `:=` initializer,并写入当前 statement 的 local slot pending overlay。 + +Chain binding runner 消费 top binding overlay 与 local stabilization pending / committed slot fact,发布 `resolvedMembers()` 与 chain-owned `resolvedCalls()` overlay。 + +Expr typing runner 消费 binding、member、call 与 local slot overlay,发布 `expressionTypes()` 与 bare-call `resolvedCalls()` overlay。`backfillInferredLocalType(...)` 在目标架构中仍只保留 guard-only 协议检查。 + +Var type post procedure 消费 expression type 与 source-facing local slot overlay,发布 final `slotTypes()` overlay。 + +Pending fact flush 把当前 statement pending overlay 转入 current-suite committed overlay,供后续 statement 读取。Flush 不得写入 stable side table 或 stable `BlockScope` slot。 + +这个顺序不能重排。尤其 chain binding 读取 receiver local slot 时,必须先看到 local stabilization 对前序 statement 或当前 statement 前序子过程写入的 exact slot fact。 + +## 6. `TypedLexicalEnvironment` + +`FrontendTypedLexicalEnvironment` 是 body 层所有 value/type lookup 的 effective view。它包装 `Scope` 与 suite-local typed overlay,但不替换 `Scope` 本身。 + +读取顺序固定为: + +1. 当前 statement pending overlay。 +2. 当前 suite committed overlay。 +3. 已发布 stable slot facts。 +4. parent typed lexical environment。 +5. `FrontendTypedLexicalBaseline` 提供的冻结 source-facing fallback;`BlockScope` / `CallableScope` 仍负责 lexical inventory 名称查找。 +6. class / global / singleton / type-meta lookup。 + +Pending overlay 只对当前 statement 后续 owner 子过程可见。`flushPendingFacts()` 后,pending facts 才进入 current-suite committed overlay,并对后续 statement 可见。Committed overlay 仍不是 stable publication。 + +`TypedLexicalEnvironment` 的目标是模拟 source-order body resolution 中“前缀 statement 已解析出的类型可被后续 statement 使用”的行为,同时避免提前污染 stable semantic facts。 + +Parent environment 只能读取 parent 链上的 overlay,不能读取 child environment 的 pending 或 +committed facts。Child facts 只能通过 callable-scoped export batch 在 root callable 完成后进入 +stable side tables。 + +## 7. Fact 生命周期 + +目标架构固定四层事实可见性模型: + +1. Owner procedure transient cache:当前由 `BodyExpressionResolver` 的 expression / finalized-expression / call caches、`FrontendChainReductionFacade.reducedChains` 与 helper bounded retry 承担;只给当前 chain / expr reduction 的 retry 回调读取,owner 子过程结束即丢弃。 +2. Current statement pending overlay:当前 statement 后续 owner 子过程可读,只接受每个 AST key 的最终 publication fact。 +3. Current-suite committed overlay:由 pending fact flush 合并而来,后续 statement 可读,但仍不是 stable publication。 +4. `FrontendAnalysisData` stable side tables / `BlockScope` stable slot:只在 root callable export batch 的 per-owner patch apply / stable export helper 后更新。 + +Nested chain / argument retry 的中间事实只能存在于 owner-local transient cache 中。Retry 中出现的临时 `DEFERRED`、暂定 `Variant`、中间 status 或 detailReason 不得写入 pending overlay、committed overlay 或 stable side table。 + +`expressionTypes()` 对同一 key 只能发布最终 fact 一次。如果一个 expression 在 reduction 过程中需要先得到临时 fact 再得到 exact result,中间状态必须留在 owner-local transient cache 或专用非导出状态中。 + +## 8. Overlay 写入、Flush 与 Export + +Statement owner runner 只能写当前 statement pending overlay;callable-entry var type post 在 statement 循环前写参数 pending overlay。Overlay write 必须携带 owner metadata,并在写入时执行 owner、conflict、idempotent、exact-type 与 compiler-only guard。 + +`flushPendingFacts(...)` 只把 pending overlay 合并到 current-suite committed overlay。Flush 必须复用 suite export 使用的同一个 type-bearing field walker,不能在 scratch 层接受 export 层会拒绝的 payload。 + +Suite 收敛后,current-suite committed overlay 只能导出为按 owner 有序的 patch transaction,不能导出为一个跨 owner patch。Nested suite transaction 必须追加到 root callable 的 `FrontendCallableExportBatch`,不得在 child suite 边界独立 apply。Stable side table 与 `BlockScope.resetLocalType(...)` 只能在 root callable batch 的 per-owner patch apply 或 stable export helper 中更新;该集中 apply 只定义时机和顺序,不构成原子 commit boundary。 + +Diagnostics-only phase、compile gate 与 lowering 只能读取 suite export 后的 stable facts。 + +## 9. Per-owner Patch Transaction + +目标架构使用 `gd.script.gdcc.frontend.sema.patch` 包承载 patch 相关类型。核心类型包括: + +- `FrontendOwnerPatch` 或等价 sealed interface。 +- `FrontendTopBindingPatch`。 +- `FrontendLocalTypeStabilizationPatch`。 +- `FrontendChainBindingPatch`。 +- `FrontendExprTypePatch`。 +- `FrontendVarTypePostPatch`。 +- `FrontendPatchTransaction`。 +- `FrontendCallableExportBatch`。 +- `FrontendLocalSlotTypeUpdate`。 + +每个 per-owner patch 只能携带该 owner 允许发布的 payload: + +- `FrontendTopBindingPatch`:`symbolBindings()` delta。 +- `FrontendLocalTypeStabilizationPatch`:`FrontendLocalSlotTypeUpdate` delta。 +- `FrontendChainBindingPatch`:`resolvedMembers()` + chain-owned `resolvedCalls()` delta。 +- `FrontendExprTypePatch`:`expressionTypes()` + bare-call `resolvedCalls()` delta。 +- `FrontendVarTypePostPatch`:`slotTypes()` delta。 + +`FrontendPatchTransaction` 按固定 owner 顺序 apply:top binding -> local stabilization -> chain binding -> expr typing -> var type post。`Transaction` 在这里不表示原子提交;后续 patch 失败时,先前 patch 已产生的 stable mutation 不回滚。 + +`FrontendCallableExportBatch` 按 suite 收敛顺序保存 root callable 与 nested suite 的 transaction,且仅在 root callable suite 返回后依次 apply。它不预检 queued transaction 之间的冲突,也不提供跨 transaction 原子性或回滚。Property initializer 是独立 root,不加入 callable-scoped batch。 + +Merge 规则如下: + +- 新 key 直接写入 stable side table。 +- 旧 key + 相同 value 允许,视为 idempotent。 +- 旧 key + 不同 value 默认 fail-fast。 +- `symbolBindings()` 允许由 local slot commit helper 派生刷新同 declaration 的 resolved value payload。 +- `FrontendLocalTypeStabilizationPatch` 本身不得携带独立 `symbolBindings()` delta。 +- `resolvedCalls()` 中 chain-owned call 与 bare-call 由 semantic owner patch 与 key-space contract 区分,不能相互覆盖。 +- `expressionTypes()` 同 key republish 只有 status、publishedType 与 detailReason 全等时允许。 +- `expressionTypes()` 不允许 `Variant -> exact`、parent -> child、status upgrade 或 detailReason change。 +- `slotTypes()` 不允许同一 source slot 被不同类型覆盖,同类型 no-op 允许。 + +这些 merge 规则只保证当前 per-owner patch 的 conflict check。Fail-fast 不撤销同一 transaction 内更早的 patch,也不撤销 callable batch 内更早的 transaction;跨 queued transaction 的冲突盲区是当前已知且暂不修复的限制。 + +## 10. Scope Slot Mutation + +`BlockScope.resetLocalType(...)` 不是 side-table 写入,但它影响后续 resolver 与 published binding payload。目标架构把这类 mutation 建模为 owner-controlled slot update。 + +`FrontendLocalSlotTypeUpdate` 至少记录: + +- `BlockScope scope`。 +- `String name`。 +- `Object declaration`。 +- `GdType type`。 + +应用规则如下: + +- 只有 local stabilization owner 可以产生 source-facing local slot update。 +- 只允许 `Variant -> exact` 或 exact same-type no-op。 +- 不允许 exact A -> exact B。 +- 不允许写入 `GdVoidType`。 +- 不允许写入 `GdCompilerType`。 +- 应用后必须刷新已发布且指向同一 declaration 的 `symbolBindings()` payload。 + +Overlay 可在 export 前提供 effective type,但最终 stable `BlockScope.resetLocalType(...)` 与 binding payload refresh 必须通过同一个 commit helper 执行,避免出现第二条 slot mutation side channel。 + +## 11. Compiler-only Guard + +任何 source-facing typed publication surface 都不得泄漏 `GdCompilerType`。目标架构要求使用同一个 shared type-bearing field walker,覆盖 overlay pending write、statement flush、suite export 与任何保留 source-facing publication 语义的 whole-table publication API。 + +Shared walker 至少覆盖: + +- `FrontendBinding.resolvedValue().type()`。 +- `FrontendResolvedMember.receiverType()`。 +- `FrontendResolvedMember.resultType()`。 +- `FrontendResolvedCall.receiverType()`。 +- `FrontendResolvedCall.returnType()`。 +- `FrontendResolvedCall.argumentTypes()`。 +- `FrontendResolvedCall.ExactCallableBoundary.fixedParameterTypes()`。 +- `FrontendExpressionType.publishedType()`。 +- `slotTypes()` value。 +- `FrontendLocalSlotTypeUpdate.type()`。 + +Guard 必须在 pending overlay write 时 fail-fast,不能等 suite export 时再补救。只要一个 fact 命中 compiler-only payload,write API 就必须拒绝该 fact,不能先写入 pending / committed overlay 再回滚。 + +## 12. Resolver 与结构边界 + +`FrontendVisibleValueResolver` 继续依赖完整 source inventory 与 declaration-order filter。重构后的 resolver 能读取 `TypedLexicalEnvironment` effective view,但不能让 overlay 绕过 resolver filter。 + +基本规则如下: + +- Resolver 先按 request domain、AST boundary、current scope、declaration order 与 initializer self-reference 过滤候选 declaration。 +- 过滤通过后,才从 `TypedLexicalEnvironment` 读取该候选的 effective type 或 binding payload。 +- 当前 statement pending slot fact 可被同一 statement 后续 owner 子过程消费。 +- pending slot fact 不能让 `var x := x` 的右侧 `x` 绕过 self-reference 过滤。 +- committed fact 可被后续 statement 消费。 +- future declaration 仍必须报告 `DECLARATION_AFTER_USE_SITE` filtered hit。 + +Resolver 保留三类彼此独立的结构检查: + +1. Request-domain hard boundary:只有 `EXECUTABLE_BODY` 进入 ordinary lookup。 +2. AST boundary:parameter default、lambda、match 与 block-local `const` 返回精确 deferred domain;for header/body edge 已转正。 +3. Current-scope backstop:lambda callable/body 与 match section body 即使 AST edge 缺失也继续 fail-closed;`FOR_BODY` 是 supported executable scope。 + +这些检查不读取 typed overlay 以决定结构支持,也没有 pending/published lifecycle。`FrontendSuiteContext` 根据 structural policy 创建 request;`FrontendSuiteResolver` 在进入 root/child body前使用 completeness certificate 验证 interface facts。Typed overlay 只改变过滤通过后的 effective type/binding payload。 + +## 13. Diagnostics 与 Compile Gate + +Diagnostics-only phase 在 suite export 后运行,消费 stable facts,不发布新的 semantic side table。 + +Diagnostic owner 保持单一: + +- top binding 负责 `sema.binding`。 +- chain binding 负责 `sema.member_resolution` / `sema.call_resolution`。 +- expr analyzer 负责 expression resolution 相关 diagnostics。 +- var-type-post analyzer 负责 `sema.variable_slot_publication`。 +- annotation usage analyzer 负责 `sema.annotation_usage`。 +- virtual override analyzer 负责 `sema.virtual_override`。 +- type-check analyzer 负责 `sema.type_check` / `sema.type_hint`。 +- loop-control analyzer 负责 `sema.loop_control_flow`。 +- compile-only analyzer 负责 `sema.compile_check`。 + +如果同一根源错误已经有 upstream diagnostic,下游 analyzer 只能保留 side-table status,不得补第二条同级错误。 + +Diagnostics snapshot 在 interface/body path 中有明确层级:同 statement 内 owner procedure 产生的 upstream diagnostic 立即进入 live `DiagnosticManager`;statement boundary flush typed facts 后同步刷新 `FrontendAnalysisData.diagnostics()`,使后一 statement 可读取 current-suite snapshot;suite export 在 patch transaction 应用到 stable side table 后保留最终 body snapshot;interface/body hand-off 与 diagnostics-only phases 继续在各自 phase boundary 刷新 snapshot。 + +`FrontendCompileCheckAnalyzer` 只运行在 compile-only 入口。默认 shared semantic `analyze(...)`、inspection 与未来 LSP 入口不得隐式运行 compile-only gate。Lowering 只能以 `analyzeForCompile(...)` 且 diagnostics 无 error 的结果作为最低前置条件。Compile gate 在入口仍要求 stable diagnostics boundary 已发布,但 upstream duplicate suppression 使用当时 live `DiagnosticManager` 的冻结 snapshot,以覆盖 interface/body path 中尚未被调用方再次复制到 `FrontendAnalysisData` 的 upstream diagnostics。 + +`ForStatement` 已进入 shared semantic,但 compile route 尚未就绪。当前 compile-only analyzer 在 `ForStatement` root 发布单一临时 `sema.compile_check` blocker,不读取 iterable type/iteration plan,也不进入 body;后续 route-aware compile policy 只能替换该 lowering boundary,不能反向控制 semantic body entry。 + +Compile gate 的 generic published-fact blocker 仍基于 final stable facts:`BLOCKED`、`DEFERRED`、`FAILED`、`UNSUPPORTED` 阻断编译,`DYNAMIC` 是 frontend 已认可的 runtime-open fact,不得误判为 blocker。 + +## 14. 典型行为 + +对于 source-order local alias: + +```gdscript +func f(): + var a := typed_value + var b := a + var c := b +``` + +`a` 的 exact type 在第一个 statement 中写入 pending overlay,flush 后进入 current-suite committed overlay。第二个 statement 解析 `b := a` 时,resolver 先确认 `a` 对 use-site 可见,再从 typed environment 读取 `a` 的 exact slot fact。`b` flush 后,第三个 statement 可以同样读取 `b` 的 exact fact。Stable side tables 与 stable `BlockScope` 只在 suite export 后更新。 + +对于 receiver-dependent chain: + +```gdscript +func f(): + var receiver := make_exact_receiver() + var x := receiver.member +``` + +Top binding 先绑定 `receiver` use-site。Local stabilization 随后把 `receiver` 写入 exact local slot overlay。Chain binding 再解析 `receiver.member`,必须消费 exact receiver slot fact,而不是 interface baseline `Variant`。Expr typing 最后发布该 expression 的最终 type fact。 + +## 15. 完成后的核心不变量 + +计划完成后应满足以下不变量: + +- shared semantic 默认使用 interface/body pipeline。 +- shared analyzer 不再提供 legacy whole-phase body publication bypass;`FrontendSegmentedSemanticScheduler` 不再是代码资产。 +- top binding、local stabilization、chain binding、expression typing 与 var type post 只由 + `FrontendBodyOwnerProcedures` 的 root-bounded owner procedure 承载;对应 whole-module analyzer + 类已删除。 +- `FrontendWindowAnalysisContext`、`FrontendWindowPublicationSurface` 与 legacy + `FrontendAnalysisPatch` 已删除;body facts 只能通过 per-owner patches 和 + `FrontendPatchTransaction` 导出。 +- body typed resolution 按 source order 运行。 +- 每个 statement 内 owner 顺序固定为 top binding -> local stabilization -> chain binding -> expr typing -> var type post。 +- pending overlay 只对当前 statement 后续 owner 可见。 +- statement flush 只更新 current-suite committed overlay。 +- suite export 只通过 per-owner patch transaction 更新 stable side tables 与 stable slot。 +- patch transaction 与 callable export batch 是 non-atomic ordered apply;batch 不做跨 queued transaction 预检,apply 失败后当前 analysis state 必须整体丢弃。 +- `expressionTypes()` 每个 key 只导出最终 fact 一次。 +- retry 中间 facts 不进入 pending overlay、committed overlay 或 stable side table。 +- source-facing local slot mutation 只有 local stabilization owner 可以产生。 +- `backfillInferredLocalType(...)` 保持 guard-only。 +- shared compiler-only walker 覆盖所有 user-visible type-bearing publication surfaces。 +- resolver 的 declaration-order 与 self-reference filter 不能被 overlay 绕过。 +- structural policy 与 completeness certificate 分别控制支持面和 interface 完整性;certificate 对 body inventory 做 index↔scope 双向完备校验;二者都不读取 typed fact 或 compile readiness。 +- resolver 的 request-domain、AST boundary 与 current-scope backstop 保持结构性 fail-closed,且 `FOR_BODY` 直接进入 ordinary lookup。 +- diagnostics-only phase、compile gate 与 lowering 只能读取 suite export 后的 stable facts。 diff --git a/doc/analysis/frontend_semantic_analyzer_research_report.md b/doc/analysis/frontend_semantic_analyzer_research_report.md index 2864a8f4..58d08b7c 100644 --- a/doc/analysis/frontend_semantic_analyzer_research_report.md +++ b/doc/analysis/frontend_semantic_analyzer_research_report.md @@ -393,7 +393,7 @@ 1. **没有 frontend -> LIR lowering。** 当前语义主链仍止步于 side table、diagnostics 与 class skeleton;函数体不会继续产生 LIR。 2. **compile-ready 与 shared semantic 仍然分离。** `FrontendCompileCheckAnalyzer` 已经提供 compile-only final gate,但 `ConditionalExpression`、`ArrayExpression`、`DictionaryExpression`、`PreloadExpression`、`GetNodeExpression`、`CastExpression`、`TypeTestExpression` 与 `assert` 仍需要在进入 lowering 前被显式封口。 -3. **若干 executable-body 域仍保持 deferred / unsupported。** 参数默认值、lambda capture、`for` iterator binding、`match` pattern binding、block-local `const` 等尚未进入当前正式支持面。 +3. **若干 executable-body 域仍保持 deferred / unsupported。** 参数默认值、lambda capture、`match` pattern binding、block-local `const` 等尚未进入当前正式支持面。`for` iterator/body inventory 已在后续阶段 B/D0/L 转正,但 iteration planning 与 lowering route 仍未完成。 4. **`self` 核心语义已经接通,但 signal use-site 与 coroutine 语义仍未闭环。** 当前代码已经支持 `self` 的 top binding 发布、static context fail-closed、property initializer fail-closed,以及将 `self` 解析为当前类实例 receiver;仍未形成稳定 frontend 合同的是 `.emit(...)`、`await signal` 等 signal/coroutine use-site,以及更完整的 context-sensitive diagnostics。 5. **property initializer 仍不是完整实例初始化模型。** 当前支持面是“published subtree facts”,而不是 declaration-order / default-state / cycle-aware 的 class-member initializer 语义。 6. **header superclass 的支持面仍受 MVP 限制。** path-based `extends`、autoload superclass、global-script-class superclass 与跨多个 gdcc module 的 superclass 绑定依然没有接通。 diff --git a/doc/module_impl/frontend/diagnostic_manager.md b/doc/module_impl/frontend/diagnostic_manager.md index 4da55333..31b2ca4e 100644 --- a/doc/module_impl/frontend/diagnostic_manager.md +++ b/doc/module_impl/frontend/diagnostic_manager.md @@ -212,7 +212,7 @@ deferred / unsupported diagnostics 一律通过 `DiagnosticManager` 发布。 - `sema.unsupported_parameter_default_value` - variable analyzer 对 parameter default value 当前尚未接线时发出的 feature-boundary error - `sema.unsupported_variable_inventory_subtree` - - variable analyzer 对 lambda / `for` / `match` / block-local `const` inventory 边界发出的 feature-boundary error + - variable analyzer 对 lambda / `match` / block-local `const` inventory 边界发出的 feature-boundary error - `sema.variable_slot_publication` - var-type-post analyzer 对 supported callable-local `var` 因 earlier duplicate/shadowing reject 而无法发布 `slotTypes()` 时发出的 warning - warning message 若能在当前 callable 边界内找到幸存的 accepted local / parameter / capture,必须带出该幸存绑定的语义类别与声明位置 @@ -220,7 +220,7 @@ deferred / unsupported diagnostics 一律通过 `DiagnosticManager` 发布。 - `sema.binding` - top binding 命中的 blocked / unknown / shadowing 诊断 - `sema.unsupported_binding_subtree` - - top binding 对 parameter default、lambda、`for`、`match`、block-local `const` 等明确 unsupported subtree 的边界 error + - top binding 对 parameter default、lambda、`match`、block-local `const` 等明确 unsupported subtree 的边界 error - top binding 对 missing-scope / skipped subtree 的恢复诊断继续允许使用 warning - `sema.member_resolution` - chain binding 中 blocked / failed member step 的语义错误 @@ -266,10 +266,11 @@ deferred / unsupported diagnostics 一律通过 `DiagnosticManager` 发布。 - `sema.compile_check` - compile-only `FrontendCompileCheckAnalyzer` 对进入 lowering 前仍不可编译的 surface 发出的最终 error - 同时覆盖: - - 当前首批显式封口的 `assert`、`ConditionalExpression`、`ArrayExpression`、`DictionaryExpression`、`PreloadExpression`、`GetNodeExpression`、`CastExpression`、`TypeTestExpression` + - 当前首批显式封口的 `assert`、`ForStatement`、`ConditionalExpression`、`ArrayExpression`、`DictionaryExpression`、`PreloadExpression`、`GetNodeExpression`、`CastExpression`、`TypeTestExpression` - compile surface 上 `expressionTypes()` / `resolvedMembers()` / `resolvedCalls()` 中仍残留的 `BLOCKED` / `DEFERRED` / `FAILED` / `UNSUPPORTED` - supported callable-local `var` 因 `sema.variable_slot_publication` warning 仍缺失 `slotTypes()` 的 lowering-only fact 缺洞 - `assert` 在这里仍只是 compile-only blocked;共享 type-check 继续保留 Godot-compatible condition contract,不把它回退成 strict-bool `sema.type_check` + - `ForStatement` 已进入 shared semantic;当前 blocker 只锚定 statement root,不读取 iterable typed fact/iteration plan,也不进入 body,直到 route-aware CFG/lowering 接通 - 上述 7 类表达式属于 frontend 已识别但 lowering 尚未接通的 temporary compile intercept,不代表 parser / grammar / shared semantic 路径已经把它们判成不支持语法 - `ConditionalExpression` 当前单独被列入这份清单,是因为真正的 lowering 需要等 frontend CFG graph / condition-evaluation-region 合同冻结后再接通;现有 metadata-only `FrontendLoweringCfgPass` 仍属于过渡层 - `DYNAMIC` 不属于 compile blocker;它保留为 frontend 已接受的 runtime-open 事实,而不是 lowering 未实现状态 @@ -330,6 +331,7 @@ deferred / unsupported diagnostics 一律通过 `DiagnosticManager` 发布。 - `FrontendSemanticAnalyzer` 当前返回 `FrontendAnalysisData` - analyze 流程围绕同一份共享分析数据推进 +- interface/body suite resolver 路径会在每个 body statement boundary 刷新 `FrontendAnalysisData.diagnostics()`,让后一 statement 能读取 current-suite upstream diagnostic snapshot;suite export 在 patch transaction 应用后保留最终 body snapshot - analyze 现在已经具备独立的多 phase 主链路: - skeleton 结束后先发布 `updateModuleSkeleton(...)` - 再发布一次 pre-scope `updateDiagnostics(...)` @@ -348,6 +350,7 @@ deferred / unsupported diagnostics 一律通过 `DiagnosticManager` 发布。 - `analyzeForCompile(...)` 在共享 11 phase 之后追加: - 调用 `FrontendCompileCheckAnalyzer.analyze(...)` - 再次 `updateDiagnostics(...)`,把 compile-only final gate 的诊断写回最终边界快照 +- compile gate 运行前仍要求 stable diagnostics boundary 已发布,但 duplicate suppression 会冻结 live `DiagnosticManager.snapshot()`,避免 interface/body upstream diagnostics 因调用方尚未再次复制到 `FrontendAnalysisData` 而被漏看 - 共享 `analyze(...)` 的结果当前仍只是 frontend semantic snapshot,不应直接视为 lowering-ready - inspection 与未来 LSP 入口继续消费共享 `analyze(...)`,不应被 `sema.compile_check` 污染 - 未来 frontend -> LIR lowering 调用方必须: diff --git a/doc/module_impl/frontend/frontend_chain_binding_expr_type_implementation.md b/doc/module_impl/frontend/frontend_chain_binding_expr_type_implementation.md index a9775cc4..02a2aab2 100644 --- a/doc/module_impl/frontend/frontend_chain_binding_expr_type_implementation.md +++ b/doc/module_impl/frontend/frontend_chain_binding_expr_type_implementation.md @@ -1,11 +1,14 @@ # FrontendChainBinding / ExprType 实现说明 -> 本文档作为 `FrontendChainBindingAnalyzer`、`FrontendExprTypeAnalyzer` 及其共享 body-phase support 的长期事实源,定义当前 phase 顺序、side-table / diagnostic owner 边界、局部 chain reduction 架构、已冻结的 published contract,以及后续工程必须遵守的 fail-closed 边界。本文档替代旧的规划性文档与验收流水账,不再保留进度记录或阶段日志。 +> 本文档作为 `FrontendBodyOwnerProcedures` 中 chain-binding、expression-typing owner 及其 +> 共享 body-phase support 的长期事实源,定义当前 phase 顺序、side-table / diagnostic owner +> 边界、局部 chain reduction 架构、已冻结的 published contract,以及后续工程必须遵守的 +> fail-closed 边界。 ## 文档状态 -- 状态:事实源维护中(`resolvedMembers()` / `resolvedCalls()` / `expressionTypes()`、shared expression semantic support、unary/binary expression semantics、class property initializer support island、subscript / assignment typed contract、explicit self assignment-target prefix publication、`:=` 局部类型稳定化与 expr-owned diagnostics 已落地) -- 更新时间:2026-06-27 +- 状态:事实源维护中(`resolvedMembers()` / `resolvedCalls()` / `expressionTypes()`、SuiteResolver statement-local owner procedures、for header/body dispatch、typed overlay-aware expression semantics、property initializer support island与 expr-owned diagnostics 已落地) +- 更新时间:2026-07-20 - 适用范围: - `src/main/java/gd/script/gdcc/frontend/sema/**` - `src/main/java/gd/script/gdcc/frontend/sema/analyzer/**` @@ -31,7 +34,8 @@ - 不引入 whole-module fixpoint,不把 body 语义改造成多轮全局收敛 - 不新增新的全局 side table,也不让已有 side table 互相越权 - 不把 `FrontendBinding` 重塑为 usage-aware 模型 - - 不在这里转正 parameter default、lambda、`for`、`match`、block-local `const`、class constant 的正式 body 语义 + - 不在这里转正 parameter default、lambda、`match`、block-local `const`、class constant 的正式 body 语义 + - 不在这里实现 for iteration planning、iterator slot refinement 或 lowering route classification - 不在这里扩张 keyed builtin、numeric promotion 或其它 typed-boundary 兼容矩阵;`StringName` / `String` 互转由 `frontend_implicit_conversion_matrix.md` 与 shared boundary helper 独立管理 --- @@ -40,7 +44,7 @@ ### 1.1 主链路位置 -当前 `FrontendSemanticAnalyzer` 的稳定顺序是: +当前 production `FrontendSemanticAnalyzer` 的稳定顺序是: 1. `FrontendClassSkeletonBuilder.build(...)` 2. `analysisData.updateModuleSkeleton(...)` @@ -49,21 +53,18 @@ 5. `analysisData.updateDiagnostics(...)` 6. `FrontendVariableAnalyzer.analyze(...)` 7. `analysisData.updateDiagnostics(...)` -8. `FrontendTopBindingAnalyzer.analyze(...)` -9. `analysisData.updateDiagnostics(...)` -10. `FrontendLocalTypeStabilizationAnalyzer.analyze(...)` -11. `analysisData.updateDiagnostics(...)` -12. `FrontendChainBindingAnalyzer.analyze(...)` -13. `analysisData.updateDiagnostics(...)` -14. `FrontendExprTypeAnalyzer.analyze(...)` -15. `analysisData.updateDiagnostics(...)` +8. `FrontendInterfacePhase.analyze(...)` +9. `FrontendSuiteResolver.resolve(...)`,内部按 statement root 固定执行 top binding -> local type stabilization -> chain binding -> expr typing -> var type post +10. `analysisData.updateDiagnostics(...)` 这意味着: -- `FrontendLocalTypeStabilizationAnalyzer` 运行在 top binding 之后、chain binding 之前,只回写符合条件的 `:=` local `BlockScope` slot -- `FrontendChainBindingAnalyzer` 只运行在 skeleton、scope graph、variable inventory、`symbolBindings()` 与局部类型稳定化结果已发布之后 -- `FrontendExprTypeAnalyzer` 只运行在 `symbolBindings()`、`resolvedMembers()` 与当前 `resolvedCalls()` published surface 已发布之后 -- body phase 仍保持“先发布 member/call,再发布 expression type”的顶层边界,不回头重开更早 phase +- production body facts 只通过 SuiteResolver 的 `FrontendTypedLexicalEnvironment` overlay 与 per-owner patch transaction 导出 +- chain binding owner procedure 只在当前 statement root 内消费已发布 / pending 的 binding 与 stabilized local slot fact +- expr typing owner procedure 只在 chain-owned member/call facts 已对当前 root 可见后发布 expression facts 与 bare-call facts +- 阶段 K 已删除 standalone chain/expr whole-module analyzer 与 window shim;focused tests + 直接覆盖 `FrontendBodyOwnerProcedures`、typed overlay 与 per-owner patch export,不再维护 + comparison publication path ### 1.2 当前 owner 边界 @@ -479,12 +480,13 @@ writable / compatibility 规则为: - parameter default - lambda subtree -- `for` subtree - `match` subtree - block-local `const` - class constant - scope-local 手动 `type-meta` +`ForStatement` 已使用 header-only statement boundary:iterator type 与 iterable expression 在外层 lexical context 中运行 owner procedures,flush 后通过普通 child-suite path 解析 `FOR_BODY`。For body 中的 ordinary expressions 与 locals 复用本合同;typed result 不能决定是否进入 body。Iteration plan 与 iterator exact refinement 属于后续独立 owner,不由 chain/expr owner 猜测。 + 当前 remaining explicit-deferred expression set 固定为: - `ConditionalExpression` diff --git a/doc/module_impl/frontend/frontend_compile_check_analyzer_implementation.md b/doc/module_impl/frontend/frontend_compile_check_analyzer_implementation.md index fde6dd65..e0bd03a7 100644 --- a/doc/module_impl/frontend/frontend_compile_check_analyzer_implementation.md +++ b/doc/module_impl/frontend/frontend_compile_check_analyzer_implementation.md @@ -4,8 +4,8 @@ ## 文档状态 -- 状态:事实源维护中(compile-only final gate、显式 AST 封口、generic published-fact blocker、explicit self assignment-target prefix 去重、shared/compile 分流边界、unary/binary 非 blocker 合同已落地) -- 更新时间:2026-04-26 +- 状态:事实源维护中(compile-only final gate、for statement-root 临时 blocker、显式 AST 封口、generic published-fact blocker、shared/compile 分流边界与 SuiteResolver stable facts 已落地) +- 更新时间:2026-07-20(阶段 L:for shared semantic 已解封,compile-only 仅保留 root blocker) - 适用范围: - `src/main/java/gd/script/gdcc/frontend/sema/**` - `src/main/java/gd/script/gdcc/frontend/sema/analyzer/**` @@ -39,11 +39,12 @@ 1. `analyze(...)` - 共享 frontend 语义入口 - - 负责发布 8 个稳定 frontend phase 的 semantic facts + - 负责发布 skeleton/scope/variable、interface/body suite、shared semantic publication 与 diagnostics-only phase 的 frontend facts / diagnostics snapshots + - body semantic facts 只来自 `FrontendSuiteResolver` 的 per-owner patch transaction;shared analyzer 不再提供 legacy whole-phase body publication bypass - 不保证 lowering-ready 2. `analyzeForCompile(...)` - compile-only 入口 - - 先运行共享 8 phase + - 先运行共享 semantic pipeline - 再运行 `FrontendCompileCheckAnalyzer` - 最后刷新最终 diagnostics snapshot @@ -54,6 +55,7 @@ inspection 与未来 LSP 必须继续消费共享 `analyze(...)`,而不是隐 `FrontendCompileCheckAnalyzer` 当前只负责 diagnostics-only final gate: - 读取已经发布的 frontend 事实 +- 读取 compile-gate 入口处冻结的 live `DiagnosticManager` snapshot 作为 upstream duplicate-suppression 输入 - 对 compile mode 仍不可接受的 surface 发出 `sema.compile_check` - 不创建新的 side table - 不改写已有 side table @@ -99,12 +101,11 @@ compile gate 可以沿 callable body 和支持岛 property initializer 继续递 - parameter default - lambda subtree -- `for` subtree - `match` subtree - block-local `const` - missing-scope / skipped subtree -这条边界的目的不是“少报错”,而是避免 compile gate 把已经被上游明确封口的恢复域重新打平成 lowering surface。 +这条边界的目的不是“少报错”,而是避免 compile gate 把已经被上游明确封口的恢复域重新打平成 lowering surface。`ForStatement` 不属于该跳过集合:它已进入 shared semantic,compile gate 会命中 statement root,但不会进入 body 重扫 facts。 --- @@ -112,7 +113,7 @@ compile gate 可以沿 callable body 和支持岛 property initializer 继续递 ### 3.1 statement 级封口 -`AssertStatement` 当前由 compile gate 显式拦截,并直接发出 `sema.compile_check` `error`。 +`AssertStatement` 与 `ForStatement` 当前由 compile gate 显式拦截,并直接发出 `sema.compile_check` `error`。 这里需要同时保持两条事实: @@ -121,6 +122,14 @@ compile gate 可以沿 callable body 和支持岛 property initializer 继续递 因此,`assert` 的 compile-only block 只表达“lowering/backend 尚未接通”,而不是 source contract 已被收紧。 +`ForStatement` 使用更窄的临时 bridge: + +- blocker 只锚定 owning `ForStatement` root。 +- 不进入 body,不重新扫描 iterator/body 已发布的 semantic facts。 +- 不读取 iterable type、iteration plan、route readiness 或 diagnostic-derived state。 +- shared `analyze(...)` 不包含该 blocker;只有 `analyzeForCompile(...)` 会阻止 for 进入尚未支持的 CFG/lowering。 +- 后续 route-aware compile policy 只能替换该 blocker,不能控制 iterator inventory、completeness certificate 或 child-suite dispatch。 + ### 3.2 declaration 级封口 脚本类 `static var` declaration 当前同样由 compile gate 显式拦截,并直接发出 @@ -335,6 +344,7 @@ compile gate 当前统一使用: 当前“已有 upstream error”按以下条件判定: +- upstream 诊断集合来自 compile gate 入口处冻结的 live `DiagnosticManager.snapshot()`,而不是之后会被本 gate 继续追加的 mutable manager - 同一 `sourcePath` - 同一 `FrontendRange` - severity 为 `ERROR` @@ -371,6 +381,7 @@ compile gate 当前统一使用: 这条规则同样适用于: - `assert` +- `ForStatement` - `ConditionalExpression` - `ArrayExpression` - `DictionaryExpression` @@ -397,6 +408,7 @@ compile gate 当前统一使用: - `DYNAMIC` 不误判为 blocker - `ConditionalExpression` 只在 compile-only 路径被拦截,不污染 shared analyze - `assert` 继续保持 shared condition contract,只在 compile-only 路径被拦截 + - for shared semantic 正常发布 body facts,compile-only 路径只产生单一 statement-root blocker且不扫描 body - `FrontendSemanticAnalyzerFrameworkTest` - `analyze(...)` 与 `analyzeForCompile(...)` 的分离 - compile gate 在 type-check 之后执行 @@ -415,7 +427,7 @@ compile gate 当前统一使用: - frontend -> LIR lowering 入口必须强制使用 `analyzeForCompile(...)` - lowering 在继续前必须检查 `diagnostics().hasErrors() == false` -- `assert` 与 8 类显式拦截表达式的真正 lowering/backend 支持仍待后续阶段补齐 +- `assert`、for route-aware CFG/lowering 与 8 类显式拦截表达式的真正 lowering/backend 支持仍待后续阶段补齐 若未来需要为 LSP 单独呈现 compile-only blocker,正确方向仍是: diff --git a/doc/module_impl/frontend/frontend_for_range_loop_implementation_plan.md b/doc/module_impl/frontend/frontend_for_range_loop_implementation_plan.md new file mode 100644 index 00000000..66beee62 --- /dev/null +++ b/doc/module_impl/frontend/frontend_for_range_loop_implementation_plan.md @@ -0,0 +1,848 @@ +# Frontend for-in loop 实施计划 + +> 本文档是 `for iterator[: Type] in expr` 的长期实施事实源,定义 shared semantic / compile / lowering 三层支持面的架构合同、核心设计与分阶段实施步骤。不再保留已完成阶段的验收流水账。 + +## 文档状态 + +- 状态:实施中(shared semantic 结构支持已完成;bare `range(...)` header 预路由、iteration plan、CFG、lowering 尚未实施) +- 创建日期:2026-07-03 +- 更新时间:2026-07-23 +- 适用范围: + - `src/main/java/gd/script/gdcc/frontend/sema/**` + - `src/main/java/gd/script/gdcc/frontend/lowering/**` + - `src/main/java/gd/script/gdcc/lir/**` + - `src/main/java/gd/script/gdcc/backend/c/**` + - `src/main/java/gd/script/gdcc/type/**` + - `src/main/c/codegen/**` + - `src/test/java/gd/script/gdcc/frontend/**` + - `src/test/java/gd/script/gdcc/backend/**` + - `src/test/java/gd/script/gdcc/type/**` +- 关联事实源: + - `doc/module_impl/common_rules.md` + - `doc/module_impl/frontend/frontend_rules.md` + - `doc/module_impl/frontend/frontend_resolution_pipeline_implementation.md` + - `doc/module_impl/frontend/frontend_variable_analyzer_implementation.md` + - `doc/module_impl/frontend/frontend_visible_value_resolver_implementation.md` + - `doc/module_impl/frontend/frontend_compile_check_analyzer_implementation.md` + - `doc/module_impl/frontend/frontend_lowering_plan.md` + - `doc/module_impl/frontend/frontend_lowering_cfg_pass_implementation.md` + - `doc/module_impl/frontend/frontend_lowering_func_pre_pass_implementation.md` + - `doc/module_impl/frontend/frontend_gdcompiler_type_implementation.md` + - `doc/module_impl/backend/variant_abi_contract.md` + - `doc/module_impl/backend/typed_array_abi_contract.md` + - `doc/module_impl/backend/typed_dictionary_abi_contract.md` + - `doc/gdcc_type_system.md` + - `doc/gdcc_lir_intrinsic.md` + - `doc/gdcc_runtime_lib.md` +- 外部语义参考: + - Godot docs `tutorials/scripting/gdscript/gdscript_basics.rst` 的 `for` 语义说明 + - Godot source `modules/gdscript/gdscript_analyzer.cpp` 的 `GDScriptAnalyzer::resolve_for(...)` + - Godot source `modules/gdscript/gdscript_compiler.cpp` 的 for-statement lowering path + - Godot source `modules/gdscript/gdscript_byte_codegen.cpp` 的 `write_for(...)` + - Godot source `modules/gdscript/gdscript_vm.cpp` 的 `OPCODE_ITERATE*` + - Godot source `core/variant/variant_setget.cpp` 的 `Variant::iter_init` / `iter_next` / `iter_get` + - Godot GDExtension API `variant_iter_init` / `variant_iter_next` / `variant_iter_get` + +- 明确非目标: + - 不在这里定义 generic Variant iterator route 的 runtime helper 或 C backend 实现细节 + - 不在这里定义 known iterable 专用 route 的 helper 准备度 + - 不在这里改变 Godot range runtime 语义 + +## 1. 范围与非目标 + +本计划把 `for-in` 拆成两个互不混淆的支持面: + +- shared semantic 支持面:所有 `for iterator[: Type] in expr` 都是 supported body statement。 +- compile / lowering 支持面:最终所有 `for-in` 都通过 iteration plan 进入 lowering;实现上可以先接通 range route,再接通 generic Variant route,再追加 known iterable 专用 route。 + +shared semantic 第一轮必须支持: + +- `for i in range(stop):` +- `for i in range(start, end):` +- `for i in range(start, end, step):` +- `for i in 3:`、`for i in limit:`、`for i in values:`、`for i in some_object:` 等任意 iterable expression 形态。 +- `for i: Type in expr:` 显式 iterator type。 +- body 内 iterator local lookup、body local `var` inventory、source-order local stabilization、`break` / `continue` legality。 + +compile / lowering 最终目标必须覆盖: + +- `range(...)` 专用 route,复用现有 `gdcc.for_range_iter.*` intrinsic 与 `GdccForRangeIterType`。 +- 编译期可确定的 known iterable route,例如 `int` 数值简写、`String`、`Array`、`Dictionary`、packed array family。每个 route 是否第一轮落地由对应 helper 与 ABI 准备度决定。 +- 编译期不能确定 iterable 类型时的 generic Variant iterator route,运行时通过 Godot Variant iteration API 或 gdcc runtime wrapper 分派。 + +本轮明确不做的事情: + +- 不把 `range(...)` 当作 ordinary user-facing callable route;它在 `for` iterable 位置仍是 loop-specific form,不向普通 `resolvedCalls()` 发布 utility function route。 +- 不把 `GdccForRangeIterType` 或未来 compiler-only iterator state type 发布到 ordinary `expressionTypes()`、source-facing `slotTypes()`、declared type parser、public ABI、`Variant` pack/unpack 或普通 Godot runtime 调用路径。 +- 不通过 AST rewrite 把 `for i in limit:`、`for i in 3:` 伪造成 `range(...)` call。 +- 不在 frontend semantic 阶段强制证明任意 `expr` 一定可迭代;无法静态确定时保留 `Variant` iterator type,并让 generic runtime helper 处理 Godot 运行时语义。 +- 不在第一批 known-route 中强行复刻所有 Godot 特例。`float` 数值简写、`Vector2` / `Vector3` 迭代、Object `_iter_*` 专用优化可以在 generic route 之后逐步转成专用 route。 +- 不恢复已删除的 whole-module body analyzers;所有 typed fact 仍必须通过 `SuiteResolver`、statement-local owner procedures 与 per-owner patch transaction 发布。 +- 不把 hidden iterator state 编码成普通 CFG value id、`CfgValueMaterializationKind`、`MergeValueItem` result 或 `operandValueIds()` entry。 +- 不用 `sourceOrder == 0` 推导 runtime slot id。该值只表示每个 `FOR_BODY` declaration inventory 中 iterator 是 synthetic 第 0 项。 + +## 2. 当前基线 + +已完成: + +- `gdparser` 的 `ForStatement` AST 已包含 `iterator`、`iteratorType`、`iterable`、`body`、`range` 字段;其中 `range()` 是 AST source-location anchor,不是 `range(...)` builtin classifier,pre-route 必须检查 `iterable` expression shape。 +- `FrontendScopeAnalyzer` 已为 `ForStatement` 建立 `FOR_BODY` scope,`iteratorType` 与 `iterable` 在外层 scope 下遍历,`body` 在独立 `FOR_BODY` scope 下遍历。 +- `FrontendLoopControlFlowAnalyzer` 已把 `for` 视为 loop boundary,`break` / `continue` 在 `for` body 内合法。 +- `FrontendVariableAnalyzer` 已无条件发布 iterator 与 for body ordinary local inventory;`FrontendInterfacePhase` 已发布 iterator declaration index、typed baseline 与 suite entry;`FrontendStatementResolver` 已通过 header-only statement boundary 进入普通 child-suite path;`FrontendVisibleValueResolver` 已允许 for header/body ordinary lookup。 +- `FrontendBodyLocalDeclaration` 与 `FrontendBodyDeclarationIndex` 已支持 `Node` declaration identity,以 `ForStatement` 作为 `ITERATOR` entry identity。 +- `FrontendBodySemanticSupportPolicy` 已将 `FOR_BODY` 映射为 `EXECUTABLE_BODY`;`FrontendBodyStructuralCompleteness` 已实现 `FOR_BODY` 双向校验(iterator entry 位于 sourceOrder==0)。 +- `GdccForRangeIterType.FOR_RANGE_ITER` 已作为 compiler-only `GdCompilerType` 子类型存在,backend 与 runtime helper 已有对应实现。 +- `doc/gdcc_lir_intrinsic.md` 已冻结四个 range intrinsic:`gdcc.for_range_iter.init`、`gdcc.for_range_iter.should_continue`、`gdcc.for_range_iter.next`、`gdcc.for_range_iter.get`。 + +尚未实施: + +- `FrontendStatementResolver.resolveForStatement(...)` 仍无条件对整个 `forStatement.iterable()` 调用 ordinary `runSupportedRoot(...)`。bare `range(...)` 缺少专用预路由,canonical `for i in range(3)` 会为 callee 发布 unknown binding 并让 call root expression typing 失败。 +- `FrontendForIterationPlan`、`FrontendForIterationRoute` 不存在;iteration planning owner 未实现。 +- `FrontendCompileCheckAnalyzer` 对每个 `ForStatement` root 发布临时、无条件的 `sema.compile_check` blocker。 +- `FrontendCfgGraphBuilder.processStatement(...)` 没有 `ForStatement` 分支;`FrontendCfgRegion` 只允许 `BlockRegion`、`FrontendIfRegion`、`FrontendElifRegion`、`FrontendWhileRegion`。 +- `FrontendCfgGraphBuilder.ExecutableBodyBuild` 与 `FunctionLoweringContext` 没有 compiler-only hidden-local registry。 +- generic Variant iterator helper、typed container iterator helper、Object `_iter_*` helper 尚未实现。 + +实施目标:所有 `for-in` 在 shared semantic 中都是 supported body;iteration plan 决定 iterator type refinement 与 lowering route;compile surface 按 route helper 准备度分阶段打开。 + +## 3. 核心设计 + +### 3.1 supported body 与 route classification 解耦 + +`for` body 是否进入 shared semantic 不再依赖 iterable 的最终 typed fact。早期 inventory 阶段无条件发布 for body inventory: + +- iterator binding 先进入 `FOR_BODY` scope。 +- body 内 ordinary local `var` 按完整 lexical inventory 模型发布。 +- `FrontendInterfacePhase` 为 for body 建立 body declaration index 与 typed baseline。 +- `FrontendVisibleValueResolver` 不再把 `FOR_BODY` 固定判定为 `FOR_SUBTREE` deferred boundary。 + +iterable typed fact 只影响两个后续事实: + +- iterator 的 source-facing effective slot type 是否能从 `Variant` 精化为 exact element type。 +- lowering 使用哪个 `FrontendForIterationPlan` route。 + +示例: + +```gdscript +func f(): + var limit := 3 + for i in limit: + var x := i + 1 +``` + +正确顺序是: + +1. Variable inventory 发布 `limit` 与 `i`。`i` 的 baseline 是 `Variant`,或显式 iterator type。 +2. `SuiteResolver` 解析 `var limit := 3`,把 `limit` 稳定为 `int` 并 flush 到 current-suite overlay。 +3. `SuiteResolver` 解析 for header `limit`,构造 `FrontendForIterationPlan(INT_RANGE_SHORTHAND)`。 +4. iteration planning owner 把 `i` 从 `Variant` 精化为 `int`。 +5. child body resolver 进入 body,`var x := i + 1` 读取到 `i:int`。 + +反例也必须成立: + +```gdscript +func f(values): + for item in values: + print(item) +``` + +如果 `values` 没有静态 element type,body 仍正常解析;`item` 保持 `Variant`,iteration plan 是 `GENERIC_VARIANT`,lowering 走 runtime helper。 + +### 3.2 iterator declaration identity + +Iterator 不是 ordinary `VariableDeclaration`,不能伪造 AST local declaration。计划采用 `ForStatement` 自身作为 iterator declaration identity: + +- `BlockScope.defineLocal(iteratorName, baselineType, forStatement)` 发布 iterator binding。 +- `ScopeValue.kind()` 使用 `LOCAL`,使 assignment、lookup、slot update 与普通 local 共用路径。 +- `ScopeValue.declaration()` 必须严格等于 owning `ForStatement`,以便 local slot update 的 declaration identity guard 生效。 +- iterator 在 body 中从第一条 statement 开始可见,在 loop 后不可见。 +- iterator 和 body ordinary local 使用同一个 duplicate / shadowing 规则,不能与同 callable 内已有 parameter/local 静默重名。 + +因此需要把 body inventory index 从“ordinary `VariableDeclaration` index”扩展为“body local inventory index”: + +```java +enum FrontendBodyLocalDeclarationKind { + ITERATOR, + ORDINARY_VAR +} + +record FrontendBodyLocalDeclaration( + @NotNull Node declaration, + @NotNull ScopeValue binding, + @NotNull FrontendBodyLocalDeclarationKind kind, + int sourceOrder +) {} +``` + +约束: + +- `ORDINARY_VAR` 的 `declaration` 仍必须是 `VariableDeclaration`,`sourceOrder >= 0`(在 `FOR_BODY` 中 iterator 占用 `0`,因此 ordinary local 从 `sourceOrder >= 1` 开始连续编号)。 +- `ITERATOR` 的 `declaration` 必须是 owning `ForStatement`,作为 synthetic 第 0 项固定使用 `sourceOrder == 0`,位于 body declaration list 头部、在所有 ordinary statement 之前可见;不使用独立前置 sentinel(`sourceOrder` 禁止负数)。 +- `FrontendBodyDeclarationIndex` 必须支持按 `Object` / `Node` declaration identity 查询,而不是只接受 `VariableDeclaration`。 +- `FrontendVisibleValueResolver` 的 published inventory guard 必须覆盖 iterator local,不能让 iterator 成为绕过 declaration index 的 side channel。 + +### 3.3 iterator baseline 与 slot refinement + +无显式 iterator type 时,iterator baseline 一律是 `GdVariantType.VARIANT`。这样才能复用现有 local slot “从保守类型到 exact type”的模型。 + +显式 iterator type 时,baseline 是 declared type: + +```gdscript +for i: float in range(3): + print(i) +``` + +此时 body 中 `i` 的 source-facing type 是 `float`,iteration plan 的 raw element type 仍是 `int`。type-check 阶段必须验证 raw element 可以进入 explicit iterator type;lowering 必要时插入 per-element conversion。 + +无显式 type 时: + +- `range(...)`、`int` 数值简写 -> raw element type 为 `int`,iterator 可精化为 `int`。 +- typed `Array[T]` -> raw element type 为 `T`,iterator 可精化为 `T`。 +- typed `Dictionary[K, V]` -> Godot 语义迭代 key,raw element type 为 `K`。 +- plain `Dictionary`、unknown `Variant`、Object custom iterator -> raw element type 为 `Variant`,iterator 保持 `Variant`。 + +当前 `FrontendTypedLexicalEnvironment.addLocalSlotTypeUpdate(...)` 只允许 `LOCAL_TYPE_STABILIZATION` owner 写 local slot update,并拒绝 exact -> exact 改写。for iteration planning 需要显式扩展: + +- 新增 `FrontendSemanticStage.FOR_ITERATION_PLANNING` 或等价 stage。 +- 新增 `FrontendForIterationPlanningPatch`,或允许该 stage 发布受限的 `FrontendLocalSlotTypeUpdate`。 +- 校验规则仍必须保持:只允许当前 effective local 是 `Variant` 时精化为 exact type;显式 exact iterator type 不得被自动改写为另一个 exact type。 +- patch transaction 顺序扩展为 top binding -> local stabilization -> chain binding -> expr typing -> for iteration planning -> var type post。 + +### 3.4 `FrontendForIterationPlan` + +新增 stable frontend fact,key 为 `ForStatement`。建议命名为 `FrontendForIterationPlan`,由 SuiteResolver header owner 发布,供 type-check、compile gate、CFG builder 与 lowering 消费。 + +建议形状: + +```java +enum FrontendForIterationRoute { + RANGE_CALL, + INT_SHORTHAND, + FLOAT_SHORTHAND, + STRING, + ARRAY, + DICTIONARY_KEYS, + PACKED_ARRAY, + OBJECT_CUSTOM, + GENERIC_VARIANT +} + +record FrontendForIterationPlan( + @NotNull ForStatement statement, + @NotNull FrontendForIterationRoute route, + @NotNull String iteratorName, + @Nullable TypeRef declaredIteratorType, + @NotNull GdType rawElementType, + @NotNull GdType exposedIteratorType, + boolean requiresPerElementConversion, + @NotNull List sourceOperands, + @Nullable GdCompilerType iteratorStateType, + @NotNull List operationNames +) {} +``` + +约束: + +- `rawElementType` 是 runtime helper / intrinsic 产出的元素类型。 +- `exposedIteratorType` 是 body 中 iterator local 的 source-facing type。 +- `iteratorStateType` 只允许出现在该 dedicated iteration plan 与 lowering-internal state,不得进入 ordinary expression / slot / binding tables。任何被 compile gate 放行的 route 都必须具有 non-null compiler-only state type;尚无 state contract 的 route 必须保持 route-not-ready。 +- `operationNames` 由 route contract 提供,consumer 不得在 CFG builder / lowering processor 中散落硬编码 intrinsic 名称。 +- `sourceOperands` 保留源码 expression,不伪造 AST。 + +`range(...)` route 第一版使用已有 contract: + +- `iteratorStateType = GdccForRangeIterType.FOR_RANGE_ITER` +- `rawElementType = GdIntType.INT` +- `operationNames = gdcc.for_range_iter.init / should_continue / next / get` + +generic Variant route 需要新增 contract: + +- `iteratorStateType` 使用新的 compiler-only generic iterator state type。generic route 在该类型及其 lifecycle/intrinsic contract 冻结前不得进入 compile-ready surface。 +- `rawElementType = GdVariantType.VARIANT` +- `operationNames` 使用新 intrinsic,例如 `gdcc.for_variant_iter.init / should_continue / next / get`,具体名称在 intrinsic catalog 阶段冻结。 + +### 3.5 dedicated hidden iterator state slot + +loop-carried iterator state 是 lowering-owned mutable storage,不是 source expression value。每个 compile-ready `ForStatement` 必须在 frontend CFG build artifact 中发布一个 `FrontendForIteratorStateSlot` 或等价 immutable metadata,key 使用 owning `ForStatement` identity。建议形状: + +```java +record FrontendForIteratorStateSlot( + @NotNull ForStatement statement, + @NotNull String slotId, + @NotNull GdCompilerType stateType +) {} +``` + +第一版稳定命名为 `cfg_for_iter_`,其中序号由单个 executable-body CFG build 按 source traversal order 分配。该 metadata 与 graph/regions 一起复制并发布到 `FunctionLoweringContext`,由 body lowering session 声明对应 LIR function local。它不是 semantic published fact,不进入 `FrontendAnalysisData`;route、type 与 operation names 仍以 `FrontendForIterationPlan` 为唯一 sema 事实源。 + +隐藏槽必须遵守: + +- `slotId` 不是 CFG value id;不得出现在 `ValueOpItem.resultValueIdOrNull()`、`operandValueIds()`、value producer map、`cfg_tmp_*` / `cfg_merge_*` materialization collection 中。 +- 不新增 `HIDDEN_COMPILER_STATE` 一类 `CfgValueMaterializationKind`。value materialization 只负责 CFG value;hidden mutable local 由独立 registry 声明和验证。 +- 每个 `FrontendForRegion` 恰好引用一个 hidden slot;每个 hidden slot 恰好归属一个 `ForStatement` / `FrontendForRegion`。 +- 同一 executable body 内 `slotId` 唯一,nested/sibling loops 不得复用;slot type 必须严格等于对应 plan 的 non-null `iteratorStateType`。 +- range/int route 的 state type 是 `GdccForRangeIterType.FOR_RANGE_ITER`,只允许进入 dedicated plan、hidden-slot metadata、LIR function local、intrinsic argument/result 与 backend storage。 +- source-facing iterator local 仍以 `ForStatement` 为 declaration identity,类型来自 final `slotTypes()[ForStatement]`;hidden slot 与 source local 不共享 id、type 或 lifecycle。 + +CFG item 对 hidden slot 的访问是显式字段合同,而不是 ordinary operand: + +```text +ForLoopInitItem: + ordinary operands = range/int source value ids + hidden effect = initialize iteratorStateSlotId + ordinary result = none + +ForLoopShouldContinueItem: + hidden read = iteratorStateSlotId + ordinary result = bool condition value id + +ForLoopGetItem: + hidden read = iteratorStateSlotId + ordinary result = raw element value id + source effect = convert if required, then commit to iterator local + +ForLoopNextItem: + hidden read/write = iteratorStateSlotId + ordinary result = none +``` + +四个 for-loop item 都实现 `ValueOpItem`,并加入 `ValueOpItem` sealed permits;`SequenceItem` 已 permits `ValueOpItem`,无需为每个 item 单独扩展。`ForLoopInitItem` 与 `ForLoopNextItem` 必须返回 `null` result、`hasStandaloneMaterializationSlot() == false`,且只把真正的 source operands 放入 `operandValueIds()`。`ForLoopShouldContinueItem` 与 `ForLoopGetItem` 的 bool/raw result 仍是 ordinary single-definition CFG value。 + +next intrinsic 返回一个新 state,不是 in-place mutation。阶段 H 必须使用 distinct lowering-owned temp: + +```text +cfg_for_iter_next_ = next(cfg_for_iter_) +cfg_for_iter_ = cfg_for_iter_next_ +``` + +第二步使用现有 `AssignInsn` lifecycle path。不得直接生成 `cfg_for_iter_ = next(cfg_for_iter_)`;当前 range state 虽然是 direct-assign-safe POD 且 destroy 为 no-op,generic state 未来可能包含 destroyable resource,计划不能依赖该偶然事实。 + +build artifact 构造时必须执行跨表验证,而不是只在 processor 中 fail fast: + +- hidden-slot metadata 的 key、`statement`、region owner 必须 identity 一致。 +- init/condition/body/update sequence 中的四个 item 必须引用同一 slot。 +- init item 必须位于首次 condition 前;next item 必须位于 update entry,并在 backedge 前提交。 +- should-continue 结果必须是 `bool`,condition branch 必须消费该 ordinary value id。 +- get item 必须在 body statements 前提交 source-facing iterator local。 +- 缺失 metadata、重复 slot id、state type mismatch、跨 nested-loop slot 引用、把 slot id 混入 value-id surface 均为 graph construction error。 + +### 3.6 `range(...)` header 预路由与 ordinary call route 隔离 + +`range(...)` 在 `for` iterable 位置是 loop-specific syntax form: + +- `resolveForStatement(...)` 必须在对 iterable 调用 ordinary owner pipeline 前按 AST shape 识别 bare `IdentifierExpression("range")` call。该预路由不能等待 expr typing 后的 iteration planning,因为届时 unknown callee / failed call facts 已进入 pending overlay,现有 transaction 没有删除或覆盖这些 facts 的协议。 +- bare `range(...)` 的预路由识别与参数合法性验证分离:只要 callee 是 bare `IdentifierExpression("range")` 就先绕开 ordinary call root;1/2/3 positional arguments 才能形成有效 `RANGE_CALL` plan,错误 arity / argument form 由后续 range-specific type-check 诊断。 +- 命中预路由后,只按 source order 对 arguments 分别运行 top binding、local stabilization、chain binding、expr typing 与必要的 var type post。callee identifier 与 call root 都不进入 ordinary top-binding / call-resolution / expr-type publication 路径。 +- `range(...)` root 不发布 ordinary `resolvedCalls()`,也不发布 ordinary `expressionTypes()`;arguments 必须各自拥有 planning/type-check 所需的 expression type,并进入既有 `int` boundary。 +- attribute call、subscript call、`some_range(...)`、`obj.range(...)` 不触发该预路由,继续把整个 iterable 交给 ordinary `runSupportedRoot(...)`。named argument 是否被 parser 表达为 bare call argument form,不改变预路由;它必须由 range-specific validation 拒绝,而不是退回 unknown ordinary callee 诊断。D0 pre-route 仍按 source order 对 named argument 的 value expression 运行 owner procedures;named-ness 本身在阶段 E range-specific validation 中拒绝,不影响 D0 发布 argument expression facts。 +- range arguments、显式 iterator type 与后续 iteration plan facts 仍共享一个 for-statement header flush;不得为每个 argument 单独建立 statement boundary。 +- literal `step == 0` 应在 frontend 发 diagnostic;非 literal step 交给 runtime helper 的防无限循环保护。 + +该分流在 AST-shape 识别模式上与 Godot compiler 的 lowering 边界一致,但 GDCC 把识别提前到 semantic owner 调度层:Godot compiler 按 for-list AST shape 识别 bare `range(...)`,逐个处理 operands,并跳过 ordinary list-expression call lowering。GDCC 不机械复制 Godot analyzer 的内部 utility-function 模型;当前 GDCC 既没有可供 `range` 使用的 ordinary binding,又明确禁止为 for-range root 发布 ordinary call fact,因此必须在 `resolveForStatement(...)` 的 statement-local owner 调度入口完成更早的预路由。 + +### 3.7 Godot iteration 语义落点 + +外部语义参考表明 Godot 对 `for-in` 使用统一 iteration 协议: + +- `int`:类似 `range(0, n, 1)`,`n <= 0` 时 0 次迭代。 +- `float`:类似 `range(ceil(n))`,但 element 精确类型与 conversion 需要专门测试锁定后再做 high-performance route。 +- `Array` / packed array:按 index 返回元素。 +- `Dictionary`:迭代 key,不是 pair,也不是 value。 +- `String`:迭代字符。 +- `Object`:通过 `_iter_init` / `_iter_next` / `_iter_get` 协议。 +- unknown `Variant`:通过 `Variant::iter_init` / `iter_next` / `iter_get` runtime dispatch。 + +GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationRoute` 和 tests 保留扩展位。无法静态确定或尚未实现专用 helper 的类型必须落到 `GENERIC_VARIANT`,而不是重新把 body 关回 deferred boundary。 + +## 4. 分阶段实施步骤 + +range/int route 的生产链路(阶段 C → D0 → D1 → E → G → H → F 解封)属于一个原子实施边界。不得先放宽 compile gate,也不得在 plan producer 尚不存在时通过测试专用 side-table mutation 声称某阶段已完成。generic Variant route(阶段 I)与 known iterable 专用 route(阶段 J)留在后续独立实施。 + + +### 阶段 A/B:parse / scope 基线与 body inventory 解封(已完成) + +阶段 A(parse/AST/scope 基线测试)与阶段 B(body inventory 与 declaration index 解封)已完成。所有 `for-in` body 已转为 shared semantic 结构支持面;iterator binding、body local inventory、declaration index、typed baseline、suite entry 均已无条件发布。`FrontendCompileCheckAnalyzer` 对所有 `ForStatement` 发布临时无条件 `sema.compile_check` blocker,等待后续 route-aware policy 替换。 + +### 阶段 C:iteration plan 数据结构与 publication surface + +目标: + +- 建立 lowering 与 type-check 共同消费的 `FrontendForIterationPlan`。 +- 建立 iteration planning owner 后续需要的数据与 publication surface,避免把 route 选择散落到 type-check / CFG / lowering。 + +实施内容: + +- 新增 `FrontendForIterationRoute` 与 `FrontendForIterationPlan`,先覆盖: + - `RANGE_CALL` + - `INT_SHORTHAND` + - `GENERIC_VARIANT` +- 为后续保留但可以暂不生成的 route: + - `FLOAT_SHORTHAND` + - `STRING` + - `ARRAY` + - `DICTIONARY_KEYS` + - `PACKED_ARRAY` + - `OBJECT_CUSTOM` +- 在 `FrontendAnalysisData` 或等价 stable surface 中新增 `forIterationPlans()` side table,key 为 `ForStatement`。 +- 新增 patch / transaction support,使 iteration plan 通过 owner-specific patch 发布,而不是在 CFG builder 中重新推导。 +- `FrontendPublishedFactTypeGuard` 增加 iteration plan guard: + - source-facing fields 不得含 compiler-only type。 + - `iteratorStateType` 只允许在 dedicated iteration plan field 中携带。 + - operation name 必须非空,且 route 与 operation arity 合法。 +- `FrontendForLoopSupport` 只做纯分类与 plan construction helper,不读源码文本,不扫描后续 statements,不直接写 scope 或 side table。 + +验收细则: + +- `FrontendAnalysisDataTest` 覆盖 `forIterationPlans()` idempotent merge、conflict merge、compiler-only guard。 +- `FrontendForLoopSupportTest` 覆盖 bare `range(...)`、非 bare `range`、`int` shorthand、unknown iterable fallback。 +- `RANGE_CALL` plan 的 `sourceOperands` 保留源码 arguments。 +- `INT_SHORTHAND` plan 的 `sourceOperands` 只包含 stop expression,不伪造 `0` / `1` AST。 +- `GENERIC_VARIANT` plan 不携带 range iterator state type。 +- route 与 operation name 不在 CFG builder / lowering processor 中重复硬编码。 + +### 阶段 D0:SuiteResolver header-only for path 与 baseline body entry + +状态:未完成。ordinary iterable 的结构性 header/body path 已落地;bare `range(...)` 仍被错误送入 ordinary owner pipeline,canonical range header 不可用。 + +目标: + +- 让 `SuiteResolver` 先解析 for header,再以阶段 B 已发布的 iterator baseline 进入 child body。 +- 在 statement-local owner pipeline 之前完成 bare `range(...)` header 预路由,使 canonical range form 不依赖 ordinary callable binding。 +- 本阶段只依赖阶段 B,不依赖阶段 C 的 iteration plan 数据结构,也不做 iterator slot refinement。 +- 为 resolution pipeline 提供“没有 typed gate 也能进入 for body”的 production path。 + +实施内容: + +- 已落地:`FrontendStatementResolver` 增加 `resolveForStatement(...)`,替换原 `resolveUnsupportedRoot(context, forStatement)`。 +- `resolveForStatement(...)` 必须是 header-only: + - 如果 `iteratorType` 非空,解析该 type ref 相关 expression / type use-site 所需 facts。 + - 若 iterable 是 bare `range(...)`,只逐个解析 arguments,不解析 callee identifier 或 call root。 + - 其他 iterable 继续解析整个 expression root。 + - 不在 header pass 中遍历 body statements。 +- 尚未落地的 D0 blocker:在调用 ordinary `runSupportedRoot(...)` 前增加 range shape pre-route: + - `CallExpression` 的 callee 必须是名称严格为 `range` 的 bare `IdentifierExpression`。 + - 命中后按 source order 对每个 argument 单独运行现有 owner procedures;不得对 call root 或 callee 运行 ordinary top binding / call resolution / expr typing。 + - 未命中的 ordinary iterable 保持当前整 root `runSupportedRoot(context, iterable)` 路径。 + - pre-route 只负责正确划分 owner domain,不构造 iteration plan、不精化 iterator slot,也不在本阶段承担 arity/type diagnostic。 +- header facts 在同一个 statement boundary flush;不得把 `iteratorType`、`iterable` 与 body 分别当成 + 三个独立 statement flush。 +- 本阶段不增加 `runForIterationPlanning(...)`,也不要求 `FrontendForIterationPlan` 已存在。 +- `resolveForStatement(...)` 在 header facts flush 后调用 + `childSuiteResolver.resolveChildSuite(context, forStatement.body())`。 +- child body 读取阶段 B 的 iterator baseline:无显式 type 时为 `Variant`,有显式 declared type 时为该 + source-facing type。 + +验收细则: + +- D0 完成前必须增加 canonical `for i in range(3): var x := i` production resolver test;默认 shared `analyze(...)` 不得产生 `range` callee 的 `sema.binding` 或 identifier/call-root `sema.expression_resolution`。 +- 同一 canonical test 必须断言 argument `3` 已发布 expression type,而 `range` callee identifier 与 call root 均不出现在 ordinary successful `resolvedCalls()` / `expressionTypes()` 中。 +- 在 D1 尚未实现时,canonical range body 仍应正常进入,`i` 与 `x` 可以保持 `Variant`。该断言与“header 无 ordinary resolution error”必须同时成立,不能再用单独的 body-entry 绿色测试替代 header 验收。 +- `range()`、`range(1, 2, 3, 4)` 与非法 argument form 也必须走 range header pre-route:arguments 能按 source order 解析,且不产生 unknown `range` binding 噪声;精确 arity/type diagnostic 在阶段 E 落地。 +- `obj.range(3)`、`some_range(3)` 等非 bare form 必须继续走 ordinary iterable pipeline,证明 pre-route 没有按文本或任意 method name 误分类。 +- `var limit := 3; for i in limit: var x := i` 中 header 能读取前缀 `limit:int`,但 body 中 `i` 与 + `x` 在本阶段仍可以保持 `Variant`。 +- `for item in values: var x := item` 中 `item`、`x` 均进入 ordinary shared semantic,不产生 + `FOR_SUBTREE` deferred result。 +- `for i: float in range(3): print(i)` 的 body 在 iteration planning 尚未实现时已经能读取 declared + baseline `i:float`;element conversion 是否成立由后续阶段判断。 +- header pass 不遍历 body,child body 只通过普通 `resolveChildSuite(...)` 进入。 +- nested `for` 递归使用同一 D0 path,不依赖 gate registry 或 iteration plan。 +- owner procedure 不从 `SourceFile` root 重新 walk,不恢复 legacy whole-module analyzer。 + +### 阶段 D1:iteration planning 与 iterator slot refinement + +依赖:阶段 C 与完整完成的阶段 D0,包括 bare `range(...)` header 预路由及其 canonical regression tests。 + +目标: + +- 在 D0 已建立的 header-first 路径中发布 iteration plan 与 iterator slot refinement。 +- body resolver 必须看到 header 已提交的 iterator effective type;该精化只改变 typed fact,不改变 + body 是否进入 `SuiteResolver`。 + +实施内容: + +- 增加 feature-specific owner hook,例如 `runForIterationPlanning(context, forStatement)`,执行位置 + 固定在 ordinary iterable root expr typing 或 bare range arguments expr typing 完成后、iterator var type post 前。该 hook 不复用或恢复 + `runGateClassifier(...)`。 +- `runForIterationPlanning(...)`: + - 对 ordinary iterable 读取 root 的 effective expression / slot typed fact。 + - 对 bare `range(...)` 读取 D0 已发布的 argument expression facts 并检查原始 AST shape;不得要求或补发 range callee/call-root ordinary binding、expression type 或 resolved call。 + - 调用 `FrontendForLoopSupport` 构造 plan。 + - 发布 `FrontendForIterationPlan`。 + - 若 iterator baseline 是 `Variant` 且 plan 的 `exposedIteratorType` 是 exact source-facing type,则发布 iterator slot refinement。 + - 若 iterator 有显式 declared type,不自动改写 exact type,只记录 raw element -> exposed type 的 conversion requirement。 +- `runVarTypePost(...)` 扩展为支持 `ForStatement` iterator declaration: + - 为 iterator declaration key 发布 final source-facing `slotTypes()` fact。 + - `slotTypes()` value 必须是 exposed iterator type,不能是 compiler-only state type。 +- D1 落地后的完整 header 顺序必须是:iterator type facts -> ordinary iterable root 或 bare range arguments 的 owner procedures -> `FOR_ITERATION_PLANNING` -> iterator var type post -> 单次 statement flush -> child suite。child body 的 `FrontendSuiteContext` 通过 parent environment 读取 iterator slot refinement。 +- planning 不得承担“清理 ordinary range call 失败 facts”的职责。D0 pre-route 必须保证这些 facts 从未发布;pending / committed overlay 与 patch transaction 均不新增 delete/overwrite 机制来掩盖错误 owner routing。 + +验收细则: + +- `var limit := 3; for i in limit: var x := i + 1` 中 body resolver 看到 `i:int`。 +- `for item in values: var x := item` 中 `item` 在 unknown route 下保持 `Variant`。 +- `for i in range(3): var x := i + 1` 不发布 ordinary `range(...)` call route,但 `range` argument 有 expression type。 +- canonical range planning 前不得存在 callee/call-root `FAILED` expression fact 或 `sema.binding` / `sema.expression_resolution`;否则 D1 验收直接失败,不能仅凭 plan 已发布判定通过。 +- `for i: float in range(3): print(i)` 若现有 boundary 允许 `int -> float`,body 中 `i` 是 `float`,plan 记录 per-element conversion;若不允许则发 type diagnostic。 +- `for i: String in range(3): pass` 不静默通过。 +- nested `for j in range(i):` 可以读取外层 `i` 的 refined type。 +- 改变 iterable 的 resolved type 只能改变 plan/refinement/type diagnostic,不能改变 D0 已建立的 + inventory、declaration index、suite entry 或 child-body dispatch。 + +### 阶段 E:type-check 与 Godot iteration 语义 + +目标: + +- 在不关闭 body semantic 的前提下,对可静态验证的 route 进行 type-check。 +- 明确 generic route 的 runtime error / runtime dispatch 边界。 + +实施内容: + +- `FrontendTypeCheckAnalyzer` 消费 `FrontendForIterationPlan`: + - `RANGE_CALL` arguments 必须能进入 `int` slot。 + - `INT_SHORTHAND` stop expression 必须能进入 `int` slot。 + - literal `range(..., 0)` 发 frontend diagnostic。 + - explicit iterator type 必须能接收 raw element type;不新增 parallel conversion matrix。 + - `GENERIC_VARIANT` 不因无法静态证明 iterable 而发 unsupported diagnostic。 +- `FLOAT_SHORTHAND` 第一版可以保持 generic Variant route;只有在 `ceil` 语义、element exposed type 与 C helper 都被测试锁住后,才转为专用 route。 +- `DICTIONARY_KEYS` route 必须明确 iterator 是 key type。 +- Object custom iterator 的 static element type 默认仍为 `Variant`,除非未来有明确 type contract。 + +验收细则: + +- `range()` 与 `range(1, 2, 3, 4)` 有清晰 diagnostic。 +- `range(1, 2, 0)` literal zero step 有清晰 diagnostic。 +- `for i in values:` 不再产生 `FOR_SUBTREE` unsupported diagnostic。 +- `for i in 2.2:` 在未专用化前进入 generic Variant route,shared semantic 不失败。 +- typed dictionary route 测试锁定 iterator 是 key,不是 value 或 pair。 +- type-check 不把 generic route 的运行时不可迭代可能性误报为 compile-time unsupported。 + +### 阶段 F:compile gate 分阶段解封 + +状态:未实施。先实现 route-aware policy,但只有阶段 G/H 与对应测试完成后才实际解封 range/int route。 + +目标: + +- shared semantic 支持与 compile-ready 支持分离。 +- 只有对应 lowering helper 已准备好的 route 才能通过 `analyzeForCompile(...)`。 + +实施内容: + +- shared semantic 阶段完成后,`FrontendCompileCheckAnalyzer.handleForStatement(...)` 用 route-aware + policy 替换阶段 B 的临时无条件 blocker,并按 route 分阶段阻断 compile: + - range route 已有 intrinsic/backend/runtime,可以优先解封。 + - generic Variant route 必须等 LIR intrinsic、runtime helper、C backend codegen 与 tests 全部完成后再解封。 + - known iterable 专用 route 必须等对应 helper 准备好后再解封;否则先降级到 generic Variant route。 +- compile gate 解封时必须 mark: + - `ForStatement` + - iterator declaration key + - iterable / range arguments + - body block 与 body statements + - iteration plan fact +- compile gate 不能重新包装 upstream semantic/type-check error。 +- 同步更新: + - `frontend_rules.md` + - `frontend_compile_check_analyzer_implementation.md` + - `frontend_lowering_plan.md` + - `frontend_lowering_cfg_pass_implementation.md` + - `frontend_gdcompiler_type_implementation.md` 如新增 compiler-only iterator state + - `gdcc_lir_intrinsic.md` 与 `gdcc_runtime_lib.md` 如新增 generic helper + +验收细则: + +- shared semantic 支持的 `for i in values:` 在 generic helper 尚未实现时可以被 compile gate 明确阻断,但 diagnostic 必须说明缺少 lowering route,而不是 `FOR_SUBTREE` unsupported。 +- range route 在无 semantic error 时可通过 `analyzeForCompile(...)`。 +- generic Variant route 完成后,unknown iterable 的 `for-in` 可通过 compile gate。 +- compile gate 不为已有 upstream semantic error 追加同级 generic `sema.compile_check` 噪声。 + +### 阶段 G:frontend CFG graph + +状态:未实施。不得脱离阶段 C、完整 D0、D1 与 range/int 必要 type-check 单独进入 production path;必须与这些前置及阶段 H 一起原子实施。 + +目标: + +- 在 `FrontendCfgGraphBuilder` 中为 `for-in` 建立显式 CFG。 +- `break` 跳到 loop exit。 +- `continue` 跳到 iterator update,再回 condition。 +- 用 dedicated hidden mutable slot 表达 loop-carried iterator state,不扩展 `MergeValueItem` 或 CFG value producer 合同。 + +实施内容: + +- 新增 `FrontendForRegion`,不要复用 `FrontendWhileRegion`。 +- `FrontendCfgRegion` sealed permits 增加 `FrontendForRegion`。 +- 新增 AST-keyed `FrontendForIteratorStateSlot` registry 或等价 immutable graph artifact,并与 graph/regions 一起发布到 `FunctionLoweringContext`。 +- `FrontendForRegion` 至少记录 `initEntryId`(即 `entryId()`)、`conditionEntryId`、`bodyEntryId`、`updateEntryId`、`exitId` 与 `iteratorStateSlotId`。 +- `FrontendCfgGraphBuilder.processStatement(...)` 增加 `case ForStatement forStatement -> processForStatement(...)`。 +- `processForStatement(...)` 只消费已发布的 `FrontendForIterationPlan`,不得重新推导 iterable 语义。 +- 在 `frontend.lowering.cfg.item` 增加通用的 `ForLoopInitItem`、`ForLoopShouldContinueItem`、`ForLoopGetItem`、`ForLoopNextItem`。item 消费 plan 已冻结的 route/type/operation 信息,不硬编码 range intrinsic 名称。 +- 四个 item 使用独立 `iteratorStateSlotId` 字段引用 hidden slot。该 id 不进入 ordinary result/operand value-id surface: + - init:消费 source operand value ids,初始化 hidden slot,不发布 ordinary result。 + - should-continue:读取 hidden slot,发布 ordinary `bool` result。 + - get:读取 hidden slot,发布 ordinary raw element result,并在 body statements 前提交 source-facing iterator local。 + - next:读取并更新 hidden slot,不发布 ordinary result。 +- CFG shape 建议: + - iterable / source operands 在进入 loop 前按 source order 计算。 + - init entry 调用 plan 指定的 init operation,写入 dedicated hidden iterator state slot。 + - condition entry 调用 should-continue operation,产出 bool condition value。 + - body entry 调用 get operation,写入 source-facing iterator local,再执行 body statements。 + - update entry 调用 next operation,更新 iterator state,再跳回 condition entry。 + - exit sequence 是 loop 后续 continuation。 +- active loop frame 对 `for` 应使用: + - `breakTargetId = exitId` + - `continueTargetId = updateEntryId` +- hidden iterator state slot 固定使用 `cfg_for_iter_`;next commit temp 固定使用独立 `cfg_for_iter_next_` namespace。两者都不是 CFG value id。 +- build artifact 必须验证 slot owner/type/uniqueness、四个 item 的 slot 引用、init-before-condition、get-before-body、next-before-backedge,以及 hidden slot 未泄漏到 ordinary value producer/materialization surface。 + +验收细则: + +- CFG regions 中 `ForStatement` 映射到 `FrontendForRegion`。 +- `FrontendForRegion` 暴露 `initEntryId`、`conditionEntryId`、`bodyEntryId`、`updateEntryId`、`exitId` 与 `iteratorStateSlotId`。 +- 每个 region 恰好有一个 `FrontendForIteratorStateSlot` metadata;nested/sibling loop slot id 唯一。 +- `continue` 的 target 是 update entry,不是 condition entry。 +- `break` 的 target 是 exit。 +- `range(...)` arguments、`INT_SHORTHAND` stop operand 或 generic iterable value ids 在 init item 前已经发布。 +- condition branch 的 condition value type 是 `bool`,不会触发 compiler-only condition normalization。 +- hidden slot id 不在 value producer map、ordinary operand ids 或 CFG value materialization map 中。 +- init/next 不发布 ordinary result;should-continue/get 各自只有一个 ordinary single-definition result。 +- get item 的 ordinary raw result 使用独立 `cfg_tmp_*` slot,不与 source-facing iterator local alias;需要 conversion 时两者允许具有不同类型。 +- graph construction 对 missing metadata、duplicate slot id、type mismatch、cross-loop slot reference、错误 entry 中的 item 和 slot-id/value-id 混用 fail fast。 +- nested `if` 中的 `break` / `continue` 能正确连边。 +- unreachable body 后续 statement 处理仍遵守现有 reachability 规则。 + +### 阶段 H:range route LIR lowering + +状态:未实施。与阶段 C/D0/D1/E/G 和 range/int compile-gate 解封一起原子实施。 + +目标: + +- 先接通已有 backend/runtime 已支持的 `range(...)` 与 `int` shorthand route。 +- 不重新扫描 AST,不重新推导 sema facts。 + +实施内容: + +- 消费阶段 G 已冻结的 `ForLoopInitItem`、`ForLoopShouldContinueItem`、`ForLoopGetItem`、`ForLoopNextItem`,不在 body lowering 阶段重新解释 item shape。 +- body lowering 在 `FrontendSequenceItemInsnLoweringProcessors` 中增加对应 processor。 +- processor 消费阶段 G 从 `FrontendForIterationPlan` 固化到 item/hidden-slot metadata 的 operation/type payload,并生成 `CallIntrinsicInsn`;processor 不重新查询 AST shape 或重新分类 route。range route 对应: + - init:`gdcc.for_range_iter.init` + - should_continue:`gdcc.for_range_iter.should_continue` + - get:`gdcc.for_range_iter.get` + - next:`gdcc.for_range_iter.next` +- `FrontendBodyLoweringSession` 必须能为 hidden iterator state 声明 `compiler::GdccForRangeIter` local,并沿用 `GdCompilerType` storage/lifecycle 合同。 +- hidden local declaration 读取 dedicated registry,不通过 `collectCfgValueMaterializations()` 或 `slotIdForValue()` 声明。 +- `ForLoopNextItem` 必须先把 intrinsic result 写入 distinct `cfg_for_iter_next_` compiler-only temp,再用 `AssignInsn` commit 到 `cfg_for_iter_`;不得让 intrinsic result target 与 state argument slot 相同。 +- `ForLoopGetItem` 先把 raw element 写入 ordinary temp,再复用现有 typed-boundary conversion 写入 `ForStatement` identity 对应的 source local。 +- source-local declaration/lookup helper 必须接受 `ForStatement` identity,不能继续假设所有 body local 都是 `VariableDeclaration`。 +- `INT_SHORTHAND` source form 不生成伪造 `range(stop)` AST;init item/lowering processor 负责按 `(0, stop, 1)` 解释。 +- `INT_SHORTHAND` 的 `0` 与 `1` 必须先通过既有 integer constant lowering 物化为 LIR variables,再作为 `CallIntrinsicInsn` arguments 传入;intrinsic argument 位置不接受 literal。 +- 不通过 `PackVariantInsn` / `UnpackVariantInsn` materialize range iterator state。 + +验收细则: + +- LIR function variables 包含 hidden `compiler::GdccForRangeIter` local。 +- LIR function variables 包含与 state slot 不同的 next temp;instruction 顺序锁定为 `next(oldState) -> nextTemp`、`AssignInsn(state, nextTemp)`。 +- range init / should_continue / get / next 以 `CallIntrinsicInsn` 出现,参数顺序符合 `doc/gdcc_lir_intrinsic.md`。 +- `INT_SHORTHAND` source form 经由同一组 intrinsic 降低,不新增第二套数值简写专用 intrinsic。 +- source-facing loop variable slot 是 `int` 或 declared compatible type。 +- generated C 使用 `gdcc_for_range_iter_*` helper,不出现 `godot_GdccForRangeIter`、`godot_new_GdccForRangeIter...`、`Variant` pack/unpack 相关路径。 +- compiler-only state 与 next temp 的 prepare/final cleanup、每轮 overwrite lifecycle 均由既有 `GdCompilerType` / `AssignInsn` 合同覆盖,并有正反测试证明没有 public boundary 泄漏。 +- `step == 0` literal 在 frontend 阶段阻断;动态零 step 仍由 runtime helper 防止无限循环。 + +### 阶段 I:generic Variant iterator route + +目标: + +- 让无法静态专用化的 `for i in expr` 进入 compile-ready surface。 +- 复用 Godot Variant iteration 语义,而不是在 frontend 中枚举所有运行时类型。 + +实施内容: + +- 在 `doc/gdcc_lir_intrinsic.md` 新增 generic iterator intrinsic catalog,暂定命名: + - `gdcc.for_variant_iter.init` + - `gdcc.for_variant_iter.should_continue` + - `gdcc.for_variant_iter.next` + - `gdcc.for_variant_iter.get` +- 新增 compiler-only generic iterator state type,并冻结 init/copy/destroy/direct-assignment contract;该类型不得进入 ordinary type tables。 +- 在 C runtime 中新增 helper,优先通过 GDExtension Variant iteration API: + - `variant_iter_init` + - `variant_iter_next` + - `variant_iter_get` +- 如果 gdextension-lite 未暴露这些 API,应先增加薄 wrapper,再让 gdcc runtime helper 调用 wrapper。 +- `GENERIC_VARIANT` route 的 `get` 返回 `Variant`;显式 iterator type 或后续 use-site 需要通过既有 Variant boundary / unpack materialization 处理。 +- runtime helper 必须定义不可迭代值的错误策略,尽量贴近 Godot:运行时 fail / print error,而不是 frontend 编译期 unsupported。 + +验收细则: + +- `for item in values:` 在 `values` 静态类型未知时 lowering 到 generic Variant route。 +- generic route 的 iterator local body type 是 `Variant`,除非有显式 iterator type。 +- `Dictionary` 在 generic route 下按 Godot runtime 迭代 key。 +- Object custom iterator 不需要 frontend 特判;runtime route 负责调用 Godot protocol。 +- generic iterator state 不出现在 ordinary `expressionTypes()` / `slotTypes()` / public ABI。 +- C backend 对 helper 的 lifecycle、copy、destroy 路径有 focused tests。 + +### 阶段 J:known iterable 专用 route + +目标: + +- 在不改变 shared semantic 支持面的前提下,为可静态确定的 iterable 类型提供高性能 route。 +- 每个专用 route 都必须能安全 fallback 到 generic Variant route,不能因专用 helper 未实现而重新关闭 body semantic。 + +实施内容: + +- 按 helper 准备度逐个启用: + - `INT_SHORTHAND`:可复用 range route,`0..stop`。 + - `STRING`:迭代字符,element type 按 Godot 语义锁定后再启用。 + - `ARRAY`:typed `Array[T]` element type 为 `T`,plain `Array` 为 `Variant`。 + - `DICTIONARY_KEYS`:typed `Dictionary[K, V]` element type 为 `K`,plain `Dictionary` 为 `Variant`。 + - `PACKED_ARRAY`:按 packed array family 返回对应 element type。 + - `FLOAT_SHORTHAND`:必须先锁定 `ceil` 语义、element exposed type 与 C helper。 + - `OBJECT_CUSTOM`:如需专用化,必须先有明确 Object method dispatch / `_iter_*` contract。 +- known route 的 classifier 只能读取 header typed facts 与当前 suite prefix facts,不得扫描后续 statement。 +- route 切换只影响 iteration plan 与 lowering helper;body inventory 不受影响。 + +验收细则: + +- known route 成功时 iterator slot 从 `Variant` 精化为对应 element type。 +- known route 未启用时,同一源码仍能走 `GENERIC_VARIANT` route。 +- `Array[int]` route 使 body 中 iterator 是 `int`。 +- typed dictionary route 使 body 中 iterator 是 key type。 +- plain container route 不假装拥有 exact element type。 +- 每个 route 都有 lowering helper readiness gate;没有 helper 时 compile gate 不静默放行。 + +## 5. 验收测试清单 + +### Parser / scope + +- `for i in range(3): pass` 解析为 `ForStatement`。 +- `for i in 3: pass`、`for i in limit: pass`、`for i in values: pass` 都解析为 `ForStatement`,且 iterable 保持原始 expression 形态。 +- `for i: int in values: pass` 解析并保留 `iteratorType`。 +- `FrontendScopeAnalyzerTest` 继续断言 `iteratorType` / `iterable` 在外层 scope,`body` 在 `FOR_BODY`。 + +### Shared semantic inventory + +- `for i in values: var x := i` 成功发布 iterator 与 body local。 +- `i` 在 loop 后不可见。 +- iterator 与 parameter / local / body local 冲突时按现有 local conflict 规则报错。 +- nested for body 能正常发布内外层 iterator,且内层可读取外层 iterator。 +- `for` 不再产生 `sema.unsupported_variable_inventory_subtree`、`sema.unsupported_binding_subtree` 或 `FOR_SUBTREE` deferred lookup。 +- `match`、lambda、block-local `const` 的 deferred / unsupported tests 保持不变。 + +### SuiteResolver / type facts + +- D0 canonical regression:`for i in range(3): var x := i` 不产生 unknown `range` binding 或 identifier/call-root expression-resolution error;argument `3` 有 expression type,range callee/call root 没有 ordinary expression type / resolved call,body 仍能进入。 +- D0 ordinary-route regression:`for i in limit:` 继续解析整个 iterable root;`obj.range(3)` / `some_range(3)` 不被 bare range pre-route 捕获。 +- 当前已有的 `FrontendSuiteResolverTest` 只覆盖 `items` / `values` / `limit` 等 ordinary iterable,不能作为 D0 range header 的完成证据。阶段 D0 的 targeted test 必须显式包含 canonical `range(...)` source。 +- `var limit := 3; for i in limit: var x := i + 1` 中 `i` 在 body 中为 `int`。 +- `for i in range(3): var x := i + 1` 中 `i` 为 `int`,`range(...)` root 不出现在 ordinary successful `resolvedCalls()` 中。 +- `for item in values: var x := item` 中 unknown route 的 `item` 为 `Variant`。 +- `for item: String in values:` 中 body 的 `item` 为 `String`,但 plan 记录 raw element 需要 conversion 或 runtime check。 +- `GdccForRangeIterType` 不出现在 ordinary `expressionTypes()` 与 user-facing `slotTypes()` 中。 + +### Type-check + +- `range()`、`range(1, 2, 3, 4)`、literal `range(1, 2, 0)` 均有明确 diagnostic。 +- `for i: String in range(3): pass` 不静默通过。 +- `for i in 2.2:` 在未专用化前不报 frontend unsupported;若启用 float route,测试锁定 `ceil` 语义。 +- `Dictionary` route 测试锁定 iterator 是 key。 + +### Compile gate + +- range route 在无 error 时可通过 `analyzeForCompile(...)`。 +- generic route helper 未实现前,unknown iterable compile path 有明确 route-not-ready diagnostic。 +- generic route helper 完成后,`for item in values:` 可通过 compile gate。 +- compile gate 不为已有 upstream semantic error 追加同级 generic `sema.compile_check` 噪声。 + +### CFG + +- `FrontendCfgGraphBuilderTest` 覆盖 `FrontendForRegion` shape。 +- `FrontendCfgGraphBuilderTest` 覆盖 range/int source operands 在 init 前按 source order 发布,且 init/next 不产生 ordinary value result。 +- `FrontendCfgGraphTest` 或 dedicated build-artifact test 覆盖 hidden-slot metadata owner/type/uniqueness 与 item-slot cross validation。 +- 负向测试覆盖 missing metadata、duplicate slot id、state type mismatch、nested loop cross-slot reference、item 位于错误 entry、slot id 混入 ordinary operand/result value ids。 +- value producer/materialization 测试明确断言 `cfg_for_iter_` 不进入 producer map、`cfg_tmp_*` / `cfg_merge_*` collection 或 `CfgValueMaterializationKind`。 +- nested/sibling loops 获得不同 hidden slot,source-facing iterator local 仍分别以 owning `ForStatement` 为 identity。 +- `continue` target 为 update entry。 +- `break` target 为 exit。 +- nested `if` 中的 `continue` / `break` 能正确连边。 +- 空 body、只有 `pass` 的 body、body 内 `return` 的 reachable/fallthrough 行为均有断言。 + +### Body lowering / LIR / backend + +- `FrontendLoweringBuildCfgPassTest` 覆盖 function context 中发布 `FrontendForRegion`。 +- `FrontendLoweringBuildCfgPassTest` 同时覆盖 hidden-slot registry 发布,且 compile-ready range/int context 不依赖测试手工注入 iteration plan。 +- `FrontendLoweringBodyInsnPassTest` 覆盖 range intrinsic instruction sequence,并锁住 `INT_SHORTHAND` 仍走相同 intrinsic 路线。 +- update lowering 测试锁定 distinct next temp、`CallIntrinsicInsn(next)` 后接 `AssignInsn(state, nextTemp)`,并拒绝 source/target 使用同一 state slot。 +- get lowering 测试锁定 raw-element temp、必要 conversion、source-facing iterator local commit 的顺序。 +- compiler-only boundary 负向测试覆盖 state/next temp 不得进入 ordinary call argument、return、property/store、Variant pack/unpack 或 public ABI;不使用源码文本扫描代替行为断言。 +- generic Variant route 完成后,测试锁住 generic iterator intrinsic sequence。 +- `DomLirParserTest` / `DomLirSerializerTest` 覆盖新增 compiler-only iterator state round-trip。 +- `GdccForRangeIterTypeTest` / `GdCompilerTypeTest` 继续覆盖 compiler-only type contract。 +- `GdccForRangeIterIntrinsicTest` / `CallIntrinsicInsnGenTest` 继续覆盖 intrinsic C generation。 +- 如 test-suite 已具备可运行 GDScript fixture,再增加 `range(3)`、`range(1, 3)`、`range(2, 8, 2)`、`range(8, 2, -2)`、generic `Array` / `Dictionary` route 的 runtime output 锚点。 + +## 6. 建议 targeted test 命令 + +开发时按阶段运行,不要一开始全量跑: + +```bash +script/run-gradle-targeted-tests.sh --tests FrontendParseSmokeTest,FrontendScopeAnalyzerTest,FrontendLoopControlFlowAnalyzerTest +``` + +```bash +script/run-gradle-targeted-tests.sh --tests FrontendVariableAnalyzerTest,FrontendVisibleValueResolverTest,FrontendInterfacePhaseTest,FrontendSuiteResolverTest +``` + +```bash +script/run-gradle-targeted-tests.sh --tests FrontendTypeCheckAnalyzerTest,FrontendCompileCheckAnalyzerTest +``` + +```bash +script/run-gradle-targeted-tests.sh --tests FrontendCfgGraphBuilderTest,FrontendLoweringBuildCfgPassTest,FrontendLoweringBodyInsnPassTest +``` + +```bash +script/run-gradle-targeted-tests.sh --tests GdCompilerTypeTest,GdccForRangeIterTypeTest,DomLirParserTest,DomLirSerializerTest,GdccForRangeIterIntrinsicTest,CallIntrinsicInsnGenTest +``` + +阶段完成后再考虑: + +```bash +./gradlew classes --no-daemon --info --console=plain +``` + +## 7. 风险与未定点 + +- `FOR_BODY` 已加入 unconditional supported block kind;resolver 的 request domain、AST edge 与 current-scope 三处必须持续允许 for ordinary lookup,否则会重新出现 inventory 已发布但 lookup fail-closed 的矛盾。 +- Iterator 使用 `ForStatement` 作为 declaration identity 会触及 `FrontendBodyLocalDeclaration`、`FrontendBodyDeclarationIndex`、visible resolver inventory guard、slot type publication 与 lowering lookup;这些位置必须一起改,不能只改 scope binding。 +- Header-derived iterator refinement 发生在 expr typing 之后,不能伪装成原有 statement-local local stabilization。需要明确的 iteration planning owner / patch order。 +- bare `range(...)` 的“expr typing 之后”指 arguments typing 之后,不是 call root ordinary typing 之后。若先让 call root 失败再运行 planning,错误 diagnostic 与 `FAILED` fact 已进入 pending overlay,现有 transaction 无法删除或覆盖,D1 不能补救 D0 owner routing 错误。 +- `range(...)` root 是否发布 expression type 必须统一。当前计划是不发布 ordinary root type,只发布 arguments 与 iteration plan,避免把 range 当作 user-facing builtin call。 +- bare `range` 与同名 local/callable shadowing 的语义必须在 D0 实现前用 Godot focused case 锁定;不得把 `ForStatement.range()` source anchor 当作 classifier,也不得在没有语义证据时擅自让 ordinary binding 改写 AST-shape pre-route。 +- `int` 简写不允许通过 AST rewrite 伪装成 `range(stop)` call;否则 parser/scope/diagnostic anchor、future object-iterator classifier 与测试都会被污染。 +- explicit iterator type 的兼容规则必须复用 ordinary typed-boundary helper;不要为 `for` 私下硬编码 `int -> T` 或 `Variant -> T` 特例。 +- `continue` 对 `for` 的目标不是 condition entry,而是 update entry。这一点和 `while` 不同,不能复用 `FrontendWhileRegion`。 +- `sourceOrder == 0` 只是 `FOR_BODY` inventory 的 declaration order。若把它复用为 hidden slot number、LIR variable index 或每轮 definition version,会把 lexical fact 与 runtime storage 错误耦合。 +- loop-carried iterator state 不是 CFG expression value。不得通过两个 `MergeValueItem` 分别把 init/next 写入同一 result id,也不得扩展现有 branch-result merge 合同来掩盖 mutable storage。 +- dedicated hidden slot 若只在 lowering processor 中拼接名称而不随 graph artifact 发布,就会绕过 owner/type/uniqueness 与 nested-loop cross-reference validation。registry、region 与 items 必须在 CFG build 完成时交叉验证。 +- 不得新增 `HIDDEN_COMPILER_STATE` value materialization kind。该做法仍会让 hidden storage 进入 value-id producer/materialization 模型,只是更换枚举名称,并未解决模型混淆。 +- `next` intrinsic 是 new-value operation,不是 in-place mutation。即使当前 `GdccForRangeIterType` destroy 为 no-op,也必须使用 distinct next temp 再通过 `AssignInsn` commit,为 future destroyable generic state 保留正确生命周期顺序。 +- 阶段 G 的 production path 依赖 C/D0/D1;阶段 F 的 range/int 解封又依赖 G/H。必须按依赖链原子提交,不能用手工注入 plan 的 builder test 替代真实 source -> sema -> CFG/LIR integration test。 +- generic Variant iterator helper 当前不存在。shared semantic 可以先完成,但 compile gate 不得在 helper / backend / runtime 未完成时静默放行 generic route。 +- Godot 的 `float`、Vector2 / Vector3、Object custom iterator 等语义应优先通过 generic route 保持运行时一致;专用 route 必须有 dedicated semantic tests 后再启用。 +- `Dictionary` 迭代返回 key。任何 typed dictionary route 都必须锁住 key type,不能误用 value type。 +- known iterable 专用 route 是优化,不是 body semantic 前置条件;专用 helper 缺失时应 fallback generic route 或 compile-gate route-not-ready,而不是恢复 `FOR_SUBTREE`。 + +## 8. 完成定义 + +本计划完成后,必须同时满足: + +1. 所有 `for iterator[: Type] in expr` 在 shared semantic 中都不再触发 `FOR_SUBTREE` unsupported boundary。 +2. loop iterator 在 body 内是 source-facing local;无显式 type 时 baseline 为 `Variant`,可由 iteration plan 精化为 exact element type;显式 type 时 body 使用 declared type。 +3. `FrontendForIterationPlan` 是 type-check、compile gate、CFG builder 与 lowering 选择 route 的唯一事实源;CFG / lowering 不重新扫描 AST 推导 iterable 语义。 +4. `range(...)` 与 `int` shorthand 复用 `gdcc.for_range_iter.*` route,且不把 `range(...)` 作为 ordinary call 发布。 +5. generic Variant route 有完整 LIR intrinsic、runtime helper、C backend codegen 与 tests;unknown iterable 不再需要 compile-time unsupported。 +6. compiler-only iterator state type 只出现在 dedicated iteration plan、CFG hidden-slot metadata、hidden LIR local / intrinsic operand-result / backend C storage 路径;hidden slot id 不属于 CFG value-id/materialization surface。 +7. CFG 中存在独立 `FrontendForRegion` 与 validated hidden-slot registry,且 `continue` / `break` 连边正确;同一 region/item/processor 基础设施能承载 range、generic Variant 与 known iterable route。 +8. 文档、正反测试、targeted tests、compile check、lowering plan、runtime / intrinsic catalog 全部同步。 + +阶段性完成门槛:D0 只有在 ordinary iterable regression 与 canonical `for i in range(3)` regression 同时通过,且后者证明“arguments 有 facts、callee/call root 无 ordinary facts、header 无 ordinary resolution diagnostic、body 仍进入”后,才能重新标注为已完成。仅证明 header 错误不关闭 body,或仅证明全部现有相关测试为绿色,都不满足 D0 完成定义。 + +range/int 原子闭环只有在以下条件同时满足后才能合并:C/D0/D1/E facts 由真实 production pipeline 发布;阶段 G graph/region/hidden-slot validation 通过正反测试;阶段 H 生成 temp-then-commit LIR 并通过 lifecycle/boundary 测试;compile gate 最后解封且 end-to-end source test 无手工 side-table mutation。任一条件缺失时应保留 range/int route blocker,不得把局部绿色测试标记为阶段完成。 diff --git a/doc/module_impl/frontend/frontend_gdcompiler_type_implementation.md b/doc/module_impl/frontend/frontend_gdcompiler_type_implementation.md index 61f3596f..2576d4d9 100644 --- a/doc/module_impl/frontend/frontend_gdcompiler_type_implementation.md +++ b/doc/module_impl/frontend/frontend_gdcompiler_type_implementation.md @@ -5,7 +5,7 @@ ## 文档状态 - 状态:事实源维护中(`GdCompilerType` sealed 抽象层、`GdccForRangeIterType`、LIR-only grammar、frontend/backend leak guards、public ABI validator、range iterator intrinsic 已落地) -- 更新时间:2026-07-03 +- 更新时间:2026-07-20 - 适用范围: - `src/main/java/gd/script/gdcc/type/**` - `src/main/java/gd/script/gdcc/frontend/**` @@ -36,7 +36,7 @@ - 不让 compiler-only type 进入 public / hidden function parameter 或 return ABI - 不让 compiler-only type 进入 property、signal、capture、typed container outward metadata - 不让 compiler-only type 参与 `Variant` pack / unpack、engine method、utility、global、operator、index、property 普通 Godot runtime 路径 - - 不在这里实现 `for` parser / analyzer / lowering 或 async / function-state 全量 lowering + - 不在这里实现 for iteration planning / CFG / lowering 或 async / function-state 全量 lowering;for shared semantic inventory/body entry 已由 frontend 独立支持 --- diff --git a/doc/module_impl/frontend/frontend_local_type_stabilization_implementation.md b/doc/module_impl/frontend/frontend_local_type_stabilization_implementation.md index 6a4ffec4..63bad8ab 100644 --- a/doc/module_impl/frontend/frontend_local_type_stabilization_implementation.md +++ b/doc/module_impl/frontend/frontend_local_type_stabilization_implementation.md @@ -1,11 +1,13 @@ # FrontendLocalTypeStabilization 实现说明 -> 本文档作为 `FrontendLocalTypeStabilizationAnalyzer` 及其相邻 `:=` 局部类型稳定化合同的长期事实源,定义当前 phase 位置、输入输出边界、slot owner、稳定化规则、fail-closed 边界与测试锚点。本文档替代旧的实施计划与阶段记录,不再保留进度流水账、已完成任务列表或回滚步骤。 +> 本文档作为 `FrontendBodyOwnerProcedures` 中 local-stabilization owner 及其相邻 `:=` +> 局部类型稳定化合同的长期事实源,定义当前 phase 位置、输入输出边界、slot owner、 +> 稳定化规则、fail-closed 边界与测试锚点。 ## 文档状态 -- 状态:事实源维护中(source-order local `:=` slot stabilization、parameter/local alias 传播、复杂 initializer 求型、assignment initializer 与 bare `TYPE_META` fail-closed、parent/child block 边界合同已落地) -- 更新时间:2026-06-20 +- 状态:事实源维护中(source-order local `:=` slot stabilization、for body ordinary local、parameter/local alias 传播、复杂 initializer 求型、fail-closed 边界与 SuiteResolver overlay/export 路径已落地) +- 更新时间:2026-07-20 - 适用范围: - `src/main/java/gd/script/gdcc/frontend/sema/**` - `src/main/java/gd/script/gdcc/frontend/sema/analyzer/**` @@ -28,7 +30,8 @@ - 不在这里做 property `:=` metadata backfill - 不在这里做 whole-module fixed-point - 不在这里做 CFG / control-flow merge aware local type refinement - - 不在这里转正 parameter default、lambda、capture、`for`、`match`、block-local `const`、class `const` + - 不在这里转正 parameter default、lambda、capture、`match`、block-local `const`、class `const` + - 不在这里实现 for iteration planning 或 iterator slot refinement - 不在这里新增公共 frontend API;如需 helper,优先保持在 analyzer/support 包内且不拥有 phase facts --- @@ -37,29 +40,29 @@ ### 1.1 主链路位置 -当前 `FrontendSemanticAnalyzer` 的稳定顺序是: +当前 production `FrontendSemanticAnalyzer` 的稳定顺序是: 1. skeleton 2. scope 3. variable inventory -4. top binding -5. local type stabilization -6. chain binding -7. expr typing -8. callable-local slot-type republish -9. annotation usage -10. virtual override -11. type check -12. loop-control legality -13. compile-only final gate(仅 `analyzeForCompile(...)`) +4. interface phase +5. SuiteResolver body publication,内部 owner 顺序固定为 top binding -> local type stabilization -> chain binding -> expr typing -> callable-local slot-type republish +6. annotation usage +7. virtual override +8. type check +9. loop-control legality +10. compile-only final gate(仅 `analyzeForCompile(...)`) -每个 phase 结束后,`FrontendSemanticAnalyzer` 都会调用 `analysisData.updateDiagnostics(...)` 刷新共享诊断快照。因此 local type stabilization 虽然不拥有 diagnostics,仍然参与稳定的 phase-boundary refresh。 +每个 shared phase 结束后,`FrontendSemanticAnalyzer` 都会调用 `analysisData.updateDiagnostics(...)` 刷新共享诊断快照。SuiteResolver body path 还会在 statement boundary 刷新快照,让后一 statement 读取 current-suite upstream diagnostics。 -`FrontendLocalTypeStabilizationAnalyzer` 固定运行在 `FrontendTopBindingAnalyzer` 之后、`FrontendChainBindingAnalyzer` 之前。它的职责是先把 block-local `:=` slot 稳定到当前可得到的 exact type,再让 chain binding 消费这些 receiver slot,避免第一次正式发布 member/call facts 时把本应静态的 receiver 误读成 `Variant`。 +生产 body path 中,local stabilization 作为 `FrontendBodyOwnerProcedures` 的 statement-local +owner procedure 运行在 top binding 之后、chain binding 之前。阶段 K 已删除 standalone +whole-module analyzer 与 window shim;focused coverage 通过 SuiteResolver、typed overlay 和 +per-owner patch transaction 锚定,不再存在第二条发布路径。 ### 1.2 当前职责 -`FrontendLocalTypeStabilizationAnalyzer` 当前只负责一件事:在已发布的 callable executable body 中,按源码顺序稳定符合条件的 local `var := initializer` slot type。 +local stabilization 当前只负责一件事:在已发布的 callable executable body 中,按源码顺序稳定符合条件的 local `var := initializer` slot type。 它当前稳定负责: @@ -150,7 +153,7 @@ BlockScope.resetLocalType(...) - declaration 所在 scope 是 `BlockScope` - `FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(blockScope.kind()) == true` -这条边界继承 callable-local inventory 合同,不会把 class property、class const、block-local `const` 或当前未支持的 subtree 混进来。 +这条边界继承 structural callable-local inventory 合同。`FOR_BODY` 中的 ordinary `VariableDeclaration` 可以参与稳定化;iterator identity 是 `ForStatement`,不是 eligible declaration,由后续 iteration planning 负责精化。 ### 3.2 Source-order 单遍 @@ -276,14 +279,13 @@ package-private `probe(...)` 仅作为测试观察窗口存在,用来运行与 - parameter default - lambda / capture -- `for` - `match` - block-local `const` - class `const` - unsupported subtree - control-flow merge / join -这些边界若未来要打开,必须与 variable inventory、visible-value resolution、chain binding、expr typing 和 type check 一起收口,不能在本 phase 单独偷开支持面。 +这些边界若未来要打开,必须与 variable inventory、visible-value resolution、chain binding、expr typing 和 type check 一起收口,不能在本 phase 单独偷开支持面。For body 已经通过普通 SuiteResolver path 进入本 owner,但 iterator refinement 仍不属于 local `var :=` stabilization。 --- @@ -332,6 +334,7 @@ focused case 至少要继续覆盖: - forward local reference - assignment-based local type refinement - CFG branch / loop merge aware refinement -- `for` / `match` / lambda / capture local inventory +- `match` / lambda / capture local inventory +- for iterator route-aware refinement - property `:=` metadata backfill - 跨 callable 或 whole-module 的类型收敛 diff --git a/doc/module_impl/frontend/frontend_loop_control_flow_analyzer_implementation.md b/doc/module_impl/frontend/frontend_loop_control_flow_analyzer_implementation.md index 35e0b1ac..cb83c570 100644 --- a/doc/module_impl/frontend/frontend_loop_control_flow_analyzer_implementation.md +++ b/doc/module_impl/frontend/frontend_loop_control_flow_analyzer_implementation.md @@ -5,7 +5,7 @@ ## 文档状态 - 状态:已完成,事实源维护中 -- 最后更新:2026-04-01 +- 最后更新:2026-07-20 - 适用范围: - `src/main/java/gd/script/gdcc/frontend/sema/analyzer/**` - `src/test/java/gd/script/gdcc/frontend/sema/**` @@ -20,7 +20,7 @@ - 明确非目标: - 不在这里实现 `break` / `continue` 的 lowering - 不在这里修改 compile-only `FrontendCompileCheckAnalyzer` 的职责边界 - - 不在这里为 `for` / `match` / `lambda` 补齐完整 body semantic 支持 + - 不在这里为 `match` / `lambda` 补齐完整 body semantic 支持,也不实现 for iteration planning 或 lowering - 不在这里新增 side table 或改写已有 side table 结构 --- @@ -124,7 +124,7 @@ owner 固定为: 本次实现采用以下边界: - `while` 与 `for` 都视为 loop boundary - - 原因:即使 `for` 的其他 body semantic 仍未完全支持,`break` / `continue` 在 `for` 中是否合法仍是独立的 source-level 事实 + - `for` body 已进入 shared semantic;loop-control legality 仍是独立 diagnostics-only source fact,不读取 iterator type、iteration route 或 compile readiness - `function` / `constructor` / `lambda` 视为新的 callable boundary - 外层 loop 不得跨 callable 泄漏到内层 callable - `if` / `elif` / `else` / `match` / 普通 block` 不重置 loop depth` diff --git a/doc/module_impl/frontend/frontend_resolution_pipeline_implementation.md b/doc/module_impl/frontend/frontend_resolution_pipeline_implementation.md new file mode 100644 index 00000000..639b91f8 --- /dev/null +++ b/doc/module_impl/frontend/frontend_resolution_pipeline_implementation.md @@ -0,0 +1,425 @@ +# Frontend Resolution Pipeline 实现说明 + +> 本文档是 frontend shared semantic resolution pipeline 的长期事实源,定义 interface/body 双层 pipeline 的架构合同、不变量、核心设计与风险边界。不再保留阶段流水账或已完成任务日志。 + +## 文档状态 + +- 状态:已完成,事实源维护中 +- 更新时间:2026-07-23 +- 适用范围: + - `src/main/java/gd/script/gdcc/frontend/sema/**` + - `src/main/java/gd/script/gdcc/frontend/scope/**` + - `src/test/java/gd/script/gdcc/frontend/sema/**` +- 关联文档: + - `doc/module_impl/frontend/frontend_rules.md` + - `doc/module_impl/frontend/frontend_variable_analyzer_implementation.md` + - `doc/module_impl/frontend/frontend_visible_value_resolver_implementation.md` + - `doc/module_impl/frontend/frontend_local_type_stabilization_implementation.md` + - `doc/module_impl/frontend/frontend_chain_binding_expr_type_implementation.md` + - `doc/module_impl/frontend/frontend_type_check_analyzer_implementation.md` + - `doc/module_impl/frontend/frontend_compile_check_analyzer_implementation.md` + - `doc/module_impl/frontend/frontend_lowering_plan.md` + - `doc/module_impl/frontend/frontend_lowering_cfg_pass_implementation.md` + - `doc/module_impl/frontend/frontend_for_range_loop_implementation_plan.md` + - `doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md` +- 明确非目标: + - 不在这里定义 `for-range` lowering 或 Godot range runtime 语义 + - 不在这里转正 `lambda` / `match` / block-local `const` + - 不在这里定义 backend codegen 或 LIR intrinsic 合同 + +--- + +## 1. 背景与动机 + +原 `FrontendSemanticAnalyzer.analyze(...)` 是 whole-module phase pipeline(skeleton → scope → variable → top binding → local stabilization → chain binding → expr typing → var type post → diagnostics-only phases → compile gate)。该顺序让每个 phase 消费前一个 phase 的完整 module 事实,但无法自然表达 Godot 的 body 解析模型: + +- Godot analyzer 在 `resolve_body()` 中进入 `resolve_suite()`,按源码顺序逐个 statement 调用 `resolve_node(...)`。 +- `var x := expr` 的类型稳定、initializer expression reduce、assignment compatibility check 都在同一个 source-order 语句解析链中完成。 +- `for i in iterable:` 先 reduce iterable expression,再决定 iterator 类型,最后才解析 loop body suite。 + +原 statement-window segmented runner 方案试图在现有 phase pipeline 上模拟 source-order,但暴露出结构性阻塞:`analyzeInWindow(...)` 只改变发布表面不限制遍历范围;`FrontendVarTypePostAnalyzer.analyzeInWindow(...)` 直接 clear/write stable `slotTypes()` 再复制到 window scratch,破坏 scratch-over-stable 承诺。 + +因此 pipeline 改为 interface/body 双层结构: + +- Interface 层基于基础结构层已发布的 lexical inventory 建立 declaration index、signature/interface facts 与 typed baseline。 +- Body 层用 `FrontendSuiteResolver` 按源码顺序解析 supported body statements。 +- `FrontendTypedLexicalEnvironment` overlay 在 body 层提供"当前语句已知 typed fact 对后续语义立即可见"能力,同时保留 side-table owner、patch conflict 与 compiler-only type 隔离。 +- Suite 收敛后的 stable export 采用按 owner 有序的 patch transaction。 + +--- + +## 2. 架构决定:无条件结构性 inventory 与禁止 typed-dependent body gate + +本决定优先于本文后续章节中所有与之冲突的旧 gate 设计,也约束所有后续新增或更新的 frontend feature 实施文档。 + +- 所有待支持的 AST 节点必须在 body typed resolution 前无条件建立完整的结构性 lexical inventory。该 inventory 包括 body `BlockScope`、parameter、iterator、ordinary local 等 source-facing binding、`FrontendBodyDeclarationIndex` entry 与 typed baseline;不得等待 header 或 initializer 的 typed fact,也不得把 inventory publication 作为 typed fact 的副作用。 +- 类型事实只能用于 source-facing type refinement、semantic route 分类和 lowering / compile readiness。它们不得决定 body inventory 是否发布,也不得决定 `SuiteResolver` 或 `FrontendVisibleValueResolver` 是否进入一个已支持 body。 +- 后续 feature 不得新增 typed-dependent body entry gate。一个 AST 节点在其结构性 inventory path 尚未实现前可以整体保持 unsupported / deferred;一旦转正,其 body 必须无条件进入 shared semantic,不能通过 `PENDING`、`SUPPORTED`、`PUBLISHED` 或等价 typed readiness 状态延迟解封。 +- 新 feature 的实施顺序固定为:scope graph → 完整 lexical inventory → declaration index / baseline → `SuiteResolver` body entry → typed resolution → type refinement / lowering route。compile gate 的 route readiness 属于 lowering 边界,不是 semantic body entry gate。 + +生产代码不存在 registry、readiness fallback、pending-gate 注册或 synthetic classifier;后续 feature 不得恢复等价 lifecycle 或 publication protocol。 + +--- + +## 3. 不变量 + +### 3.1 Phase owner 边界 + +Owner 边界是语义合同: + +- `FrontendVariableAnalyzer` 拥有 parameter / ordinary local inventory publication。 +- `FrontendTopBindingAnalyzer` 拥有 `symbolBindings()`。 +- `FrontendLocalTypeStabilizationAnalyzer` 只拥有 source-facing local `:=` slot rewrite,不拥有 diagnostics,不发布 `resolvedMembers()` / `resolvedCalls()` / `expressionTypes()` / `slotTypes()`。 +- `FrontendChainBindingAnalyzer` 拥有 `resolvedMembers()` 与 chain-owned `resolvedCalls()`。 +- `FrontendExprTypeAnalyzer` 拥有 `expressionTypes()` 与 bare-call `resolvedCalls()`。 +- `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 不得恢复为第二个 slot mutation owner;必须保持 strict no-op / guard-only。 +- `FrontendVarTypePostAnalyzer` 拥有 `slotTypes()`。 +- `FrontendTypeCheckAnalyzer`、`FrontendLoopControlFlowAnalyzer`、`FrontendCompileCheckAnalyzer` 是 diagnostics-only consumer。 + +SuiteResolver 下的 owner 子过程是 statement-local procedure:由外层 statement dispatcher 提供 suite context、block context、restriction/static/property initializer context 与 typed lexical environment,而不是让每个 analyzer 自己从 module root walk。 + +### 3.2 完整 lexical inventory 先于 body typed resolution + +GDCC 的完整 lexical inventory 要求来自自己的 resolver filtered-hit 模型。`BlockScope.resolveValueHere(...)` 对当前层只做无 source-order 过滤的 map lookup;`FrontendVisibleValueResolver.resolve(...)` 在拿到当前层 hit 后,再由 `filterInvisibleCurrentLayerHit(...)` 按 source byte order 过滤。 + +因此 `Scope.resolveValueHere(...)` 必须能看到同层 future declaration: + +- declaration 已结束于 use-site 前:直接可见。 +- declaration 位于 use-site 后:记录 `DECLARATION_AFTER_USE_SITE` filtered hit。 +- use-site 位于同一 declaration initializer 内:记录 `SELF_REFERENCE_IN_INITIALIZER` filtered hit。 + +若把 local inventory 裁剪成只包含当前 statement 前缀,resolver 就看不到 future declaration,`var x := y; var y := 1` 会被误判为普通 miss 或外层 fallback,并丢失 declaration-after-use provenance。 + +正确模型: + +- 基础结构层沿用 `FrontendVariableAnalyzer` + scope graph 发布完整 ordinary local inventory;interface 层建立 body declaration index / typed baseline view。 +- local `:=` 初始类型仍是 `Variant`。 +- body `SuiteResolver` 只负责按源码顺序稳定类型并发布 use-site facts;不得通过 typed fact 决定 child body 的 inventory 或 entry readiness。 +- 若某个 child feature 后续转正,必须在 interface/inventory 阶段无条件发布该 child body 的结构性 binding 与完整 local inventory,再解析 body suite。 + +### 3.3 `FrontendAnalysisData` 稳定引用合同 + +- 保留 `updateXxx(...)` whole-table publication API 和测试。 +- 保留 `applyPatch(...)` 或等价 merge API 表达部分提交,但其输入必须是单一 owner patch。 +- 同一个 side table 的 stable reference 不替换。 +- 增量 merge 必须检测冲突,不能静默覆盖不兼容 fact。 +- `TypedLexicalEnvironment` 的 overlay fact 只有在 owner 合法、冲突校验和 compiler-only guard 通过后,才能封装为对应 owner patch 并导出到 stable side table。 +- 所有 production patch carrier、patch transaction 与 local slot update carrier 都位于 `gd.script.gdcc.frontend.sema.patch` 包。 + +### 3.4 skipped / deferred subtree 合同 + +- skeleton 发布的 `skippedSubtreeRoots()` 仍是后续 phase 的硬边界。 +- unsupported feature boundary 不能降级为 `NOT_FOUND`。 +- `DEFERRED` / `BLOCKED` / `DYNAMIC` / `FAILED` / `UNSUPPORTED` status 不能被压扁。 +- compile gate 仍只在 `analyzeForCompile(...)` 运行,且只消费最终 published facts。 + +### 3.5 compiler-only type 隔离 + +任何 stable publication 或 overlay export 都必须拒绝将 `GdCompilerType` 写入用户可见 facts: + +- `expressionTypes()` 的 `publishedType()` +- source-facing local / parameter / iterator 的 `slotTypes()` +- ordinary local `ScopeValue.type()` +- `symbolBindings()` 中 source-facing `resolvedValue.type()` +- `resolvedMembers()` 中 user-visible `receiverType` / `resultType` +- `resolvedCalls()` 中 user-visible `receiverType` / `returnType` / `argumentTypes` / callable boundary parameter types + +Feature-specific `GdCompilerType` 只能作为 hidden compiler state contract 被对应 feature 的 lowering 消费。 + +统一 guard 合同: + +- patch commit、overlay pending write、overlay flush、任何仍保留的 source-facing `updateXxx(...)` whole-table publish,都必须复用同一套 type-bearing field walker(`FrontendPublishedFactTypeGuard`)。 +- walker 至少递归访问 `FrontendBinding.resolvedValue().type()`、`FrontendResolvedMember.receiverType()` / `resultType()`、`FrontendResolvedCall.receiverType()` / `returnType()` / `argumentTypes()` / exact callable boundary parameter types、`FrontendExpressionType.publishedType()`、`slotTypes()` value、`FrontendLocalSlotTypeUpdate.type()`。 +- compiler-only guard 必须在 pending overlay write 时就 fail-fast,而不是等 suite export 时才补救。 + +### 3.6 Diagnostics owner 与去重合同 + +- interface phase 与 body phase 都必须通过既有 `DiagnosticManager` 发布 diagnostics。 +- upstream 已有同 anchor error 时,下游 analyzer 不能补同级重复错误。 +- `sema.binding` / `sema.member_resolution` / `sema.expression_resolution` / `sema.type_check` / `sema.compile_check` 的 owner 边界不能漂移。 + +### 3.7 已实施资产可回退但不能静默改变语义 + +任何代码回退必须保持: + +- `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` guard-only 合同。 +- `FrontendAnalysisData` stable reference 合同。 +- patch merge 的冲突检测与 compiler-only guard。 +- unsupported / deferred subtree fail-closed 合同。 + +--- + +## 4. 核心设计 + +### 4.1 三层 pipeline + +Shared semantic pipeline 分成三层加一个 diagnostics-only 层。 + +基础结构层: + +1. skeleton +2. scope graph +3. baseline inventory + +职责是建立 `FrontendModuleSkeleton`、`scopesByAst()`、callable parameter inventory、supported ordinary local inventory,以及 skipped/deferred subtree 的硬边界。不做 body expression typing。 + +Interface 层: + +1. class / callable / property signature interface +2. per-callable body declaration index +3. source-order local typed baseline + +借鉴 Godot `resolve_interface()` 与 `resolve_body()` 之间的边界:不直接 lowering body,也不发布 compile-ready body facts,但必须准备 body `SuiteResolver` 所需的 typed lexical baseline。 + +Body 层: + +1. `SuiteResolver` 按源码顺序进入 supported body suite +2. statement resolver 驱动 top binding / local stabilization / chain binding / expr typing / slot post 的 owner 子过程 +3. `TypedLexicalEnvironment` 为当前 statement 和当前 suite 提供 effective typed lookup +4. body suite 收敛后导出 stable side tables + +诊断-only 层在 body facts 完全收敛后运行: + +1. annotation usage +2. virtual override +3. type check +4. loop control +5. compile-only final gate + +### 4.2 Interface phase + +`FrontendInterfacePhase` 在 skeleton/scope/variable analyzer 之后建立 body 解析所需的 interface surface。 + +输入: + +- `FrontendModuleSkeleton` +- `scopesByAst()` +- baseline parameter / ordinary local inventory +- current diagnostics snapshot +- `ClassRegistry` + +输出: + +- `FrontendBodyDeclarationIndex`:每个 supported block 的完整 declaration 列表与 source order;production resolver 用它验证 scope local 的 published-inventory identity。 +- `FrontendTypedLexicalBaseline`:参数、显式 typed local、已可静态确定的 interface-level source-facing slot baseline;production `TypedLexicalEnvironment` 将其作为冻结 fallback。 +- `FrontendSuiteEntryRoots`:body layer 可进入的 callable/property initializer/supported block 根列表。 + +禁止: + +- 发布 `expressionTypes()`、`resolvedMembers()` / `resolvedCalls()`。 +- 将已转正 AST 节点的 body inventory 延迟到 typed fact 可用后。 +- 将 `GdCompilerType` 写入 source-facing lexical baseline。 + +### 4.3 Body `SuiteResolver` + +`FrontendSuiteResolver` 按 Godot `resolve_suite()` 的形状处理 body: + +```text +resolveCallableOwner(context, callableOwner): + exportBatch = new FrontendCallableExportBatch() + context = newSuiteContext(callableOwner, exportBatch) + runCallableEntryVarTypePost(context, callableOwner) + resolveSuite(context, callableOwner.body) + exportBatch.applyTo(context.analysisData()) + +resolveSuite(context, block): + for statement in block.statements(): + resolveStatement(context, statement) + context.exportBatch().accumulate(context.typedEnvironment().exportPatchTransaction()) +``` + +参数是 callable-entry `VAR_TYPE_POST` facts,在进入 statement 循环前写入 pending overlay 并 flush 到 current-suite committed overlay。 + +`resolveStatement(...)` 内部的 owner 子过程顺序是硬不变量: + +0. Callable-entry var type post pre-publication(仅 callable 入口)。 +1. Top binding runner。 +2. Local stabilization runner。 +3. Chain binding runner。 +4. Expr typing runner。 +5. Feature-specific semantic route planning(仅在该 statement kind 已有 concrete owner 时运行)。 +6. Var type post procedure。 +7. Pending fact flush。 + +不得重排 1-6,且 feature-specific stage 只能插在 expr typing 与对应 var type post 之间。 + +Suite 收敛后,committed overlay 构造为按 owner 有序的 patch transaction。嵌套 suite 只把 transaction 追加到同一 callable-scoped export batch,根 callable suite 收敛后才由 batch 按追加顺序 apply。Transaction / batch 不表达原子提交;失败后当前 `FrontendAnalysisData` 必须整体丢弃。 + +第一版 body statement 支持面: + +- `VariableDeclaration`(ordinary local `var` 与 supported property initializer) +- `ExpressionStatement`、`ReturnStatement`、`AssertStatement` +- `IfStatement` / `ElifClause` / `else` +- `WhileStatement` +- `ForStatement`(structural supported,header-first,body 通过 child-suite path 进入) +- `MatchStatement`、`LambdaExpression`、block-local `const` 继续 deferred / unsupported + +### 4.4 `TypedLexicalEnvironment` overlay + +`FrontendTypedLexicalEnvironment` 是 body 层所有 value/type lookup 的 effective view,包装 `Scope` 与当前 suite 的 typed overlay。 + +读取顺序: + +1. 当前 statement pending overlay +2. 当前 suite committed overlay +3. 已发布 stable slot facts +4. parent typed lexical environment +5. interface typed baseline 冻结 source-facing fallback +6. class/global/singleton/type-meta lookup + +四层事实可见性模型: + +- Owner procedure transient cache:owner 子过程私有,结束即丢弃。 +- 当前 statement pending overlay:只给当前 statement 后续 owner 子过程读取。 +- current-suite committed overlay:由 pending fact flush 合并而来,给后续 statement 读取;仍不是 stable publication。 +- `FrontendAnalysisData` stable side tables / `BlockScope` stable slot:只在 suite export 的 per-owner patch apply 后更新。 + +写入规则: + +- 每个 owner 只能写自己对应的 overlay。 +- overlay fact 必须带 owner metadata,并在导出前执行冲突检测、idempotent 检查和 compiler-only guard。 +- `expressionTypes()` overlay 只接受每个 AST key 的最终 publication fact。retry 中间计算留在 owner-local transient cache。 +- `expressionTypes()` overlay 不提供 `Variant → exact`、parent → child 或 terminal status → success 的 narrowing 例外。 + +写入与导出时机: + +- Statement owner runner 只能写当前 statement pending overlay。 +- `flushPendingFacts(...)` 把 pending overlay 合并到 current-suite committed overlay。 +- Suite 收敛时,committed overlay 导出为按 owner 有序的 patch transaction。 +- 嵌套 suite 的 transaction 追加到 root callable 共享的 `FrontendCallableExportBatch`。 +- Stable side table 与 `BlockScope.resetLocalType(...)` 只能在 callable export batch 的 per-owner patch apply 中更新。 +- Diagnostics-only phase、compile gate 与 lowering 只能读取 suite export 后的 stable facts。 + +### 4.5 Structural support 与 completeness certificate + +Body entry 由两个互不替代的结构事实决定: + +- `FrontendBodySemanticSupportPolicy` 只按 AST/scope kind 回答某类位置是否发布 lexical inventory、是否进入 `FrontendSuiteResolver`,以及 unsupported 位置使用哪个精确 deferred domain。 +- `FrontendBodyStructuralCompleteness` 对本次 Interface surface 做 fail-fast 验证:scope identity、suite entry、body declaration index、declaration/binding/scope identity 与 source-facing typed baseline 必须同时完整;完整性对 published body inventory 是双向的。 + +Policy 不读取 expression type、typed overlay、iteration plan、diagnostic state 或 compile surface;certificate 不读取 pending/committed overlay、slot refinement 或 semantic/lowering route。 + +### 4.6 Stable export 与 per-owner patch transaction + +`gd.script.gdcc.frontend.sema.patch` 包承载 patch 相关类型: + +- `FrontendOwnerPatch`(sealed interface):`FrontendTopBindingPatch`、`FrontendLocalTypeStabilizationPatch`、`FrontendChainBindingPatch`、`FrontendExprTypePatch`、`FrontendVarTypePostPatch`。 +- `FrontendPatchTransaction`:按固定 owner 顺序 apply per-owner patches。 +- `FrontendCallableExportBatch`:累积单个 callable root 及其 nested suite 的 transaction,root suite 收敛后按追加顺序逐个 apply。 +- `FrontendLocalSlotTypeUpdate`:local stabilization owner 的专用 carrier。 + +Per-owner patch merge 规则: + +- 新 key 直接写入。 +- 旧 key + 相同 value 视为 idempotent。 +- 旧 key + 不同 value 默认 fail-fast。 +- `expressionTypes()` 同 key republish 只有 `sameExpressionType(...)` 判定为同值时允许(status + publishedType + detailReason 严格相等)。 +- `slotTypes()` 不允许同一 source slot 被不同类型覆盖。 +- merge 前统一检查 compiler-only type 不泄漏。 + +### 4.7 Scope slot mutation + +`FrontendLocalSlotTypeUpdate` 应用规则: + +- `FrontendLocalTypeStabilizationAnalyzer` 是唯一允许产生 source-facing slot update 的 analyzer。 +- 只允许 `Variant → exact` 或 exact same-type no-op。 +- 不允许 exact A → exact B、`GdVoidType`、`GdCompilerType`。 +- 应用后必须刷新已发布且指向同一 declaration 的 `symbolBindings()` payload。 + +`FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 必须保持 guard-only:不调用 `BlockScope.resetLocalType(...)`、不刷新 `symbolBindings()` payload、不产生 `FrontendLocalSlotTypeUpdate`。 + +### 4.8 Resolver 复用规则 + +`FrontendVisibleValueResolver` 继续一次性索引完整 source AST,继续读取已发布的完整 lexical inventory。 + +三道结构检查: + +1. Request-domain hard boundary:`EXECUTABLE_BODY` 才进入 ordinary lookup。 +2. AST boundary:parameter default、lambda body、match pattern/guard/body 与 block-local `const` initializer 直接返回 structural deferred boundary;`ForStatement.body()`、iterator type 与 iterable edge 不封口。 +3. Current-scope backstop:`LAMBDA_BODY`、`MATCH_SECTION_BODY` 与 lambda callable scope 继续 fail-closed;`FOR_BODY` 是 supported executable scope。 + +Overlay 不得绕过 resolver filter:resolver 先按 request-domain gate、AST boundary、current-scope gate、declaration order 与 initializer self-reference 过滤候选 declaration,再从 `TypedLexicalEnvironment` 读取该候选的 effective type / binding payload。 + +--- + +## 5. 风险与缓解 + +### R1:side-table 冲突被静默覆盖 + +overlay export 必须通过 per-owner patch merge API;默认不同 value fail-fast。fail-fast 只阻止当前冲突 patch 覆盖既有 value,不意味着 transaction 或 callable batch 失败后无部分提交。 + +### R2:resolver 看不到未来声明 + +interface phase 必须基于基础结构层已发布的 inventory 为 supported suite 建立完整 local declaration index。禁止 resolver 自己扫描普通 `var` 弥补缺口。 + +### R3:scope slot mutation 与 published binding payload 脱节 + +local slot rewrite 通过 `FrontendLocalSlotTypeUpdate` 统一应用,并由同一个 commit helper 派生刷新同 declaration 的 `symbolBindings()`。 + +### R4:历史 backfill 路径恢复第二个 slot mutation owner + +`backfillInferredLocalType(...)` 必须是 strict no-op / guard-only。需要新的 narrowing 能力时只能扩展 local stabilization。 + +### R5:structural capability 与实际 inventory completeness 被混为一谈 + +immutable structural support matrix 只回答某种 AST/scope kind 的 inventory path 是否已实现;completeness certificate 对 published body inventory 做双向校验。缺失结构事实必须 fail-fast,不能静默跳过 body,也不能重新引入 `PENDING` / `PUBLISHED` lifecycle。 + +### R6:`SuiteResolver` 绕过 phase owner 边界 + +`SuiteResolver` 只编排 owner 子过程,不直接写 owner side table。`FrontendSuiteContext` 校验当前 runner identity 与目标 overlay owner 匹配。 + +### R7:diagnostics 重复或顺序漂移 + +interface phase、body statement、suite export、diagnostics-only phase 都有明确 diagnostics snapshot 边界。 + +### R8:unsupported subtree 被过早打开 + +support matrix 对 lambda、match、block-local `const`、parameter default 和 unknown/skipped structure 显式返回各自 deferred domain;新增 scope/AST kind 必须通过 exhaustive mapping 显式选择 policy。 + +### R9:compiler-only type 泄漏 + +`FrontendPublishedFactTypeGuard` shared walker 覆盖 binding/member/call/expression/slot/update 六类 source-facing typed payload,并接入 patch、overlay 与保留的 whole-table publication API。新增 type-bearing payload 时必须同步扩展 shared walker 与 regression tests。 + +### R10:statement 内 owner 顺序或 overlay 导出时机漂移 + +base owner procedure 顺序不可重排;pending → committed → stable export 时机不可折叠。 + +### R11:resolver structural domain、AST edge 与 current scope 漂移 + +supported `FOR_BODY` 的 request domain、AST edge 与 current scope 必须同时允许 ordinary executable lookup;unsupported feature 由同一 structural policy 返回精确 deferred domain。 + +### R12:retry 中间 expression type 被导出导致 patch 冲突 + +chain / argument retry 的中间 facts 只能存放在 owner procedure 内部非导出 transient cache;overlay 与 stable table 都只能包含每 key 最终单条 fact。 + +### R13:single-stage patch 被误用为 multi-owner suite export + +suite export 生产路径不得构造跨 owner `FrontendAnalysisPatch`。`FrontendPatchTransaction` 按固定 owner 顺序 apply per-owner patches,拒绝乱序、重复 owner 或单一 patch 内跨 owner payload。 + +### R14:ordered transaction / callable batch 被误认为原子提交 + +`FrontendPatchTransaction` 只保证 owner 顺序,`FrontendCallableExportBatch` 只保证延迟到 root callable 返回后按追加顺序 apply。二者都不做整体 prepare、不提供失败回滚。production path 必须传播 apply 异常并丢弃整个 `FrontendAnalysisData`。 + +--- + +## 6. 核心不变量清单 + +- frontend shared semantic 默认使用 interface/body pipeline。 +- interface phase 建立完整 local declaration index、typed baseline 与 suite entry roots,但不做 body typed resolution。 +- `SuiteResolver` 按 source order 解析 supported body,并在 child body 前验证 structural completeness certificate;typed refinement 不参与 body-entry 决策。 +- base statement owner 顺序固定为 top binding → local stabilization → chain binding → expr typing → var type post;feature-specific semantic route owner 只能插在 expr typing 与对应 var type post 之间。 +- Production SuiteResolver path 不调用 `analyzeInWindow(...)`、不从 module `SourceFile` root 启动内部 walker,也不通过整表 `updateXxx(...)` 表达 body typed result。 +- typed overlay 区分 current statement pending facts 与 current suite committed facts,export 前不污染 stable side table 或 `BlockScope`。 +- `FrontendAnalysisData` 支持带 conflict / guard 校验的 per-owner patch merge,并保持 stable reference 合同。 +- Suite export 使用按固定 owner 顺序 apply 的 `FrontendPatchTransaction`;生产路径不使用 single-stage `FrontendAnalysisPatch` 承载多 owner facts。 +- 所有 production patch 相关类型位于 `gd.script.gdcc.frontend.sema.patch` 包。 +- compiler-only guard 覆盖所有 user-visible type-bearing publication surfaces,由 shared walker 统一执行。 +- production body path 不通过可变 stable 引用直接 `put()` / `clear()` / `putAll()`。 +- `backfillInferredLocalType(...)` 保持 guard-only。 +- 完整 local inventory 先于 body typed resolution,declaration-after-use filtered hit 行为不退化。 +- chain binding 消费 receiver local slot 时,必须看到 local stabilization 已发布到 overlay 的 exact type。 +- immutable structural support matrix 是 AST/scope kind 支持面的单一事实源;不读取 typed fact、compile readiness 或 lifecycle state。 +- resolver 的三道结构检查不读取 registry;supported `FOR_BODY` 三处均放行,unsupported feature 继续 structural fail-closed。 +- nested chain / argument retry 不产生 stable `expressionTypes()` narrowing rewrite;每个 key 最多一个最终 fact。 +- unsupported subtree 通过 structural policy、AST boundary 与 current-scope backstop fail-closed。 +- compile gate、lowering-ready fact 边界和 compiler-only type 隔离不变。 diff --git a/doc/module_impl/frontend/frontend_rules.md b/doc/module_impl/frontend/frontend_rules.md index 9af238c7..4e350a2a 100644 --- a/doc/module_impl/frontend/frontend_rules.md +++ b/doc/module_impl/frontend/frontend_rules.md @@ -36,7 +36,7 @@ - `break` / `continue` 的位置合法性属于 shared semantic contract;`FrontendLoopControlFlowAnalyzer` 必须在进入 compile-only gate 前就对非法 loop control 发出 `sema.loop_control_flow`,lowering 中的 loop-frame fail-fast 只能保留为实现不变量保护。 - `_field_init_`、`_field_getter_`、`_field_setter_` 是 compiler-owned synthetic property helper 前缀;source class member 一旦以这些前缀开头,skeleton phase 必须发出清晰的 `sema.class_skeleton` 并跳过该 member subtree,而不是等到 lowering/backend 再因 helper 名冲突抛异常。 - `FrontendCompileCheckAnalyzer` 只能挂在 compile-only 入口上;默认共享 `FrontendSemanticAnalyzer.analyze(...)`、inspection 与未来 LSP 入口不得隐式附带 compile-only gate。 -- compile-only gate 只允许扫描未来 lowering 会消费的 compile surface:supported executable body 与 supported property initializer island;不得重新深入 parameter default、lambda、`for`、`match`、block-local `const` 或 skipped subtree。 +- compile-only gate 只允许扫描未来 lowering 会消费的 compile surface:supported executable body 与 supported property initializer island;不得重新深入 parameter default、lambda、`match`、block-local `const` 或 skipped subtree。`ForStatement` 已进入 shared semantic,但 route-aware lowering 尚未落地,因此 compile-only gate 当前只在 statement root 发布一个临时 blocker,不进入 body 重扫 semantic facts。 - compile-only gate 一旦放行 supported property initializer,默认 lowering pipeline 必须把它 materialize 为真实 `init_func` helper;backend 不得再把同名 shell-only function 当作可修补中间态消费。 - compile-only gate 对已发布 side-table 事实的最终阻断范围固定为 `BLOCKED` / `DEFERRED` / `FAILED` / `UNSUPPORTED`;`DYNAMIC` 继续保留为 frontend 已认可的 runtime-open 事实,不得在 compile gate 中误判成 blocker。 - executable-body body lowering 对 call 的 lowering-ready surface 必须与 compile gate / CFG builder 保持一致:`resolvedCalls()` 只要已发布为 `RESOLVED` 或 `DYNAMIC` 就允许进入 body pass;其中 `DYNAMIC_FALLBACK` 当前只允许 instance route,必须继续复用 `CallMethodInsn` surface,结果类型真源来自 call anchor 的 `expressionTypes()`,frontend 不得为该路由重做 callable signature 推导。runtime-open 调用本身仍由 backend dynamic dispatch 承接,但其已发布的 `Variant` 结果若继续跨越 ordinary typed boundary,则仍由 frontend ordinary boundary helper 负责后续 `(un)pack`;若同一 call site 还发布了 writable receiver access-chain payload,则 body lowering 必须整体消费这条 payload 做 receiver-side leaf selection / post-call commit,不得把同一条 chain 重新拆成额外 step item 或回头重跑 AST;同时 sequence-item lowering 必须继续线程化当前 continuation block,避免 runtime-gated writeback 把后续 lowering 错挂回原始 lexical block。长合同以 `frontend_dynamic_call_lowering_implementation.md` 为准。 @@ -62,7 +62,7 @@ ## MVP 支持约定 - 下述 MVP 约定描述的是当前 frontend 共享语义、body analyzer 与 compile surface 的正式支持面;它们不否认 parser 与 scope phase 对部分语法结构已经能识别或建图。 -- `lambda`、`match`、`for` 当前不在 frontend body semantic MVP 正式支持面;相关子树仍按 deferred / unsupported boundary fail-closed。 +- `for` 已进入 frontend shared body semantic 支持面:iterator、ordinary body local、declaration index、typed baseline 与 suite entry 在 typed resolution 前按结构无条件发布,header 解析后通过普通 child-suite path 进入 body。该支持不代表 compile-ready;在 route-aware compile policy、CFG 与 lowering 完成前,compile-only gate 继续以 statement-root 临时 blocker 阻断。`lambda`、`match` 仍按结构性 deferred / unsupported boundary fail-closed。 - 协程与 signal-based coroutine 当前不在 frontend semantic MVP 范围内;`await` / `.emit(...)` 等 use-site 语义仍未闭环。 - path-based `extends`、autoload superclass、global-script-class superclass 绑定不实施。 - 多 gdcc module 的 header superclass 绑定不在最小可行产品范围内。 diff --git a/doc/module_impl/frontend/frontend_top_binding_analyzer_implementation.md b/doc/module_impl/frontend/frontend_top_binding_analyzer_implementation.md index 32f6b184..156dd223 100644 --- a/doc/module_impl/frontend/frontend_top_binding_analyzer_implementation.md +++ b/doc/module_impl/frontend/frontend_top_binding_analyzer_implementation.md @@ -1,11 +1,13 @@ # FrontendTopBindingAnalyzer 实现说明 -> 本文档作为 `FrontendTopBindingAnalyzer` 的长期事实源,定义当前职责边界、`symbolBindings()` 发布合同、命名空间分流规则、遍历与恢复语义,以及后续工程必须遵守的接线约束。本文档替代旧的实施计划与进度记录,不再保留阶段清单、验收流水账或已完成任务日志。 +> 本文档作为 `FrontendBodyOwnerProcedures` 中 top-binding owner 的长期事实源,定义当前 +> 职责边界、`symbolBindings()` 发布合同、命名空间分流规则、遍历与恢复语义,以及后续 +> 工程必须遵守的接线约束。 ## 文档状态 -- 状态:事实源维护中(`symbolBindings()` 重建、builtin / global enum / class-like top-level `TYPE_META` 规则、value-position bare callable / bare `TYPE_META` ordinary-value misuse 合同、class property initializer support island、root-level skipped-subtree 恢复合同、usage-agnostic binding 模型与核心单元测试已落地) -- 更新时间:2026-06-27 +- 状态:事实源维护中(statement-local `symbolBindings()` publication、for header/body binding、builtin / global enum / class-like `TYPE_META` 规则、property initializer support island、结构性 skipped-subtree 恢复合同与核心单元测试已落地) +- 更新时间:2026-07-20 - 适用范围: - `src/main/java/gd/script/gdcc/frontend/sema/**` - `src/main/java/gd/script/gdcc/frontend/sema/analyzer/**` @@ -26,7 +28,7 @@ - 不在这里发布 `resolvedMembers()`、`resolvedCalls()` 或 `expressionTypes()` - 不在这里解析显式 receiver 尾部成员或调用步骤 - 不在这里建模 read / write / call / assignable / lvalue 语义 - - 不在这里实现 parameter default、lambda、`for`、`match`、block-local `const` 的正式 binding + - 不在这里实现 parameter default、lambda、`match`、block-local `const` 的正式 binding - 不在这里扩展 shared `Scope` 协议 - 不在这里处理 class constant binding;该能力仍延后到 MVP 之后 @@ -362,6 +364,7 @@ builtin static namespace 当前仍 direct-only,因为 ExtensionAPI builtin met - `elif` body - `else` body - `while` body +- `for` iterator type / iterable header 与 `FOR_BODY` - ordinary local `var` initializer expression subtree - class property `var` initializer expression subtree @@ -379,7 +382,6 @@ builtin static namespace 当前仍 direct-only,因为 ExtensionAPI builtin met - parameter default subtree - lambda subtree -- `for` subtree - `match` subtree - block-local `const` initializer subtree - 任何缺少稳定 `scopesByAst()` 记录的 skipped subtree @@ -464,7 +466,6 @@ builtin static namespace 当前仍 direct-only,因为 ExtensionAPI builtin met - parameter default subtree - lambda subtree -- `for` subtree - `match` subtree - block-local `const` initializer subtree - missing-scope / skipped subtree @@ -647,7 +648,9 @@ func ping(values): print(item) ``` -- `for` subtree 当前 deferred +- `values` 绑定为 `PARAMETER` +- body 内 `item` 绑定为 iterator `LOCAL_VAR` +- `print` 按 utility function route 绑定;for body 不再产生 root-level unsupported binding error ```gdscript func ping(value): @@ -694,5 +697,6 @@ func ping(): - explicit receiver 只绑定链头与 step/index arguments,不绑定尾部 segment - assignment 左右两侧都会递归进入 binding - ordinary local initializer 继续属于支持面 -- parameter default / lambda / `for` / `match` / block-local `const` 当前继续走 root-level unsupported error +- parameter default / lambda / `match` / block-local `const` 当前继续走 root-level unsupported error +- for header/body use-site 通过普通 executable-body resolver path 发布 binding,且不依赖 iterable typed result 决定 body entry - skipped executable subtree 的 warning 当前按 root-level 发布,而不是静默跳过或逐 use-site 降级 diff --git a/doc/module_impl/frontend/frontend_type_check_analyzer_implementation.md b/doc/module_impl/frontend/frontend_type_check_analyzer_implementation.md index 00cd92c2..1ddd4540 100644 --- a/doc/module_impl/frontend/frontend_type_check_analyzer_implementation.md +++ b/doc/module_impl/frontend/frontend_type_check_analyzer_implementation.md @@ -4,8 +4,8 @@ ## 文档状态 -- 状态:事实源维护中(diagnostics-only type check、utility void normalization、Godot-compatible condition contract、unary/binary stable-fact consumption、property initializer boundary consumption、bare-return contract 收紧、`@onready` usage validation 已落地) -- 更新时间:2026-04-05 +- 状态:事实源维护中(diagnostics-only type check、for body stable-fact consumption、utility void normalization、Godot-compatible condition contract、property initializer boundary consumption与 `@onready` usage validation 已落地) +- 更新时间:2026-07-20 - 适用范围: - `src/main/java/gd/script/gdcc/frontend/sema/**` - `src/main/java/gd/script/gdcc/frontend/sema/analyzer/**` @@ -33,7 +33,8 @@ - 不在这里新增 `FrontendAnalysisData` side table - 不在这里重做表达式求值、binding、member/call 解析或 scope 构建 - 不在这里补 suite merge、missing-return、all-path return exhaustiveness 分析 - - 不在这里转正 `lambda`、`for`、`match`、parameter default、block-local `const`、class `const` 的正式 body 语义 + - 不在这里转正 `lambda`、`match`、parameter default、block-local `const`、class `const` 的正式 body 语义 + - 不在这里实现 `FrontendForIterationPlan`、iterable route validation 或 iterator element conversion;这些属于 for-range 后续阶段 - 不在这里实现 property-side inference/backfill,也不在 type-check analyzer 内维护平行 implicit conversion 规则;`int -> float`、同维度 `Vector*i -> Vector*` 与 `StringName` / `String` 互转只通过 `frontend_implicit_conversion_matrix.md` 与 shared boundary helper 生效 - 不在这里实现 frontend -> LIR 的 truthiness lowering 或 `@onready` 的 runtime / ready-time 语义 @@ -385,7 +386,8 @@ owner 分工固定为: - property-side inference / metadata backfill - frontend -> LIR 的 truthiness / condition normalization - `@onready` runtime / ready-time lowering -- `lambda`、`for`、`match`、parameter default、block-local `const`、class `const` 的正式 body semantics +- `lambda`、`match`、parameter default、block-local `const`、class `const` 的正式 body semantics +- for iteration plan、iterator element compatibility 与 route-aware type diagnostics;for body 本身已经进入 shared semantic,并复用普通 stable-fact type-check path 后续工程若继续扩展本区域,必须遵守以下约束: diff --git a/doc/module_impl/frontend/frontend_variable_analyzer_implementation.md b/doc/module_impl/frontend/frontend_variable_analyzer_implementation.md index bce73de6..1330f957 100644 --- a/doc/module_impl/frontend/frontend_variable_analyzer_implementation.md +++ b/doc/module_impl/frontend/frontend_variable_analyzer_implementation.md @@ -5,7 +5,7 @@ ## 文档状态 - 性质:长期事实源 -- 最后校对:2026-04-02 +- 最后校对:2026-07-20(阶段 L:for iterator/body inventory 已转正,结构支持面不再依赖 typed gate) - 适用范围: - `src/main/java/gd/script/gdcc/frontend/sema/**` - `src/main/java/gd/script/gdcc/frontend/sema/analyzer/**` @@ -24,7 +24,7 @@ - 不在这里实现完整 frontend binder / body - 不在这里写入 `symbolBindings()` 或表达式类型 side-table - 不在这里实现成员解析、调用解析或 use-site declaration-order 可见性 - - 不在这里接入 lambda / `for` / `match` / block-local `const` 的完整 inventory + - 不在这里接入 lambda / `match` / block-local `const` 的完整 inventory - 不改变 shared `Scope` lookup 协议 --- @@ -42,12 +42,15 @@ 5. `analysisData.updateDiagnostics(...)` 6. `FrontendVariableAnalyzer.analyze(...)` 7. `analysisData.updateDiagnostics(...)` +8. `FrontendInterfacePhase.analyze(...)` +9. `FrontendSuiteResolver.resolve(...)` 这意味着: - `FrontendVariableAnalyzer` 只运行在 skeleton 与 scope graph 已发布之后 - 它消费 `moduleSkeleton`、`diagnostics` 与 `scopesByAst` - 它不会重建 scope graph,而是就地 enrich 已发布的 `CallableScope` / `BlockScope` +- 它发布的是 interface/body pipeline 的 inventory 前置条件;body 事实不再由 shared analyzer 的 legacy whole-phase owner bypass 回填,而是由 SuiteResolver 的 statement-local owner procedures 通过 per-owner patch transaction 导出 ### 1.2 当前职责 @@ -55,13 +58,13 @@ - 把 `FunctionDeclaration` / `ConstructorDeclaration` 参数写入对应 `CallableScope` - 把 supported executable subtree 中的普通局部 `var` 写入对应 `BlockScope` +- 为每个 `ForStatement` 在其 `FOR_BODY` scope 发布 iterator binding,并遍历 body 发布 ordinary local inventory - 对当前明确不支持的 variable-inventory 来源发出显式 error,避免静默跳过 - 对 duplicate / shadowing / target scope kind mismatch 发布恢复性 diagnostic,并保持其他 subtree 继续处理 `FrontendVariableAnalyzer` 明确不负责: - lambda parameter / local / capture inventory -- `for` iterator binding 与 loop body local inventory - `match` pattern binding 与 section local inventory - block-local `const` binding - 参数默认值表达式的类型/绑定分析 @@ -80,6 +83,8 @@ - `ConstructorDeclaration` parameter -> owning `CallableScope` - function / constructor body 中的普通局部 `var` -> body `BlockScope` - supported nested executable block 中的普通局部 `var` -> 对应 `BlockScope` +- `ForStatement` iterator -> 对应 `FOR_BODY` scope,declaration identity 为 owning `ForStatement` +- for body 中的普通局部 `var` -> 对应 `FOR_BODY` scope 当前支持写入 ordinary local `var` 的 `BlockScopeKind` 为: @@ -90,6 +95,7 @@ - `ELIF_BODY` - `ELSE_BODY` - `WHILE_BODY` +- `FOR_BODY` ### 2.2 当前不写入的绑定 @@ -98,8 +104,6 @@ - lambda parameter - lambda local - lambda capture -- `for` iterator binding -- `for` body local - `match` pattern binding - `match` section local - block-local `const` @@ -188,7 +192,7 @@ 需要特别注意: - binder 主遍历不会把 arbitrary expression subtree 当作 local-binding 域 -- lambda / `for` / `match` subtree 不会进入 binding walk +- lambda / `match` subtree 不会进入 binding walk;`ForStatement` 使用专用 iterator/body inventory path - unsupported feature-boundary error 由独立的 boundary reporter 扫描 callable body 后补发 ### 4.3 executable-context 判定 @@ -225,7 +229,6 @@ - 参数默认值当前被忽略 - `sema.unsupported_variable_inventory_subtree` - lambda subtree 当前不支持 - - `for` subtree 当前不支持 - `match` subtree 当前不支持 - block-local `const` 当前不支持 @@ -250,7 +253,9 @@ - 当前 declaration range - 冲突 declaration range(若已有 declaration 可回溯到 AST) - enclosing callable / block 语义归属 - - source path +- source path + +For iterator 与 parameter、外层 local 或 body local 冲突时仍发布 `sema.variable_binding`,但会保留以 owning `ForStatement` 为 declaration identity 的 iterator recovery binding。普通源码重名不能制造缺失 iterator index/baseline 的结构洞;后续 body resolver 因此仍可继续,并由原始合法 binding 保持冲突可见性。 ### 5.4 缺 scope 记录的语义 @@ -314,7 +319,7 @@ 后续最直接的增量工作包括: - lambda parameter / local / capture inventory -- `for` iterator 与 loop-body inventory +- for iteration planning 与 iterator slot refinement - `match` pattern binding 与 section inventory - block-local `const` inventory - `frontend.sema.resolver.FrontendVisibleValueResolver` @@ -329,6 +334,7 @@ - `FrontendVariableAnalyzerTest` - parameter/local 正向写入 + - for iterator、body local、nested for 与冲突恢复 binding - unsupported feature-boundary error - duplicate / shadowing / kind mismatch 负向路径 - duplicate / shadowing local 详细 diagnostic message 锚点 diff --git a/doc/module_impl/frontend/frontend_visible_value_resolver_implementation.md b/doc/module_impl/frontend/frontend_visible_value_resolver_implementation.md index 69a47aab..26f36cb4 100644 --- a/doc/module_impl/frontend/frontend_visible_value_resolver_implementation.md +++ b/doc/module_impl/frontend/frontend_visible_value_resolver_implementation.md @@ -4,8 +4,8 @@ ## 文档状态 -- 状态:事实源维护中(request/result 合同、declaration-order 过滤、initializer 自引用过滤、deferred boundary 封口、current-scope fail-closed hardening、shared support matrix 与核心单元测试已落地) -- 更新时间:2026-03-16 +- 状态:事实源维护中(request/result 合同、declaration-order 过滤、initializer 自引用过滤、结构性 deferred boundary、typed overlay-aware resolve、for header/body 解封与核心单元测试已落地) +- 更新时间:2026-07-20(阶段 L:删除 typed-dependent readiness,resolver 只消费 structural policy、完整 inventory 与 typed overlay) - 适用范围: - `src/main/java/gd/script/gdcc/frontend/sema/**` - `src/main/java/gd/script/gdcc/frontend/sema/analyzer/**` @@ -23,7 +23,7 @@ - 不修改 shared `Scope.resolveValue(...)` 协议 - 不让 unsupported 子域伪装成正常 `NOT_FOUND` - 不在 resolver 内直接生产 diagnostics 或写入 `symbolBindings()` - - 不在当前 frontend 中为 parameter default、lambda、`for`、`match`、block-local `const` 提供正式 local inventory 解析 + - 不在当前 frontend 中为 parameter default、lambda、`match`、block-local `const` 提供正式 local inventory 解析 --- @@ -55,7 +55,7 @@ ### 2.1 与 `FrontendVariableAnalyzer` 的分工 - `FrontendVariableAnalyzer` 负责把 parameter / supported ordinary local inventory 发布到 `CallableScope` / `BlockScope` -- inventory 发布范围由 `FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(...)` 统一定义 +- inventory 发布范围由 `FrontendBodySemanticSupportPolicy` 统一定义;`FrontendExecutableInventorySupport` 只是该 policy 的兼容委托入口 - 当前允许发布 callable-local value inventory 的 `BlockScopeKind` 只有: - `FUNCTION_BODY` - `CONSTRUCTOR_BODY` @@ -64,6 +64,7 @@ - `ELIF_BODY` - `ELSE_BODY` - `WHILE_BODY` + - `FOR_BODY` - resolver 与 variable analyzer 必须共用同一 support matrix,不能各自维护名单 ### 2.2 与 shared `Scope` 的分工 @@ -82,6 +83,10 @@ shared `Scope` 继续负责: - filtered hit provenance 保留 - deferred / unsupported 域显式封口 +### 2.3 与 `FrontendTypedLexicalEnvironment` 的分工 + +SuiteResolver body path 中的 caller 必须优先通过 typed overlay-aware `resolve(request, environment)` 入口读取 current statement / current suite 已发布的 local slot 与 binding payload。resolver 仍保留 request-domain hard boundary、AST boundary 与 current-scope backstop;overlay 只改变可见事实来源,不允许绕过 declaration-order、initializer self-reference 或 structural unsupported boundary。 + --- ## 3. 支持域与封口域 @@ -103,16 +108,14 @@ resolver 当前只对 `EXECUTABLE_BODY` 域提供正常 lookup,并要求同时 - parameter default-value expression - lambda body - block-local `const` initializer subtree -- `for` iterator type / iterable / body - `match` pattern / guard / section body - 任何缺少稳定 scope 记录的 subtree - 任何 current scope 自身就是未发布 inventory 的 scope,例如: - - `BlockScopeKind.FOR_BODY` - `BlockScopeKind.MATCH_SECTION_BODY` - `BlockScopeKind.LAMBDA_BODY` - `CallableScopeKind.LAMBDA_EXPRESSION` -这些域当前不能静默回退到 outer local / class property / global,也不能伪装成普通 `NOT_FOUND`。 +这些域当前不能静默回退到 outer local / class property / global,也不能伪装成普通 `NOT_FOUND`。`ForStatement` 的 iterator type、iterable 与 body edge 已转正,`FOR_BODY` current scope 直接进入 normal lookup。 ### 3.3 `ClassScope` / `ClassRegistry` 不是封口域 @@ -181,7 +184,6 @@ resolver 当前只对 `EXECUTABLE_BODY` 域提供正常 lookup,并要求同时 - `PARAMETER_DEFAULT` - `LAMBDA_SUBTREE` - `BLOCK_LOCAL_CONST_SUBTREE` -- `FOR_SUBTREE` - `MATCH_SUBTREE` - `UNKNOWN_OR_SKIPPED_SUBTREE` @@ -189,7 +191,6 @@ resolver 当前只对 `EXECUTABLE_BODY` 域提供正常 lookup,并要求同时 - `UNSUPPORTED_DOMAIN` - `MISSING_SCOPE_OR_SKIPPED_SUBTREE` -- `VARIABLE_INVENTORY_NOT_PUBLISHED` --- @@ -198,7 +199,7 @@ resolver 当前只对 `EXECUTABLE_BODY` 域提供正常 lookup,并要求同时 `FrontendVisibleValueResolver.resolve(request)` 当前行为冻结为: 1. 若 `request.domain != EXECUTABLE_BODY`,直接返回 `DEFERRED_UNSUPPORTED(domain = request.domain, reason = UNSUPPORTED_DOMAIN)` -2. 先做 AST boundary 检测,识别 parameter default、lambda body、block-local `const` initializer、`for`、`match` 等共享外层 scope 的 unsupported 子树 +2. 先做 AST boundary 检测,识别 parameter default、lambda body、block-local `const` initializer、`match` 等共享外层 scope 的 unsupported 子树;for header/body edge 不封口 3. 若 use-site 缺少 current scope 记录,返回 `DEFERRED_UNSUPPORTED` 4. 对 current scope 执行 fail-closed 校验;若 current scope 自身没有已发布 inventory,也返回 `DEFERRED_UNSUPPORTED` 5. 在继续 outer fallback 之前,检查 enclosing supported block 中是否已经出现同名且当前可见的 block-local `const`;若存在,也必须封口为 `BLOCK_LOCAL_CONST_SUBTREE` @@ -238,7 +239,7 @@ resolver 当前只对 `EXECUTABLE_BODY` 域提供正常 lookup,并要求同时 ### 6.2 support matrix 必须共享 -`FrontendVariableAnalyzer` 发布 inventory 的范围,与 resolver 允许正常 lookup 的范围,本质上是同一份事实。该事实现在由 `FrontendExecutableInventorySupport` 统一承载;后续若扩展支持域,必须同时评估: +`FrontendVariableAnalyzer` 发布 inventory 的范围,与 resolver 允许正常 lookup 的范围,本质上是同一份事实。该事实现在由 `FrontendBodySemanticSupportPolicy` 统一承载;后续若扩展支持域,必须同时评估: - variable inventory 是否真的发布 - resolver 是否真的能在该域内给出不误导的绑定结果 @@ -264,7 +265,8 @@ resolver 当前只对 `EXECUTABLE_BODY` 域提供正常 lookup,并要求同时 - parameter / ordinary local / class property 三类基本路径的正向解析 - future local / initializer self-reference 会进入 `filteredHits`,且不会误命中当前 declaration - `FOUND_BLOCKED` 不会被错误降级成 `NOT_FOUND` -- parameter default、lambda、block-local `const`、`for`、`match`、missing-scope 都返回 `DEFERRED_UNSUPPORTED` +- parameter default、lambda、block-local `const`、`match`、missing-scope 都返回 `DEFERRED_UNSUPPORTED` +- for header/body/current-scope lookup 正常进入 resolver,iterator 与 body local 仍受 published inventory、declaration-order 与 self-reference guard 约束 - 真实 AST deferred 场景与 synthetic current-scope remap 场景都已覆盖,证明即使 AST boundary 漏判,resolver 仍按未发布 inventory fail-closed - shared `ClassScope` / `ClassRegistry` 抛出的 `ScopeLookupException` 会原样传播 diff --git a/doc/module_impl/frontend/scope_analyzer_implementation.md b/doc/module_impl/frontend/scope_analyzer_implementation.md index 35a6bde0..ce485460 100644 --- a/doc/module_impl/frontend/scope_analyzer_implementation.md +++ b/doc/module_impl/frontend/scope_analyzer_implementation.md @@ -271,15 +271,15 @@ mapped top-level gdcc class 当前已同时满足: --- -## 6. Deferred Binding 边界 +## 6. Downstream Binding 边界 当前 scope analyzer 只发布 lexical graph,不做 binding prefill。 -仍然明确 deferred 的内容包括: +不由 scope analyzer 自身写入的内容包括: - callable parameters 的 binding 写入 - 普通 `var` / `const` 的 binding 写入 -- `for` iterator 的 binding 与类型落地 +- `for` iterator 的 binding 与类型落地;该工作现由后续 `FrontendVariableAnalyzer` 完成 - captures 的推导与写入 - `PatternBindingExpression` 的 binding 写入 - 成员解析、调用解析、表达式类型推断 @@ -288,7 +288,7 @@ mapped top-level gdcc class 当前已同时满足: - `Parameter` 节点必须拥有 side-table scope 记录,但这不等价于 parameter 已经能通过 `CallableScope.resolveValue(...)` 被解析 - `for` 的 `iteratorType` 与 `iterable` 在进入 loop body 之前按外层 scope 分析 -- `for` body 拥有独立 `BlockScope`,但 `iterator` 仍未预填 +- `for` body 拥有独立 `BlockScope`;scope phase 不预填 iterator,后续 variable inventory phase 以 owning `ForStatement` 为 declaration identity 发布 iterator 与 body local - `MatchSection` 的 pattern、guard 与 body 共享同一个 branch scope,但 pattern binding 仍未注册成 local 后续若要实现 binder,必须在完整 scope graph 建成之后单独做 binding pass,而不是把 binding 写回夹进当前 walker。 diff --git a/doc/module_impl/frontend/scope_architecture_refactor_plan.md b/doc/module_impl/frontend/scope_architecture_refactor_plan.md index 97c5c6ec..ab966f51 100644 --- a/doc/module_impl/frontend/scope_architecture_refactor_plan.md +++ b/doc/module_impl/frontend/scope_architecture_refactor_plan.md @@ -413,8 +413,7 @@ AST 节点与 scope 的关联仍应由 side-table 维护,而不是把 AST 节 当前仍需继续保持 deferred 的 scope-prefill / binding 内容包括: -- block local / local const prefill -- `for` iterator binding +- block-local `const` prefill - `match` pattern binding - lambda capture 推导与 `CallableScope.defineCapture(...)` 的生产接线 @@ -559,15 +558,16 @@ frontend binder 侧的 declaration-order 可见性修正层,详见: - function / constructor parameter -> `CallableScope` - function / constructor body 与 supported nested block 中的 ordinary local `var` -> `BlockScope` +- `ForStatement` iterator 与 for body ordinary local -> `FOR_BODY` scope;iterator declaration identity 与 fallback baseline 已冻结 - same-callable parameter/local shadowing 在 variable phase 直接诊断并拒绝写入 -- 参数默认值、lambda、`for`、`match`、block-local `const` 继续 deferred +- 参数默认值、lambda、`match`、block-local `const` 继续 deferred 后续 binder phase 在接线前或接线过程中仍需要冻结: - 同一 callable 内的 parameter/local/capture shadowing 规则 - 参数默认值的可见性顺序 - ordinary local `var` initializer 已按 `FrontendVisibleValueResolver` 合同进入当前支持面,不再作为 binder 的前置阻塞项 -- `for` iterator 的 declaration site 与 fallback type 策略 +- for iteration planning 与 iterator exact slot refinement - `match` pattern binding 在 guard/body 中的可见性 - lambda capture 是同 phase 落地还是继续 deferred diff --git a/doc/module_impl/frontend/scope_type_resolver_implementation.md b/doc/module_impl/frontend/scope_type_resolver_implementation.md index 0cda2bb6..605b0ffe 100644 --- a/doc/module_impl/frontend/scope_type_resolver_implementation.md +++ b/doc/module_impl/frontend/scope_type_resolver_implementation.md @@ -4,8 +4,8 @@ ## 文档状态 -- 状态:事实源维护中(shared resolver、skeleton 接入、真实 scope graph 接入、compatibility mapper 与 caller-side remap 接入已落地) -- 更新时间:2026-03-25 +- 状态:事实源维护中(shared resolver、skeleton/variable 接入、真实 scope graph、for iterator declared type、compatibility mapper 与 caller-side remap 已落地) +- 更新时间:2026-07-20 - 适用范围: - `src/main/java/gd/script/gdcc/frontend/sema/**` - `src/main/java/gd/script/gdcc/frontend/scope/**` @@ -135,6 +135,16 @@ - outer class 只通过 type-meta parent chain 暴露 outer type - outer value/function namespace 仍不应被 inner class 直接继承 +### 2.5 For iterator declared type + +`FrontendVariableAnalyzer` 对 `for iterator: Type in expr` 复用 `FrontendDeclaredTypeSupport` 与本 shared strict resolver: + +- 显式 iterator type 按 iterator/header 所在外层 lexical type namespace 解析。 +- 未写显式 type 时,iterator source-facing baseline 为 `Variant`。 +- strict type miss 继续由 caller 发布 `sema.type_resolution` 并恢复为 `Variant`;resolver 本身不产出 diagnostic。 +- iterator declaration identity 是 owning `ForStatement`,但这不改变类型名解析协议。 +- iterable 的 typed result 不参与 declared-type lookup,也不能决定 `FOR_BODY` inventory 或 SuiteResolver entry。 + --- ## 3. strict 与 compatibility API 分工 diff --git a/src/main/java/gd/script/gdcc/exception/FrontendAnalysisPatchException.java b/src/main/java/gd/script/gdcc/exception/FrontendAnalysisPatchException.java new file mode 100644 index 00000000..d121f9df --- /dev/null +++ b/src/main/java/gd/script/gdcc/exception/FrontendAnalysisPatchException.java @@ -0,0 +1,10 @@ +package gd.script.gdcc.exception; + +import org.jetbrains.annotations.NotNull; + +/// Exception thrown when a segmented frontend analysis patch violates publication contracts. +public final class FrontendAnalysisPatchException extends GdccException { + public FrontendAnalysisPatchException(@NotNull String message) { + super(message); + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/lowering/pass/body/FrontendBodyLoweringSession.java b/src/main/java/gd/script/gdcc/frontend/lowering/pass/body/FrontendBodyLoweringSession.java index 58643855..5c9e1009 100644 --- a/src/main/java/gd/script/gdcc/frontend/lowering/pass/body/FrontendBodyLoweringSession.java +++ b/src/main/java/gd/script/gdcc/frontend/lowering/pass/body/FrontendBodyLoweringSession.java @@ -133,7 +133,7 @@ public void run() { return binding; } - /// `FrontendTopBindingAnalyzer` only publishes binding kind `SELF` for explicit + /// The top-binding owner procedure only publishes binding kind `SELF` for explicit /// `SelfExpression`. Any identifier node that still carries `SELF` means some earlier /// publication step leaked an impossible surface into lowering, so all body-lowering entry /// points must reject it consistently instead of silently rewriting the identifier to `self`. @@ -1032,7 +1032,7 @@ boolean isStaticPropertyBinding(@NotNull FrontendBinding binding) { ); } - @NotNull GdObjectType requireSingletonType(@NotNull FrontendBinding binding) { + void checkSingletonBindingType(@NotNull FrontendBinding binding) { var singletonType = classRegistry.findSingletonType(binding.symbolName()); if (singletonType == null) { throw new IllegalStateException( @@ -1040,7 +1040,6 @@ boolean isStaticPropertyBinding(@NotNull FrontendBinding binding) { + "' is missing registry-validated object metadata" ); } - return singletonType; } /// Allocates one body-local helper temp owned by writable-route lowering. diff --git a/src/main/java/gd/script/gdcc/frontend/lowering/pass/body/FrontendOpaqueExprInsnLoweringProcessors.java b/src/main/java/gd/script/gdcc/frontend/lowering/pass/body/FrontendOpaqueExprInsnLoweringProcessors.java index 372875f7..ec5a24bb 100644 --- a/src/main/java/gd/script/gdcc/frontend/lowering/pass/body/FrontendOpaqueExprInsnLoweringProcessors.java +++ b/src/main/java/gd/script/gdcc/frontend/lowering/pass/body/FrontendOpaqueExprInsnLoweringProcessors.java @@ -49,7 +49,7 @@ private FrontendOpaqueExprInsnLoweringProcessors() { /// (local/parameter/capture/property/self/singleton); it must not re-run any scope lookup or /// member inference. /// - /// `FrontendTopBindingAnalyzer` publishes `FrontendBindingKind.SELF` only for explicit + /// The top-binding owner procedure publishes `FrontendBindingKind.SELF` only for explicit /// `SelfExpression`. If an `IdentifierExpression` arrives here with binding kind `SELF`, some /// earlier publication step violated that contract and body lowering must fail fast instead of /// silently recovering to `"self"`. @@ -87,7 +87,7 @@ private static final class FrontendIdentifierOpaqueExprInsnLoweringProcessor block.appendNonTerminatorInstruction(new LoadPropertyInsn(resultSlotId, binding.symbolName(), "self")); } case SINGLETON -> { - session.requireSingletonType(binding); + session.checkSingletonBindingType(binding); block.appendNonTerminatorInstruction(new LoadStaticInsn( resultSlotId, "@GlobalScope", diff --git a/src/main/java/gd/script/gdcc/frontend/scope/BlockScope.java b/src/main/java/gd/script/gdcc/frontend/scope/BlockScope.java index e7042791..f7aab34a 100644 --- a/src/main/java/gd/script/gdcc/frontend/scope/BlockScope.java +++ b/src/main/java/gd/script/gdcc/frontend/scope/BlockScope.java @@ -9,7 +9,10 @@ import gd.script.gdcc.type.GdType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnmodifiableView; +import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -45,6 +48,15 @@ public BlockScope(@NotNull Scope parentScope, @NotNull BlockScopeKind kind) { return kind; } + /// Returns an unmodifiable view of all value bindings owned by this block. + /// + /// Insertion order matches the order in which locals and constants were published. Callers that need + /// the Interface-phase body inventory should filter to [ScopeValueKind#LOCAL]; block-local constants + /// may appear here once supported, but are not part of the current body declaration index. + public @NotNull @UnmodifiableView Collection localValues() { + return Collections.unmodifiableCollection(valuesByName.values()); + } + /// Registers a mutable local binding owned by the current block. public void defineLocal( @NotNull String name, diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendAnalysisData.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendAnalysisData.java index b2ba4bc1..bb48d59c 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendAnalysisData.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendAnalysisData.java @@ -1,11 +1,21 @@ package gd.script.gdcc.frontend.sema; +import dev.superice.gdparser.frontend.ast.Node; +import gd.script.gdcc.exception.FrontendAnalysisPatchException; +import gd.script.gdcc.frontend.sema.patch.FrontendLocalSlotTypeUpdate; +import gd.script.gdcc.frontend.sema.patch.FrontendOwnerPatch; +import gd.script.gdcc.frontend.sema.patch.FrontendPublishedFactTypeGuard; import gd.script.gdcc.frontend.diagnostic.DiagnosticSnapshot; import gd.script.gdcc.scope.Scope; +import gd.script.gdcc.scope.ScopeValue; +import gd.script.gdcc.scope.ScopeValueKind; import gd.script.gdcc.type.GdType; +import gd.script.gdcc.type.GdVariantType; +import gd.script.gdcc.type.GdVoidType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -91,24 +101,29 @@ public void updateScopesByAst(@NotNull FrontendAstSideTable scopesByAst) } public void updateSymbolBindings(@NotNull FrontendAstSideTable symbolBindings) { + FrontendPublishedFactTypeGuard.checkSymbolBindings(symbolBindings); replaceSideTableContents(this.symbolBindings, symbolBindings, "symbolBindings"); } /// Replaces the published expression-fact snapshot in place while preserving the stable table /// reference. Callers may publish both expression-root facts and attribute-step facts here. public void updateExpressionTypes(@NotNull FrontendAstSideTable expressionTypes) { + FrontendPublishedFactTypeGuard.checkExpressionTypes(expressionTypes); replaceSideTableContents(this.expressionTypes, expressionTypes, "expressionTypes"); } public void updateResolvedMembers(@NotNull FrontendAstSideTable resolvedMembers) { + FrontendPublishedFactTypeGuard.checkResolvedMembers(resolvedMembers); replaceSideTableContents(this.resolvedMembers, resolvedMembers, "resolvedMembers"); } public void updateResolvedCalls(@NotNull FrontendAstSideTable resolvedCalls) { + FrontendPublishedFactTypeGuard.checkResolvedCalls(resolvedCalls); replaceSideTableContents(this.resolvedCalls, resolvedCalls, "resolvedCalls"); } public void updateSlotTypes(@NotNull FrontendAstSideTable slotTypes) { + FrontendPublishedFactTypeGuard.checkSlotTypes(slotTypes); replaceSideTableContents( this.slotTypes, slotTypes, @@ -116,6 +131,65 @@ public void updateSlotTypes(@NotNull FrontendAstSideTable slotTypes) { ); } + /// Applies one single-owner patch without replacing any stable side-table reference. + /// + /// Conflict checks and local-slot validation are scoped to this patch. Repeated calls, including + /// calls from `FrontendPatchTransaction`, do not form an atomic unit and do not roll back earlier patches. + public void applyPatch(@NotNull FrontendOwnerPatch patch) { + var checkedPatch = Objects.requireNonNull(patch, "patch must not be null"); + FrontendPublishedFactTypeGuard.checkOwnerPatch(checkedPatch); + applyPatchFields( + checkedPatch.stage(), + checkedPatch.symbolBindings(), + checkedPatch.resolvedMembers(), + checkedPatch.resolvedCalls(), + checkedPatch.expressionTypes(), + checkedPatch.slotTypes(), + checkedPatch.localSlotTypeUpdates() + ); + } + + private void applyPatchFields( + @NotNull FrontendSemanticStage stage, + @NotNull FrontendAstSideTable patchSymbolBindings, + @NotNull FrontendAstSideTable patchResolvedMembers, + @NotNull FrontendAstSideTable patchResolvedCalls, + @NotNull FrontendAstSideTable patchExpressionTypes, + @NotNull FrontendAstSideTable patchSlotTypes, + @NotNull List localSlotTypeUpdates + ) { + var validatedLocalSlotUpdates = validateLocalSlotTypeUpdates(stage, localSlotTypeUpdates); + checkPatchConflicts(symbolBindings, patchSymbolBindings, "symbolBindings", FrontendAnalysisData::sameBinding); + checkPatchConflicts( + resolvedMembers, + patchResolvedMembers, + "resolvedMembers", + FrontendAnalysisData::sameResolvedMember + ); + checkPatchConflicts( + resolvedCalls, + patchResolvedCalls, + "resolvedCalls", + FrontendAnalysisData::sameResolvedCall + ); + checkPatchConflicts( + expressionTypes, + patchExpressionTypes, + "expressionTypes", + FrontendAnalysisData::sameExpressionType + ); + checkPatchConflicts(slotTypes, patchSlotTypes, "slotTypes", FrontendAnalysisData::sameType); + + mergeSideTable(symbolBindings, patchSymbolBindings); + mergeSideTable(resolvedMembers, patchResolvedMembers); + mergeSideTable(resolvedCalls, patchResolvedCalls); + mergeSideTable(expressionTypes, patchExpressionTypes); + mergeSideTable(slotTypes, patchSlotTypes); + for (var validatedUpdate : validatedLocalSlotUpdates) { + applyLocalSlotTypeUpdate(validatedUpdate); + } + } + public @NotNull FrontendModuleSkeleton moduleSkeleton() { return requirePublished(moduleSkeleton, "moduleSkeleton"); } @@ -159,6 +233,48 @@ public void updateSlotTypes(@NotNull FrontendAstSideTable slotTypes) { return slotTypes; } + /// Refreshes published local bindings after a verified local-slot rewrite. + /// + /// This helper intentionally updates only the `resolvedValue` payload. The binding kind, source + /// name, and declaration identity stay frozen from top binding so downstream phases keep seeing + /// the same lexical choice while observing the narrowed slot type. + public void refreshPublishedLocalBindingPayloads( + @NotNull FrontendLocalSlotTypeUpdate slotTypeUpdate, + @NotNull ScopeValue updatedValue + ) { + var checkedUpdate = Objects.requireNonNull(slotTypeUpdate, "slotTypeUpdate must not be null"); + var checkedUpdatedValue = Objects.requireNonNull(updatedValue, "updatedValue must not be null"); + if (checkedUpdatedValue.declaration() != checkedUpdate.declaration()) { + throw patchFailure( + "local slot refresh resolved a different declaration for '" + + checkedUpdate.name() + + "'" + ); + } + if (!sameType(checkedUpdatedValue.type(), checkedUpdate.type())) { + throw patchFailure( + "local slot refresh resolved an unexpected type for '" + + checkedUpdate.name() + + "': expected " + + checkedUpdate.type().getTypeName() + + ", got " + + checkedUpdatedValue.type().getTypeName() + ); + } + checkNoCompilerOnlyLeak( + checkedUpdatedValue.type(), + "symbolBindings local payload refresh for '" + checkedUpdate.name() + "'" + ); + for (var entry : symbolBindings.entrySet()) { + var binding = entry.getValue(); + var resolvedValue = binding.resolvedValue(); + if (resolvedValue == null || resolvedValue.declaration() != checkedUpdate.declaration()) { + continue; + } + entry.setValue(binding.withResolvedValue(checkedUpdatedValue)); + } + } + private @NotNull T requirePublished(@Nullable T value, @NotNull String fieldName) { if (value == null) { throw new IllegalStateException(fieldName + " has not been published yet"); @@ -166,6 +282,246 @@ public void updateSlotTypes(@NotNull FrontendAstSideTable slotTypes) { return value; } + private @NotNull List validateLocalSlotTypeUpdates( + @NotNull FrontendSemanticStage stage, + @NotNull List localSlotTypeUpdates + ) { + if (localSlotTypeUpdates.isEmpty()) { + return List.of(); + } + if (stage != FrontendSemanticStage.LOCAL_TYPE_STABILIZATION) { + throw patchFailure( + "Only LOCAL_TYPE_STABILIZATION patches may publish local slot type updates, but got " + + stage + ); + } + var validatedUpdates = new ArrayList(localSlotTypeUpdates.size()); + for (var update : localSlotTypeUpdates) { + validatedUpdates.add(validateLocalSlotTypeUpdate(update, validatedUpdates)); + } + return List.copyOf(validatedUpdates); + } + + private @NotNull ValidatedLocalSlotTypeUpdate validateLocalSlotTypeUpdate( + @NotNull FrontendLocalSlotTypeUpdate update, + @NotNull List validatedUpdates + ) { + checkNoVoidLocalSlotType(update.type(), update.name()); + checkNoCompilerOnlyLeak(update.type(), "local slot update for '" + update.name() + "'"); + var existingValue = lookupPendingLocalSlotValue(update, validatedUpdates); + if (existingValue == null) { + existingValue = requireCurrentLocalSlotValue(update); + } + if (existingValue.declaration() != update.declaration()) { + throw patchFailure( + "local slot update targeted a different declaration for '" + update.name() + "'" + ); + } + if (sameType(existingValue.type(), update.type())) { + return new ValidatedLocalSlotTypeUpdate(update, null); + } + if (!(existingValue.type() instanceof GdVariantType)) { + throw patchFailure( + "local slot update changed exact type for '" + + update.name() + + "' from " + + existingValue.type().getTypeName() + + " to " + + update.type().getTypeName() + ); + } + return new ValidatedLocalSlotTypeUpdate( + update, + new ScopeValue( + existingValue.name(), + update.type(), + existingValue.kind(), + existingValue.declaration(), + existingValue.constant(), + existingValue.writable(), + existingValue.staticMember() + ) + ); + } + + private @Nullable ScopeValue lookupPendingLocalSlotValue( + @NotNull FrontendLocalSlotTypeUpdate target, + @NotNull List validatedUpdates + ) { + for (var i = validatedUpdates.size() - 1; i >= 0; i--) { + var validatedUpdate = validatedUpdates.get(i); + var update = validatedUpdate.update(); + if (update.scope() != target.scope() + || !update.name().equals(target.name()) + || update.declaration() != target.declaration()) { + continue; + } + return validatedUpdate.refreshedValue() != null + ? validatedUpdate.refreshedValue() + : requireCurrentLocalSlotValue(update); + } + return null; + } + + private @NotNull ScopeValue requireCurrentLocalSlotValue(@NotNull FrontendLocalSlotTypeUpdate update) { + var currentValue = update.scope().resolveValueHere(update.name()); + if (currentValue == null) { + throw patchFailure("local slot update targeted a missing binding for '" + update.name() + "'"); + } + if (currentValue.kind() != ScopeValueKind.LOCAL) { + throw patchFailure("local slot update targeted a non-local binding for '" + update.name() + "'"); + } + return currentValue; + } + + private void applyLocalSlotTypeUpdate(@NotNull ValidatedLocalSlotTypeUpdate validatedUpdate) { + var refreshedValue = validatedUpdate.refreshedValue(); + if (refreshedValue == null) { + return; + } + var update = validatedUpdate.update(); + update.scope().resetLocalType(update.name(), update.declaration(), update.type()); + refreshPublishedLocalBindingPayloads(update, refreshedValue); + } + + private void checkPatchConflicts( + @NotNull FrontendAstSideTable stableTable, + @NotNull FrontendAstSideTable patchTable, + @NotNull String fieldName, + @NotNull SameValueChecker sameValueChecker + ) { + for (var entry : patchTable.entrySet()) { + var existingValue = stableTable.get(entry.getKey()); + if (existingValue == null || sameValueChecker.sameValue(existingValue, entry.getValue())) { + continue; + } + throw patchFailure( + fieldName + + " patch conflicted on " + + describeNode(entry.getKey()) + ); + } + } + + private static void mergeSideTable( + @NotNull FrontendAstSideTable stableTable, + @NotNull FrontendAstSideTable patchTable + ) { + for (var entry : patchTable.entrySet()) { + if (!stableTable.containsKey(entry.getKey())) { + stableTable.put(entry.getKey(), entry.getValue()); + } + } + } + + static void checkNoCompilerOnlyLeak(@Nullable GdType type, @NotNull String fieldName) { + FrontendPublishedFactTypeGuard.checkNoCompilerOnlyLeak(type, fieldName); + } + + static void checkNoVoidLocalSlotType(@NotNull GdType type, @NotNull String localName) { + if (type instanceof GdVoidType) { + throw patchFailure("local slot update for '" + localName + "' must not publish void"); + } + } + + static boolean sameBinding(@NotNull FrontendBinding first, @NotNull FrontendBinding second) { + return first.kind() == second.kind() + && first.symbolName().equals(second.symbolName()) + && first.declarationSite() == second.declarationSite() + && first.valueAccessStatus() == second.valueAccessStatus() + && sameScopeValue(first.resolvedValue(), second.resolvedValue()); + } + + static boolean sameResolvedMember( + @NotNull FrontendResolvedMember first, + @NotNull FrontendResolvedMember second + ) { + return first.bindingKind() == second.bindingKind() + && first.status() == second.status() + && first.receiverKind() == second.receiverKind() + && first.ownerKind() == second.ownerKind() + && first.declarationSite() == second.declarationSite() + && first.memberName().equals(second.memberName()) + && sameType(first.receiverType(), second.receiverType()) + && sameType(first.resultType(), second.resultType()) + && Objects.equals(first.detailReason(), second.detailReason()); + } + + static boolean sameResolvedCall(@NotNull FrontendResolvedCall first, @NotNull FrontendResolvedCall second) { + return first.callKind() == second.callKind() + && first.status() == second.status() + && first.receiverKind() == second.receiverKind() + && first.ownerKind() == second.ownerKind() + && first.declarationSite() == second.declarationSite() + && first.callableName().equals(second.callableName()) + && sameType(first.receiverType(), second.receiverType()) + && sameType(first.returnType(), second.returnType()) + && sameTypeList(first.argumentTypes(), second.argumentTypes()) + && sameExactCallableBoundary(first.exactCallableBoundary(), second.exactCallableBoundary()) + && Objects.equals(first.detailReason(), second.detailReason()); + } + + static boolean sameExactCallableBoundary( + @Nullable FrontendResolvedCall.ExactCallableBoundary first, + @Nullable FrontendResolvedCall.ExactCallableBoundary second + ) { + if (first == null || second == null) { + return first == second; + } + return first.isVararg() == second.isVararg() + && sameTypeList(first.fixedParameterTypes(), second.fixedParameterTypes()); + } + + static boolean sameExpressionType( + @NotNull FrontendExpressionType first, + @NotNull FrontendExpressionType second + ) { + return first.status() == second.status() + && sameType(first.publishedType(), second.publishedType()) + && Objects.equals(first.detailReason(), second.detailReason()); + } + + static boolean sameScopeValue(@Nullable ScopeValue first, @Nullable ScopeValue second) { + if (first == null || second == null) { + return first == second; + } + return first.kind() == second.kind() + && first.constant() == second.constant() + && first.writable() == second.writable() + && first.staticMember() == second.staticMember() + && first.declaration() == second.declaration() + && first.name().equals(second.name()) + && sameType(first.type(), second.type()); + } + + static boolean sameTypeList(@NotNull List first, @NotNull List second) { + if (first.size() != second.size()) { + return false; + } + for (var i = 0; i < first.size(); i++) { + if (!sameType(first.get(i), second.get(i))) { + return false; + } + } + return true; + } + + static boolean sameType(@Nullable GdType first, @Nullable GdType second) { + if (first == null || second == null) { + return first == second; + } + return first == second + || (first.getClass() == second.getClass() && first.getTypeName().equals(second.getTypeName())); + } + + static @NotNull String describeNode(@NotNull Node node) { + return node.getClass().getSimpleName(); + } + + static @NotNull FrontendAnalysisPatchException patchFailure(@NotNull String message) { + return new FrontendAnalysisPatchException(message); + } + private static void replaceSideTableContents( @NotNull FrontendAstSideTable target, @NotNull FrontendAstSideTable source, @@ -179,4 +535,18 @@ private static void replaceSideTableContents( target.clear(); target.putAll(source); } + + @FunctionalInterface + private interface SameValueChecker { + boolean sameValue(@NotNull V first, @NotNull V second); + } + + private record ValidatedLocalSlotTypeUpdate( + @NotNull FrontendLocalSlotTypeUpdate update, + @Nullable ScopeValue refreshedValue + ) { + private ValidatedLocalSlotTypeUpdate { + Objects.requireNonNull(update, "update must not be null"); + } + } } diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyDeclarationIndex.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyDeclarationIndex.java new file mode 100644 index 00000000..7fe0e97d --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyDeclarationIndex.java @@ -0,0 +1,71 @@ +package gd.script.gdcc.frontend.sema; + +import dev.superice.gdparser.frontend.ast.Node; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/// Interface-layer index of ordinary locals and for-iterators that already exist in baseline +/// `BlockScope` inventory. +/// +/// The index is intentionally a view over published inventory, not a second declaration publisher. +/// `FrontendBodyStructuralCompleteness` requires the view to cover every published `LOCAL` binding in +/// the body scope; production body lookup uses declaration identity to verify that a scope local +/// belongs to this published inventory; source-range filtering still determines declaration-order +/// visibility. +/// +/// For each `FOR_BODY` root the published list must contain exactly one iterator entry at position 0 +/// with `sourceOrder == 0`; ordinary body locals follow at contiguous `sourceOrder >= 1`. +public final class FrontendBodyDeclarationIndex { + private final @NotNull Map> declarationsByBodyRoot; + private final @NotNull Map declarationsByDeclaration; + + public FrontendBodyDeclarationIndex( + @NotNull Map> declarationsByBodyRoot + ) { + Objects.requireNonNull(declarationsByBodyRoot, "declarationsByBodyRoot"); + var copiedDeclarations = new IdentityHashMap>(); + var copiedDeclarationsByDeclaration = new IdentityHashMap(); + for (var entry : declarationsByBodyRoot.entrySet()) { + var bodyRoot = Objects.requireNonNull(entry.getKey(), "bodyRoot"); + var declarations = List.copyOf(Objects.requireNonNull(entry.getValue(), "declarations")); + copiedDeclarations.put( + bodyRoot, + declarations + ); + for (var declaration : declarations) { + var previous = copiedDeclarationsByDeclaration.put( + declaration.declaration(), + declaration + ); + if (previous != null) { + throw new IllegalArgumentException("ordinary local declaration belongs to multiple body roots"); + } + } + } + this.declarationsByBodyRoot = Collections.unmodifiableMap(copiedDeclarations); + declarationsByDeclaration = Collections.unmodifiableMap(copiedDeclarationsByDeclaration); + } + + public @NotNull List declarationsFor(@NotNull Node bodyRoot) { + return declarationsByBodyRoot.getOrDefault(Objects.requireNonNull(bodyRoot, "bodyRoot"), List.of()); + } + + public boolean containsBodyRoot(@NotNull Node bodyRoot) { + return declarationsByBodyRoot.containsKey(Objects.requireNonNull(bodyRoot, "bodyRoot")); + } + + /// Returns the published inventory entry for one source local or iterator declaration, if any. + public @Nullable FrontendBodyLocalDeclaration declarationFor(@NotNull Node declaration) { + return declarationsByDeclaration.get(Objects.requireNonNull(declaration, "declaration")); + } + + public @NotNull Map> declarationsByBodyRoot() { + return declarationsByBodyRoot; + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyLocalDeclaration.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyLocalDeclaration.java new file mode 100644 index 00000000..56a8cfc2 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyLocalDeclaration.java @@ -0,0 +1,45 @@ +package gd.script.gdcc.frontend.sema; + +import dev.superice.gdparser.frontend.ast.Node; +import gd.script.gdcc.scope.ScopeValue; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +/// One ordinary local or for-iterator declaration published by the baseline inventory layer for a +/// supported body. +/// +/// `sourceOrder` is local to the owning body root and must remain contiguous from zero in AST +/// start-byte order. Production lookup still uses declaration identity plus source byte-range filters +/// for visibility; `sourceOrder` is a structural inventory fact certified at suite entry. +/// +/// For `FOR_BODY` inventory the contract is stricter: exactly one [Kind#ITERATOR] entry must exist as a +/// synthetic 0th item occupying list position 0 with `sourceOrder == 0` (no negative sentinel is used; +/// `sourceOrder` is never negative). Ordinary body locals follow at `sourceOrder >= 1`. +/// [FrontendBodyStructuralCompleteness] and the Interface-phase publisher both preserve this shape. +public record FrontendBodyLocalDeclaration( + @NotNull Node declaration, + @NotNull ScopeValue binding, + @NotNull Kind kind, + int sourceOrder +) { + public FrontendBodyLocalDeclaration { + Objects.requireNonNull(declaration, "declaration"); + Objects.requireNonNull(binding, "binding"); + Objects.requireNonNull(kind, "kind"); + if (binding.declaration() != declaration) { + throw new IllegalArgumentException("binding declaration must match the local declaration"); + } + if (sourceOrder < 0) { + throw new IllegalArgumentException("sourceOrder must not be negative"); + } + } + + public enum Kind { + /// Loop iterator published into a `FOR_BODY` inventory. Must be the sole iterator entry and + /// always use `sourceOrder == 0` at the head of that body's declaration list. + ITERATOR, + /// Ordinary `var` published into a supported body inventory after any leading iterator entry. + ORDINARY_VAR + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodySemanticSupportPolicy.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodySemanticSupportPolicy.java new file mode 100644 index 00000000..fc4348c1 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodySemanticSupportPolicy.java @@ -0,0 +1,145 @@ +package gd.script.gdcc.frontend.sema; + +import gd.script.gdcc.frontend.scope.BlockScopeKind; +import gd.script.gdcc.frontend.scope.CallableScopeKind; +import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueDomain; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +/// Immutable structural support matrix for frontend body locations. +/// +/// A policy answers three independent structural questions: whether the location owns feature-specific lexical +/// inventory, whether it enters the segmented `FrontendSuiteResolver` body pipeline, and which +/// [FrontendVisibleValueDomain] must be used for bare-name lookup. The answer is derived only from AST/scope +/// structure. Expression types, typed overlays, diagnostics, iteration routes, compile readiness, and lifecycle +/// state are intentionally absent. +/// +/// [#EXECUTABLE_BODY] represents every body whose scope/inventory implementation is complete, including +/// `FOR_BODY`. Unsupported or not-yet-implemented features receive a precise deferred domain instead of a pending +/// or published lifecycle. [#FOR_HEADER] is a special structural location: it uses the surrounding executable +/// lookup domain but owns no body inventory and does not itself enter the suite resolver. +/// +/// Mapping switches are exhaustive and deliberately have no `default` branch. Adding a new [BlockScopeKind] or +/// [CallableScopeKind] therefore requires an explicit support decision rather than silently opening the new kind +/// as an executable body. +public enum FrontendBodySemanticSupportPolicy { + EXECUTABLE_BODY(true, true, FrontendVisibleValueDomain.EXECUTABLE_BODY), + FOR_HEADER(false, false, FrontendVisibleValueDomain.EXECUTABLE_BODY), + LAMBDA_SUBTREE(false, false, FrontendVisibleValueDomain.LAMBDA_SUBTREE), + MATCH_SUBTREE(false, false, FrontendVisibleValueDomain.MATCH_SUBTREE), + BLOCK_LOCAL_CONST_SUBTREE(false, false, FrontendVisibleValueDomain.BLOCK_LOCAL_CONST_SUBTREE), + PARAMETER_DEFAULT(false, false, FrontendVisibleValueDomain.PARAMETER_DEFAULT), + UNKNOWN_OR_SKIPPED_SUBTREE(false, false, FrontendVisibleValueDomain.UNKNOWN_OR_SKIPPED_SUBTREE); + + private final boolean publishesLexicalInventory; + private final boolean entersSuiteResolver; + private final @NotNull FrontendVisibleValueDomain visibleValueDomain; + + /// Creates one immutable row in the structural support matrix. + /// + /// The two boolean capabilities are kept as separate fields even though they currently move together for + /// body policies. This preserves the distinction between publishing lexical inventory and entering semantic + /// body resolution, preventing either capability from being inferred indirectly by consumers. + /// + /// @param publishesLexicalInventory whether the location owns and publishes feature-specific lexical inventory + /// @param entersSuiteResolver whether the location is a body root accepted by `FrontendSuiteResolver` + /// @param visibleValueDomain the request/deferred domain used by visible-value resolution at this location + FrontendBodySemanticSupportPolicy( + boolean publishesLexicalInventory, + boolean entersSuiteResolver, + @NotNull FrontendVisibleValueDomain visibleValueDomain + ) { + this.publishesLexicalInventory = publishesLexicalInventory; + this.entersSuiteResolver = entersSuiteResolver; + this.visibleValueDomain = Objects.requireNonNull(visibleValueDomain, "visibleValueDomain"); + } + + /// Reports whether this structural location publishes its feature-owned lexical inventory before body typing. + /// + /// The result describes structural publication capability only. It never reflects whether expression typing, + /// route planning, diagnostics, or lowering succeeded for a particular source body. + /// + /// @return `true` only for locations whose complete lexical inventory is part of the supported Interface surface + public boolean publishesLexicalInventory() { + return publishesLexicalInventory; + } + + /// Reports whether a body with this policy may enter `FrontendSuiteResolver`. + /// + /// [FrontendBodyStructuralCompleteness] consumes this capability as the first certificate check, then verifies + /// that the current Interface surface actually contains all required structural facts. + /// + /// @return `true` when this policy represents a supported suite body + public boolean entersSuiteResolver() { + return entersSuiteResolver; + } + + /// Reports whether this structural location is a supported suite body root. + /// + /// This is the single semantic entry consumed both by `FrontendInterfacePhase` (to decide which blocks become + /// suite-entry roots with a published declaration index) and by `FrontendBodyStructuralCompleteness` (as the + /// first certificate gate). Both consumers must read this method instead of inferring body-entry from + /// `publishesLexicalInventory()` or `entersSuiteResolver()` independently, so the structural matrix stays the + /// single source of truth even if the two boolean capabilities diverge for future features. Inventory-only + /// consumers (visible-value boundary, deferred const detection, inferred-local-scope eligibility) continue to + /// read `publishesLexicalInventory()` directly, because they answer a different question. + /// + /// @return `true` when this policy represents a body root that may enter `FrontendSuiteResolver` + public boolean isSupportedSuiteBodyRoot() { + return entersSuiteResolver; + } + + /// Returns the visible-value lookup domain associated with this structural location. + /// + /// Supported bodies and for headers use [FrontendVisibleValueDomain#EXECUTABLE_BODY]. Unsupported locations + /// retain a precise non-executable domain so request-domain, AST-boundary, and current-scope checks fail closed + /// without consulting typed readiness. + /// + /// @return the immutable resolver domain for this policy + public @NotNull FrontendVisibleValueDomain visibleValueDomain() { + return visibleValueDomain; + } + + /// Maps a block scope kind to its explicit structural body policy. + /// + /// Function, constructor, ordinary block, conditional, loop, and `FOR_BODY` scopes are executable bodies. + /// Lambda and match-section scopes remain in their feature-specific deferred domains. The exhaustive switch is + /// intentional: a newly added block scope kind must make a compile-time-visible support choice here. + /// + /// @param kind the block scope kind whose structural support is being queried + /// @return the policy that controls inventory publication, suite entry, and visible-value domain + /// @throws NullPointerException if `kind` is `null` + public static @NotNull FrontendBodySemanticSupportPolicy forBlockScopeKind(@NotNull BlockScopeKind kind) { + return switch (Objects.requireNonNull(kind, "kind")) { + case BLOCK_STATEMENT, + FUNCTION_BODY, + CONSTRUCTOR_BODY, + IF_BODY, + ELIF_BODY, + ELSE_BODY, + WHILE_BODY, + FOR_BODY -> EXECUTABLE_BODY; + case LAMBDA_BODY -> LAMBDA_SUBTREE; + case MATCH_SECTION_BODY -> MATCH_SUBTREE; + }; + } + + /// Maps a callable scope kind to its explicit structural body policy. + /// + /// Function and constructor callables own executable bodies. Lambda callables remain deferred until their + /// parameter, capture, and body inventory are implemented as one feature-owned surface. As with block scopes, + /// the exhaustive switch prevents new callable kinds from receiving support by default. + /// + /// @param kind the callable scope kind whose structural support is being queried + /// @return the policy for the callable's body/inventory boundary + /// @throws NullPointerException if `kind` is `null` + public static @NotNull FrontendBodySemanticSupportPolicy forCallableScopeKind( + @NotNull CallableScopeKind kind + ) { + return switch (Objects.requireNonNull(kind, "kind")) { + case FUNCTION_DECLARATION, CONSTRUCTOR_DECLARATION -> EXECUTABLE_BODY; + case LAMBDA_EXPRESSION -> LAMBDA_SUBTREE; + }; + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyStructuralCompleteness.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyStructuralCompleteness.java new file mode 100644 index 00000000..f2bfe9c1 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyStructuralCompleteness.java @@ -0,0 +1,244 @@ +package gd.script.gdcc.frontend.sema; + +import dev.superice.gdparser.frontend.ast.Block; +import dev.superice.gdparser.frontend.ast.ForStatement; +import dev.superice.gdparser.frontend.ast.Node; +import dev.superice.gdparser.frontend.ast.VariableDeclaration; +import gd.script.gdcc.frontend.scope.BlockScope; +import gd.script.gdcc.frontend.scope.BlockScopeKind; +import gd.script.gdcc.scope.ScopeValueKind; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Objects; + +/// Verifies that a structurally supported body has the complete Interface-phase surface required by the +/// segmented body pipeline. +/// +/// This class is the completeness half of the body-entry contract. Structural support is decided separately +/// by [FrontendBodySemanticSupportPolicy]; this verifier proves that the current analysis run actually +/// published every required structural fact for one supported body. It inspects only the scope graph and the +/// immutable [FrontendInterfaceSurface]: suite-entry roots, the body declaration index, and the source-facing +/// typed baseline. +/// +/// Completeness is bidirectional for published body inventory: +/// - every indexed declaration must agree with scope identity, binding identity, and typed baseline +/// - every `LOCAL` binding in the body's [BlockScope] inventory must appear in the declaration index +/// - indexed `sourceOrder` must be contiguous and non-decreasing by AST start-byte range +/// +/// Typed overlays, expression types, slot refinements, iteration plans, diagnostics, and compile readiness are +/// deliberately excluded. Consequently, later semantic facts can refine a body after entry, but cannot make an +/// incomplete body appear complete or change whether [gd.script.gdcc.frontend.sema.analyzer.FrontendSuiteResolver] +/// enters it. +/// +/// Every failure is an [IllegalStateException] because a missing or inconsistent structural fact is a phase +/// protocol breach, not a recoverable source error. This utility never mutates side tables and never publishes +/// diagnostics. +public final class FrontendBodyStructuralCompleteness { + /// This class is a stateless certificate utility and is not instantiated. + private FrontendBodyStructuralCompleteness() { + } + + /// Requires all structural facts needed to enter `body` through `FrontendSuiteResolver`. + /// + /// Validation proceeds from the coarsest body-entry fact to declaration-level identities: + /// + /// 1. `expectedScope.kind()` must map to a policy that is a supported suite body root. + /// 2. The scope graph must map `body` to the exact `expectedScope` instance. + /// 3. The Interface surface must list `body` as a supported suite-entry root. + /// 4. The declaration index must contain `body`, including an explicit empty entry for a body without locals. + /// 5. Indexed declarations must have contiguous `sourceOrder`, non-decreasing AST start-byte order, + /// matching binding/declaration/scope identities, and a source-facing typed baseline equal to the + /// published lexical binding type. + /// 6. Every published `LOCAL` value in `expectedScope` must have a matching declaration-index entry. + /// 7. A `FOR_BODY` must contain exactly one iterator entry identified by its owning `ForStatement`, and + /// that entry must be the synthetic 0th item occupying `sourceOrder` 0 at the head of the body + /// inventory list. + /// + /// The method intentionally returns no boolean. A caller may enter a supported suite only after this method + /// returns normally; any missing fact is an internal phase-order or publication error that must stop analysis. + /// + /// @param analysisData the stable analysis surface supplying the published AST-to-scope graph + /// @param interfaceSurface the immutable Interface-phase declaration, baseline, and suite-entry facts + /// @param body the supported body whose structural surface is being certified + /// @param expectedScope the exact block scope that the caller intends to use while resolving `body` + /// @throws NullPointerException if any argument is `null` + /// @throws IllegalStateException if structural support or any required Interface-phase fact is missing or + /// inconsistent + public static void requireStructurallyCompleteBody( + @NotNull FrontendAnalysisData analysisData, + @NotNull FrontendInterfaceSurface interfaceSurface, + @NotNull Block body, + @NotNull BlockScope expectedScope + ) { + Objects.requireNonNull(analysisData, "analysisData"); + Objects.requireNonNull(interfaceSurface, "interfaceSurface"); + Objects.requireNonNull(body, "body"); + Objects.requireNonNull(expectedScope, "expectedScope"); + + var policy = FrontendBodySemanticSupportPolicy.forBlockScopeKind(expectedScope.kind()); + if (!policy.isSupportedSuiteBodyRoot()) { + throw incomplete(body, "scope kind " + expectedScope.kind() + " is not a supported suite body"); + } + if (analysisData.scopesByAst().get(body) != expectedScope) { + throw incomplete(body, "body scope identity does not match the published scope graph"); + } + if (!interfaceSurface.suiteEntryRoots().containsSupportedBlock(body)) { + throw incomplete(body, "suite entry roots do not contain the supported body"); + } + + var declarationIndex = interfaceSurface.bodyDeclarationIndex(); + if (!declarationIndex.containsBodyRoot(body)) { + throw incomplete(body, "body declaration index does not contain the supported body"); + } + + var declarations = declarationIndex.declarationsFor(body); + for (var sourceOrder = 0; sourceOrder < declarations.size(); sourceOrder++) { + var declaration = declarations.get(sourceOrder); + if (declaration.sourceOrder() != sourceOrder) { + throw incomplete(body, "declaration source order is not contiguous"); + } + if (sourceOrder > 0 + && declarationStartByte(declaration) < declarationStartByte(declarations.get(sourceOrder - 1))) { + throw incomplete(body, "declaration source order does not match AST range order"); + } + requireCompleteDeclaration( + analysisData, + interfaceSurface, + body, + expectedScope, + declaration + ); + } + + requireScopeInventoryPublished(declarationIndex, body, expectedScope); + + if (expectedScope.kind() == BlockScopeKind.FOR_BODY) { + requireForBodyIteratorInventory(body, declarations); + } + } + + /// Requires one indexed body-local declaration to agree with all other published structural views. + /// + /// Ordinary locals must be `VariableDeclaration` nodes owned by `expectedScope`. Iterators instead use the + /// owning `ForStatement` as declaration identity, live in its `FOR_BODY` scope, and require the statement to + /// remain mapped to the parent scope where the for header is analyzed. Both kinds must resolve back to the + /// exact indexed binding and have a matching source-facing typed baseline. + /// + /// @param analysisData the stable AST-to-scope graph + /// @param interfaceSurface the Interface-phase baseline and declaration-index surface + /// @param body the body that owns the indexed declaration + /// @param expectedScope the exact scope that must contain the indexed binding + /// @param indexedDeclaration the declaration-index entry being certified + /// @throws IllegalStateException if declaration, binding, scope, body ownership, or baseline identities drift + private static void requireCompleteDeclaration( + @NotNull FrontendAnalysisData analysisData, + @NotNull FrontendInterfaceSurface interfaceSurface, + @NotNull Block body, + @NotNull BlockScope expectedScope, + @NotNull FrontendBodyLocalDeclaration indexedDeclaration + ) { + var declaration = indexedDeclaration.declaration(); + var binding = indexedDeclaration.binding(); + if (binding.declaration() != declaration) { + throw incomplete(body, "indexed binding declaration identity drifted"); + } + var baselineType = interfaceSurface.typedLexicalBaseline().typeFor(declaration); + if (baselineType == null || !baselineType.equals(binding.type())) { + throw incomplete(body, "indexed declaration is missing its source-facing typed baseline"); + } + + switch (indexedDeclaration.kind()) { + case ORDINARY_VAR -> { + if (!(declaration instanceof VariableDeclaration variableDeclaration) + || analysisData.scopesByAst().get(variableDeclaration) != expectedScope + || expectedScope.resolveValueHere(variableDeclaration.name().trim()) != binding) { + throw incomplete(body, "ordinary local declaration, binding, and scope identities disagree"); + } + } + case ITERATOR -> { + if (!(declaration instanceof ForStatement forStatement) + || forStatement.body() != body + || analysisData.scopesByAst().get(forStatement) != expectedScope.getParentScope() + || expectedScope.resolveValueHere(forStatement.iterator().trim()) != binding) { + throw incomplete(body, "iterator declaration, binding, and `for` scope identities disagree"); + } + } + } + } + + /// Requires every published `LOCAL` scope binding to appear in the body declaration index. + /// + /// This is the reverse half of inventory completeness: the index is only a view over `BlockScope` + /// inventory, so a producer that omits an accepted local must fail before suite entry rather than only + /// when that local is later looked up. + private static void requireScopeInventoryPublished( + @NotNull FrontendBodyDeclarationIndex declarationIndex, + @NotNull Block body, + @NotNull BlockScope expectedScope + ) { + for (var value : expectedScope.localValues()) { + if (value.kind() != ScopeValueKind.LOCAL) { + continue; + } + if (!(value.declaration() instanceof Node declarationNode) + || (!(declarationNode instanceof VariableDeclaration) + && !(declarationNode instanceof ForStatement))) { + throw incomplete( + body, + "scope inventory contains a local that is not a body declaration-index identity" + ); + } + var indexedDeclaration = declarationIndex.declarationFor(declarationNode); + if (indexedDeclaration == null + || indexedDeclaration.binding().declaration() != value.declaration() + || indexedDeclaration.binding().kind() != value.kind()) { + throw incomplete( + body, + "scope inventory local is missing from body declaration index" + ); + } + } + } + + /// Requires the Interface-phase `FOR_BODY` inventory contract: exactly one iterator entry as the + /// synthetic 0th item, list head, `sourceOrder == 0`. Ordinary body locals may follow only at contiguous + /// `sourceOrder >= 1`. + private static void requireForBodyIteratorInventory( + @NotNull Block body, + @NotNull List declarations + ) { + var iteratorCount = 0; + for (var declaration : declarations) { + if (declaration.kind() == FrontendBodyLocalDeclaration.Kind.ITERATOR) { + iteratorCount++; + } + } + if (iteratorCount != 1) { + throw incomplete(body, "`for` body declaration index must contain exactly one iterator"); + } + var firstDeclaration = declarations.getFirst(); + if (firstDeclaration.kind() != FrontendBodyLocalDeclaration.Kind.ITERATOR + || firstDeclaration.sourceOrder() != 0) { + throw incomplete(body, "`for` body iterator must be the first declaration with sourceOrder 0"); + } + } + + private static int declarationStartByte(@NotNull FrontendBodyLocalDeclaration declaration) { + return declaration.declaration().range().startByte(); + } + + /// Creates the uniform protocol-breach exception used by this certificate. + /// + /// Including the body's start byte keeps failures attributable to a concrete Interface body even though the + /// exception represents an internal invariant violation rather than a source diagnostic. + /// + /// @param body the incomplete body used as the failure anchor + /// @param detail the specific structural invariant that was violated + /// @return an exception ready to be thrown by the failing certificate check + private static @NotNull IllegalStateException incomplete(@NotNull Block body, @NotNull String detail) { + return new IllegalStateException( + "Structurally supported body at byte " + body.range().startByte() + " is incomplete: " + detail + ); + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendExecutableInventorySupport.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendExecutableInventorySupport.java index 0efbb5e3..d0110380 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendExecutableInventorySupport.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendExecutableInventorySupport.java @@ -10,15 +10,21 @@ private FrontendExecutableInventorySupport() { } public static boolean canPublishCallableLocalValueInventory(@NotNull BlockScopeKind kind) { - return switch (kind) { - case FUNCTION_BODY, - CONSTRUCTOR_BODY, - BLOCK_STATEMENT, - IF_BODY, - ELIF_BODY, - ELSE_BODY, - WHILE_BODY -> true; - default -> false; - }; + return FrontendBodySemanticSupportPolicy.forBlockScopeKind(kind).publishesLexicalInventory(); } + + /// Reports whether a block scope kind is a supported suite body root. + /// + /// This is the single body-entry semantic entry consumed by `FrontendInterfacePhase` when deciding which blocks + /// become suite-entry roots. It must agree with the first certificate gate in + /// `FrontendBodyStructuralCompleteness`, which reads + /// `FrontendBodySemanticSupportPolicy.isSupportedSuiteBodyRoot()` directly; both consumers therefore route + /// through the same semantic entry instead of inferring body-entry from inventory publication. + /// + /// @param kind the block scope kind whose body-entry support is being queried + /// @return `true` when the kind maps to a policy that is a supported suite body root + public static boolean isSupportedSuiteBodyRoot(@NotNull BlockScopeKind kind) { + return FrontendBodySemanticSupportPolicy.forBlockScopeKind(kind).isSupportedSuiteBodyRoot(); + } + } diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendInterfaceSurface.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendInterfaceSurface.java new file mode 100644 index 00000000..3113a598 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendInterfaceSurface.java @@ -0,0 +1,18 @@ +package gd.script.gdcc.frontend.sema; + +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +/// Complete Interface-phase surface consumed by future body-suite orchestration. +public record FrontendInterfaceSurface( + @NotNull FrontendBodyDeclarationIndex bodyDeclarationIndex, + @NotNull FrontendTypedLexicalBaseline typedLexicalBaseline, + @NotNull FrontendSuiteEntryRoots suiteEntryRoots +) { + public FrontendInterfaceSurface { + Objects.requireNonNull(bodyDeclarationIndex, "bodyDeclarationIndex"); + Objects.requireNonNull(typedLexicalBaseline, "typedLexicalBaseline"); + Objects.requireNonNull(suiteEntryRoots, "suiteEntryRoots"); + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendSemanticStage.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendSemanticStage.java new file mode 100644 index 00000000..42a0b330 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendSemanticStage.java @@ -0,0 +1,10 @@ +package gd.script.gdcc.frontend.sema; + +/// Segment-local semantic stages that may publish incremental frontend facts. +public enum FrontendSemanticStage { + TOP_BINDING, + LOCAL_TYPE_STABILIZATION, + CHAIN_BINDING, + EXPR_TYPE, + VAR_TYPE_POST +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendSuiteEntryRoots.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendSuiteEntryRoots.java new file mode 100644 index 00000000..7b928d69 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendSuiteEntryRoots.java @@ -0,0 +1,73 @@ +package gd.script.gdcc.frontend.sema; + +import dev.superice.gdparser.frontend.ast.Block; +import dev.superice.gdparser.frontend.ast.Node; +import dev.superice.gdparser.frontend.ast.VariableDeclaration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/// Body-entry roots produced by the Interface phase. +/// +/// Only structurally supported roots listed here are legal for `FrontendSuiteResolver` to enter. +public record FrontendSuiteEntryRoots( + @NotNull List callableOwners, + @NotNull List propertyInitializers, + @NotNull List supportedBlocks, + @NotNull Map sourcePathsByEntryRoot +) { + public FrontendSuiteEntryRoots( + @NotNull List callableOwners, + @NotNull List propertyInitializers, + @NotNull List supportedBlocks + ) { + this(callableOwners, propertyInitializers, supportedBlocks, Map.of()); + } + + public FrontendSuiteEntryRoots { + callableOwners = List.copyOf(Objects.requireNonNull(callableOwners, "callableOwners")); + propertyInitializers = List.copyOf(Objects.requireNonNull(propertyInitializers, "propertyInitializers")); + supportedBlocks = List.copyOf(Objects.requireNonNull(supportedBlocks, "supportedBlocks")); + sourcePathsByEntryRoot = copyIdentityPathMap(sourcePathsByEntryRoot); + } + + public boolean containsSupportedBlock(@NotNull Block block) { + return identitySet(supportedBlocks).contains(Objects.requireNonNull(block, "block")); + } + + public boolean containsCallableOwner(@NotNull Node callableOwner) { + return identitySet(callableOwners).contains(Objects.requireNonNull(callableOwner, "callableOwner")); + } + + public boolean containsPropertyInitializer(@NotNull VariableDeclaration property) { + return identitySet(propertyInitializers).contains(Objects.requireNonNull(property, "property")); + } + + public @Nullable Path sourcePathFor(@NotNull Node entryRoot) { + return sourcePathsByEntryRoot.get(Objects.requireNonNull(entryRoot, "entryRoot")); + } + + private static @NotNull Set identitySet(@NotNull List values) { + var set = java.util.Collections.newSetFromMap(new IdentityHashMap()); + set.addAll(values); + return set; + } + + private static @NotNull Map copyIdentityPathMap(@NotNull Map sourcePathsByEntryRoot) { + var copiedPaths = new IdentityHashMap(); + for (var entry : Objects.requireNonNull(sourcePathsByEntryRoot, "sourcePathsByEntryRoot").entrySet()) { + copiedPaths.put( + Objects.requireNonNull(entry.getKey(), "entryRoot"), + Objects.requireNonNull(entry.getValue(), "sourcePath") + ); + } + return Collections.unmodifiableMap(copiedPaths); + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalBaseline.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalBaseline.java new file mode 100644 index 00000000..b30001f1 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalBaseline.java @@ -0,0 +1,65 @@ +package gd.script.gdcc.frontend.sema; + +import dev.superice.gdparser.frontend.ast.Node; +import gd.script.gdcc.type.GdCompilerType; +import gd.script.gdcc.type.GdType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Objects; + +/// Source-facing slot type baseline prepared by the Interface phase. +/// +/// It records only parameter and ordinary-local slot facts that the baseline inventory layer has +/// already published. Compiler-only storage types are rejected here because body phases must never +/// expose `GdCompilerType` through source-facing lexical lookup. +public final class FrontendTypedLexicalBaseline { + private final @NotNull Map typesByDeclaration; + + private FrontendTypedLexicalBaseline(@NotNull Map typesByDeclaration) { + this.typesByDeclaration = Collections.unmodifiableMap(new IdentityHashMap<>(typesByDeclaration)); + } + + public static @NotNull Builder builder() { + return new Builder(); + } + + public @Nullable GdType typeFor(@NotNull Node declaration) { + return typesByDeclaration.get(Objects.requireNonNull(declaration, "declaration")); + } + + public boolean containsDeclaration(@NotNull Node declaration) { + return typesByDeclaration.containsKey(Objects.requireNonNull(declaration, "declaration")); + } + + public @NotNull Map typesByDeclaration() { + return typesByDeclaration; + } + + public static final class Builder { + private final @NotNull Map typesByDeclaration = new IdentityHashMap<>(); + + private Builder() { + } + + public @NotNull Builder put(@NotNull Node declaration, @NotNull GdType type) { + Objects.requireNonNull(declaration, "declaration"); + Objects.requireNonNull(type, "type"); + if (type instanceof GdCompilerType) { + throw new IllegalArgumentException( + "compiler-only type leaked into source-facing typed lexical baseline: " + + type.getTypeName() + ); + } + typesByDeclaration.put(declaration, type); + return this; + } + + public @NotNull FrontendTypedLexicalBaseline build() { + return new FrontendTypedLexicalBaseline(typesByDeclaration); + } + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java new file mode 100644 index 00000000..0f4f6f9f --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java @@ -0,0 +1,552 @@ +package gd.script.gdcc.frontend.sema; + +import dev.superice.gdparser.frontend.ast.Node; +import gd.script.gdcc.frontend.scope.BlockScope; +import gd.script.gdcc.frontend.sema.patch.FrontendChainBindingPatch; +import gd.script.gdcc.frontend.sema.patch.FrontendExprTypePatch; +import gd.script.gdcc.frontend.sema.patch.FrontendLocalSlotTypeUpdate; +import gd.script.gdcc.frontend.sema.patch.FrontendLocalTypeStabilizationPatch; +import gd.script.gdcc.frontend.sema.patch.FrontendOwnerPatch; +import gd.script.gdcc.frontend.sema.patch.FrontendPatchTransaction; +import gd.script.gdcc.frontend.sema.patch.FrontendPublishedFactTypeGuard; +import gd.script.gdcc.frontend.sema.patch.FrontendTopBindingPatch; +import gd.script.gdcc.frontend.sema.patch.FrontendVarTypePostPatch; +import gd.script.gdcc.scope.Scope; +import gd.script.gdcc.scope.ScopeValue; +import gd.script.gdcc.scope.ScopeValueKind; +import gd.script.gdcc.type.GdType; +import gd.script.gdcc.type.GdVariantType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/// Effective typed view used by the body suite resolver. +/// +/// The environment keeps statement-local pending facts separate from current-suite committed facts. +/// Stable side tables and `BlockScope` slots are only changed after exporting a per-owner patch +/// transaction and applying it to `FrontendAnalysisData`. The Interface-layer baseline supplies +/// immutable source-facing slot types until a body fact supersedes them. +public final class FrontendTypedLexicalEnvironment { + private final @NotNull Scope scope; + private final @NotNull FrontendAnalysisData stableData; + private final @Nullable FrontendTypedLexicalEnvironment parent; + private final @Nullable FrontendTypedLexicalBaseline typedBaseline; + private final @NotNull OverlayFacts pendingFacts = new OverlayFacts(); + private final @NotNull OverlayFacts committedFacts = new OverlayFacts(); + + public FrontendTypedLexicalEnvironment( + @NotNull Scope scope, + @NotNull FrontendAnalysisData stableData + ) { + this(scope, stableData, null); + } + + public FrontendTypedLexicalEnvironment( + @NotNull Scope scope, + @NotNull FrontendAnalysisData stableData, + @Nullable FrontendTypedLexicalEnvironment parent + ) { + this(scope, stableData, parent, null); + } + + public FrontendTypedLexicalEnvironment( + @NotNull Scope scope, + @NotNull FrontendAnalysisData stableData, + @Nullable FrontendTypedLexicalEnvironment parent, + @Nullable FrontendTypedLexicalBaseline typedBaseline + ) { + this.scope = Objects.requireNonNull(scope, "scope must not be null"); + this.stableData = Objects.requireNonNull(stableData, "stableData must not be null"); + this.parent = parent; + this.typedBaseline = typedBaseline; + } + + public @NotNull Scope scope() { + return scope; + } + + public @Nullable FrontendTypedLexicalEnvironment parent() { + return parent; + } + + public @Nullable FrontendBinding symbolBinding(@NotNull Node astNode) { + var localBinding = firstNonNull( + pendingFacts.symbolBindings.get(astNode), + committedFacts.symbolBindings.get(astNode), + stableData.symbolBindings().get(astNode) + ); + if (localBinding != null) { + return effectiveBinding(localBinding); + } + return parent != null ? parent.symbolBinding(astNode) : null; + } + + public @Nullable FrontendResolvedMember resolvedMember(@NotNull Node astNode) { + return firstNonNull( + pendingFacts.resolvedMembers.get(astNode), + committedFacts.resolvedMembers.get(astNode), + stableData.resolvedMembers().get(astNode), + parent != null ? parent.resolvedMember(astNode) : null + ); + } + + public @Nullable FrontendResolvedCall resolvedCall(@NotNull Node astNode) { + return firstNonNull( + pendingFacts.resolvedCall(astNode), + committedFacts.resolvedCall(astNode), + stableData.resolvedCalls().get(astNode), + parent != null ? parent.resolvedCall(astNode) : null + ); + } + + public @Nullable FrontendExpressionType expressionType(@NotNull Node astNode) { + return firstNonNull( + pendingFacts.expressionTypes.get(astNode), + committedFacts.expressionTypes.get(astNode), + stableData.expressionTypes().get(astNode), + parent != null ? parent.expressionType(astNode) : null + ); + } + + public @Nullable GdType slotType(@NotNull Node astNode) { + var localSlotType = firstNonNull( + pendingFacts.slotTypes.get(astNode), + committedFacts.slotTypes.get(astNode), + stableData.slotTypes().get(astNode) + ); + if (localSlotType != null) { + return localSlotType; + } + if (parent != null) { + var parentSlotType = parent.slotType(astNode); + if (parentSlotType != null) { + return parentSlotType; + } + } + return typedBaseline != null ? typedBaseline.typeFor(astNode) : null; + } + + /// Returns the effective local value without mutating the owning `BlockScope` slot. + public @NotNull ScopeValue effectiveScopeValue(@NotNull ScopeValue value, @NotNull Scope owningScope) { + var checkedValue = Objects.requireNonNull(value, "value must not be null"); + if (!(owningScope instanceof BlockScope blockScope) + || checkedValue.kind() != ScopeValueKind.LOCAL + || checkedValue.declaration() == null) { + return checkedValue; + } + var effectiveType = localSlotType(blockScope, checkedValue.name(), checkedValue.declaration()); + if (effectiveType == null || FrontendAnalysisData.sameType(checkedValue.type(), effectiveType)) { + return checkedValue; + } + return withType(checkedValue, effectiveType); + } + + public @Nullable GdType localSlotType( + @NotNull BlockScope blockScope, + @NotNull String name, + @NotNull Object declaration + ) { + var pendingType = pendingFacts.localSlotType(blockScope, name, declaration); + if (pendingType != null) { + return pendingType; + } + var committedType = committedFacts.localSlotType(blockScope, name, declaration); + if (committedType != null) { + return committedType; + } + if (parent != null) { + var parentType = parent.localSlotType(blockScope, name, declaration); + if (parentType != null) { + return parentType; + } + } + if (declaration instanceof Node astNode) { + var publishedType = slotType(astNode); + if (publishedType != null) { + return publishedType; + } + } + var stableValue = blockScope.resolveValueHere(name); + if (stableValue != null && stableValue.declaration() == declaration) { + return stableValue.type(); + } + return null; + } + + public void putSymbolBinding( + @NotNull FrontendSemanticStage owner, + @NotNull Node astNode, + @NotNull FrontendBinding binding + ) { + requireOwner(owner, FrontendSemanticStage.TOP_BINDING); + FrontendPublishedFactTypeGuard.checkBinding(binding); + putSideTable( + stableData.symbolBindings(), + committedFacts.symbolBindings, + pendingFacts.symbolBindings, + astNode, + binding, + "symbolBindings", + FrontendAnalysisData::sameBinding + ); + } + + public void addLocalSlotTypeUpdate( + @NotNull FrontendSemanticStage owner, + @NotNull FrontendLocalSlotTypeUpdate update + ) { + requireOwner(owner, FrontendSemanticStage.LOCAL_TYPE_STABILIZATION); + validateLocalSlotTypeUpdate(update); + pendingFacts.localSlotTypeUpdates.add(update); + } + + public void putResolvedMember( + @NotNull FrontendSemanticStage owner, + @NotNull Node astNode, + @NotNull FrontendResolvedMember member + ) { + requireOwner(owner, FrontendSemanticStage.CHAIN_BINDING); + FrontendPublishedFactTypeGuard.checkResolvedMember(member); + putSideTable( + stableData.resolvedMembers(), + committedFacts.resolvedMembers, + pendingFacts.resolvedMembers, + astNode, + member, + "resolvedMembers", + FrontendAnalysisData::sameResolvedMember + ); + } + + public void putResolvedCall( + @NotNull FrontendSemanticStage owner, + @NotNull Node astNode, + @NotNull FrontendResolvedCall call + ) { + FrontendPublishedFactTypeGuard.checkResolvedCall(call); + switch (owner) { + case CHAIN_BINDING -> putResolvedCall(pendingFacts.chainResolvedCalls, astNode, call); + case EXPR_TYPE -> putResolvedCall(pendingFacts.exprResolvedCalls, astNode, call); + default -> throw FrontendAnalysisData.patchFailure(owner + " cannot publish resolvedCalls() overlay facts"); + } + } + + public void putExpressionType( + @NotNull FrontendSemanticStage owner, + @NotNull Node astNode, + @NotNull FrontendExpressionType expressionType + ) { + requireOwner(owner, FrontendSemanticStage.EXPR_TYPE); + FrontendPublishedFactTypeGuard.checkExpressionType(expressionType); + putSideTable( + stableData.expressionTypes(), + committedFacts.expressionTypes, + pendingFacts.expressionTypes, + astNode, + expressionType, + "expressionTypes", + FrontendAnalysisData::sameExpressionType + ); + } + + public void putSlotType( + @NotNull FrontendSemanticStage owner, + @NotNull Node astNode, + @NotNull GdType slotType + ) { + requireOwner(owner, FrontendSemanticStage.VAR_TYPE_POST); + FrontendPublishedFactTypeGuard.checkNoCompilerOnlyLeak(slotType, "slotTypes() value"); + putSideTable( + stableData.slotTypes(), + committedFacts.slotTypes, + pendingFacts.slotTypes, + astNode, + slotType, + "slotTypes", + FrontendAnalysisData::sameType + ); + } + + /// Moves pending facts into the suite overlay without touching stable data or scopes. + /// + /// Callers use this at both statement and callable-entry boundaries. + public void flushPendingFacts() { + pendingFacts.checkNoCompilerOnlyLeaks(); + committedFacts.mergeFrom(pendingFacts); + pendingFacts.clear(); + } + + /// Exports committed suite facts as ordered single-owner patches. Stable data remains unchanged. + /// + /// Callable body resolution adds this transaction to its callable-scoped export batch. Property + /// initializers remain independent roots and apply their transaction directly. Export validates + /// this environment only; it does not preflight against transactions queued by other suite environments. + public @NotNull FrontendPatchTransaction exportPatchTransaction() { + committedFacts.checkNoCompilerOnlyLeaks(); + return new FrontendPatchTransaction(committedFacts.toOwnerPatches()); + } + + public boolean hasPendingFacts() { + return !pendingFacts.isEmpty(); + } + + public boolean hasCommittedFacts() { + return !committedFacts.isEmpty(); + } + + private void putResolvedCall( + @NotNull FrontendAstSideTable targetTable, + @NotNull Node astNode, + @NotNull FrontendResolvedCall call + ) { + checkResolvedCallConflict(astNode, call, stableData.resolvedCalls().get(astNode)); + checkResolvedCallConflict(astNode, call, committedFacts.chainResolvedCalls.get(astNode)); + checkResolvedCallConflict(astNode, call, committedFacts.exprResolvedCalls.get(astNode)); + checkResolvedCallConflict(astNode, call, pendingFacts.chainResolvedCalls.get(astNode)); + checkResolvedCallConflict(astNode, call, pendingFacts.exprResolvedCalls.get(astNode)); + targetTable.put(astNode, call); + } + + private void validateLocalSlotTypeUpdate(@NotNull FrontendLocalSlotTypeUpdate update) { + FrontendAnalysisData.checkNoVoidLocalSlotType(update.type(), update.name()); + FrontendPublishedFactTypeGuard.checkLocalSlotTypeUpdate(update); + var currentValue = requireEffectiveLocalSlotValue(update); + if (currentValue.declaration() != update.declaration()) { + throw FrontendAnalysisData.patchFailure( + "local slot update targeted a different declaration for '" + update.name() + "'" + ); + } + if (FrontendAnalysisData.sameType(currentValue.type(), update.type())) { + return; + } + if (!(currentValue.type() instanceof GdVariantType)) { + throw FrontendAnalysisData.patchFailure( + "local slot update changed exact type for '" + + update.name() + + "' from " + + currentValue.type().getTypeName() + + " to " + + update.type().getTypeName() + ); + } + } + + private @NotNull ScopeValue requireEffectiveLocalSlotValue(@NotNull FrontendLocalSlotTypeUpdate update) { + var stableValue = update.scope().resolveValueHere(update.name()); + if (stableValue == null) { + throw FrontendAnalysisData.patchFailure("local slot update targeted a missing binding for '" + update.name() + "'"); + } + if (stableValue.kind() != ScopeValueKind.LOCAL) { + throw FrontendAnalysisData.patchFailure("local slot update targeted a non-local binding for '" + update.name() + "'"); + } + return effectiveScopeValue(stableValue, update.scope()); + } + + private @NotNull FrontendBinding effectiveBinding(@NotNull FrontendBinding binding) { + var resolvedValue = binding.resolvedValue(); + if (resolvedValue == null || !(binding.declarationSite() instanceof Node declarationNode)) { + return binding; + } + var owningScope = stableData.scopesByAst().get(declarationNode); + if (owningScope == null) { + return binding; + } + var effectiveValue = effectiveScopeValue(resolvedValue, owningScope); + return effectiveValue == resolvedValue ? binding : binding.withResolvedValue(effectiveValue); + } + + private static @NotNull ScopeValue withType(@NotNull ScopeValue value, @NotNull GdType type) { + return new ScopeValue( + value.name(), + type, + value.kind(), + value.declaration(), + value.constant(), + value.writable(), + value.staticMember() + ); + } + + private void putSideTable( + @NotNull FrontendAstSideTable stableTable, + @NotNull FrontendAstSideTable committedTable, + @NotNull FrontendAstSideTable pendingTable, + @NotNull Node astNode, + @NotNull V value, + @NotNull String fieldName, + @NotNull SameValueChecker sameValueChecker + ) { + var checkedNode = Objects.requireNonNull(astNode, "astNode must not be null"); + var checkedValue = Objects.requireNonNull(value, "value must not be null"); + checkConflict(fieldName, checkedNode, stableTable.get(checkedNode), checkedValue, sameValueChecker); + checkConflict(fieldName, checkedNode, committedTable.get(checkedNode), checkedValue, sameValueChecker); + checkConflict(fieldName, checkedNode, pendingTable.get(checkedNode), checkedValue, sameValueChecker); + pendingTable.put(checkedNode, checkedValue); + } + + private static void checkResolvedCallConflict( + @NotNull Node astNode, + @NotNull FrontendResolvedCall newValue, + @Nullable FrontendResolvedCall existingValue + ) { + checkConflict( + "resolvedCalls", + astNode, + existingValue, + newValue, + FrontendAnalysisData::sameResolvedCall + ); + } + + private static void checkConflict( + @NotNull String fieldName, + @NotNull Node astNode, + @Nullable V existingValue, + @NotNull V newValue, + @NotNull SameValueChecker sameValueChecker + ) { + if (existingValue == null || sameValueChecker.sameValue(existingValue, newValue)) { + return; + } + throw FrontendAnalysisData.patchFailure( + fieldName + " overlay write conflicted on " + FrontendAnalysisData.describeNode(astNode) + ); + } + + private static void requireOwner(@NotNull FrontendSemanticStage actual, @NotNull FrontendSemanticStage expected) { + if (actual != expected) { + throw FrontendAnalysisData.patchFailure(actual + " cannot publish " + expected + " overlay facts"); + } + } + + @SafeVarargs + private static @Nullable T firstNonNull(@Nullable T... values) { + for (var value : values) { + if (value != null) { + return value; + } + } + return null; + } + + @FunctionalInterface + private interface SameValueChecker { + boolean sameValue(@NotNull V first, @NotNull V second); + } + + private static final class OverlayFacts { + private final @NotNull FrontendAstSideTable symbolBindings = new FrontendAstSideTable<>(); + private final @NotNull FrontendAstSideTable resolvedMembers = new FrontendAstSideTable<>(); + private final @NotNull FrontendAstSideTable chainResolvedCalls = new FrontendAstSideTable<>(); + private final @NotNull FrontendAstSideTable exprResolvedCalls = new FrontendAstSideTable<>(); + private final @NotNull FrontendAstSideTable expressionTypes = new FrontendAstSideTable<>(); + private final @NotNull FrontendAstSideTable slotTypes = new FrontendAstSideTable<>(); + private final @NotNull List localSlotTypeUpdates = new ArrayList<>(); + + private @Nullable FrontendResolvedCall resolvedCall(@NotNull Node astNode) { + var chainCall = chainResolvedCalls.get(astNode); + return chainCall != null ? chainCall : exprResolvedCalls.get(astNode); + } + + private @Nullable GdType localSlotType( + @NotNull BlockScope scope, + @NotNull String name, + @NotNull Object declaration + ) { + for (var i = localSlotTypeUpdates.size() - 1; i >= 0; i--) { + var update = localSlotTypeUpdates.get(i); + if (update.scope() == scope + && update.declaration() == declaration + && update.name().equals(name)) { + return update.type(); + } + } + return null; + } + + private void mergeFrom(@NotNull OverlayFacts incoming) { + mergeSideTable(symbolBindings, incoming.symbolBindings, "symbolBindings", FrontendAnalysisData::sameBinding); + mergeSideTable(resolvedMembers, incoming.resolvedMembers, "resolvedMembers", FrontendAnalysisData::sameResolvedMember); + mergeSideTable( + chainResolvedCalls, + incoming.chainResolvedCalls, + "resolvedCalls", + FrontendAnalysisData::sameResolvedCall + ); + mergeSideTable( + exprResolvedCalls, + incoming.exprResolvedCalls, + "resolvedCalls", + FrontendAnalysisData::sameResolvedCall + ); + mergeSideTable(expressionTypes, incoming.expressionTypes, "expressionTypes", FrontendAnalysisData::sameExpressionType); + mergeSideTable(slotTypes, incoming.slotTypes, "slotTypes", FrontendAnalysisData::sameType); + localSlotTypeUpdates.addAll(incoming.localSlotTypeUpdates); + } + + private void checkNoCompilerOnlyLeaks() { + FrontendPublishedFactTypeGuard.checkSymbolBindings(symbolBindings); + FrontendPublishedFactTypeGuard.checkResolvedMembers(resolvedMembers); + FrontendPublishedFactTypeGuard.checkResolvedCalls(chainResolvedCalls); + FrontendPublishedFactTypeGuard.checkResolvedCalls(exprResolvedCalls); + FrontendPublishedFactTypeGuard.checkExpressionTypes(expressionTypes); + FrontendPublishedFactTypeGuard.checkSlotTypes(slotTypes); + FrontendPublishedFactTypeGuard.checkLocalSlotTypeUpdates(localSlotTypeUpdates); + } + + private @NotNull List toOwnerPatches() { + var patches = new ArrayList(); + if (!symbolBindings.isEmpty()) { + patches.add(new FrontendTopBindingPatch(symbolBindings)); + } + if (!localSlotTypeUpdates.isEmpty()) { + patches.add(new FrontendLocalTypeStabilizationPatch(localSlotTypeUpdates)); + } + if (!resolvedMembers.isEmpty() || !chainResolvedCalls.isEmpty()) { + patches.add(new FrontendChainBindingPatch(resolvedMembers, chainResolvedCalls)); + } + if (!expressionTypes.isEmpty() || !exprResolvedCalls.isEmpty()) { + patches.add(new FrontendExprTypePatch(expressionTypes, exprResolvedCalls)); + } + if (!slotTypes.isEmpty()) { + patches.add(new FrontendVarTypePostPatch(slotTypes)); + } + return patches; + } + + private boolean isEmpty() { + return symbolBindings.isEmpty() + && resolvedMembers.isEmpty() + && chainResolvedCalls.isEmpty() + && exprResolvedCalls.isEmpty() + && expressionTypes.isEmpty() + && slotTypes.isEmpty() + && localSlotTypeUpdates.isEmpty(); + } + + private void clear() { + symbolBindings.clear(); + resolvedMembers.clear(); + chainResolvedCalls.clear(); + exprResolvedCalls.clear(); + expressionTypes.clear(); + slotTypes.clear(); + localSlotTypeUpdates.clear(); + } + + private static void mergeSideTable( + @NotNull FrontendAstSideTable target, + @NotNull FrontendAstSideTable source, + @NotNull String fieldName, + @NotNull SameValueChecker sameValueChecker + ) { + for (var entry : source.entrySet()) { + checkConflict(fieldName, entry.getKey(), target.get(entry.getKey()), entry.getValue(), sameValueChecker); + target.put(entry.getKey(), entry.getValue()); + } + } + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProcedures.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProcedures.java new file mode 100644 index 00000000..650a2502 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProcedures.java @@ -0,0 +1,1443 @@ +package gd.script.gdcc.frontend.sema.analyzer; + +import dev.superice.gdparser.frontend.ast.AssignmentExpression; +import dev.superice.gdparser.frontend.ast.AssertStatement; +import dev.superice.gdparser.frontend.ast.AttributeExpression; +import dev.superice.gdparser.frontend.ast.AttributeCallStep; +import dev.superice.gdparser.frontend.ast.AttributeSubscriptStep; +import dev.superice.gdparser.frontend.ast.BinaryExpression; +import dev.superice.gdparser.frontend.ast.CallExpression; +import dev.superice.gdparser.frontend.ast.ConstructorDeclaration; +import dev.superice.gdparser.frontend.ast.DeclarationKind; +import dev.superice.gdparser.frontend.ast.Expression; +import dev.superice.gdparser.frontend.ast.ExpressionStatement; +import dev.superice.gdparser.frontend.ast.FunctionDeclaration; +import dev.superice.gdparser.frontend.ast.IdentifierExpression; +import dev.superice.gdparser.frontend.ast.LambdaExpression; +import dev.superice.gdparser.frontend.ast.LiteralExpression; +import dev.superice.gdparser.frontend.ast.MatchStatement; +import dev.superice.gdparser.frontend.ast.Node; +import dev.superice.gdparser.frontend.ast.ReturnStatement; +import dev.superice.gdparser.frontend.ast.SelfExpression; +import dev.superice.gdparser.frontend.ast.SubscriptExpression; +import dev.superice.gdparser.frontend.ast.UnaryExpression; +import dev.superice.gdparser.frontend.ast.VariableDeclaration; +import gd.script.gdcc.frontend.diagnostic.FrontendRange; +import gd.script.gdcc.frontend.scope.BlockScope; +import gd.script.gdcc.frontend.scope.CallableScope; +import gd.script.gdcc.frontend.sema.FrontendAnalysisData; +import gd.script.gdcc.frontend.sema.FrontendBinding; +import gd.script.gdcc.frontend.sema.FrontendBindingKind; +import gd.script.gdcc.frontend.sema.FrontendBodySemanticSupportPolicy; +import gd.script.gdcc.frontend.sema.FrontendBodyDeclarationIndex; +import gd.script.gdcc.frontend.sema.FrontendCallResolutionKind; +import gd.script.gdcc.frontend.sema.FrontendCallResolutionStatus; +import gd.script.gdcc.frontend.sema.FrontendDeclaredTypeSupport; +import gd.script.gdcc.frontend.sema.FrontendExpressionType; +import gd.script.gdcc.frontend.sema.FrontendExpressionTypeStatus; +import gd.script.gdcc.frontend.sema.FrontendReceiverKind; +import gd.script.gdcc.frontend.sema.FrontendResolvedCall; +import gd.script.gdcc.frontend.sema.FrontendSemanticStage; +import gd.script.gdcc.frontend.sema.analyzer.support.FrontendAssignmentSemanticSupport; +import gd.script.gdcc.frontend.sema.analyzer.support.FrontendChainReductionFacade; +import gd.script.gdcc.frontend.sema.analyzer.support.FrontendChainReductionHelper; +import gd.script.gdcc.frontend.sema.analyzer.support.FrontendChainStatusBridge; +import gd.script.gdcc.frontend.sema.analyzer.support.FrontendDualRoleTypeMetaRouteSupport; +import gd.script.gdcc.frontend.sema.analyzer.support.FrontendExpressionSemanticSupport; +import gd.script.gdcc.frontend.sema.analyzer.support.FrontendPropertyInitializerSupport; +import gd.script.gdcc.frontend.sema.patch.FrontendLocalSlotTypeUpdate; +import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueResolution; +import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueResolver; +import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueStatus; +import gd.script.gdcc.gdextension.ExtensionBuiltinClass; +import gd.script.gdcc.gdextension.ExtensionUtilityFunction; +import gd.script.gdcc.scope.FunctionDef; +import gd.script.gdcc.scope.Scope; +import gd.script.gdcc.scope.ScopeLookupStatus; +import gd.script.gdcc.scope.ScopeTypeMeta; +import gd.script.gdcc.scope.ScopeOwnerKind; +import gd.script.gdcc.scope.ScopeValue; +import gd.script.gdcc.scope.ScopeValueKind; +import gd.script.gdcc.type.GdCompilerType; +import gd.script.gdcc.type.GdType; +import gd.script.gdcc.type.GdVariantType; +import gd.script.gdcc.type.GdVoidType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +/// Statement-local owner procedures used by the body SuiteResolver path. +/// +/// This class is intentionally root-bounded: it may walk the current statement/header expression, but +/// it must never start from a `SourceFile` or invoke a whole-module analyzer entrypoint. Facts +/// are written only through `FrontendTypedLexicalEnvironment`, so pending/current-suite visibility and +/// ordered per-owner export stay centralized in one place. +public final class FrontendBodyOwnerProcedures implements FrontendStatementResolver.OwnerProcedures { + /// Shared category for a callable-local slot that cannot become lowering-ready. + public static final @NotNull String VARIABLE_SLOT_PUBLICATION_CATEGORY = "sema.variable_slot_publication"; + private static final @NotNull String BINDING_CATEGORY = "sema.binding"; + private static final @NotNull String MEMBER_RESOLUTION_CATEGORY = "sema.member_resolution"; + private static final @NotNull String CALL_RESOLUTION_CATEGORY = "sema.call_resolution"; + private static final @NotNull String EXPRESSION_RESOLUTION_CATEGORY = "sema.expression_resolution"; + private static final @NotNull String DISCARDED_EXPRESSION_CATEGORY = "sema.discarded_expression"; + private static final @NotNull String UNSAFE_CALL_ARGUMENT_CATEGORY = "sema.unsafe_call_argument"; + private static final @NotNull String DEFERRED_CHAIN_RESOLUTION_CATEGORY = "sema.deferred_chain_resolution"; + private static final @NotNull String DEFERRED_EXPRESSION_RESOLUTION_CATEGORY = + "sema.deferred_expression_resolution"; + private static final @NotNull String UNSUPPORTED_BINDING_SUBTREE_CATEGORY = + "sema.unsupported_binding_subtree"; + private static final @NotNull String UNSUPPORTED_CHAIN_ROUTE_CATEGORY = "sema.unsupported_chain_route"; + private static final @NotNull String UNSUPPORTED_EXPRESSION_ROUTE_CATEGORY = "sema.unsupported_expression_route"; + + private FrontendAnalysisData cachedAnalysisData; + private FrontendBodyDeclarationIndex cachedBodyDeclarationIndex; + private FrontendVisibleValueResolver cachedVisibleValueResolver; + + @Override + public void runTopBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { + forEachExpression(root, expression -> { + if (expression instanceof AttributeExpression attributeExpression) { + tryApplyAttributeChainHeadTypeMetaBias(context, attributeExpression); + } else if (expression instanceof IdentifierExpression identifierExpression) { + bindIdentifier(context, identifierExpression); + } else if (expression instanceof LiteralExpression literalExpression) { + bindLiteral(context, literalExpression); + } else if (expression instanceof SelfExpression selfExpression) { + bindSelf(context, selfExpression); + } else if (expression instanceof LambdaExpression lambdaExpression) { + reportUnsupportedBinding(context, lambdaExpression, "lambda subtree"); + } + }); + } + + private void tryApplyAttributeChainHeadTypeMetaBias( + @NotNull FrontendSuiteContext context, + @NotNull AttributeExpression attributeExpression + ) { + if (!(attributeExpression.base() instanceof IdentifierExpression identifierExpression)) { + return; + } + if (context.typedEnvironment().symbolBinding(identifierExpression) != null) { + return; + } + var currentScope = currentScopeFor(context, identifierExpression); + if (currentScope == null) { + return; + } + var valueResolution = resolveVisibleValue(context, identifierExpression); + var biasedTypeMeta = FrontendDualRoleTypeMetaRouteSupport.resolveBiasedTypeMeta( + attributeExpression, + valueResolution, + currentScope, + context.restriction(), + context.analysisData().moduleSkeleton(), + context.classRegistry() + ); + if (biasedTypeMeta != null) { + publishTypeMetaBinding(context, identifierExpression, biasedTypeMeta); + return; + } + + var typeMetaResult = context.analysisData().moduleSkeleton().resolveSourceFacingTypeMeta( + currentScope, + identifierExpression.name(), + context.restriction() + ); + if (FrontendDualRoleTypeMetaRouteSupport.shouldPreferGlobalEnumTypeMeta(valueResolution, typeMetaResult)) { + publishTypeMetaBinding(context, identifierExpression, typeMetaResult.requireValue()); + } + } + + private static void publishTypeMetaBinding( + @NotNull FrontendSuiteContext context, + @NotNull IdentifierExpression identifierExpression, + @NotNull ScopeTypeMeta typeMeta + ) { + context.typedEnvironment().putSymbolBinding( + FrontendSemanticStage.TOP_BINDING, + identifierExpression, + new FrontendBinding( + identifierExpression.name(), + FrontendBindingKind.TYPE_META, + typeMeta.declaration() + ) + ); + } + + @Override + public void runLocalTypeStabilization(@NotNull FrontendSuiteContext context, @NotNull Node root) { + if (!(root instanceof VariableDeclaration variableDeclaration)) { + return; + } + var blockScope = eligibleInferredLocalScope(context, variableDeclaration); + if (blockScope == null || variableDeclaration.value() == null) { + return; + } + var initializer = variableDeclaration.value(); + var guardedFailure = typeMetaOrdinaryValueInitializerFailure(context, initializer); + if (guardedFailure == null) { + guardedFailure = assignmentOrdinaryValueInitializerFailure(initializer); + } + var initializerType = guardedFailure != null + ? guardedFailure + : new BodyExpressionResolver(context).resolveExpressionType(initializer, false); + var stableType = stableLocalTypeOrNull(initializerType); + if (stableType == null) { + return; + } + context.typedEnvironment().addLocalSlotTypeUpdate( + FrontendSemanticStage.LOCAL_TYPE_STABILIZATION, + new FrontendLocalSlotTypeUpdate( + blockScope, + variableDeclaration.name().trim(), + variableDeclaration, + stableType + ) + ); + } + + @Override + public void runChainBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { + var resolver = new BodyExpressionResolver(context); + forEachExpression(root, expression -> { + if (expression instanceof AttributeExpression attributeExpression) { + var reduced = resolver.reduceAttributeExpression(attributeExpression); + if (reduced != null) { + publishReduction(context, reduced); + } + } else if (expression instanceof LambdaExpression lambdaExpression) { + reportUnsupportedChain(context, lambdaExpression, "lambda subtree"); + } + }); + } + + @Override + public void runExprType(@NotNull FrontendSuiteContext context, @NotNull Node root) { + var resolver = new BodyExpressionResolver(context); + publishRootExpressionTypes(context, resolver, root); + if (root instanceof ExpressionStatement expressionStatement) { + reportDiscardedExpressionWarning( + context, + expressionStatement.expression(), + context.typedEnvironment().expressionType(expressionStatement.expression()) + ); + } + if (root instanceof VariableDeclaration variableDeclaration && variableDeclaration.value() != null) { + var declarationScope = context.analysisData().scopesByAst().get(variableDeclaration); + checkInferredLocalTypeConsistency( + variableDeclaration, + declarationScope instanceof BlockScope blockScope ? blockScope : null, + context.typedEnvironment().expressionType(variableDeclaration.value()) + ); + } + } + + private static void reportDiscardedExpressionWarning( + @NotNull FrontendSuiteContext context, + @NotNull Expression expression, + @Nullable FrontendExpressionType expressionType + ) { + if (expressionType == null + || expressionType.status() != FrontendExpressionTypeStatus.RESOLVED + || expressionType.publishedType() == null + || expressionType.publishedType() instanceof GdVoidType) { + return; + } + context.diagnosticManager().warning( + DISCARDED_EXPRESSION_CATEGORY, + "Discarded expression result of type '" + expressionType.publishedType().getTypeName() + "'", + context.sourcePath(), + FrontendRange.fromAstRange(expression.range()) + ); + } + + /// Verifies the local-stabilization result without creating a second slot-mutation owner. + static void checkInferredLocalTypeConsistency( + @NotNull VariableDeclaration variableDeclaration, + @Nullable BlockScope blockScope, + @Nullable FrontendExpressionType initializerType + ) { + if (!FrontendDeclaredTypeSupport.isInferredTypeRef(variableDeclaration.type()) + || variableDeclaration.value() == null + || blockScope == null + || initializerType == null) { + return; + } + var existingLocal = blockScope.resolveValueHere(variableDeclaration.name().trim()); + if (existingLocal == null || existingLocal.declaration() != variableDeclaration) { + return; + } + var resolvedInitializerType = switch (initializerType.status()) { + case RESOLVED -> initializerType.publishedType(); + case DYNAMIC, BLOCKED, DEFERRED, FAILED, UNSUPPORTED -> null; + }; + if (resolvedInitializerType == null || resolvedInitializerType instanceof GdVoidType) { + return; + } + if (resolvedInitializerType instanceof GdCompilerType compilerOnlyType) { + throw new IllegalStateException( + "compiler-only type leaked into frontend local consistency guard: " + + compilerOnlyType.getTypeName() + ); + } + if (!(existingLocal.type() instanceof GdVariantType) + && !existingLocal.type().getTypeName().equals(resolvedInitializerType.getTypeName())) { + throw new IllegalStateException( + "Inferred local slot type changed after stabilization for '" + + variableDeclaration.name().trim() + + "': existing=" + + existingLocal.type().getTypeName() + + ", initializer=" + + resolvedInitializerType.getTypeName() + ); + } + } + + @Override + public void runVarTypePost(@NotNull FrontendSuiteContext context, @NotNull Node root) { + if (!(root instanceof VariableDeclaration variableDeclaration) + || variableDeclaration.kind() != DeclarationKind.VAR) { + return; + } + var declarationScope = context.analysisData().scopesByAst().get(variableDeclaration); + if (!(declarationScope instanceof BlockScope blockScope)) { + return; + } + var localName = variableDeclaration.name().trim(); + var slot = blockScope.resolveValueHere(localName); + if (slot == null || slot.declaration() != variableDeclaration) { + reportRejectedLocalSlotPublication(context, blockScope, variableDeclaration, slot); + return; + } + var effectiveSlot = context.typedEnvironment().effectiveScopeValue(slot, blockScope); + context.typedEnvironment().putSlotType( + FrontendSemanticStage.VAR_TYPE_POST, + variableDeclaration, + effectiveSlot.type() + ); + } + + private void reportRejectedLocalSlotPublication( + @NotNull FrontendSuiteContext context, + @NotNull BlockScope blockScope, + @NotNull VariableDeclaration variableDeclaration, + @Nullable ScopeValue currentLayerSlot + ) { + var survivingSlot = findSurvivingCallableLocalBinding(blockScope, variableDeclaration.name().trim(), currentLayerSlot); + var message = new StringBuilder() + .append("Local variable '") + .append(variableDeclaration.name().trim()) + .append("' in ") + .append(describeLocalContext(blockScope, context.callableOwner())) + .append(" has no lowering-ready published slot type at ") + .append(formatRange(variableDeclaration)) + .append(" in ") + .append(context.sourcePath()) + .append("; the declaration was not accepted into callable-local inventory"); + if (survivingSlot != null && survivingSlot.declaration() instanceof Node survivingDeclaration) { + message.append("; surviving slot currently resolves to ") + .append(describeSurvivingDeclaration(survivingSlot)) + .append(" at ") + .append(formatRange(survivingDeclaration)); + } else { + message.append("; this usually means earlier variable analysis rejected the declaration as duplicate or shadowing"); + } + context.diagnosticManager().warning( + VARIABLE_SLOT_PUBLICATION_CATEGORY, + message.toString(), + context.sourcePath(), + FrontendRange.fromAstRange(variableDeclaration.range()) + ); + } + + private @Nullable ScopeValue findSurvivingCallableLocalBinding( + @NotNull BlockScope declarationScope, + @NotNull String variableName, + @Nullable ScopeValue currentLayerSlot + ) { + if (currentLayerSlot != null) { + return currentLayerSlot; + } + return findCallableLocalBindingUpScopes(declarationScope.getParentScope(), variableName); + } + + /// Walks outer scopes from [startScope] up to the enclosing callable boundary, returning the + /// first binding named [variableName] found in an intermediate block scope or the callable scope + /// itself. Returns `null` when no such binding exists before the callable boundary. + static @Nullable ScopeValue findCallableLocalBindingUpScopes( + @Nullable Scope startScope, + @NotNull String variableName + ) { + Scope currentScope = startScope; + while (currentScope != null) { + if (currentScope instanceof BlockScope outerBlockScope) { + var outerLocal = outerBlockScope.resolveValueHere(variableName); + if (outerLocal != null) { + return outerLocal; + } + currentScope = outerBlockScope.getParentScope(); + continue; + } + if (currentScope instanceof CallableScope callableScope) { + return callableScope.resolveValueHere(variableName); + } + return null; + } + return null; + } + + private static @NotNull String describeLocalContext( + @NotNull BlockScope blockScope, + @NotNull Node callableOwner + ) { + return switch (blockScope.kind()) { + case FUNCTION_BODY, CONSTRUCTOR_BODY -> describeCallableContext(callableOwner); + case BLOCK_STATEMENT -> "block statement of " + describeCallableContext(callableOwner); + case IF_BODY -> "if-body of " + describeCallableContext(callableOwner); + case ELIF_BODY -> "elif-body of " + describeCallableContext(callableOwner); + case ELSE_BODY -> "else-body of " + describeCallableContext(callableOwner); + case WHILE_BODY -> "while-body of " + describeCallableContext(callableOwner); + case LAMBDA_BODY -> "lambda-body of " + describeCallableContext(callableOwner); + case FOR_BODY -> "`for` body of " + describeCallableContext(callableOwner); + case MATCH_SECTION_BODY -> "`match` section of " + describeCallableContext(callableOwner); + }; + } + + private static @NotNull String describeCallableContext(@NotNull Node callableOwner) { + return switch (callableOwner) { + case FunctionDeclaration functionDeclaration -> "function '" + functionDeclaration.name().trim() + "'"; + case ConstructorDeclaration _ -> "constructor '_init'"; + default -> callableOwner.getClass().getSimpleName(); + }; + } + + private static @NotNull String describeSurvivingDeclaration(@NotNull ScopeValue survivingSlot) { + return switch (survivingSlot.kind()) { + case LOCAL -> "another accepted local declaration"; + case PARAMETER -> "the parameter declaration"; + case CAPTURE -> "the capture declaration"; + case CONSTANT -> "the constant declaration"; + default -> "an accepted callable-local binding"; + }; + } + + private static @NotNull String formatRange(@NotNull Node node) { + var range = FrontendRange.fromAstRange(node.range()); + if (range == null) { + return ""; + } + return "%d:%d-%d:%d".formatted( + range.start().line(), + range.start().column(), + range.end().line(), + range.end().column() + ); + } + + @Override + public void runUnsupported(@NotNull FrontendSuiteContext context, @NotNull Node root) { + switch (root) { + case VariableDeclaration variableDeclaration when variableDeclaration.value() != null -> { + reportUnsupportedBinding(context, variableDeclaration.value(), "block-local const initializer"); + reportUnsupportedChain(context, variableDeclaration.value(), "block-local const initializer"); + } + case MatchStatement matchStatement -> { + reportUnsupportedBinding(context, matchStatement, "match subtree"); + reportUnsupportedChain(context, matchStatement, "match subtree"); + // `match` sections remain sealed as a structurally deferred domain, but the subject + // expression belongs to the enclosing executable surface and must stay visible. + runTopBinding(context, matchStatement.value()); + runChainBinding(context, matchStatement.value()); + runExprType(context, matchStatement.value()); + } + default -> { + } + } + } + + private void bindIdentifier( + @NotNull FrontendSuiteContext context, + @NotNull IdentifierExpression identifierExpression + ) { + if (context.typedEnvironment().symbolBinding(identifierExpression) != null) { + return; + } + var valueResolution = resolveVisibleValue(context, identifierExpression); + if (valueResolution.status() == FrontendVisibleValueStatus.FOUND_ALLOWED + || valueResolution.status() == FrontendVisibleValueStatus.FOUND_BLOCKED) { + publishScopeValueBinding(context, identifierExpression, valueResolution); + if (valueResolution.status() == FrontendVisibleValueStatus.FOUND_BLOCKED) { + reportBindingError( + context, + identifierExpression, + "Binding '" + identifierExpression.name() + "' is not accessible in the current context" + ); + } + return; + } + if (valueResolution.status() == FrontendVisibleValueStatus.DEFERRED_UNSUPPORTED) { + return; + } + if (tryPublishFunctionBinding(context, identifierExpression)) { + return; + } + if (tryPublishTypeMetaBinding(context, identifierExpression)) { + return; + } + context.typedEnvironment().putSymbolBinding( + FrontendSemanticStage.TOP_BINDING, + identifierExpression, + new FrontendBinding(identifierExpression.name(), FrontendBindingKind.UNKNOWN, null) + ); + reportBindingError( + context, + identifierExpression, + "Unable to resolve value binding '" + identifierExpression.name() + "'" + ); + } + + private void bindSelf( + @NotNull FrontendSuiteContext context, + @NotNull SelfExpression selfExpression + ) { + if (context.typedEnvironment().symbolBinding(selfExpression) == null) { + context.typedEnvironment().putSymbolBinding( + FrontendSemanticStage.TOP_BINDING, + selfExpression, + new FrontendBinding("self", FrontendBindingKind.SELF, null) + ); + } + if (context.propertyInitializerContext() != null) { + reportUnsupportedBindingMessage( + context, + selfExpression, + FrontendPropertyInitializerSupport.unsupportedSelfMessage() + ); + return; + } + if (context.staticContext()) { + reportBindingError(context, selfExpression, "Keyword 'self' is not available in static context"); + } + } + + private void bindLiteral( + @NotNull FrontendSuiteContext context, + @NotNull LiteralExpression literalExpression + ) { + if (context.typedEnvironment().symbolBinding(literalExpression) != null) { + return; + } + context.typedEnvironment().putSymbolBinding( + FrontendSemanticStage.TOP_BINDING, + literalExpression, + new FrontendBinding(literalExpression.sourceText(), FrontendBindingKind.LITERAL, null) + ); + } + + private @NotNull FrontendVisibleValueResolution resolveVisibleValue( + @NotNull FrontendSuiteContext context, + @NotNull IdentifierExpression identifierExpression + ) { + if (context.propertyInitializerContext() != null) { + var result = context.currentScope().resolveValue(identifierExpression.name(), context.restriction()); + return switch (result.status()) { + case FOUND_ALLOWED -> FrontendVisibleValueResolution.foundAllowed(result.requireValue(), List.of()); + case FOUND_BLOCKED -> FrontendVisibleValueResolution.foundBlocked(result.requireValue(), List.of()); + case NOT_FOUND -> FrontendVisibleValueResolution.notFound(List.of()); + }; + } + return visibleValueResolver(context).resolve( + context.visibleValueResolveRequest(identifierExpression.name(), identifierExpression), + context.typedEnvironment() + ); + } + + private void publishScopeValueBinding( + @NotNull FrontendSuiteContext context, + @NotNull IdentifierExpression identifierExpression, + @NotNull FrontendVisibleValueResolution resolution + ) { + var resolvedValue = Objects.requireNonNull(resolution.visibleValue(), "visibleValue must not be null"); + var accessStatus = resolution.status() == FrontendVisibleValueStatus.FOUND_ALLOWED + ? ScopeLookupStatus.FOUND_ALLOWED + : ScopeLookupStatus.FOUND_BLOCKED; + context.typedEnvironment().putSymbolBinding( + FrontendSemanticStage.TOP_BINDING, + identifierExpression, + new FrontendBinding( + identifierExpression.name(), + toBindingKind(resolvedValue.kind()), + resolvedValue.declaration(), + resolvedValue, + accessStatus + ) + ); + } + + private boolean tryPublishFunctionBinding( + @NotNull FrontendSuiteContext context, + @NotNull IdentifierExpression identifierExpression + ) { + var currentScope = currentScopeFor(context, identifierExpression); + if (currentScope == null) { + return false; + } + var functionResult = currentScope.resolveFunctions(identifierExpression.name(), context.restriction()); + if (!functionResult.isAllowed() && !functionResult.isBlocked()) { + return false; + } + var bindingKind = classifyFunctionBinding(functionResult.requireValue()); + if (bindingKind == null) { + return false; + } + context.typedEnvironment().putSymbolBinding( + FrontendSemanticStage.TOP_BINDING, + identifierExpression, + new FrontendBinding( + identifierExpression.name(), + bindingKind, + List.copyOf(functionResult.requireValue()) + ) + ); + return true; + } + + private boolean tryPublishTypeMetaBinding( + @NotNull FrontendSuiteContext context, + @NotNull IdentifierExpression identifierExpression + ) { + var currentScope = currentScopeFor(context, identifierExpression); + if (currentScope == null) { + return false; + } + var typeMetaResult = context.analysisData().moduleSkeleton().resolveSourceFacingTypeMeta( + currentScope, + identifierExpression.name(), + context.restriction() + ); + if (!typeMetaResult.isAllowed()) { + return false; + } + context.typedEnvironment().putSymbolBinding( + FrontendSemanticStage.TOP_BINDING, + identifierExpression, + new FrontendBinding( + identifierExpression.name(), + FrontendBindingKind.TYPE_META, + typeMetaResult.requireValue().declaration() + ) + ); + return true; + } + + private @Nullable BlockScope eligibleInferredLocalScope( + @NotNull FrontendSuiteContext context, + @NotNull VariableDeclaration variableDeclaration + ) { + if (variableDeclaration.kind() != DeclarationKind.VAR + || variableDeclaration.value() == null + || !FrontendDeclaredTypeSupport.isInferredTypeRef(variableDeclaration.type())) { + return null; + } + var declarationScope = context.analysisData().scopesByAst().get(variableDeclaration); + if (!(declarationScope instanceof BlockScope blockScope) + || !FrontendBodySemanticSupportPolicy.forBlockScopeKind( + blockScope.kind() + ).publishesLexicalInventory()) { + return null; + } + var survivingLocal = blockScope.resolveValueHere(variableDeclaration.name().trim()); + if (survivingLocal == null || survivingLocal.declaration() != variableDeclaration) { + return null; + } + return blockScope; + } + + private @Nullable FrontendExpressionType typeMetaOrdinaryValueInitializerFailure( + @NotNull FrontendSuiteContext context, + @NotNull Expression initializer + ) { + if (!(initializer instanceof IdentifierExpression identifierExpression)) { + return null; + } + var binding = context.typedEnvironment().symbolBinding(identifierExpression); + if (binding == null || binding.kind() != FrontendBindingKind.TYPE_META) { + return null; + } + return FrontendExpressionType.failed( + "Type-meta initializer '" + identifierExpression.name() + + "' cannot stabilize an inferred local because it is not an ordinary value" + ); + } + + private static @Nullable FrontendExpressionType assignmentOrdinaryValueInitializerFailure( + @NotNull Expression initializer + ) { + if (!(initializer instanceof AssignmentExpression)) { + return null; + } + return FrontendExpressionType.failed( + "Assignment initializer cannot stabilize an inferred local because it is not an ordinary value" + ); + } + + private static @Nullable GdType stableLocalTypeOrNull(@NotNull FrontendExpressionType initializerType) { + if (initializerType.status() != FrontendExpressionTypeStatus.RESOLVED) { + return null; + } + var publishedType = initializerType.publishedType(); + if (publishedType instanceof GdVoidType) { + return null; + } + if (publishedType instanceof GdCompilerType compilerOnlyType) { + throw new IllegalStateException( + "compiler-only type leaked into frontend local stabilization: " + + compilerOnlyType.getTypeName() + ); + } + return publishedType; + } + + private static void publishReduction( + @NotNull FrontendSuiteContext context, + @NotNull FrontendChainReductionHelper.ReductionResult result + ) { + for (var trace : result.stepTraces()) { + if (trace.suggestedMember() != null) { + context.typedEnvironment().putResolvedMember( + FrontendSemanticStage.CHAIN_BINDING, + trace.step(), + trace.suggestedMember() + ); + reportMemberTrace(context, trace); + } + if (trace.suggestedCall() != null) { + context.typedEnvironment().putResolvedCall( + FrontendSemanticStage.CHAIN_BINDING, + trace.step(), + trace.suggestedCall() + ); + reportCallTrace(context, trace); + } + } + reportRecoveryBoundary(context, result); + for (var note : result.notes()) { + context.diagnosticManager().warning( + CALL_RESOLUTION_CATEGORY, + note.message(), + context.sourcePath(), + FrontendRange.fromAstRange(note.anchor().range()) + ); + } + } + + private static void reportMemberTrace( + @NotNull FrontendSuiteContext context, + @NotNull FrontendChainReductionHelper.StepTrace trace + ) { + if (trace.status() != FrontendChainReductionHelper.Status.BLOCKED + && trace.status() != FrontendChainReductionHelper.Status.FAILED) { + return; + } + context.diagnosticManager().error( + MEMBER_RESOLUTION_CATEGORY, + Objects.requireNonNull(trace.detailReason(), "detailReason must not be null"), + context.sourcePath(), + FrontendRange.fromAstRange(trace.step().range()) + ); + } + + private static void reportCallTrace( + @NotNull FrontendSuiteContext context, + @NotNull FrontendChainReductionHelper.StepTrace trace + ) { + if (trace.status() != FrontendChainReductionHelper.Status.BLOCKED + && trace.status() != FrontendChainReductionHelper.Status.FAILED) { + return; + } + context.diagnosticManager().error( + CALL_RESOLUTION_CATEGORY, + Objects.requireNonNull(trace.detailReason(), "detailReason must not be null"), + context.sourcePath(), + FrontendRange.fromAstRange(trace.step().range()) + ); + } + + private static void reportRecoveryBoundary( + @NotNull FrontendSuiteContext context, + @NotNull FrontendChainReductionHelper.ReductionResult result + ) { + var recoveryRoot = result.recoveryRoot(); + if (recoveryRoot == null || result.stepTraces().isEmpty()) { + return; + } + var firstNonResolved = result.stepTraces().stream() + .filter(trace -> trace.status() != FrontendChainReductionHelper.Status.RESOLVED) + .findFirst() + .orElse(null); + if (firstNonResolved == null) { + return; + } + if (firstNonResolved.status() == FrontendChainReductionHelper.Status.DEFERRED) { + context.diagnosticManager().warning( + DEFERRED_CHAIN_RESOLUTION_CATEGORY, + Objects.requireNonNull(firstNonResolved.detailReason(), "detailReason must not be null"), + context.sourcePath(), + FrontendRange.fromAstRange(recoveryRoot.range()) + ); + return; + } + if (firstNonResolved.status() == FrontendChainReductionHelper.Status.UNSUPPORTED) { + context.diagnosticManager().error( + UNSUPPORTED_CHAIN_ROUTE_CATEGORY, + Objects.requireNonNull(firstNonResolved.detailReason(), "detailReason must not be null"), + context.sourcePath(), + FrontendRange.fromAstRange(recoveryRoot.range()) + ); + } + } + + private static void publishRootExpressionTypes( + @NotNull FrontendSuiteContext context, + @NotNull BodyExpressionResolver resolver, + @NotNull Node root + ) { + switch (root) { + case VariableDeclaration variableDeclaration when variableDeclaration.value() != null -> + publishExpressionType(context, resolver, variableDeclaration.value(), false); + case ExpressionStatement expressionStatement -> + publishExpressionType(context, resolver, expressionStatement.expression(), true); + case ReturnStatement returnStatement when returnStatement.value() != null -> + publishExpressionType(context, resolver, returnStatement.value(), false); + case AssertStatement assertStatement -> { + publishExpressionType(context, resolver, assertStatement.condition(), false); + if (assertStatement.message() != null) { + publishExpressionType(context, resolver, assertStatement.message(), false); + } + } + case Expression expression -> publishExpressionType(context, resolver, expression, false); + default -> + forEachExpression(root, expression -> publishExpressionType(context, resolver, expression, false)); + } + } + + private static void publishExpressionType( + @NotNull FrontendSuiteContext context, + @NotNull BodyExpressionResolver resolver, + @NotNull Expression expression, + boolean allowStatementResult + ) { + resolver.populateRootExpressionTransientCaches(expression, allowStatementResult); + for (var entry : resolver.finalizedExpressionTypes().entrySet()) { + if (resolver.isRouteHeadOnlyTypeMeta(entry.getKey())) { + continue; + } + if (!resolver.isAssignmentTargetPrefixExpression(entry.getKey())) { + reportExpressionDiagnostic(context, resolver, entry.getKey(), entry.getValue()); + } + context.typedEnvironment().putExpressionType( + FrontendSemanticStage.EXPR_TYPE, + entry.getKey(), + entry.getValue() + ); + if (entry.getKey() instanceof AttributeExpression attributeExpression) { + publishAttributeStepExpressionTypes(context, resolver.reduceAttributeExpression(attributeExpression)); + } + } + if (expression instanceof AssignmentExpression assignmentExpression) { + publishAssignmentTargetStepExpressionTypes(context, resolver, assignmentExpression.left()); + } + for (var entry : resolver.resolvedCalls().entrySet()) { + context.typedEnvironment().putResolvedCall( + FrontendSemanticStage.EXPR_TYPE, + entry.getKey(), + entry.getValue() + ); + reportUnsafeCallArgumentWarning(context, entry.getKey(), entry.getValue()); + } + } + + private static void publishAssignmentTargetStepExpressionTypes( + @NotNull FrontendSuiteContext context, + @NotNull BodyExpressionResolver resolver, + @NotNull Expression targetExpression + ) { + if (targetExpression instanceof AttributeExpression attributeExpression) { + publishAttributeStepExpressionTypes(context, resolver.assignmentTargetReduction(attributeExpression)); + } + } + + private static void reportUnsafeCallArgumentWarning( + @NotNull FrontendSuiteContext context, + @NotNull CallExpression callExpression, + @NotNull FrontendResolvedCall publishedCall + ) { + if (!isUnsafeBuiltinVariantConstructorRoute(publishedCall)) { + return; + } + var sourceType = publishedCall.argumentTypes().getFirst(); + var targetType = Objects.requireNonNull(publishedCall.returnType(), "returnType must not be null"); + context.diagnosticManager().warning( + UNSAFE_CALL_ARGUMENT_CATEGORY, + "Unsafe call argument for builtin constructor '" + publishedCall.callableName() + + "(...)': static argument type '" + sourceType.getTypeName() + + "' requires runtime conversion to '" + targetType.getTypeName() + "'", + context.sourcePath(), + FrontendRange.fromAstRange(callExpression.range()) + ); + } + + private static boolean isUnsafeBuiltinVariantConstructorRoute(@NotNull FrontendResolvedCall publishedCall) { + return publishedCall.status() == FrontendCallResolutionStatus.RESOLVED + && publishedCall.callKind() == FrontendCallResolutionKind.CONSTRUCTOR + && publishedCall.receiverKind() == FrontendReceiverKind.TYPE_META + && publishedCall.ownerKind() == ScopeOwnerKind.BUILTIN + && publishedCall.argumentTypes().size() == 1 + && publishedCall.argumentTypes().getFirst() instanceof GdVariantType + && publishedCall.declarationSite() instanceof ExtensionBuiltinClass; + } + + private static void reportUnsupportedBinding( + @NotNull FrontendSuiteContext context, + @NotNull Node anchor, + @NotNull String domain + ) { + context.diagnosticManager().error( + UNSUPPORTED_BINDING_SUBTREE_CATEGORY, + "Binding analysis is not supported in " + domain, + context.sourcePath(), + FrontendRange.fromAstRange(anchor.range()) + ); + } + + private static void reportBindingError( + @NotNull FrontendSuiteContext context, + @NotNull Node anchor, + @NotNull String message + ) { + context.diagnosticManager().error( + BINDING_CATEGORY, + message, + context.sourcePath(), + FrontendRange.fromAstRange(anchor.range()) + ); + } + + private static void reportUnsupportedBindingMessage( + @NotNull FrontendSuiteContext context, + @NotNull Node anchor, + @NotNull String message + ) { + context.diagnosticManager().error( + UNSUPPORTED_BINDING_SUBTREE_CATEGORY, + message, + context.sourcePath(), + FrontendRange.fromAstRange(anchor.range()) + ); + } + + private static void reportUnsupportedChain( + @NotNull FrontendSuiteContext context, + @NotNull Node anchor, + @NotNull String domain + ) { + context.diagnosticManager().error( + UNSUPPORTED_CHAIN_ROUTE_CATEGORY, + "Chain binding analysis is not supported in " + domain, + context.sourcePath(), + FrontendRange.fromAstRange(anchor.range()) + ); + } + + private static void reportUnsupportedExpression( + @NotNull FrontendSuiteContext context, + @NotNull Node anchor, + @NotNull String detailReason + ) { + context.diagnosticManager().error( + UNSUPPORTED_EXPRESSION_ROUTE_CATEGORY, + detailReason, + context.sourcePath(), + FrontendRange.fromAstRange(anchor.range()) + ); + } + + private static void reportExpressionDiagnostic( + @NotNull FrontendSuiteContext context, + @NotNull BodyExpressionResolver resolver, + @NotNull Expression expression, + @NotNull FrontendExpressionType expressionType + ) { + if (!resolver.markExpressionDiagnosticReported(expression)) { + return; + } + switch (expressionType.status()) { + case FAILED -> context.diagnosticManager().error( + EXPRESSION_RESOLUTION_CATEGORY, + Objects.requireNonNull(expressionType.detailReason(), "detailReason must not be null"), + context.sourcePath(), + FrontendRange.fromAstRange(expression.range()) + ); + case DEFERRED -> context.diagnosticManager().warning( + DEFERRED_EXPRESSION_RESOLUTION_CATEGORY, + Objects.requireNonNull(expressionType.detailReason(), "detailReason must not be null"), + context.sourcePath(), + FrontendRange.fromAstRange(expression.range()) + ); + case UNSUPPORTED -> reportUnsupportedExpression( + context, + expression, + Objects.requireNonNull(expressionType.detailReason(), "detailReason must not be null") + ); + default -> { + } + } + } + + private static void publishAttributeStepExpressionTypes( + @NotNull FrontendSuiteContext context, + @Nullable FrontendChainReductionHelper.ReductionResult reduction + ) { + if (reduction == null) { + return; + } + for (var trace : reduction.stepTraces()) { + context.typedEnvironment().putExpressionType( + FrontendSemanticStage.EXPR_TYPE, + trace.step(), + resolvePublishedAttributeStepType(trace) + ); + } + } + + private static @NotNull FrontendExpressionType resolvePublishedAttributeStepType( + @NotNull FrontendChainReductionHelper.StepTrace trace + ) { + if (trace.suggestedMember() != null) { + return FrontendChainStatusBridge.toPublishedExpressionType(trace.suggestedMember()); + } + if (trace.suggestedCall() != null) { + return FrontendChainStatusBridge.toPublishedExpressionType(trace.suggestedCall()); + } + if (trace.status() == FrontendChainReductionHelper.Status.BLOCKED + && trace.routeKind() == FrontendChainReductionHelper.RouteKind.UPSTREAM_BLOCKED) { + return FrontendExpressionType.blocked( + null, + Objects.requireNonNull(trace.detailReason(), "detailReason must not be null") + ); + } + return FrontendChainStatusBridge.toPublishedExpressionType(trace.outgoingReceiver()); + } + + private @NotNull FrontendVisibleValueResolver visibleValueResolver(@NotNull FrontendSuiteContext context) { + var bodyDeclarationIndex = context.interfaceSurface().bodyDeclarationIndex(); + if (cachedAnalysisData != context.analysisData() + || cachedBodyDeclarationIndex != bodyDeclarationIndex + || cachedVisibleValueResolver == null) { + cachedAnalysisData = context.analysisData(); + cachedBodyDeclarationIndex = bodyDeclarationIndex; + cachedVisibleValueResolver = new FrontendVisibleValueResolver( + context.analysisData(), + bodyDeclarationIndex + ); + } + return cachedVisibleValueResolver; + } + + private static @Nullable Scope currentScopeFor( + @NotNull FrontendSuiteContext context, + @NotNull Node node + ) { + return context.analysisData().scopesByAst().get(node); + } + + private static @Nullable FrontendBindingKind classifyFunctionBinding(@NotNull List overloadSet) { + var overloads = List.copyOf(Objects.requireNonNull(overloadSet, "overloadSet must not be null")); + if (overloads.isEmpty()) { + return null; + } + if (overloads.stream().allMatch(ExtensionUtilityFunction.class::isInstance)) { + return FrontendBindingKind.UTILITY_FUNCTION; + } + if (overloads.stream().anyMatch(ExtensionUtilityFunction.class::isInstance)) { + return null; + } + if (overloads.stream().allMatch(FunctionDef::isStatic)) { + return FrontendBindingKind.STATIC_METHOD; + } + if (overloads.stream().anyMatch(FunctionDef::isStatic)) { + return null; + } + return FrontendBindingKind.METHOD; + } + + private static @NotNull FrontendBindingKind toBindingKind(@NotNull ScopeValueKind scopeValueKind) { + return switch (Objects.requireNonNull(scopeValueKind, "scopeValueKind must not be null")) { + case LOCAL -> FrontendBindingKind.LOCAL_VAR; + case PARAMETER -> FrontendBindingKind.PARAMETER; + case CAPTURE -> FrontendBindingKind.CAPTURE; + case PROPERTY -> FrontendBindingKind.PROPERTY; + case SIGNAL -> FrontendBindingKind.SIGNAL; + case CONSTANT -> FrontendBindingKind.CONSTANT; + case SINGLETON -> FrontendBindingKind.SINGLETON; + case GLOBAL_ENUM -> FrontendBindingKind.GLOBAL_ENUM; + case TYPE_META -> FrontendBindingKind.TYPE_META; + }; + } + + private static void forEachExpression(@NotNull Node root, @NotNull Consumer consumer) { + walkRootBounded(root, node -> { + if (node instanceof Expression expression) { + consumer.accept(expression); + } + }); + } + + private static void walkRootBounded(@NotNull Node node, @NotNull Consumer consumer) { + consumer.accept(node); + if (node instanceof LambdaExpression) { + return; + } + for (var child : node.getChildren()) { + walkRootBounded(child, consumer); + } + } + + private static final class BodyExpressionResolver { + private final @NotNull FrontendSuiteContext context; + // Procedure-local transient caches back bounded retry without joining the typed overlay. + // Only explicit owner publication below can move final facts into pending/committed state. + private final @NotNull IdentityHashMap expressionTypes = + new IdentityHashMap<>(); + private final @NotNull IdentityHashMap finalizedExpressionTypes = + new IdentityHashMap<>(); + private final @NotNull IdentityHashMap assignmentTargetPrefixExpressions = + new IdentityHashMap<>(); + private final @NotNull IdentityHashMap + assignmentTargetReductions = new IdentityHashMap<>(); + private final @NotNull IdentityHashMap routeHeadOnlyTypeMetaExpressions = + new IdentityHashMap<>(); + private final @NotNull IdentityHashMap resolvedCalls = + new IdentityHashMap<>(); + private final @NotNull IdentityHashMap reportedExpressionDiagnostics = + new IdentityHashMap<>(); + private final @NotNull FrontendChainReductionFacade chainReduction; + private final @NotNull FrontendAssignmentSemanticSupport.Context assignmentSemanticContext; + private final @NotNull FrontendExpressionSemanticSupport expressionSemanticSupport; + private final @NotNull IdentityHashMap + assignmentUsages = new IdentityHashMap<>(); + + private BodyExpressionResolver(@NotNull FrontendSuiteContext context) { + this.context = Objects.requireNonNull(context, "context must not be null"); + chainReduction = new FrontendChainReductionFacade( + context.analysisData(), + context.analysisData().scopesByAst(), + context::restriction, + context::staticContext, + context::propertyInitializerContext, + context.classRegistry(), + this::resolveExpressionDependency, + identifier -> context.typedEnvironment().symbolBinding(identifier) + ); + assignmentSemanticContext = FrontendAssignmentSemanticSupport.createContext( + context.analysisData().symbolBindings(), + identifier -> context.typedEnvironment().symbolBinding(identifier), + context.analysisData().scopesByAst(), + context.analysisData().moduleSkeleton(), + context::restriction, + context.classRegistry(), + chainReduction + ); + expressionSemanticSupport = new FrontendExpressionSemanticSupport( + identifier -> context.typedEnvironment().symbolBinding(identifier), + context.analysisData().scopesByAst(), + context::restriction, + context::propertyInitializerContext, + context.classRegistry(), + chainReduction::headReceiverSupport + ); + } + + private @NotNull FrontendExpressionType resolveExpressionType( + @NotNull Expression expression, + boolean finalizeWindow + ) { + var published = context.typedEnvironment().expressionType(expression); + if (published != null) { + return published; + } + var cache = finalizeWindow ? finalizedExpressionTypes : expressionTypes; + var cached = cache.get(expression); + if (cached != null) { + return cached; + } + var computed = computeExpressionType(expression, finalizeWindow); + cache.put(expression, computed); + if (finalizeWindow) { + expressionTypes.put(expression, computed); + } + return computed; + } + + /// Computes the root expression for side effects only: it records statement-vs-value + /// assignment usage and fills owner-local transient caches that the caller publishes in bulk. + private void populateRootExpressionTransientCaches( + @NotNull Expression expression, + boolean allowStatementResult + ) { + if (expression instanceof AssignmentExpression assignmentExpression) { + assignmentUsages.put( + assignmentExpression, + allowStatementResult + ? FrontendAssignmentSemanticSupport.AssignmentUsage.STATEMENT_ROOT + : FrontendAssignmentSemanticSupport.AssignmentUsage.VALUE_REQUIRED + ); + } + resolveExpressionType(expression, true); + } + + private @NotNull FrontendExpressionType computeExpressionType( + @NotNull Expression expression, + boolean finalizeWindow + ) { + return switch (expression) { + case LiteralExpression literalExpression -> expressionSemanticSupport + .resolveLiteralExpressionType(literalExpression) + .expressionType(); + case SelfExpression selfExpression -> expressionSemanticSupport + .resolveSelfExpressionType(selfExpression) + .expressionType(); + case IdentifierExpression identifierExpression -> expressionSemanticSupport + .resolveIdentifierExpressionType(identifierExpression) + .expressionType(); + case AttributeExpression attributeExpression -> + resolveAttributeExpressionType(attributeExpression, finalizeWindow); + case AssignmentExpression assignmentExpression -> resolveAssignmentExpressionType( + assignmentExpression, + finalizeWindow + ); + case CallExpression callExpression -> resolveCallExpressionType(callExpression, finalizeWindow); + case SubscriptExpression subscriptExpression -> expressionSemanticSupport + .resolveSubscriptExpressionType( + subscriptExpression, + this::resolveExpressionType, + finalizeWindow + ) + .expressionType(); + case LambdaExpression lambdaExpression -> expressionSemanticSupport + .resolveLambdaExpressionType( + lambdaExpression, + this::resolveExpressionType, + false, + finalizeWindow + ) + .expressionType(); + case UnaryExpression unaryExpression -> expressionSemanticSupport + .resolveUnaryExpressionType( + unaryExpression, + this::resolveExpressionType, + finalizeWindow + ) + .expressionType(); + case BinaryExpression binaryExpression -> expressionSemanticSupport + .resolveBinaryExpressionType( + binaryExpression, + this::resolveExpressionType, + finalizeWindow + ) + .expressionType(); + default -> expressionSemanticSupport + .resolveRemainingExplicitExpressionType( + expression, + this::resolveExpressionType, + true, + finalizeWindow + ) + .expressionType(); + }; + } + + private @NotNull FrontendExpressionType resolveAssignmentExpressionType( + @NotNull AssignmentExpression assignmentExpression, + boolean finalizeWindow + ) { + var result = FrontendAssignmentSemanticSupport.resolveAssignmentExpressionType( + assignmentSemanticContext, + assignmentExpression, + assignmentUsages.getOrDefault( + assignmentExpression, + FrontendAssignmentSemanticSupport.AssignmentUsage.VALUE_REQUIRED + ), + this::resolveExpressionType, + finalizeWindow + ).expressionType(); + if (finalizeWindow) { + finalizeAssignmentTargetExpressionTypes(assignmentExpression.left()); + } + return result; + } + + private void finalizeAssignmentTargetExpressionTypes(@NotNull Expression targetExpression) { + switch (targetExpression) { + case AttributeExpression attributeExpression -> + finalizeAttributeAssignmentTargetExpressionTypes(attributeExpression, false); + case SubscriptExpression subscriptExpression -> + finalizeSubscriptAssignmentTargetExpressionTypes(subscriptExpression, false); + default -> { + } + } + } + + private void finalizeAssignmentTargetValueExpression(@NotNull Expression expression) { + switch (expression) { + case AttributeExpression attributeExpression -> + finalizeAttributeAssignmentTargetExpressionTypes(attributeExpression, true); + case SubscriptExpression subscriptExpression -> + finalizeSubscriptAssignmentTargetExpressionTypes(subscriptExpression, true); + default -> markAndResolveAssignmentTargetPrefixExpression(expression); + } + } + + private void finalizeAttributeAssignmentTargetExpressionTypes( + @NotNull AttributeExpression attributeExpression, + boolean publishRootExpression + ) { + finalizeAssignmentTargetValueExpression(attributeExpression.base()); + for (var step : attributeExpression.steps()) { + if (step instanceof AttributeCallStep attributeCallStep) { + for (var argument : attributeCallStep.arguments()) { + resolveExpressionType(argument, true); + } + } else if (step instanceof AttributeSubscriptStep attributeSubscriptStep) { + for (var argument : attributeSubscriptStep.arguments()) { + resolveExpressionType(argument, true); + } + } + } + var reduction = reduceAttributeExpression(attributeExpression); + if (reduction != null) { + assignmentTargetReductions.put(attributeExpression, reduction); + } + if (publishRootExpression) { + markAndResolveAssignmentTargetPrefixExpression(attributeExpression); + } + } + + private void finalizeSubscriptAssignmentTargetExpressionTypes( + @NotNull SubscriptExpression subscriptExpression, + boolean publishRootExpression + ) { + finalizeAssignmentTargetValueExpression(subscriptExpression.base()); + for (var argument : subscriptExpression.arguments()) { + resolveExpressionType(argument, true); + } + if (publishRootExpression) { + markAndResolveAssignmentTargetPrefixExpression(subscriptExpression); + } + } + + private void markAndResolveAssignmentTargetPrefixExpression(@NotNull Expression expression) { + // Lowering materializes assignment receivers, but binding/chain/assignment-root owners + // still own their diagnostics. Publish the type fact without creating duplicate expr errors. + assignmentTargetPrefixExpressions.put(expression, Boolean.TRUE); + resolveExpressionType(expression, true); + } + + private @NotNull FrontendExpressionType resolveCallExpressionType( + @NotNull CallExpression callExpression, + boolean finalizeWindow + ) { + var result = expressionSemanticSupport.resolveCallExpressionType( + callExpression, + this::resolveExpressionType, + true, + finalizeWindow + ); + if (result.publishedCallOrNull() != null) { + resolvedCalls.put(callExpression, result.publishedCallOrNull()); + } + return result.expressionType(); + } + + private @NotNull FrontendExpressionType resolveAttributeExpressionType( + @NotNull AttributeExpression attributeExpression, + boolean finalizeWindow + ) { + if (isTypeMetaRouteHead(attributeExpression.base())) { + routeHeadOnlyTypeMetaExpressions.put(attributeExpression.base(), Boolean.TRUE); + } + resolveExpressionType(attributeExpression.base(), finalizeWindow); + for (var step : attributeExpression.steps()) { + if (step instanceof AttributeCallStep attributeCallStep) { + for (var argument : attributeCallStep.arguments()) { + resolveExpressionType(argument, finalizeWindow); + } + continue; + } + if (step instanceof AttributeSubscriptStep attributeSubscriptStep) { + for (var argument : attributeSubscriptStep.arguments()) { + resolveExpressionType(argument, finalizeWindow); + } + } + } + var reduced = reduceAttributeExpression(attributeExpression); + if (reduced == null) { + return FrontendExpressionType.unsupported( + "Nested chain expression is inside an unsupported or skipped subtree" + ); + } + return FrontendChainStatusBridge.toPublishedExpressionType(reduced); + } + + private @Nullable FrontendChainReductionHelper.ReductionResult reduceAttributeExpression( + @NotNull AttributeExpression attributeExpression + ) { + return chainReduction.reduce(attributeExpression).result(); + } + + private @Nullable FrontendChainReductionHelper.ReductionResult assignmentTargetReduction( + @NotNull AttributeExpression attributeExpression + ) { + return assignmentTargetReductions.get(attributeExpression); + } + + private @NotNull IdentityHashMap finalizedExpressionTypes() { + return finalizedExpressionTypes; + } + + private boolean isAssignmentTargetPrefixExpression(@NotNull Expression expression) { + return assignmentTargetPrefixExpressions.containsKey(expression); + } + + private boolean isRouteHeadOnlyTypeMeta(@NotNull Expression expression) { + return routeHeadOnlyTypeMetaExpressions.containsKey(expression); + } + + private boolean markExpressionDiagnosticReported(@NotNull Expression expression) { + return reportedExpressionDiagnostics.putIfAbsent(expression, Boolean.TRUE) == null; + } + + private boolean isTypeMetaRouteHead(@NotNull Expression expression) { + if (!(expression instanceof IdentifierExpression identifierExpression)) { + return false; + } + var binding = context.typedEnvironment().symbolBinding(identifierExpression); + return binding != null && binding.kind() == FrontendBindingKind.TYPE_META; + } + + private @NotNull IdentityHashMap resolvedCalls() { + return resolvedCalls; + } + + private @NotNull FrontendChainReductionHelper.ExpressionTypeResult resolveExpressionDependency( + @NotNull Expression expression, + boolean finalizeWindow + ) { + return FrontendChainStatusBridge.toExpressionTypeResult(resolveExpressionType(expression, finalizeWindow)); + } + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendChainBindingAnalyzer.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendChainBindingAnalyzer.java deleted file mode 100644 index 5e9208b0..00000000 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendChainBindingAnalyzer.java +++ /dev/null @@ -1,761 +0,0 @@ -package gd.script.gdcc.frontend.sema.analyzer; - -import gd.script.gdcc.frontend.diagnostic.DiagnosticManager; -import gd.script.gdcc.frontend.diagnostic.FrontendRange; -import gd.script.gdcc.frontend.sema.FrontendAnalysisData; -import gd.script.gdcc.frontend.sema.FrontendAstSideTable; -import gd.script.gdcc.frontend.sema.FrontendExpressionType; -import gd.script.gdcc.frontend.sema.analyzer.support.FrontendPropertyInitializerSupport; -import gd.script.gdcc.frontend.sema.analyzer.support.FrontendAssignmentSemanticSupport; -import gd.script.gdcc.frontend.sema.FrontendResolvedCall; -import gd.script.gdcc.frontend.sema.FrontendResolvedMember; -import gd.script.gdcc.frontend.sema.analyzer.support.FrontendChainReductionFacade; -import gd.script.gdcc.frontend.sema.analyzer.support.FrontendChainReductionHelper; -import gd.script.gdcc.frontend.sema.analyzer.support.FrontendChainStatusBridge; -import gd.script.gdcc.frontend.sema.analyzer.support.FrontendExpressionSemanticSupport; -import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueDomain; -import gd.script.gdcc.scope.ClassRegistry; -import gd.script.gdcc.scope.ResolveRestriction; -import gd.script.gdcc.scope.Scope; -import dev.superice.gdparser.frontend.ast.ASTNodeHandler; -import dev.superice.gdparser.frontend.ast.ASTWalker; -import dev.superice.gdparser.frontend.ast.AssertStatement; -import dev.superice.gdparser.frontend.ast.AssignmentExpression; -import dev.superice.gdparser.frontend.ast.AttributeExpression; -import dev.superice.gdparser.frontend.ast.BinaryExpression; -import dev.superice.gdparser.frontend.ast.Block; -import dev.superice.gdparser.frontend.ast.CallExpression; -import dev.superice.gdparser.frontend.ast.ClassDeclaration; -import dev.superice.gdparser.frontend.ast.ConstructorDeclaration; -import dev.superice.gdparser.frontend.ast.DeclarationKind; -import dev.superice.gdparser.frontend.ast.ElifClause; -import dev.superice.gdparser.frontend.ast.Expression; -import dev.superice.gdparser.frontend.ast.ExpressionStatement; -import dev.superice.gdparser.frontend.ast.ForStatement; -import dev.superice.gdparser.frontend.ast.FrontendASTTraversalDirective; -import dev.superice.gdparser.frontend.ast.FunctionDeclaration; -import dev.superice.gdparser.frontend.ast.IdentifierExpression; -import dev.superice.gdparser.frontend.ast.IfStatement; -import dev.superice.gdparser.frontend.ast.LambdaExpression; -import dev.superice.gdparser.frontend.ast.LiteralExpression; -import dev.superice.gdparser.frontend.ast.MatchStatement; -import dev.superice.gdparser.frontend.ast.Node; -import dev.superice.gdparser.frontend.ast.Parameter; -import dev.superice.gdparser.frontend.ast.ReturnStatement; -import dev.superice.gdparser.frontend.ast.SelfExpression; -import dev.superice.gdparser.frontend.ast.SourceFile; -import dev.superice.gdparser.frontend.ast.Statement; -import dev.superice.gdparser.frontend.ast.SubscriptExpression; -import dev.superice.gdparser.frontend.ast.UnaryExpression; -import dev.superice.gdparser.frontend.ast.VariableDeclaration; -import dev.superice.gdparser.frontend.ast.WhileStatement; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.nio.file.Path; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Objects; - -/// Publishes body-phase chain member/call results from already-published binding and scope facts. -/// -/// The analyzer keeps ordinary expression publication in `FrontendExprTypeAnalyzer`. -/// It only performs the local dependency typing needed to keep one chain reducing left-to-right. -/// Local `:=` receiver slots are expected to be settled by `FrontendLocalTypeStabilizationAnalyzer` -/// before this phase; chain binding consumes those slots but does not own slot inference. -public class FrontendChainBindingAnalyzer { - private static final @NotNull String MEMBER_RESOLUTION_CATEGORY = "sema.member_resolution"; - private static final @NotNull String CALL_RESOLUTION_CATEGORY = "sema.call_resolution"; - private static final @NotNull String DEFERRED_CHAIN_RESOLUTION_CATEGORY = "sema.deferred_chain_resolution"; - private static final @NotNull String UNSUPPORTED_CHAIN_ROUTE_CATEGORY = "sema.unsupported_chain_route"; - - public void analyze( - @NotNull ClassRegistry classRegistry, - @NotNull FrontendAnalysisData analysisData, - @NotNull DiagnosticManager diagnosticManager - ) { - Objects.requireNonNull(classRegistry, "classRegistry must not be null"); - Objects.requireNonNull(analysisData, "analysisData must not be null"); - Objects.requireNonNull(diagnosticManager, "diagnosticManager must not be null"); - - var moduleSkeleton = analysisData.moduleSkeleton(); - analysisData.diagnostics(); - - var scopesByAst = analysisData.scopesByAst(); - for (var sourceClassRelation : moduleSkeleton.sourceClassRelations()) { - var sourceFile = sourceClassRelation.unit().ast(); - if (!scopesByAst.containsKey(sourceFile)) { - throw new IllegalStateException( - "Scope graph has not been published for source file: " + sourceClassRelation.unit().path() - ); - } - } - - var resolvedMembers = new FrontendAstSideTable(); - var resolvedCalls = new FrontendAstSideTable(); - for (var sourceClassRelation : moduleSkeleton.sourceClassRelations()) { - new AstWalkerChainBinder( - sourceClassRelation.unit().path(), - classRegistry, - analysisData, - scopesByAst, - resolvedMembers, - resolvedCalls, - diagnosticManager - ).walk(sourceClassRelation.unit().ast()); - } - analysisData.updateResolvedMembers(resolvedMembers); - analysisData.updateResolvedCalls(resolvedCalls); - } - - private static final class AstWalkerChainBinder implements ASTNodeHandler { - private final @NotNull Path sourcePath; - private final @NotNull ClassRegistry classRegistry; - private final @NotNull FrontendAnalysisData analysisData; - private final @NotNull FrontendAstSideTable scopesByAst; - private final @NotNull FrontendAstSideTable resolvedMembers; - private final @NotNull FrontendAstSideTable resolvedCalls; - private final @NotNull DiagnosticManager diagnosticManager; - private final @NotNull ASTWalker astWalker; - private final @NotNull IdentityHashMap reportedDeferredRoots = new IdentityHashMap<>(); - private final @NotNull IdentityHashMap reportedUnsupportedRoots = new IdentityHashMap<>(); - private final @NotNull FrontendChainReductionFacade chainReduction; - private final @NotNull FrontendAssignmentSemanticSupport.Context assignmentSemanticContext; - private final @NotNull FrontendExpressionSemanticSupport expressionSemanticSupport; - private int supportedExecutableBlockDepth; - private @NotNull ResolveRestriction currentRestriction = ResolveRestriction.unrestricted(); - private boolean currentStaticContext; - private @Nullable FrontendPropertyInitializerSupport.PropertyInitializerContext currentPropertyInitializerContext; - - private AstWalkerChainBinder( - @NotNull Path sourcePath, - @NotNull ClassRegistry classRegistry, - @NotNull FrontendAnalysisData analysisData, - @NotNull FrontendAstSideTable scopesByAst, - @NotNull FrontendAstSideTable resolvedMembers, - @NotNull FrontendAstSideTable resolvedCalls, - @NotNull DiagnosticManager diagnosticManager - ) { - this.sourcePath = Objects.requireNonNull(sourcePath, "sourcePath must not be null"); - this.classRegistry = Objects.requireNonNull(classRegistry, "classRegistry must not be null"); - this.analysisData = Objects.requireNonNull(analysisData, "analysisData must not be null"); - this.scopesByAst = Objects.requireNonNull(scopesByAst, "scopesByAst must not be null"); - this.resolvedMembers = Objects.requireNonNull(resolvedMembers, "resolvedMembers must not be null"); - this.resolvedCalls = Objects.requireNonNull(resolvedCalls, "resolvedCalls must not be null"); - this.diagnosticManager = Objects.requireNonNull(diagnosticManager, "diagnosticManager must not be null"); - astWalker = new ASTWalker(this); - chainReduction = new FrontendChainReductionFacade( - analysisData, - scopesByAst, - () -> currentRestriction, - () -> currentStaticContext, - () -> currentPropertyInitializerContext, - classRegistry, - this::resolveExpressionType - ); - assignmentSemanticContext = FrontendAssignmentSemanticSupport.createContext( - analysisData.symbolBindings(), - scopesByAst, - analysisData.moduleSkeleton(), - () -> currentRestriction, - classRegistry, - chainReduction - ); - expressionSemanticSupport = new FrontendExpressionSemanticSupport( - analysisData.symbolBindings(), - scopesByAst, - () -> currentRestriction, - () -> currentPropertyInitializerContext, - classRegistry, - chainReduction::headReceiverSupport - ); - } - - private void walk(@NotNull SourceFile sourceFile) { - astWalker.walk(sourceFile); - } - - @Override - public @NotNull FrontendASTTraversalDirective handleNode(@NotNull Node node) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleSourceFile(@NotNull SourceFile sourceFile) { - walkNonExecutableContainerStatements(sourceFile.statements()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleClassDeclaration(@NotNull ClassDeclaration classDeclaration) { - if (isNotPublished(classDeclaration)) { - reportDeferredSubtree(classDeclaration, FrontendVisibleValueDomain.UNKNOWN_OR_SKIPPED_SUBTREE); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkNonExecutableContainerStatements(classDeclaration.body().statements()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleFunctionDeclaration( - @NotNull FunctionDeclaration functionDeclaration - ) { - if (isNotPublished(functionDeclaration)) { - reportDeferredSubtree(functionDeclaration, FrontendVisibleValueDomain.UNKNOWN_OR_SKIPPED_SUBTREE); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - reportDeferredParameterDefaults(functionDeclaration.parameters()); - walkCallableBody( - functionDeclaration, - functionDeclaration.body(), - functionDeclaration.isStatic() - ? ResolveRestriction.staticContext() - : ResolveRestriction.instanceContext(), - functionDeclaration.isStatic() - ); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleConstructorDeclaration( - @NotNull ConstructorDeclaration constructorDeclaration - ) { - if (isNotPublished(constructorDeclaration)) { - reportDeferredSubtree(constructorDeclaration, FrontendVisibleValueDomain.UNKNOWN_OR_SKIPPED_SUBTREE); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - reportDeferredParameterDefaults(constructorDeclaration.parameters()); - walkCallableBody( - constructorDeclaration, - constructorDeclaration.body(), - ResolveRestriction.instanceContext(), - false - ); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleBlock(@NotNull Block block) { - if (supportedExecutableBlockDepth <= 0) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - if (isNotPublished(block)) { - reportDeferredSubtree(block, FrontendVisibleValueDomain.UNKNOWN_OR_SKIPPED_SUBTREE); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkStatements(block.statements()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleExpressionStatement( - @NotNull ExpressionStatement expressionStatement - ) { - if (supportedExecutableBlockDepth <= 0) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkValueExpression(expressionStatement.expression()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleReturnStatement(@NotNull ReturnStatement returnStatement) { - if (supportedExecutableBlockDepth <= 0 || returnStatement.value() == null) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkValueExpression(returnStatement.value()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleAssertStatement(@NotNull AssertStatement assertStatement) { - if (supportedExecutableBlockDepth <= 0) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkValueExpression(assertStatement.condition()); - if (assertStatement.message() != null) { - walkValueExpression(assertStatement.message()); - } - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleVariableDeclaration( - @NotNull VariableDeclaration variableDeclaration - ) { - if (supportedExecutableBlockDepth > 0) { - if (variableDeclaration.value() == null) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - if (variableDeclaration.kind() == DeclarationKind.CONST) { - reportDeferredSubtree( - variableDeclaration.value(), - FrontendVisibleValueDomain.BLOCK_LOCAL_CONST_SUBTREE - ); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - if (variableDeclaration.kind() != DeclarationKind.VAR) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkValueExpression(variableDeclaration.value()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - if (!FrontendPropertyInitializerSupport.isSupportedPropertyInitializer(scopesByAst, variableDeclaration)) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkPropertyInitializer(variableDeclaration); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleIfStatement(@NotNull IfStatement ifStatement) { - if (supportedExecutableBlockDepth <= 0) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - if (isNotPublished(ifStatement)) { - reportDeferredSubtree(ifStatement, FrontendVisibleValueDomain.UNKNOWN_OR_SKIPPED_SUBTREE); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkValueExpression(ifStatement.condition()); - walkSupportedExecutableBlock(ifStatement.body()); - for (var elifClause : ifStatement.elifClauses()) { - astWalker.walk(elifClause); - } - if (ifStatement.elseBody() != null) { - walkSupportedExecutableBlock(ifStatement.elseBody()); - } - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleElifClause(@NotNull ElifClause elifClause) { - if (supportedExecutableBlockDepth <= 0) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - if (isNotPublished(elifClause)) { - reportDeferredSubtree(elifClause, FrontendVisibleValueDomain.UNKNOWN_OR_SKIPPED_SUBTREE); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkValueExpression(elifClause.condition()); - walkSupportedExecutableBlock(elifClause.body()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleWhileStatement(@NotNull WhileStatement whileStatement) { - if (supportedExecutableBlockDepth <= 0) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - if (isNotPublished(whileStatement)) { - reportDeferredSubtree(whileStatement, FrontendVisibleValueDomain.UNKNOWN_OR_SKIPPED_SUBTREE); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkValueExpression(whileStatement.condition()); - walkSupportedExecutableBlock(whileStatement.body()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleForStatement(@NotNull ForStatement forStatement) { - if (supportedExecutableBlockDepth <= 0) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - if (isNotPublished(forStatement)) { - reportDeferredSubtree(forStatement, FrontendVisibleValueDomain.UNKNOWN_OR_SKIPPED_SUBTREE); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - reportDeferredSubtree(forStatement, FrontendVisibleValueDomain.FOR_SUBTREE); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleMatchStatement(@NotNull MatchStatement matchStatement) { - if (supportedExecutableBlockDepth <= 0) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - if (isNotPublished(matchStatement)) { - reportDeferredSubtree(matchStatement, FrontendVisibleValueDomain.UNKNOWN_OR_SKIPPED_SUBTREE); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkValueExpression(matchStatement.value()); - if (!matchStatement.sections().isEmpty()) { - reportDeferredSubtree(matchStatement, FrontendVisibleValueDomain.MATCH_SUBTREE); - } - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleLambdaExpression(@NotNull LambdaExpression lambdaExpression) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - private void walkCallableBody( - @NotNull Node callableOwner, - @Nullable Block body, - @NotNull ResolveRestriction restriction, - boolean staticContext - ) { - if (isNotPublished(callableOwner)) { - reportDeferredSubtree(callableOwner, FrontendVisibleValueDomain.UNKNOWN_OR_SKIPPED_SUBTREE); - return; - } - if (isNotPublished(body)) { - reportDeferredSubtree(body, FrontendVisibleValueDomain.UNKNOWN_OR_SKIPPED_SUBTREE); - return; - } - var previousRestriction = currentRestriction; - var previousStaticContext = currentStaticContext; - currentRestriction = Objects.requireNonNull(restriction, "restriction must not be null"); - currentStaticContext = staticContext; - try { - walkSupportedExecutableBlock(body); - } finally { - currentRestriction = previousRestriction; - currentStaticContext = previousStaticContext; - } - } - - private void walkStatements(@NotNull List statements) { - for (var statement : statements) { - astWalker.walk(statement); - } - } - - private void walkNonExecutableContainerStatements(@NotNull List statements) { - var previousDepth = supportedExecutableBlockDepth; - supportedExecutableBlockDepth = 0; - try { - walkStatements(statements); - } finally { - supportedExecutableBlockDepth = previousDepth; - } - } - - private void walkSupportedExecutableBlock(@Nullable Block block) { - if (isNotPublished(block)) { - reportDeferredSubtree(block, FrontendVisibleValueDomain.UNKNOWN_OR_SKIPPED_SUBTREE); - return; - } - supportedExecutableBlockDepth++; - try { - astWalker.walk(block); - } finally { - supportedExecutableBlockDepth--; - } - } - - /// Property initializers share chain/member/call publication with executable expressions, - /// but they must keep class-body traversal sealed everywhere except the initializer subtree. - private void walkPropertyInitializer(@NotNull VariableDeclaration variableDeclaration) { - var initializer = Objects.requireNonNull( - variableDeclaration.value(), - "property initializer value must not be null" - ); - var previousRestriction = currentRestriction; - var previousStaticContext = currentStaticContext; - var previousPropertyInitializerContext = currentPropertyInitializerContext; - currentRestriction = FrontendPropertyInitializerSupport.restrictionFor(variableDeclaration); - currentStaticContext = variableDeclaration.isStatic(); - currentPropertyInitializerContext = FrontendPropertyInitializerSupport.contextFor( - scopesByAst, - variableDeclaration - ); - try { - walkValueExpression(initializer); - } finally { - currentPropertyInitializerContext = previousPropertyInitializerContext; - currentRestriction = previousRestriction; - currentStaticContext = previousStaticContext; - } - } - - private void walkValueExpression(@Nullable Expression expression) { - if (expression == null) { - return; - } - switch (expression) { - case AttributeExpression attributeExpression -> reduceAttributeExpression(attributeExpression); - case LambdaExpression lambdaExpression -> - reportDeferredSubtree(lambdaExpression, FrontendVisibleValueDomain.LAMBDA_SUBTREE); - default -> walkGenericExpressionChildren(expression); - } - } - - private void walkGenericExpressionChildren(@NotNull Node node) { - for (var child : node.getChildren()) { - if (child instanceof Expression childExpression) { - walkValueExpression(childExpression); - continue; - } - walkGenericExpressionChildren(child); - } - } - - /// Attribute-expression reduction is cached so argument typing can request nested chains on - /// demand without double-publishing diagnostics or side-table entries. - private @Nullable FrontendChainReductionHelper.ReductionResult reduceAttributeExpression( - @NotNull AttributeExpression attributeExpression - ) { - var reduced = chainReduction.reduce(attributeExpression); - if (reduced.computedNow() && reduced.result() != null) { - publishReduction(reduced.result()); - } - return reduced.result(); - } - - private void publishReduction(@NotNull FrontendChainReductionHelper.ReductionResult result) { - for (var trace : result.stepTraces()) { - if (trace.suggestedMember() != null) { - resolvedMembers.put(trace.step(), trace.suggestedMember()); - reportMemberTrace(trace); - } - if (trace.suggestedCall() != null) { - resolvedCalls.put(trace.step(), trace.suggestedCall()); - reportCallTrace(trace); - } - } - reportRecoveryBoundary(result); - for (var note : result.notes()) { - diagnosticManager.warning( - CALL_RESOLUTION_CATEGORY, - note.message(), - sourcePath, - FrontendRange.fromAstRange(note.anchor().range()) - ); - } - } - - private void reportMemberTrace(@NotNull FrontendChainReductionHelper.StepTrace trace) { - if (trace.status() != FrontendChainReductionHelper.Status.BLOCKED - && trace.status() != FrontendChainReductionHelper.Status.FAILED) { - return; - } - diagnosticManager.error( - MEMBER_RESOLUTION_CATEGORY, - Objects.requireNonNull(trace.detailReason(), "detailReason must not be null"), - sourcePath, - FrontendRange.fromAstRange(trace.step().range()) - ); - } - - private void reportCallTrace(@NotNull FrontendChainReductionHelper.StepTrace trace) { - if (trace.status() != FrontendChainReductionHelper.Status.BLOCKED - && trace.status() != FrontendChainReductionHelper.Status.FAILED) { - return; - } - diagnosticManager.error( - CALL_RESOLUTION_CATEGORY, - Objects.requireNonNull(trace.detailReason(), "detailReason must not be null"), - sourcePath, - FrontendRange.fromAstRange(trace.step().range()) - ); - } - - private void reportRecoveryBoundary(@NotNull FrontendChainReductionHelper.ReductionResult result) { - var recoveryRoot = result.recoveryRoot(); - if (recoveryRoot == null || result.stepTraces().isEmpty()) { - return; - } - var firstNonResolved = result.stepTraces().stream() - .filter(trace -> trace.status() != FrontendChainReductionHelper.Status.RESOLVED) - .findFirst() - .orElse(null); - if (firstNonResolved == null) { - return; - } - if (firstNonResolved.status() == FrontendChainReductionHelper.Status.DEFERRED) { - if (reportedDeferredRoots.putIfAbsent(recoveryRoot, Boolean.TRUE) != null) { - return; - } - diagnosticManager.warning( - DEFERRED_CHAIN_RESOLUTION_CATEGORY, - Objects.requireNonNull(firstNonResolved.detailReason(), "detailReason must not be null"), - sourcePath, - FrontendRange.fromAstRange(recoveryRoot.range()) - ); - return; - } - if (firstNonResolved.status() == FrontendChainReductionHelper.Status.UNSUPPORTED) { - if (shouldSuppressPropertyInitializerHeadBoundary(recoveryRoot)) { - return; - } - if (reportedUnsupportedRoots.putIfAbsent(recoveryRoot, Boolean.TRUE) != null) { - return; - } - diagnosticManager.error( - UNSUPPORTED_CHAIN_ROUTE_CATEGORY, - Objects.requireNonNull(firstNonResolved.detailReason(), "detailReason must not be null"), - sourcePath, - FrontendRange.fromAstRange(recoveryRoot.range()) - ); - } - } - - private @NotNull FrontendChainReductionHelper.ExpressionTypeResult resolveExpressionType( - @NotNull Expression expression, - boolean finalizeWindow - ) { - return switch (expression) { - case LiteralExpression literalExpression -> bridgeExpressionResolution( - expressionSemanticSupport.resolveLiteralExpressionType(literalExpression) - ); - case SelfExpression selfExpression -> bridgeExpressionResolution( - expressionSemanticSupport.resolveSelfExpressionType(selfExpression) - ); - case IdentifierExpression identifierExpression -> bridgeExpressionResolution( - expressionSemanticSupport.resolveIdentifierExpressionType(identifierExpression) - ); - case AttributeExpression attributeExpression -> resolveAttributeExpressionType(attributeExpression); - case CallExpression callExpression -> bridgeExpressionResolution( - expressionSemanticSupport.resolveCallExpressionType( - callExpression, - this::resolveExpressionDependencyType, - false, - finalizeWindow - ) - ); - case AssignmentExpression assignmentExpression -> bridgeExpressionResolution( - FrontendAssignmentSemanticSupport.resolveAssignmentExpressionType( - assignmentSemanticContext, - assignmentExpression, - FrontendAssignmentSemanticSupport.AssignmentUsage.VALUE_REQUIRED, - this::resolveExpressionDependencyType, - finalizeWindow - ) - ); - case SubscriptExpression subscriptExpression -> bridgeExpressionResolution( - expressionSemanticSupport.resolveSubscriptExpressionType( - subscriptExpression, - this::resolveExpressionDependencyType, - finalizeWindow - ) - ); - case LambdaExpression lambdaExpression -> bridgeExpressionResolution( - expressionSemanticSupport.resolveLambdaExpressionType( - lambdaExpression, - this::resolveExpressionDependencyType, - false, - finalizeWindow - ) - ); - case UnaryExpression unaryExpression -> bridgeExpressionResolution( - expressionSemanticSupport.resolveUnaryExpressionType( - unaryExpression, - this::resolveExpressionDependencyType, - finalizeWindow - ) - ); - case BinaryExpression binaryExpression -> bridgeExpressionResolution( - expressionSemanticSupport.resolveBinaryExpressionType( - binaryExpression, - this::resolveExpressionDependencyType, - finalizeWindow - ) - ); - default -> bridgeExpressionResolution( - expressionSemanticSupport.resolveRemainingExplicitExpressionType( - expression, - this::resolveExpressionDependencyType, - false, - finalizeWindow - ) - ); - }; - } - - private @NotNull FrontendChainReductionHelper.ExpressionTypeResult resolveAttributeExpressionType( - @NotNull AttributeExpression attributeExpression - ) { - var result = reduceAttributeExpression(attributeExpression); - if (result == null) { - return FrontendChainReductionHelper.ExpressionTypeResult.unsupported( - "Nested chain expression is inside an unsupported or skipped subtree" - ); - } - return FrontendChainStatusBridge.toExpressionTypeResult(result); - } - - private void reportDeferredParameterDefaults(@NotNull List parameters) { - for (var parameter : parameters) { - if (parameter.defaultValue() != null) { - reportDeferredSubtree(parameter.defaultValue(), FrontendVisibleValueDomain.PARAMETER_DEFAULT); - } - } - } - - private void reportDeferredSubtree( - @Nullable Node subtreeRoot, - @NotNull FrontendVisibleValueDomain domain - ) { - if (subtreeRoot == null) { - return; - } - if (domain == FrontendVisibleValueDomain.UNKNOWN_OR_SKIPPED_SUBTREE) { - if (reportedDeferredRoots.putIfAbsent(subtreeRoot, Boolean.TRUE) != null) { - return; - } - diagnosticManager.warning( - DEFERRED_CHAIN_RESOLUTION_CATEGORY, - "Chain binding analysis is skipped in " + formatDomain(domain), - sourcePath, - FrontendRange.fromAstRange(subtreeRoot.range()) - ); - return; - } - if (reportedUnsupportedRoots.putIfAbsent(subtreeRoot, Boolean.TRUE) != null) { - return; - } - diagnosticManager.error( - UNSUPPORTED_CHAIN_ROUTE_CATEGORY, - "Chain binding analysis is not supported in " + formatDomain(domain), - sourcePath, - FrontendRange.fromAstRange(subtreeRoot.range()) - ); - } - - private @NotNull String formatDomain(@NotNull FrontendVisibleValueDomain domain) { - return switch (Objects.requireNonNull(domain, "domain must not be null")) { - case EXECUTABLE_BODY -> "executable body"; - case PARAMETER_DEFAULT -> "parameter default"; - case LAMBDA_SUBTREE -> "lambda subtree"; - case BLOCK_LOCAL_CONST_SUBTREE -> "block-local const initializer"; - case FOR_SUBTREE -> "for subtree"; - case MATCH_SUBTREE -> "match subtree"; - case UNKNOWN_OR_SKIPPED_SUBTREE -> "skipped subtree"; - }; - } - - private boolean shouldSuppressPropertyInitializerHeadBoundary(@Nullable Node recoveryRoot) { - return FrontendPropertyInitializerSupport.isTopBindingOwnedUnsupportedHead( - currentPropertyInitializerContext, - recoveryRoot - ); - } - - private boolean isNotPublished(@Nullable Node node) { - return node == null || !scopesByAst.containsKey(node); - } - - private @NotNull FrontendChainReductionHelper.ExpressionTypeResult bridgeExpressionResolution( - @NotNull FrontendExpressionSemanticSupport.ExpressionSemanticResult resolution - ) { - return FrontendChainStatusBridge.toExpressionTypeResult(resolution.expressionType()); - } - - - private @NotNull FrontendExpressionType resolveExpressionDependencyType( - @NotNull Expression expression, - boolean finalizeWindow - ) { - return FrontendChainStatusBridge.toPublishedExpressionType(resolveExpressionType(expression, finalizeWindow)); - } - - public @NotNull ClassRegistry getClassRegistry() { - return classRegistry; - } - - public @NotNull FrontendAnalysisData getAnalysisData() { - return analysisData; - } - } -} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendCompileCheckAnalyzer.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendCompileCheckAnalyzer.java index bf6f074e..02d87adf 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendCompileCheckAnalyzer.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendCompileCheckAnalyzer.java @@ -61,12 +61,8 @@ import org.jetbrains.annotations.Nullable; import java.nio.file.Path; -import java.util.Collections; -import java.util.IdentityHashMap; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; -import java.util.Set; +import java.util.*; +import java.util.function.Predicate; /// Compile-only final frontend gate that runs after the shared semantic pipeline. /// @@ -86,13 +82,13 @@ public class FrontendCompileCheckAnalyzer { /// gate's own hard-stop diagnostic. Those categories stay configurable here so future warning- /// based blockers do not need another dedicated ignore-upstream branch. private static final @NotNull Map NON_CONFLICTING_UPSTREAM_DIAGNOSTIC_CATEGORIES = Map.of( - FrontendVarTypePostAnalyzer.VARIABLE_SLOT_PUBLICATION_CATEGORY, + FrontendBodyOwnerProcedures.VARIABLE_SLOT_PUBLICATION_CATEGORY, "slot-publication warning explains the missing lowering-ready fact and must coexist with compile_check" ); /// Compile mode usually blocks only on upstream `ERROR`s. This set is the narrow exception list /// for already-published non-error diagnostics that still represent a lowering-blocking gap. private static final @NotNull Set NON_ERROR_BLOCKING_DIAGNOSTIC_CATEGORIES = Set.of( - FrontendVarTypePostAnalyzer.VARIABLE_SLOT_PUBLICATION_CATEGORY + FrontendBodyOwnerProcedures.VARIABLE_SLOT_PUBLICATION_CATEGORY ); public void analyze( @@ -103,7 +99,13 @@ public void analyze( Objects.requireNonNull(diagnosticManager, "diagnosticManager must not be null"); var moduleSkeleton = analysisData.moduleSkeleton(); - var publishedDiagnostics = analysisData.diagnostics(); + // Keep the stable boundary precondition even though dedup reads the live manager below. + analysisData.diagnostics(); + // Freeze the live manager at compile-gate entry. This captures the latest interface/body + // upstream diagnostics even if a caller has not yet copied that manager state back into + // `FrontendAnalysisData`, while still preventing diagnostics emitted by this gate from + // suppressing later checks in the same run. + var publishedDiagnostics = diagnosticManager.snapshot(); var scopesByAst = analysisData.scopesByAst(); for (var sourceClassRelation : moduleSkeleton.sourceClassRelations()) { @@ -416,9 +418,17 @@ private void walk(@NotNull SourceFile sourceFile) { return FrontendASTTraversalDirective.SKIP_CHILDREN; } - /// `for` remains outside compile mode until the lowering/backend route is implemented. + /// `for` is shared-semantic supported but remains blocked before CFG/lowering is entered. @Override public @NotNull FrontendASTTraversalDirective handleForStatement(@NotNull ForStatement forStatement) { + if (supportedExecutableBlockDepth <= 0 || isNotPublished(forStatement)) { + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } + reportExplicitCompileBlock( + forStatement, + "For statement is supported by shared semantic analysis but cannot be compiled until its CFG " + + "and lowering route is implemented" + ); return FrontendASTTraversalDirective.SKIP_CHILDREN; } @@ -437,14 +447,14 @@ private void walkCallableBody(@NotNull Node callableOwner, @Nullable Block body) } /// Replay the AST walker over a flat statement list in source order. - private void walkStatements(@NotNull java.util.List statements) { + private void walkStatements(@NotNull List statements) { for (var statement : statements) { astWalker.walk(statement); } } /// Traverse declarations with executable depth pinned to zero so nested bodies must opt in explicitly. - private void walkNonExecutableContainerStatements(@NotNull java.util.List statements) { + private void walkNonExecutableContainerStatements(@NotNull List statements) { var previousDepth = supportedExecutableBlockDepth; supportedExecutableBlockDepth = 0; try { @@ -542,7 +552,10 @@ private void scanExpressionTypeCompileBlocks() { if (!isCompileBlocking(publishedType.status()) || !compileSurfaceNodes.contains(anchor)) { continue; } - if (isAssignmentRootCoveredByExplicitSelfPrefixDiagnostic(anchor, publishedType)) { + if (isAssignmentRootCoveredByExplicitSelfPrefixDiagnostic(anchor)) { + continue; + } + if (isCoveredByStaticSelfBindingDiagnostic(publishedType.detailReason())) { continue; } var compileAnchor = compileAnchorForExpressionType(anchor); @@ -568,6 +581,9 @@ private void scanResolvedMemberCompileBlocks() { if (!isCompileBlocking(publishedMember.status()) || !compileSurfaceNodes.contains(anchor)) { continue; } + if (isCoveredByStaticSelfBindingDiagnostic(publishedMember.detailReason())) { + continue; + } reportCompileBlock( anchor, publishedCompileBlockedMessage( @@ -678,10 +694,7 @@ private void scanSlotTypeCompileBlocks() { /// Assignment root facts can propagate a prefix-owned blocked `self` route. When that exact /// prefix already has the upstream binding diagnostic, keep ownership there instead of adding /// a generic root-level compile blocker for the same cause. - private boolean isAssignmentRootCoveredByExplicitSelfPrefixDiagnostic( - @NotNull Node anchor, - @NotNull FrontendExpressionType publishedType - ) { + private boolean isAssignmentRootCoveredByExplicitSelfPrefixDiagnostic(@NotNull Node anchor) { if (!(anchor instanceof AssignmentExpression assignmentExpression)) { return false; } @@ -689,13 +702,18 @@ private boolean isAssignmentRootCoveredByExplicitSelfPrefixDiagnostic( if (selfExpression == null) { return false; } - var selfType = expressionTypes.get(selfExpression); - if (selfType == null - || selfType.status() != publishedType.status() - || !isCompileBlocking(selfType.status())) { + return hasPublishedConflictingDiagnosticAt(selfExpression); + } + + private boolean isCoveredByStaticSelfBindingDiagnostic(@Nullable String detailReason) { + if (detailReason == null || !detailReason.contains("Keyword 'self' is not available in static context")) { return false; } - return hasPublishedConflictingDiagnosticAt(selfExpression); + return publishedDiagnostics.asList().stream().anyMatch(diagnostic -> + diagnostic.category().equals("sema.binding") + && diagnostic.message().contains("Keyword 'self' is not available in static context") + && (diagnostic.sourcePath() != null && diagnostic.sourcePath().equals(FrontendDiagnostic.sourcePathText(sourcePath))) + ); } private static @Nullable SelfExpression directExplicitSelfAssignmentTargetPrefixOrNull( @@ -823,7 +841,7 @@ private boolean hasPublishedConflictingDiagnosticAt(@NotNull Node anchor) { /// Find one previously published diagnostic that exactly matches the anchor range in the same file. private @Nullable FrontendDiagnostic findPublishedDiagnosticAt( @NotNull Node anchor, - @NotNull java.util.function.Predicate predicate + @NotNull Predicate predicate ) { var anchorRange = FrontendRange.fromAstRange(anchor.range()); return publishedDiagnostics.asList().stream() diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzer.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzer.java deleted file mode 100644 index 7f4ce0d7..00000000 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzer.java +++ /dev/null @@ -1,996 +0,0 @@ -package gd.script.gdcc.frontend.sema.analyzer; - -import gd.script.gdcc.frontend.diagnostic.DiagnosticManager; -import gd.script.gdcc.frontend.diagnostic.FrontendRange; -import gd.script.gdcc.frontend.sema.FrontendAnalysisData; -import gd.script.gdcc.frontend.sema.*; -import gd.script.gdcc.frontend.sema.FrontendDeclaredTypeSupport; -import gd.script.gdcc.frontend.sema.FrontendResolvedCall; -import gd.script.gdcc.frontend.sema.analyzer.support.FrontendPropertyInitializerSupport; -import gd.script.gdcc.frontend.sema.analyzer.support.FrontendAssignmentSemanticSupport; -import gd.script.gdcc.frontend.sema.analyzer.support.FrontendChainReductionFacade; -import gd.script.gdcc.frontend.sema.analyzer.support.FrontendChainReductionHelper; -import gd.script.gdcc.frontend.sema.analyzer.support.FrontendChainStatusBridge; -import gd.script.gdcc.frontend.sema.analyzer.support.FrontendExpressionSemanticSupport; -import gd.script.gdcc.frontend.scope.BlockScope; -import gd.script.gdcc.scope.ClassRegistry; -import gd.script.gdcc.scope.ResolveRestriction; -import gd.script.gdcc.scope.Scope; -import gd.script.gdcc.scope.ScopeOwnerKind; -import gd.script.gdcc.scope.ScopeValue; -import gd.script.gdcc.type.GdCompilerType; -import gd.script.gdcc.type.GdType; -import gd.script.gdcc.type.GdVariantType; -import gd.script.gdcc.type.GdVoidType; -import dev.superice.gdparser.frontend.ast.ASTNodeHandler; -import dev.superice.gdparser.frontend.ast.ASTWalker; -import dev.superice.gdparser.frontend.ast.AssertStatement; -import dev.superice.gdparser.frontend.ast.AssignmentExpression; -import dev.superice.gdparser.frontend.ast.AttributeCallStep; -import dev.superice.gdparser.frontend.ast.AttributeExpression; -import dev.superice.gdparser.frontend.ast.AttributePropertyStep; -import dev.superice.gdparser.frontend.ast.AttributeSubscriptStep; -import dev.superice.gdparser.frontend.ast.BinaryExpression; -import dev.superice.gdparser.frontend.ast.Block; -import dev.superice.gdparser.frontend.ast.CallExpression; -import dev.superice.gdparser.frontend.ast.ClassDeclaration; -import dev.superice.gdparser.frontend.ast.ConstructorDeclaration; -import dev.superice.gdparser.frontend.ast.DeclarationKind; -import dev.superice.gdparser.frontend.ast.ElifClause; -import dev.superice.gdparser.frontend.ast.Expression; -import dev.superice.gdparser.frontend.ast.ExpressionStatement; -import dev.superice.gdparser.frontend.ast.ForStatement; -import dev.superice.gdparser.frontend.ast.FrontendASTTraversalDirective; -import dev.superice.gdparser.frontend.ast.FunctionDeclaration; -import dev.superice.gdparser.frontend.ast.IdentifierExpression; -import dev.superice.gdparser.frontend.ast.IfStatement; -import dev.superice.gdparser.frontend.ast.LambdaExpression; -import dev.superice.gdparser.frontend.ast.LiteralExpression; -import dev.superice.gdparser.frontend.ast.MatchStatement; -import dev.superice.gdparser.frontend.ast.Node; -import dev.superice.gdparser.frontend.ast.ReturnStatement; -import dev.superice.gdparser.frontend.ast.SelfExpression; -import dev.superice.gdparser.frontend.ast.SourceFile; -import dev.superice.gdparser.frontend.ast.Statement; -import dev.superice.gdparser.frontend.ast.SubscriptExpression; -import dev.superice.gdparser.frontend.ast.UnaryExpression; -import dev.superice.gdparser.frontend.ast.VariableDeclaration; -import dev.superice.gdparser.frontend.ast.WhileStatement; -import gd.script.gdcc.gdextension.ExtensionBuiltinClass; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.nio.file.Path; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Objects; - -/// Publishes frontend expression typing facts after chain/member/call results are already visible. -/// -/// The phase rebuilds `expressionTypes()` in place so nested chain reduction can immediately consume -/// freshly published inner expression facts without introducing a second temporary table. -/// `expressionTypes()` is not expression-only: besides ordinary expression roots, this phase also -/// publishes attribute property/call/subscript steps when downstream compile/lowering needs the step -/// itself as the stable fact anchor. Inferred local slot stabilization is owned by the earlier local -/// stabilization phase; the backfill path below exists only for still-unstabilized local `:=` slots. -public class FrontendExprTypeAnalyzer { - private static final @NotNull String EXPRESSION_RESOLUTION_CATEGORY = "sema.expression_resolution"; - private static final @NotNull String DEFERRED_EXPRESSION_RESOLUTION_CATEGORY = - "sema.deferred_expression_resolution"; - private static final @NotNull String UNSUPPORTED_EXPRESSION_ROUTE_CATEGORY = - "sema.unsupported_expression_route"; - private static final @NotNull String DISCARDED_EXPRESSION_CATEGORY = "sema.discarded_expression"; - private static final @NotNull String UNSAFE_CALL_ARGUMENT_CATEGORY = "sema.unsafe_call_argument"; - - public void analyze( - @NotNull ClassRegistry classRegistry, - @NotNull FrontendAnalysisData analysisData, - @NotNull DiagnosticManager diagnosticManager - ) { - Objects.requireNonNull(classRegistry, "classRegistry must not be null"); - Objects.requireNonNull(analysisData, "analysisData must not be null"); - Objects.requireNonNull(diagnosticManager, "diagnosticManager must not be null"); - - var moduleSkeleton = analysisData.moduleSkeleton(); - analysisData.diagnostics(); - - var scopesByAst = analysisData.scopesByAst(); - for (var sourceClassRelation : moduleSkeleton.sourceClassRelations()) { - var sourceFile = sourceClassRelation.unit().ast(); - if (!scopesByAst.containsKey(sourceFile)) { - throw new IllegalStateException( - "Scope graph has not been published for source file: " + sourceClassRelation.unit().path() - ); - } - } - - var expressionTypes = analysisData.expressionTypes(); - expressionTypes.clear(); - for (var sourceClassRelation : moduleSkeleton.sourceClassRelations()) { - new AstWalkerExprTypePublisher( - sourceClassRelation.unit().path(), - classRegistry, - analysisData, - scopesByAst, - expressionTypes, - diagnosticManager - ).walk(sourceClassRelation.unit().ast()); - } - analysisData.updateExpressionTypes(expressionTypes); - } - - private static final class AstWalkerExprTypePublisher implements ASTNodeHandler { - private final @NotNull Path sourcePath; - private final @NotNull ClassRegistry classRegistry; - private final @NotNull FrontendAnalysisData analysisData; - private final @NotNull FrontendAstSideTable scopesByAst; - private final @NotNull FrontendAstSideTable expressionTypes; - private final @NotNull DiagnosticManager diagnosticManager; - private final @NotNull ASTWalker astWalker; - private final @NotNull FrontendChainReductionFacade chainReduction; - private final @NotNull FrontendAssignmentSemanticSupport.Context assignmentSemanticContext; - private final @NotNull FrontendExpressionSemanticSupport expressionSemanticSupport; - private final @NotNull IdentityHashMap parentByNode = new IdentityHashMap<>(); - private final @NotNull IdentityHashMap reportedExpressionRoots = new IdentityHashMap<>(); - private final @NotNull IdentityHashMap reportedDeferredRoots = new IdentityHashMap<>(); - private final @NotNull IdentityHashMap reportedUnsupportedRoots = new IdentityHashMap<>(); - private final @NotNull IdentityHashMap reportedDiscardedRoots = new IdentityHashMap<>(); - private int supportedExecutableBlockDepth; - private @NotNull ResolveRestriction currentRestriction = ResolveRestriction.unrestricted(); - private boolean currentStaticContext; - private @Nullable FrontendPropertyInitializerSupport.PropertyInitializerContext currentPropertyInitializerContext; - - private AstWalkerExprTypePublisher( - @NotNull Path sourcePath, - @NotNull ClassRegistry classRegistry, - @NotNull FrontendAnalysisData analysisData, - @NotNull FrontendAstSideTable scopesByAst, - @NotNull FrontendAstSideTable expressionTypes, - @NotNull DiagnosticManager diagnosticManager - ) { - this.sourcePath = Objects.requireNonNull(sourcePath, "sourcePath must not be null"); - this.classRegistry = Objects.requireNonNull(classRegistry, "classRegistry must not be null"); - this.analysisData = Objects.requireNonNull(analysisData, "analysisData must not be null"); - this.scopesByAst = Objects.requireNonNull(scopesByAst, "scopesByAst must not be null"); - this.expressionTypes = Objects.requireNonNull(expressionTypes, "expressionTypes must not be null"); - this.diagnosticManager = Objects.requireNonNull(diagnosticManager, "diagnosticManager must not be null"); - astWalker = new ASTWalker(this); - chainReduction = new FrontendChainReductionFacade( - analysisData, - scopesByAst, - () -> currentRestriction, - () -> currentStaticContext, - () -> currentPropertyInitializerContext, - classRegistry, - this::resolveExpressionDependency - ); - assignmentSemanticContext = FrontendAssignmentSemanticSupport.createContext( - analysisData.symbolBindings(), - scopesByAst, - analysisData.moduleSkeleton(), - () -> currentRestriction, - classRegistry, - chainReduction - ); - expressionSemanticSupport = new FrontendExpressionSemanticSupport( - analysisData.symbolBindings(), - scopesByAst, - () -> currentRestriction, - () -> currentPropertyInitializerContext, - classRegistry, - chainReduction::headReceiverSupport - ); - } - - private void walk(@NotNull SourceFile sourceFile) { - indexSubtree(Objects.requireNonNull(sourceFile, "sourceFile must not be null"), null); - astWalker.walk(sourceFile); - } - - @Override - public @NotNull FrontendASTTraversalDirective handleNode(@NotNull Node node) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleSourceFile(@NotNull SourceFile sourceFile) { - walkNonExecutableContainerStatements(sourceFile.statements()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleClassDeclaration(@NotNull ClassDeclaration classDeclaration) { - if (isNotPublished(classDeclaration)) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkNonExecutableContainerStatements(classDeclaration.body().statements()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleFunctionDeclaration( - @NotNull FunctionDeclaration functionDeclaration - ) { - if (isNotPublished(functionDeclaration)) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkCallableBody( - functionDeclaration, - functionDeclaration.body(), - functionDeclaration.isStatic() - ? ResolveRestriction.staticContext() - : ResolveRestriction.instanceContext(), - functionDeclaration.isStatic() - ); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleConstructorDeclaration( - @NotNull ConstructorDeclaration constructorDeclaration - ) { - if (isNotPublished(constructorDeclaration)) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkCallableBody( - constructorDeclaration, - constructorDeclaration.body(), - ResolveRestriction.instanceContext(), - false - ); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleBlock(@NotNull Block block) { - if (supportedExecutableBlockDepth <= 0 || isNotPublished(block)) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkStatements(block.statements()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleExpressionStatement( - @NotNull ExpressionStatement expressionStatement - ) { - if (supportedExecutableBlockDepth <= 0) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - var expression = expressionStatement.expression(); - var expressionType = publishStatementExpressionType(expression); - reportDiscardedExpressionWarning(expression, expressionType); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleReturnStatement(@NotNull ReturnStatement returnStatement) { - if (supportedExecutableBlockDepth <= 0 || returnStatement.value() == null) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - publishExpressionType(returnStatement.value()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleAssertStatement(@NotNull AssertStatement assertStatement) { - if (supportedExecutableBlockDepth <= 0) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - publishExpressionType(assertStatement.condition()); - if (assertStatement.message() != null) { - publishExpressionType(assertStatement.message()); - } - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleVariableDeclaration( - @NotNull VariableDeclaration variableDeclaration - ) { - if (supportedExecutableBlockDepth > 0) { - if (variableDeclaration.value() == null) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - if (variableDeclaration.kind() != DeclarationKind.VAR) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - publishExpressionType(variableDeclaration.value()); - backfillInferredLocalType(variableDeclaration); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - if (!FrontendPropertyInitializerSupport.isSupportedPropertyInitializer(scopesByAst, variableDeclaration)) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - publishPropertyInitializerExpressionType(variableDeclaration); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleIfStatement(@NotNull IfStatement ifStatement) { - if (supportedExecutableBlockDepth <= 0 || isNotPublished(ifStatement)) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - publishExpressionType(ifStatement.condition()); - walkSupportedExecutableBlock(ifStatement.body()); - for (var elifClause : ifStatement.elifClauses()) { - astWalker.walk(elifClause); - } - if (ifStatement.elseBody() != null) { - walkSupportedExecutableBlock(ifStatement.elseBody()); - } - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleElifClause(@NotNull ElifClause elifClause) { - if (supportedExecutableBlockDepth <= 0 || isNotPublished(elifClause)) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - publishExpressionType(elifClause.condition()); - walkSupportedExecutableBlock(elifClause.body()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleWhileStatement(@NotNull WhileStatement whileStatement) { - if (supportedExecutableBlockDepth <= 0 || isNotPublished(whileStatement)) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - publishExpressionType(whileStatement.condition()); - walkSupportedExecutableBlock(whileStatement.body()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleForStatement(@NotNull ForStatement forStatement) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleMatchStatement(@NotNull MatchStatement matchStatement) { - if (supportedExecutableBlockDepth > 0) { - publishExpressionType(matchStatement.value()); - } - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleLambdaExpression(@NotNull LambdaExpression lambdaExpression) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - private void walkCallableBody( - @NotNull Node callableOwner, - @Nullable Block body, - @NotNull ResolveRestriction restriction, - boolean staticContext - ) { - if (isNotPublished(callableOwner) || isNotPublished(body)) { - return; - } - var previousRestriction = currentRestriction; - var previousStaticContext = currentStaticContext; - currentRestriction = Objects.requireNonNull(restriction, "restriction must not be null"); - currentStaticContext = staticContext; - try { - walkSupportedExecutableBlock(body); - } finally { - currentRestriction = previousRestriction; - currentStaticContext = previousStaticContext; - } - } - - private void walkStatements(@NotNull List statements) { - for (var statement : statements) { - astWalker.walk(statement); - } - } - - private void walkNonExecutableContainerStatements(@NotNull List statements) { - var previousDepth = supportedExecutableBlockDepth; - supportedExecutableBlockDepth = 0; - try { - walkStatements(statements); - } finally { - supportedExecutableBlockDepth = previousDepth; - } - } - - private void walkSupportedExecutableBlock(@Nullable Block block) { - if (isNotPublished(block)) { - return; - } - supportedExecutableBlockDepth++; - try { - astWalker.walk(block); - } finally { - supportedExecutableBlockDepth--; - } - } - - /// Property initializers reuse ordinary expression typing, but their restriction context comes - /// from the declaring property instead of an executable-body context. - private void publishPropertyInitializerExpressionType(@NotNull VariableDeclaration variableDeclaration) { - var initializer = Objects.requireNonNull( - variableDeclaration.value(), - "property initializer value must not be null" - ); - var previousRestriction = currentRestriction; - var previousStaticContext = currentStaticContext; - var previousPropertyInitializerContext = currentPropertyInitializerContext; - currentRestriction = FrontendPropertyInitializerSupport.restrictionFor(variableDeclaration); - currentStaticContext = variableDeclaration.isStatic(); - currentPropertyInitializerContext = FrontendPropertyInitializerSupport.contextFor( - scopesByAst, - variableDeclaration - ); - try { - publishExpressionType(initializer); - } finally { - currentPropertyInitializerContext = previousPropertyInitializerContext; - currentRestriction = previousRestriction; - currentStaticContext = previousStaticContext; - } - } - - private void indexSubtree(@NotNull Node node, @Nullable Node parent) { - Objects.requireNonNull(node, "node must not be null"); - if (parent != null) { - parentByNode.put(node, parent); - } - for (var child : node.getChildren()) { - indexSubtree(child, node); - } - } - - /// Ensures the ordinary value expression type for `expression` has been published when one - /// should exist in `expressionTypes()`. - /// - /// Callers use this for its side effect only: populate the published side table before a - /// parent expression needs to consume nested facts. Bare `TYPE_META` identifiers are the - /// one intentional exception: they may be computed transiently for the current reduction, - /// but they are not published as ordinary value expression facts because they only serve as - /// static-route heads in the current frontend contract. - private @Nullable FrontendExpressionType publishExpressionType(@Nullable Expression expression) { - return publishExpressionType(expression, false); - } - - private @Nullable FrontendExpressionType publishStatementExpressionType(@Nullable Expression expression) { - return publishExpressionType(expression, true); - } - - private @Nullable FrontendExpressionType publishExpressionType( - @Nullable Expression expression, - boolean allowStatementResult - ) { - if (expression == null) { - return null; - } - var published = expressionTypes.get(expression); - if (published != null) { - if (published.publishedType() instanceof GdCompilerType compilerOnlyType) { - throw new IllegalStateException( - "compiler-only type leaked into frontend expressionTypes(): " - + compilerOnlyType.getTypeName() - ); - } - return published; - } - var computed = resolveExpressionType(expression, allowStatementResult); - publishResolvedExpressionType(expression, computed); - return computed; - } - - private void publishResolvedExpressionType( - @NotNull Expression expression, - @NotNull FrontendExpressionType computed - ) { - if (isRouteHeadOnlyTypeMeta(expression)) { - return; - } - if (computed.publishedType() instanceof GdCompilerType compilerOnlyType) { - throw new IllegalStateException( - "compiler-only type leaked into frontend expressionTypes(): " - + compilerOnlyType.getTypeName() - ); - } - expressionTypes.put(expression, computed); - } - - /// Bare `TYPE_META` identifiers are valid chain heads such as `Worker.build()` but they are - /// not first-class ordinary value expressions. Skipping publication keeps static-route heads - /// out of ordinary `expressionTypes()` consumers and the local `:=` backfill guard. Local - /// stabilization has its own bare-`TYPE_META` initializer guard at the slot-write boundary. - private boolean isRouteHeadOnlyTypeMeta(@NotNull Expression expression) { - if (!(expression instanceof IdentifierExpression identifierExpression)) { - return false; - } - var binding = analysisData.symbolBindings().get(identifierExpression); - if (binding == null || binding.kind() != FrontendBindingKind.TYPE_META) { - return false; - } - var parent = parentByNode.get(identifierExpression); - return parent instanceof AttributeExpression attributeExpression - && attributeExpression.base() == identifierExpression; - } - - /// Supported local `:=` declarations should already have been stabilized before chain - /// binding. This fallback only fills an untouched `Variant` slot when no earlier phase has - /// published a narrower local type, and it must never silently override an already - /// stabilized slot. - /// - /// The check keeps inferred local ownership explicit while preserving initializer provenance - /// through the local use-site's `declarationSite()` plus the initializer expression's own - /// published type and diagnostics. - private void backfillInferredLocalType(@NotNull VariableDeclaration variableDeclaration) { - if (!FrontendDeclaredTypeSupport.isInferredTypeRef(variableDeclaration.type()) - || variableDeclaration.value() == null) { - return; - } - var declarationScope = scopesByAst.get(variableDeclaration); - if (!(declarationScope instanceof BlockScope blockScope)) { - return; - } - var existingLocal = blockScope.resolveValueHere(variableDeclaration.name().trim()); - if (existingLocal == null || existingLocal.declaration() != variableDeclaration) { - return; - } - var publishedInitializerType = expressionTypes.get(variableDeclaration.value()); - if (publishedInitializerType == null) { - return; - } - var backfilledType = switch (publishedInitializerType.status()) { - case RESOLVED, DYNAMIC -> publishedInitializerType.publishedType(); - case BLOCKED, DEFERRED, FAILED, UNSUPPORTED -> null; - }; - if (backfilledType == null || backfilledType instanceof GdVoidType) { - return; - } - if (backfilledType instanceof GdCompilerType compilerOnlyType) { - throw new IllegalStateException( - "compiler-only type leaked into frontend local backfill: " - + compilerOnlyType.getTypeName() - ); - } - if (!(existingLocal.type() instanceof GdVariantType)) { - if (!sameType(existingLocal.type(), backfilledType)) { - throw new IllegalStateException( - "Inferred local slot type changed after stabilization for '" - + variableDeclaration.name().trim() - + "': existing=" - + existingLocal.type().getTypeName() - + ", initializer=" - + backfilledType.getTypeName() - ); - } - return; - } - var localName = variableDeclaration.name().trim(); - blockScope.resetLocalType(localName, variableDeclaration, backfilledType); - var updatedValue = blockScope.resolveValueHere(localName); - if (updatedValue != null) { - refreshPublishedLocalValues(variableDeclaration, updatedValue); - } - } - - /// Mirrors local type stabilization writeback. - /// - /// Downstream expression and receiver analysis consume `FrontendBinding`'s - /// published `resolvedValue` instead of re-running scope lookup. `BlockScope.resetLocalType` - /// replaces the immutable local `ScopeValue`, so this fallback would otherwise leave earlier - /// use-site bindings pointing at the pre-backfill `Variant` slot while the block scope holds - /// the narrowed type. Refreshing by declaration identity keeps the same top-binding choice and - /// only updates that choice's rewritten slot payload. - private void refreshPublishedLocalValues( - @NotNull VariableDeclaration variableDeclaration, - @NotNull ScopeValue updatedValue - ) { - for (var entry : analysisData.symbolBindings().entrySet()) { - var binding = entry.getValue(); - var resolvedValue = binding.resolvedValue(); - if (resolvedValue == null || resolvedValue.declaration() != variableDeclaration) { - continue; - } - entry.setValue(binding.withResolvedValue(updatedValue)); - } - } - - private boolean sameType(@NotNull GdType first, @NotNull GdType second) { - return first.getTypeName().equals(second.getTypeName()); - } - - private @NotNull FrontendExpressionType resolveExpressionType(@NotNull Expression expression) { - return resolveExpressionType(expression, false); - } - - private @NotNull FrontendExpressionType resolveExpressionType( - @NotNull Expression expression, - boolean allowStatementResult - ) { - return switch (expression) { - case LiteralExpression literalExpression -> - expressionSemanticSupport.resolveLiteralExpressionType(literalExpression).expressionType(); - case SelfExpression selfExpression -> - expressionSemanticSupport.resolveSelfExpressionType(selfExpression).expressionType(); - case IdentifierExpression identifierExpression -> - expressionSemanticSupport.resolveIdentifierExpressionType(identifierExpression).expressionType(); - case AttributeExpression attributeExpression -> resolveAttributeExpressionType(attributeExpression); - case AssignmentExpression assignmentExpression -> { - publishAssignmentTargetPrefixTypes(assignmentExpression); - yield finishSemanticResolution( - assignmentExpression, - FrontendAssignmentSemanticSupport.resolveAssignmentExpressionType( - assignmentSemanticContext, - assignmentExpression, - allowStatementResult - ? FrontendAssignmentSemanticSupport.AssignmentUsage.STATEMENT_ROOT - : FrontendAssignmentSemanticSupport.AssignmentUsage.VALUE_REQUIRED, - this::resolveExpressionDependencyType, - false - ) - ); - } - case CallExpression callExpression -> { - var resolution = expressionSemanticSupport.resolveCallExpressionType( - callExpression, - this::resolveExpressionDependencyType, - true, - false - ); - publishBareResolvedCall(callExpression, resolution.publishedCallOrNull()); - yield finishSemanticResolution(callExpression, resolution); - } - case SubscriptExpression subscriptExpression -> finishSemanticResolution( - subscriptExpression, - expressionSemanticSupport.resolveSubscriptExpressionType( - subscriptExpression, - this::resolveExpressionDependencyType, - false - ) - ); - case LambdaExpression lambdaExpression -> finishSemanticResolution( - lambdaExpression, - expressionSemanticSupport.resolveLambdaExpressionType( - lambdaExpression, - this::resolveExpressionDependencyType, - false, - false - ) - ); - case UnaryExpression unaryExpression -> finishSemanticResolution( - unaryExpression, - expressionSemanticSupport.resolveUnaryExpressionType( - unaryExpression, - this::resolveExpressionDependencyType, - false - ) - ); - case BinaryExpression binaryExpression -> finishSemanticResolution( - binaryExpression, - expressionSemanticSupport.resolveBinaryExpressionType( - binaryExpression, - this::resolveExpressionDependencyType, - false - ) - ); - default -> finishSemanticResolution( - expression, - expressionSemanticSupport.resolveRemainingExplicitExpressionType( - expression, - this::resolveExpressionDependencyType, - true, - false - ) - ); - }; - } - - /// Assignment-target resolution uses a writable-target model instead of ordinary expression - /// traversal. Publish the one proven missing prefix fact before that model runs so lowering - /// can consume the same frozen `expressionTypes()` table as ordinary `self.member` reads. - private void publishAssignmentTargetPrefixTypes(@NotNull AssignmentExpression assignmentExpression) { - if (!"=".equals(assignmentExpression.operator())) { - if (assignmentExpression.left() instanceof AttributeExpression attributeExpression) { - // Compound assignment reads the current target before writing it back. Publish - // only the step facts needed by that current-value load; full expression - // publication would also widen the explicit-self assignment prefix surface. - publishAttributeStepExpressionTypes(reduceAttributeExpression(attributeExpression)); - } - return; - } - if (!(assignmentExpression.left() instanceof AttributeExpression( - var base, var steps, var range - ))) { - return; - } - if (steps.size() > 1) { - publishAttributeStepExpressionTypes(reduceAttributeExpression(new AttributeExpression( - base, - List.copyOf(steps.subList(0, steps.size() - 1)), - range - ))); - return; - } - if (steps.size() != 1 || !(steps.getFirst() instanceof AttributePropertyStep)) { - return; - } - if (base instanceof SelfExpression selfExpression) { - publishExpressionType(selfExpression); - } - } - - /// Published final-step facts cover the exact fast path. - /// When the last step is intentionally omitted because an earlier step already turned the - /// suffix into blocked/deferred/dynamic recovery, we rerun local reduction to recover the - /// final expression status without adding another global side table. - private @NotNull FrontendExpressionType resolveAttributeExpressionType( - @NotNull AttributeExpression attributeExpression - ) { - publishExpressionType(attributeExpression.base()); - for (var step : attributeExpression.steps()) { - if (step instanceof AttributeCallStep attributeCallStep) { - for (var argument : attributeCallStep.arguments()) { - publishExpressionType(argument); - } - continue; - } - if (step instanceof AttributeSubscriptStep attributeSubscriptStep) { - for (var argument : attributeSubscriptStep.arguments()) { - publishExpressionType(argument); - } - } - } - - var reduced = reduceAttributeExpression(attributeExpression); - publishAttributeStepExpressionTypes(reduced); - - var finalStep = attributeExpression.steps().getLast(); - if (finalStep instanceof AttributePropertyStep) { - var publishedMember = analysisData.resolvedMembers().get(finalStep); - if (publishedMember != null) { - return FrontendChainStatusBridge.toPublishedExpressionType(publishedMember); - } - } - if (finalStep instanceof AttributeCallStep) { - var publishedCall = analysisData.resolvedCalls().get(finalStep); - if (publishedCall != null) { - return FrontendChainStatusBridge.toPublishedExpressionType(publishedCall); - } - } - - if (reduced == null) { - return FrontendExpressionType.failed( - "No receiver fact is available for attribute expression rooted at " - + attributeExpression.base().getClass().getSimpleName() - ); - } - return FrontendChainStatusBridge.toPublishedExpressionType(reduced); - } - - /// Attribute chain steps are first-class lowering anchors. Publish each step itself into - /// `expressionTypes()` so downstream consumers can read the same semantic outcome without - /// replaying chain reduction or guessing the type of `AttributeSubscriptStep`. - private void publishAttributeStepExpressionTypes( - @Nullable FrontendChainReductionHelper.ReductionResult reduced - ) { - if (reduced == null) { - return; - } - for (var trace : reduced.stepTraces()) { - var publishedType = resolvePublishedAttributeStepType(trace); - var existingType = expressionTypes.putIfAbsent(trace.step(), publishedType); - if (existingType != null) { - throw new IllegalStateException( - "Duplicate expression type published for attribute step " - + trace.step().getClass().getSimpleName() - + ": existing=" - + existingType - + ", attempted=" - + publishedType - ); - } - } - } - - private @NotNull FrontendExpressionType resolvePublishedAttributeStepType( - @NotNull FrontendChainReductionHelper.StepTrace trace - ) { - if (trace.suggestedMember() != null) { - return FrontendChainStatusBridge.toPublishedExpressionType(trace.suggestedMember()); - } - if (trace.suggestedCall() != null) { - return FrontendChainStatusBridge.toPublishedExpressionType(trace.suggestedCall()); - } - if (trace.status() == FrontendChainReductionHelper.Status.BLOCKED - && trace.routeKind() == FrontendChainReductionHelper.RouteKind.UPSTREAM_BLOCKED) { - return FrontendExpressionType.blocked( - null, - Objects.requireNonNull(trace.detailReason(), "detailReason must not be null") - ); - } - return FrontendChainStatusBridge.toPublishedExpressionType(trace.outgoingReceiver()); - } - - private @Nullable FrontendChainReductionHelper.ReductionResult reduceAttributeExpression( - @NotNull AttributeExpression attributeExpression - ) { - return chainReduction.reduce(attributeExpression).result(); - } - - private @NotNull FrontendExpressionType resolveExpressionDependencyType( - @NotNull Expression expression, - boolean finalizeWindow - ) { - return Objects.requireNonNull( - publishExpressionType(expression), - "publishExpressionType must not return null for non-null expressions" - ); - } - - /// Bare `CallExpression` facts are now first-class published call results, so later compile - /// checks, inspection, and lowering can consume the same `resolvedCalls()` surface that - /// attribute calls already use. Duplicate publication on the same AST identity still fails - /// fast because it would indicate two semantic owners racing to define one call route. - private void publishBareResolvedCall( - @NotNull CallExpression callExpression, - @Nullable FrontendResolvedCall publishedCall - ) { - if (publishedCall == null) { - return; - } - var resolvedCalls = analysisData.resolvedCalls(); - if (resolvedCalls.containsKey(callExpression)) { - throw new IllegalStateException( - "resolvedCalls() already contains a published fact for bare CallExpression at " - + callExpression.range() - ); - } - resolvedCalls.put(callExpression, publishedCall); - reportUnsafeCallArgumentWarning(callExpression, publishedCall); - } - - /// The builtin unary-`Variant` constructor shortcut intentionally keeps the call route - /// `RESOLVED`, but it is still runtime-open at the argument boundary. Emit the warning at - /// bare-call publication time so diagnostics stay aligned with the published semantic fact. - private void reportUnsafeCallArgumentWarning( - @NotNull CallExpression callExpression, - @NotNull FrontendResolvedCall publishedCall - ) { - if (!isUnsafeBuiltinVariantConstructorRoute(publishedCall)) { - return; - } - var sourceType = publishedCall.argumentTypes().getFirst(); - var targetType = Objects.requireNonNull(publishedCall.returnType(), "returnType must not be null"); - diagnosticManager.warning( - UNSAFE_CALL_ARGUMENT_CATEGORY, - "Unsafe call argument for builtin constructor '" + publishedCall.callableName() - + "(...)': static argument type '" + sourceType.getTypeName() - + "' requires runtime conversion to '" + targetType.getTypeName() + "'", - sourcePath, - FrontendRange.fromAstRange(callExpression.range()) - ); - } - - private static boolean isUnsafeBuiltinVariantConstructorRoute(@NotNull FrontendResolvedCall publishedCall) { - return publishedCall.status() == FrontendCallResolutionStatus.RESOLVED - && publishedCall.callKind() == FrontendCallResolutionKind.CONSTRUCTOR - && publishedCall.receiverKind() == FrontendReceiverKind.TYPE_META - && publishedCall.ownerKind() == ScopeOwnerKind.BUILTIN - && publishedCall.argumentTypes().size() == 1 - && publishedCall.argumentTypes().getFirst() instanceof GdVariantType - // The dedicated route is owner-anchored instead of pretending one exact builtin - // constructor metadata entry won overload selection. - && publishedCall.declarationSite() instanceof ExtensionBuiltinClass; - } - - private @NotNull FrontendExpressionType finishSemanticResolution( - @NotNull Node root, - @NotNull FrontendExpressionSemanticSupport.ExpressionSemanticResult resolution - ) { - return finishSemanticResolution(root, resolution.expressionType(), resolution.rootOwnsOutcome()); - } - - private @NotNull FrontendExpressionType finishSemanticResolution( - @NotNull Node root, - @NotNull FrontendExpressionType expressionType, - boolean rootOwnsOutcome - ) { - if (rootOwnsOutcome) { - switch (expressionType.status()) { - case FAILED -> reportExpressionError(root, requireDetailReason(expressionType)); - case UNSUPPORTED -> reportUnsupportedExpression(root, requireDetailReason(expressionType)); - case DEFERRED -> reportDeferredExpression(root, requireDetailReason(expressionType)); - case RESOLVED, BLOCKED, DYNAMIC -> { - } - } - } - return expressionType; - } - - private @NotNull FrontendChainReductionHelper.ExpressionTypeResult resolveExpressionDependency( - @NotNull Expression expression, - boolean finalizeWindow - ) { - var published = expressionTypes.get(expression); - if (published != null) { - return FrontendChainStatusBridge.toExpressionTypeResult(published); - } - var computed = resolveExpressionType(expression); - publishResolvedExpressionType(expression, computed); - return FrontendChainStatusBridge.toExpressionTypeResult(computed); - } - - private void reportExpressionError(@NotNull Node root, @NotNull String detailReason) { - if (reportedExpressionRoots.putIfAbsent(root, Boolean.TRUE) != null) { - return; - } - diagnosticManager.error( - EXPRESSION_RESOLUTION_CATEGORY, - Objects.requireNonNull(detailReason, "detailReason must not be null"), - sourcePath, - FrontendRange.fromAstRange(root.range()) - ); - } - - private void reportDeferredExpression(@NotNull Node root, @NotNull String detailReason) { - if (reportedDeferredRoots.putIfAbsent(root, Boolean.TRUE) != null) { - return; - } - diagnosticManager.warning( - DEFERRED_EXPRESSION_RESOLUTION_CATEGORY, - Objects.requireNonNull(detailReason, "detailReason must not be null"), - sourcePath, - FrontendRange.fromAstRange(root.range()) - ); - } - - private void reportUnsupportedExpression(@NotNull Node root, @NotNull String detailReason) { - if (reportedUnsupportedRoots.putIfAbsent(root, Boolean.TRUE) != null) { - return; - } - diagnosticManager.error( - UNSUPPORTED_EXPRESSION_ROUTE_CATEGORY, - Objects.requireNonNull(detailReason, "detailReason must not be null"), - sourcePath, - FrontendRange.fromAstRange(root.range()) - ); - } - - private @NotNull String requireDetailReason(@NotNull FrontendExpressionType expressionType) { - return Objects.requireNonNull(expressionType.detailReason(), "detailReason must not be null"); - } - - private void reportDiscardedExpressionWarning( - @NotNull Expression expression, - @Nullable FrontendExpressionType expressionType - ) { - if (expressionType == null || expressionType.status() != FrontendExpressionTypeStatus.RESOLVED) { - return; - } - // Successful assignments now publish `RESOLVED(void)` and intentionally share the same - // quiet path as ordinary `void` calls. - var publishedType = expressionType.publishedType(); - if (publishedType == null || publishedType instanceof GdVoidType) { - return; - } - if (reportedDiscardedRoots.putIfAbsent(expression, Boolean.TRUE) != null) { - return; - } - diagnosticManager.warning( - DISCARDED_EXPRESSION_CATEGORY, - "Discarded expression result of type '" + publishedType.getTypeName() + "'", - sourcePath, - FrontendRange.fromAstRange(expression.range()) - ); - } - - private boolean isNotPublished(@Nullable Node astNode) { - return astNode == null || !scopesByAst.containsKey(astNode); - } - - public @NotNull ClassRegistry getClassRegistry() { - return classRegistry; - } - } -} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendInterfacePhase.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendInterfacePhase.java new file mode 100644 index 00000000..e51a32fc --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendInterfacePhase.java @@ -0,0 +1,346 @@ +package gd.script.gdcc.frontend.sema.analyzer; + +import dev.superice.gdparser.frontend.ast.ASTNodeHandler; +import dev.superice.gdparser.frontend.ast.ASTWalker; +import dev.superice.gdparser.frontend.ast.Block; +import dev.superice.gdparser.frontend.ast.ClassDeclaration; +import dev.superice.gdparser.frontend.ast.ConstructorDeclaration; +import dev.superice.gdparser.frontend.ast.DeclarationKind; +import dev.superice.gdparser.frontend.ast.ElifClause; +import dev.superice.gdparser.frontend.ast.ForStatement; +import dev.superice.gdparser.frontend.ast.FrontendASTTraversalDirective; +import dev.superice.gdparser.frontend.ast.FunctionDeclaration; +import dev.superice.gdparser.frontend.ast.IfStatement; +import dev.superice.gdparser.frontend.ast.LambdaExpression; +import dev.superice.gdparser.frontend.ast.MatchStatement; +import dev.superice.gdparser.frontend.ast.Node; +import dev.superice.gdparser.frontend.ast.Parameter; +import dev.superice.gdparser.frontend.ast.SourceFile; +import dev.superice.gdparser.frontend.ast.Statement; +import dev.superice.gdparser.frontend.ast.VariableDeclaration; +import dev.superice.gdparser.frontend.ast.WhileStatement; +import gd.script.gdcc.frontend.scope.BlockScope; +import gd.script.gdcc.frontend.scope.CallableScope; +import gd.script.gdcc.frontend.sema.FrontendAnalysisData; +import gd.script.gdcc.frontend.sema.FrontendAstSideTable; +import gd.script.gdcc.frontend.sema.FrontendBodyDeclarationIndex; +import gd.script.gdcc.frontend.sema.FrontendBodyLocalDeclaration; +import gd.script.gdcc.frontend.sema.FrontendExecutableInventorySupport; +import gd.script.gdcc.frontend.sema.FrontendInterfaceSurface; +import gd.script.gdcc.frontend.sema.FrontendSuiteEntryRoots; +import gd.script.gdcc.frontend.sema.FrontendTypedLexicalBaseline; +import gd.script.gdcc.scope.ClassRegistry; +import gd.script.gdcc.scope.Scope; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/// Builds the Interface-layer surface consumed by the future body `SuiteResolver`. +/// +/// This phase is deliberately read-only with respect to stable semantic side tables. It consumes the +/// already-published skeleton, scope graph, and variable inventory, then produces a separate surface +/// describing supported body entries, declaration source order, and typed baseline slots. +public class FrontendInterfacePhase { + public @NotNull FrontendInterfaceSurface analyze( + @NotNull ClassRegistry classRegistry, + @NotNull FrontendAnalysisData analysisData + ) { + Objects.requireNonNull(classRegistry, "classRegistry must not be null"); + Objects.requireNonNull(analysisData, "analysisData must not be null"); + + var moduleSkeleton = analysisData.moduleSkeleton(); + analysisData.diagnostics(); + var builder = new InterfaceSurfaceBuilder(analysisData.scopesByAst()); + for (var relation : moduleSkeleton.sourceClassRelations()) { + builder.walk(relation.unit().path(), relation.unit().ast()); + } + return builder.build(); + } + + private static final class InterfaceSurfaceBuilder implements ASTNodeHandler { + private final @NotNull FrontendAstSideTable scopesByAst; + private final @NotNull ASTWalker astWalker; + private final @NotNull Map> declarationsByBodyRoot = + new IdentityHashMap<>(); + private final @NotNull Map sourcePathsByEntryRoot = new IdentityHashMap<>(); + private final @NotNull FrontendTypedLexicalBaseline.Builder typedBaselineBuilder = + FrontendTypedLexicalBaseline.builder(); + private final @NotNull List callableOwners = new ArrayList<>(); + private final @NotNull List propertyInitializers = new ArrayList<>(); + private final @NotNull List supportedBlocks = new ArrayList<>(); + private @NotNull List currentBodyDeclarations = List.of(); + private @NotNull Path currentSourcePath = Path.of(""); + private int supportedBodyDepth; + + private InterfaceSurfaceBuilder(@NotNull FrontendAstSideTable scopesByAst) { + this.scopesByAst = Objects.requireNonNull(scopesByAst, "scopesByAst"); + astWalker = new ASTWalker(this); + } + + private void walk(@NotNull Path sourcePath, @NotNull SourceFile sourceFile) { + currentSourcePath = Objects.requireNonNull(sourcePath, "sourcePath must not be null"); + astWalker.walk(sourceFile); + } + + private @NotNull FrontendInterfaceSurface build() { + return new FrontendInterfaceSurface( + new FrontendBodyDeclarationIndex(declarationsByBodyRoot), + typedBaselineBuilder.build(), + new FrontendSuiteEntryRoots( + callableOwners, + propertyInitializers, + supportedBlocks, + sourcePathsByEntryRoot + ) + ); + } + + @Override + public @NotNull FrontendASTTraversalDirective handleNode(@NotNull Node node) { + return FrontendASTTraversalDirective.CONTINUE; + } + + @Override + public @NotNull FrontendASTTraversalDirective handleSourceFile(@NotNull SourceFile sourceFile) { + walkStatements(sourceFile.statements()); + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } + + @Override + public @NotNull FrontendASTTraversalDirective handleClassDeclaration(@NotNull ClassDeclaration classDeclaration) { + if (isNotPublished(classDeclaration)) { + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } + walkStatements(classDeclaration.body().statements()); + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } + + @Override + public @NotNull FrontendASTTraversalDirective handleFunctionDeclaration( + @NotNull FunctionDeclaration functionDeclaration + ) { + recordCallable(functionDeclaration, functionDeclaration.parameters(), functionDeclaration.body()); + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } + + @Override + public @NotNull FrontendASTTraversalDirective handleConstructorDeclaration( + @NotNull ConstructorDeclaration constructorDeclaration + ) { + recordCallable(constructorDeclaration, constructorDeclaration.parameters(), constructorDeclaration.body()); + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } + + @Override + public @NotNull FrontendASTTraversalDirective handleLambdaExpression(@NotNull LambdaExpression lambdaExpression) { + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } + + @Override + public @NotNull FrontendASTTraversalDirective handleBlock(@NotNull Block block) { + if (supportedBodyDepth <= 0) { + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } + enterSupportedBlock(block); + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } + + @Override + public @NotNull FrontendASTTraversalDirective handleIfStatement(@NotNull IfStatement ifStatement) { + if (supportedBodyDepth <= 0 || isNotPublished(ifStatement)) { + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } + astWalker.walk(ifStatement.condition()); + enterSupportedBlock(ifStatement.body()); + for (var elifClause : ifStatement.elifClauses()) { + astWalker.walk(elifClause); + } + if (ifStatement.elseBody() != null) { + enterSupportedBlock(ifStatement.elseBody()); + } + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } + + @Override + public @NotNull FrontendASTTraversalDirective handleElifClause(@NotNull ElifClause elifClause) { + if (supportedBodyDepth <= 0 || isNotPublished(elifClause)) { + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } + astWalker.walk(elifClause.condition()); + enterSupportedBlock(elifClause.body()); + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } + + @Override + public @NotNull FrontendASTTraversalDirective handleWhileStatement(@NotNull WhileStatement whileStatement) { + if (supportedBodyDepth <= 0 || isNotPublished(whileStatement)) { + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } + astWalker.walk(whileStatement.condition()); + enterSupportedBlock(whileStatement.body()); + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } + + @Override + public @NotNull FrontendASTTraversalDirective handleForStatement(@NotNull ForStatement forStatement) { + if (supportedBodyDepth <= 0 || isNotPublished(forStatement.body())) { + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } + if (forStatement.iteratorType() != null) { + astWalker.walk(forStatement.iteratorType()); + } + astWalker.walk(forStatement.iterable()); + enterSupportedBlock(forStatement.body(), forStatement); + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } + + @Override + public @NotNull FrontendASTTraversalDirective handleMatchStatement(@NotNull MatchStatement matchStatement) { + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } + + @Override + public @NotNull FrontendASTTraversalDirective handleVariableDeclaration( + @NotNull VariableDeclaration variableDeclaration + ) { + if (supportedBodyDepth <= 0) { + recordPropertyInitializer(variableDeclaration); + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } + if (variableDeclaration.kind() == DeclarationKind.CONST) { + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } + if (variableDeclaration.kind() == DeclarationKind.VAR) { + recordLocalDeclaration(variableDeclaration); + if (variableDeclaration.value() != null) { + astWalker.walk(variableDeclaration.value()); + } + } + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } + + private void recordCallable( + @NotNull Node callableOwner, + @NotNull List parameters, + @NotNull Block body + ) { + if (isNotPublished(callableOwner)) { + return; + } + callableOwners.add(callableOwner); + sourcePathsByEntryRoot.put(callableOwner, currentSourcePath); + recordParameterBaselines(parameters); + enterSupportedBlock(body); + } + + private void recordParameterBaselines(@NotNull List parameters) { + for (var parameter : parameters) { + var scope = scopesByAst.get(parameter); + if (!(scope instanceof CallableScope callableScope)) { + continue; + } + var binding = callableScope.resolveValueHere(parameter.name().trim()); + if (binding != null && binding.declaration() == parameter) { + typedBaselineBuilder.put(parameter, binding.type()); + } + } + } + + private void recordPropertyInitializer(@NotNull VariableDeclaration variableDeclaration) { + if (variableDeclaration.value() == null) { + return; + } + propertyInitializers.add(variableDeclaration); + sourcePathsByEntryRoot.put(variableDeclaration, currentSourcePath); + astWalker.walk(variableDeclaration.value()); + } + + private void enterSupportedBlock(@NotNull Block block) { + enterSupportedBlock(block, null); + } + + /// Opens one supported body inventory list. + /// + /// For a `for` body, the iterator is published first as a synthetic 0th item at `sourceOrder == 0` + /// before walking body statements, so ordinary locals receive contiguous `sourceOrder >= 1`. That + /// shape is part of the Interface surface contract certified by [FrontendBodyStructuralCompleteness]. + private void enterSupportedBlock(@NotNull Block block, @Nullable ForStatement ownerFor) { + var scope = scopesByAst.get(block); + if (!(scope instanceof BlockScope blockScope) + || !FrontendExecutableInventorySupport.isSupportedSuiteBodyRoot(blockScope.kind())) { + return; + } + supportedBlocks.add(block); + sourcePathsByEntryRoot.put(block, currentSourcePath); + var previousDeclarations = currentBodyDeclarations; + var bodyDeclarations = new ArrayList(); + currentBodyDeclarations = bodyDeclarations; + supportedBodyDepth++; + try { + if (ownerFor != null) { + recordIteratorDeclaration(ownerFor, blockScope); + } + walkStatements(block.statements()); + } finally { + supportedBodyDepth--; + declarationsByBodyRoot.put(block, List.copyOf(bodyDeclarations)); + currentBodyDeclarations = previousDeclarations; + } + } + + private void recordLocalDeclaration(@NotNull VariableDeclaration variableDeclaration) { + var scope = scopesByAst.get(variableDeclaration); + if (!(scope instanceof BlockScope blockScope) + || !FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(blockScope.kind())) { + return; + } + var binding = blockScope.resolveValueHere(variableDeclaration.name().trim()); + if (binding == null || binding.declaration() != variableDeclaration) { + return; + } + // When this body is a FOR_BODY, index 0 is already occupied by the iterator entry. + currentBodyDeclarations.add(new FrontendBodyLocalDeclaration( + variableDeclaration, + binding, + FrontendBodyLocalDeclaration.Kind.ORDINARY_VAR, + currentBodyDeclarations.size() + )); + typedBaselineBuilder.put(variableDeclaration, binding.type()); + } + + /// Publishes the sole for-body iterator inventory entry as the synthetic 0th item at list head with + /// `sourceOrder == 0`. + private void recordIteratorDeclaration( + @NotNull ForStatement forStatement, + @NotNull BlockScope blockScope + ) { + var binding = blockScope.resolveValueHere(forStatement.iterator().trim()); + if (binding == null || binding.declaration() != forStatement) { + return; + } + currentBodyDeclarations.add(new FrontendBodyLocalDeclaration( + forStatement, + binding, + FrontendBodyLocalDeclaration.Kind.ITERATOR, + 0 + )); + typedBaselineBuilder.put(forStatement, binding.type()); + } + + private void walkStatements(@NotNull List statements) { + for (var statement : statements) { + astWalker.walk(statement); + } + } + + private boolean isNotPublished(@NotNull Node node) { + return !scopesByAst.containsKey(node); + } + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendLocalTypeStabilizationAnalyzer.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendLocalTypeStabilizationAnalyzer.java deleted file mode 100644 index 66d49fc2..00000000 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendLocalTypeStabilizationAnalyzer.java +++ /dev/null @@ -1,677 +0,0 @@ -package gd.script.gdcc.frontend.sema.analyzer; - -import dev.superice.gdparser.frontend.ast.ASTNodeHandler; -import dev.superice.gdparser.frontend.ast.ASTWalker; -import dev.superice.gdparser.frontend.ast.AssignmentExpression; -import dev.superice.gdparser.frontend.ast.AttributeExpression; -import dev.superice.gdparser.frontend.ast.BinaryExpression; -import dev.superice.gdparser.frontend.ast.Block; -import dev.superice.gdparser.frontend.ast.CallExpression; -import dev.superice.gdparser.frontend.ast.ClassDeclaration; -import dev.superice.gdparser.frontend.ast.ConstructorDeclaration; -import dev.superice.gdparser.frontend.ast.DeclarationKind; -import dev.superice.gdparser.frontend.ast.ElifClause; -import dev.superice.gdparser.frontend.ast.Expression; -import dev.superice.gdparser.frontend.ast.ForStatement; -import dev.superice.gdparser.frontend.ast.FrontendASTTraversalDirective; -import dev.superice.gdparser.frontend.ast.FunctionDeclaration; -import dev.superice.gdparser.frontend.ast.IdentifierExpression; -import dev.superice.gdparser.frontend.ast.IfStatement; -import dev.superice.gdparser.frontend.ast.LambdaExpression; -import dev.superice.gdparser.frontend.ast.LiteralExpression; -import dev.superice.gdparser.frontend.ast.MatchStatement; -import dev.superice.gdparser.frontend.ast.Node; -import dev.superice.gdparser.frontend.ast.SelfExpression; -import dev.superice.gdparser.frontend.ast.SourceFile; -import dev.superice.gdparser.frontend.ast.Statement; -import dev.superice.gdparser.frontend.ast.SubscriptExpression; -import dev.superice.gdparser.frontend.ast.UnaryExpression; -import dev.superice.gdparser.frontend.ast.VariableDeclaration; -import dev.superice.gdparser.frontend.ast.WhileStatement; -import gd.script.gdcc.frontend.diagnostic.DiagnosticManager; -import gd.script.gdcc.frontend.scope.BlockScope; -import gd.script.gdcc.frontend.sema.FrontendAnalysisData; -import gd.script.gdcc.frontend.sema.FrontendAstSideTable; -import gd.script.gdcc.frontend.sema.FrontendBinding; -import gd.script.gdcc.frontend.sema.FrontendBindingKind; -import gd.script.gdcc.frontend.sema.FrontendDeclaredTypeSupport; -import gd.script.gdcc.frontend.sema.FrontendExecutableInventorySupport; -import gd.script.gdcc.frontend.sema.FrontendExpressionType; -import gd.script.gdcc.frontend.sema.FrontendExpressionTypeStatus; -import gd.script.gdcc.frontend.sema.analyzer.support.FrontendAssignmentSemanticSupport; -import gd.script.gdcc.frontend.sema.analyzer.support.FrontendChainReductionFacade; -import gd.script.gdcc.frontend.sema.analyzer.support.FrontendChainReductionHelper; -import gd.script.gdcc.frontend.sema.analyzer.support.FrontendChainStatusBridge; -import gd.script.gdcc.frontend.sema.analyzer.support.FrontendExpressionSemanticSupport; -import gd.script.gdcc.scope.ClassRegistry; -import gd.script.gdcc.scope.ResolveRestriction; -import gd.script.gdcc.scope.Scope; -import gd.script.gdcc.scope.ScopeValue; -import gd.script.gdcc.type.GdCompilerType; -import gd.script.gdcc.type.GdType; -import gd.script.gdcc.type.GdVoidType; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Objects; -import java.util.function.BooleanSupplier; -import java.util.function.Supplier; - -/// Local type stabilization for supported callable executable bodies. -/// -/// The scope is intentionally narrow: -/// - walk supported callable executable bodies -/// - find eligible local `var := initializer` -/// - resolve initializer types through a silent local resolver -/// - explicitly exclude bare `TYPE_META` ordinary-value initializers -/// - explicitly fail-closed for assignment initializers -/// - write back only exact stable local slot types -/// -/// The shared semantic pipeline runs this phase after top binding and before chain binding so member -/// resolution consumes exact receiver slots for source-order `:=` aliases, including parameter-to-local -/// aliases such as `var alias := typed_parameter`. Nested supported blocks keep the same contract: -/// child blocks may read parent locals after they have stabilized, but a child declaration only rewrites -/// its own `BlockScope` slot and never backwrites a parent scope. -/// -/// This phase deliberately does not: -/// - update `resolvedMembers()`, `resolvedCalls()`, `expressionTypes()`, or `slotTypes()` -/// - emit diagnostics -public class FrontendLocalTypeStabilizationAnalyzer { - public void analyze( - @NotNull ClassRegistry classRegistry, - @NotNull FrontendAnalysisData analysisData, - @NotNull DiagnosticManager diagnosticManager - ) { - run(classRegistry, analysisData, diagnosticManager, true); - } - - /// Package-private test helper for observing transient initializer typing. - /// - /// The contract stays intentionally narrow: - /// - run the same walker/resolver path as `analyze(...)` - /// - expose only transient initializer typing results - /// - keep side tables, diagnostics, and scope slots untouched - @NotNull ProbeSnapshot probe( - @NotNull ClassRegistry classRegistry, - @NotNull FrontendAnalysisData analysisData, - @NotNull DiagnosticManager diagnosticManager - ) { - return run(classRegistry, analysisData, diagnosticManager, false); - } - - static @Nullable FrontendExpressionType probeAssignmentOrdinaryValueInitializerFailure( - @NotNull Expression initializer - ) { - return assignmentOrdinaryValueInitializerFailure(initializer); - } - - static void probeStabilizeLocalSlot( - @NotNull BlockScope blockScope, - @NotNull VariableDeclaration variableDeclaration, - @NotNull FrontendExpressionType initializerType - ) { - stabilizeLocalSlot(blockScope, variableDeclaration, initializerType); - } - - private @NotNull ProbeSnapshot run( - @NotNull ClassRegistry classRegistry, - @NotNull FrontendAnalysisData analysisData, - @NotNull DiagnosticManager diagnosticManager, - boolean writeBackStableSlots - ) { - Objects.requireNonNull(classRegistry, "classRegistry must not be null"); - Objects.requireNonNull(analysisData, "analysisData must not be null"); - Objects.requireNonNull(diagnosticManager, "diagnosticManager must not be null"); - - var moduleSkeleton = analysisData.moduleSkeleton(); - var scopesByAst = analysisData.scopesByAst(); - var probes = new ArrayList(); - for (var sourceClassRelation : moduleSkeleton.sourceClassRelations()) { - var sourceFile = sourceClassRelation.unit().ast(); - if (!scopesByAst.containsKey(sourceFile)) { - throw new IllegalStateException( - "Scope graph has not been published for source file: " + sourceClassRelation.unit().path() - ); - } - } - - for (var sourceClassRelation : moduleSkeleton.sourceClassRelations()) { - new AstWalkerLocalTypeStabilizer( - sourceClassRelation.unit().path(), - classRegistry, - analysisData, - scopesByAst, - probes, - writeBackStableSlots - ).walk(sourceClassRelation.unit().ast()); - } - return new ProbeSnapshot(probes); - } - - /// Assignment expressions are statement effects in the current frontend contract, not ordinary - /// value producers. Keep this rejection next to the slot writeback gate so `var x := (a = b)` - /// remains an explicit local-stabilization fallback to the inventory-seeded `Variant`. - private static @Nullable FrontendExpressionType assignmentOrdinaryValueInitializerFailure( - @NotNull Expression initializer - ) { - if (!(initializer instanceof AssignmentExpression)) { - return null; - } - return FrontendExpressionType.failed( - "Assignment initializer cannot stabilize an inferred local because it is not an ordinary value" - ); - } - - private static @Nullable ScopeValue stabilizeLocalSlot( - @NotNull BlockScope blockScope, - @NotNull VariableDeclaration variableDeclaration, - @NotNull FrontendExpressionType initializerType - ) { - var stableType = stableLocalTypeOrNull(initializerType); - if (stableType == null) { - return null; - } - var localName = variableDeclaration.name().trim(); - blockScope.resetLocalType(localName, variableDeclaration, stableType); - return blockScope.resolveValueHere(localName); - } - - private static @Nullable GdType stableLocalTypeOrNull( - @NotNull FrontendExpressionType initializerType - ) { - // Only exact ordinary values can rewrite the inventory-seeded local slot. Failed, - // dynamic, deferred, unsupported, and void initializers keep the existing `Variant`. - if (initializerType.status() != FrontendExpressionTypeStatus.RESOLVED) { - return null; - } - var publishedType = initializerType.publishedType(); - if (publishedType instanceof GdVoidType) { - return null; - } - if (publishedType instanceof GdCompilerType compilerOnlyType) { - throw new IllegalStateException( - "compiler-only type leaked into frontend local stabilization: " - + compilerOnlyType.getTypeName() - ); - } - return publishedType; - } - - record ProbeSnapshot(@NotNull List entries) { - ProbeSnapshot { - entries = List.copyOf(Objects.requireNonNull(entries, "entries must not be null")); - } - - @Nullable ProbeEntry findVariable(@NotNull String variableName) { - Objects.requireNonNull(variableName, "variableName must not be null"); - for (var entry : entries) { - if (entry.variableName().equals(variableName)) { - return entry; - } - } - return null; - } - } - - record ProbeEntry( - @NotNull VariableDeclaration declaration, - @NotNull Expression initializer, - @NotNull FrontendExpressionType initializerType - ) { - ProbeEntry { - Objects.requireNonNull(declaration, "declaration must not be null"); - Objects.requireNonNull(initializer, "initializer must not be null"); - Objects.requireNonNull(initializerType, "initializerType must not be null"); - } - - @NotNull String variableName() { - return declaration.name(); - } - } - - private static final class AstWalkerLocalTypeStabilizer implements ASTNodeHandler { - private final @NotNull FrontendAstSideTable scopesByAst; - private final @NotNull FrontendAstSideTable symbolBindings; - private final @NotNull ASTWalker astWalker; - private final @NotNull SilentExpressionResolver silentExpressionResolver; - private final @NotNull List probes; - private final boolean writeBackStableSlots; - private int supportedExecutableBlockDepth; - private @NotNull ResolveRestriction currentRestriction = ResolveRestriction.unrestricted(); - private boolean currentStaticContext; - - private AstWalkerLocalTypeStabilizer( - @NotNull Path sourcePath, - @NotNull ClassRegistry classRegistry, - @NotNull FrontendAnalysisData analysisData, - @NotNull FrontendAstSideTable scopesByAst, - @NotNull List probes, - boolean writeBackStableSlots - ) { - var checkedAnalysisData = Objects.requireNonNull(analysisData, "analysisData must not be null"); - this.scopesByAst = Objects.requireNonNull(scopesByAst, "scopesByAst must not be null"); - symbolBindings = checkedAnalysisData.symbolBindings(); - this.probes = Objects.requireNonNull(probes, "probes must not be null"); - this.writeBackStableSlots = writeBackStableSlots; - astWalker = new ASTWalker(this); - silentExpressionResolver = new SilentExpressionResolver( - Objects.requireNonNull(sourcePath, "sourcePath must not be null"), - Objects.requireNonNull(classRegistry, "classRegistry must not be null"), - checkedAnalysisData, - scopesByAst, - () -> currentRestriction, - () -> currentStaticContext - ); - } - - private void walk(@NotNull SourceFile sourceFile) { - astWalker.walk(sourceFile); - } - - @Override - public @NotNull FrontendASTTraversalDirective handleNode(@NotNull Node node) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleSourceFile(@NotNull SourceFile sourceFile) { - walkNonExecutableContainerStatements(sourceFile.statements()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleClassDeclaration(@NotNull ClassDeclaration classDeclaration) { - if (isNotPublished(classDeclaration)) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkNonExecutableContainerStatements(classDeclaration.body().statements()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleFunctionDeclaration( - @NotNull FunctionDeclaration functionDeclaration - ) { - if (isNotPublished(functionDeclaration)) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkCallableBody( - functionDeclaration, - functionDeclaration.body(), - functionDeclaration.isStatic() - ? ResolveRestriction.staticContext() - : ResolveRestriction.instanceContext(), - functionDeclaration.isStatic() - ); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleConstructorDeclaration( - @NotNull ConstructorDeclaration constructorDeclaration - ) { - if (isNotPublished(constructorDeclaration)) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkCallableBody( - constructorDeclaration, - constructorDeclaration.body(), - ResolveRestriction.instanceContext(), - false - ); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleBlock(@NotNull Block block) { - if (supportedExecutableBlockDepth <= 0 || isNotPublished(block)) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkStatements(block.statements()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleVariableDeclaration( - @NotNull VariableDeclaration variableDeclaration - ) { - var blockScope = eligibleInferredLocalScope(variableDeclaration); - if (supportedExecutableBlockDepth <= 0 || blockScope == null) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - var initializer = Objects.requireNonNull( - variableDeclaration.value(), - "eligible inferred local initializer must not be null" - ); - var typeMetaFailure = typeMetaOrdinaryValueInitializerFailure(initializer); - if (typeMetaFailure != null) { - probes.add(new ProbeEntry(variableDeclaration, initializer, typeMetaFailure)); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - var assignmentInitializerFailure = assignmentOrdinaryValueInitializerFailure(initializer); - if (assignmentInitializerFailure != null) { - probes.add(new ProbeEntry(variableDeclaration, initializer, assignmentInitializerFailure)); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - var initializerType = silentExpressionResolver.resolveExpressionType(initializer); - probes.add(new ProbeEntry(variableDeclaration, initializer, initializerType)); - if (writeBackStableSlots) { - var updatedValue = stabilizeLocalSlot(blockScope, variableDeclaration, initializerType); - if (updatedValue != null) { - refreshPublishedLocalValues(variableDeclaration, updatedValue); - } - } - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - /// Keep published use-site values aligned with the slot type rewrite while preserving the - /// original top-binding identity by declaration. - private void refreshPublishedLocalValues( - @NotNull VariableDeclaration variableDeclaration, - @NotNull ScopeValue updatedValue - ) { - for (var entry : symbolBindings.entrySet()) { - var binding = entry.getValue(); - var resolvedValue = binding.resolvedValue(); - if (resolvedValue == null || resolvedValue.declaration() != variableDeclaration) { - continue; - } - entry.setValue(binding.withResolvedValue(updatedValue)); - } - } - - @Override - public @NotNull FrontendASTTraversalDirective handleIfStatement(@NotNull IfStatement ifStatement) { - if (supportedExecutableBlockDepth <= 0 || isNotPublished(ifStatement)) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkSupportedExecutableBlock(ifStatement.body()); - for (var elifClause : ifStatement.elifClauses()) { - astWalker.walk(elifClause); - } - if (ifStatement.elseBody() != null) { - walkSupportedExecutableBlock(ifStatement.elseBody()); - } - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleElifClause(@NotNull ElifClause elifClause) { - if (supportedExecutableBlockDepth <= 0 || isNotPublished(elifClause)) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkSupportedExecutableBlock(elifClause.body()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleWhileStatement(@NotNull WhileStatement whileStatement) { - if (supportedExecutableBlockDepth <= 0 || isNotPublished(whileStatement)) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkSupportedExecutableBlock(whileStatement.body()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleForStatement(@NotNull ForStatement forStatement) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleMatchStatement(@NotNull MatchStatement matchStatement) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleLambdaExpression(@NotNull LambdaExpression lambdaExpression) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - private void walkCallableBody( - @NotNull Node callableOwner, - @Nullable Block body, - @NotNull ResolveRestriction restriction, - boolean staticContext - ) { - if (isNotPublished(callableOwner) || isNotPublished(body)) { - return; - } - var previousRestriction = currentRestriction; - var previousStaticContext = currentStaticContext; - currentRestriction = Objects.requireNonNull(restriction, "restriction must not be null"); - currentStaticContext = staticContext; - try { - walkSupportedExecutableBlock(body); - } finally { - currentRestriction = previousRestriction; - currentStaticContext = previousStaticContext; - } - } - - private void walkStatements(@NotNull List statements) { - for (var statement : statements) { - astWalker.walk(statement); - } - } - - private void walkNonExecutableContainerStatements(@NotNull List statements) { - var previousDepth = supportedExecutableBlockDepth; - supportedExecutableBlockDepth = 0; - try { - walkStatements(statements); - } finally { - supportedExecutableBlockDepth = previousDepth; - } - } - - private void walkSupportedExecutableBlock(@Nullable Block block) { - if (isNotPublished(block)) { - return; - } - supportedExecutableBlockDepth++; - try { - astWalker.walk(block); - } finally { - supportedExecutableBlockDepth--; - } - } - - private @Nullable BlockScope eligibleInferredLocalScope(@NotNull VariableDeclaration variableDeclaration) { - if (variableDeclaration.kind() != DeclarationKind.VAR - || variableDeclaration.value() == null - || !FrontendDeclaredTypeSupport.isInferredTypeRef(variableDeclaration.type())) { - return null; - } - var declarationScope = scopesByAst.get(variableDeclaration); - if (!(declarationScope instanceof BlockScope blockScope) - || !FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(blockScope.kind())) { - return null; - } - return blockScope; - } - - /// Bare `TYPE_META` is a route head, not an ordinary runtime value. Keep this guard at the - /// slot-stabilization boundary so `var x := Worker` never depends on the silent resolver's - /// identifier failure to avoid rewriting `x` away from `Variant`. - private @Nullable FrontendExpressionType typeMetaOrdinaryValueInitializerFailure( - @NotNull Expression initializer - ) { - if (!(initializer instanceof IdentifierExpression identifierExpression)) { - return null; - } - var binding = symbolBindings.get(identifierExpression); - if (binding == null || binding.kind() != FrontendBindingKind.TYPE_META) { - return null; - } - return FrontendExpressionType.failed( - "Type-meta initializer '" + identifierExpression.name() - + "' cannot stabilize an inferred local because it is not an ordinary value" - ); - } - - private boolean isNotPublished(@Nullable Node node) { - return node == null || !scopesByAst.containsKey(node); - } - } - - /// Silent local expression resolver used only by the local type stabilization scaffold. - /// - /// It intentionally mirrors the existing body-phase semantic helper stack, but keeps every - /// result transient: - /// - no side-table publication - /// - no diagnostics - /// - no scope mutation - private static final class SilentExpressionResolver { - private final @NotNull IdentityHashMap expressionTypes = - new IdentityHashMap<>(); - private final @NotNull IdentityHashMap finalizedExpressionTypes = - new IdentityHashMap<>(); - private final @NotNull FrontendChainReductionFacade chainReduction; - private final @NotNull FrontendAssignmentSemanticSupport.Context assignmentSemanticContext; - private final @NotNull FrontendExpressionSemanticSupport expressionSemanticSupport; - - private SilentExpressionResolver( - @NotNull Path sourcePath, - @NotNull ClassRegistry classRegistry, - @NotNull FrontendAnalysisData analysisData, - @NotNull FrontendAstSideTable scopesByAst, - @NotNull Supplier restrictionSupplier, - @NotNull BooleanSupplier staticContextSupplier - ) { - Objects.requireNonNull(sourcePath, "sourcePath must not be null"); - Objects.requireNonNull(classRegistry, "classRegistry must not be null"); - Objects.requireNonNull(analysisData, "analysisData must not be null"); - Objects.requireNonNull(scopesByAst, "scopesByAst must not be null"); - Objects.requireNonNull(restrictionSupplier, "restrictionSupplier must not be null"); - Objects.requireNonNull(staticContextSupplier, "staticContextSupplier must not be null"); - - chainReduction = new FrontendChainReductionFacade( - analysisData, - scopesByAst, - restrictionSupplier, - staticContextSupplier, - classRegistry, - this::resolveExpressionDependency - ); - assignmentSemanticContext = FrontendAssignmentSemanticSupport.createContext( - analysisData.symbolBindings(), - scopesByAst, - analysisData.moduleSkeleton(), - restrictionSupplier, - classRegistry, - chainReduction - ); - expressionSemanticSupport = new FrontendExpressionSemanticSupport( - analysisData.symbolBindings(), - scopesByAst, - restrictionSupplier, - classRegistry, - chainReduction::headReceiverSupport - ); - } - - private @NotNull FrontendExpressionType resolveExpressionType(@NotNull Expression expression) { - return resolveExpressionType(expression, false); - } - - private @NotNull FrontendExpressionType resolveExpressionType( - @NotNull Expression expression, - boolean finalizeWindow - ) { - var cache = finalizeWindow ? finalizedExpressionTypes : expressionTypes; - var cached = cache.get(expression); - if (cached != null) { - return cached; - } - var computed = switch (expression) { - case LiteralExpression literalExpression -> - expressionSemanticSupport.resolveLiteralExpressionType(literalExpression).expressionType(); - case SelfExpression selfExpression -> - expressionSemanticSupport.resolveSelfExpressionType(selfExpression).expressionType(); - case IdentifierExpression identifierExpression -> - expressionSemanticSupport.resolveIdentifierExpressionType(identifierExpression).expressionType(); - case AttributeExpression attributeExpression -> resolveAttributeExpressionType(attributeExpression); - case AssignmentExpression assignmentExpression -> - FrontendAssignmentSemanticSupport.resolveAssignmentExpressionType( - assignmentSemanticContext, - assignmentExpression, - FrontendAssignmentSemanticSupport.AssignmentUsage.VALUE_REQUIRED, - this::resolveExpressionDependencyType, - finalizeWindow - ).expressionType(); - case CallExpression callExpression -> expressionSemanticSupport.resolveCallExpressionType( - callExpression, - this::resolveExpressionDependencyType, - true, - finalizeWindow - ).expressionType(); - case SubscriptExpression subscriptExpression -> - expressionSemanticSupport.resolveSubscriptExpressionType( - subscriptExpression, - this::resolveExpressionDependencyType, - finalizeWindow - ).expressionType(); - case LambdaExpression lambdaExpression -> expressionSemanticSupport.resolveLambdaExpressionType( - lambdaExpression, - this::resolveExpressionDependencyType, - false, - finalizeWindow - ).expressionType(); - case UnaryExpression unaryExpression -> expressionSemanticSupport.resolveUnaryExpressionType( - unaryExpression, - this::resolveExpressionDependencyType, - finalizeWindow - ).expressionType(); - case BinaryExpression binaryExpression -> expressionSemanticSupport.resolveBinaryExpressionType( - binaryExpression, - this::resolveExpressionDependencyType, - finalizeWindow - ).expressionType(); - default -> expressionSemanticSupport.resolveRemainingExplicitExpressionType( - expression, - this::resolveExpressionDependencyType, - true, - finalizeWindow - ).expressionType(); - }; - cache.put(expression, computed); - if (finalizeWindow) { - expressionTypes.put(expression, computed); - } - return computed; - } - - private @NotNull FrontendExpressionType resolveAttributeExpressionType( - @NotNull AttributeExpression attributeExpression - ) { - var reduced = chainReduction.reduce(attributeExpression).result(); - if (reduced == null) { - return FrontendExpressionType.unsupported( - "Nested chain expression is inside an unsupported or skipped subtree" - ); - } - return FrontendChainStatusBridge.toPublishedExpressionType(reduced); - } - - private @NotNull FrontendExpressionType resolveExpressionDependencyType( - @NotNull Expression expression, - boolean finalizeWindow - ) { - return resolveExpressionType(expression, finalizeWindow); - } - - private @NotNull FrontendChainReductionHelper.ExpressionTypeResult resolveExpressionDependency( - @NotNull Expression expression, - boolean finalizeWindow - ) { - return FrontendChainStatusBridge.toExpressionTypeResult( - resolveExpressionType(expression, finalizeWindow) - ); - } - } -} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSemanticAnalyzer.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSemanticAnalyzer.java index dd32f6a0..46913077 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSemanticAnalyzer.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSemanticAnalyzer.java @@ -16,11 +16,8 @@ /// - skeleton publication /// - lexical scope graph construction /// - callable-parameter and supported local-variable inventory -/// - top-binding publication for symbol-category resolution -/// - source-order local `:=` slot stabilization -/// - chain member/call publication -/// - expression-type publication -/// - callable-local slot-type publication +/// - interface/body suite publication for statement-local top binding, local slot stabilization, +/// chain binding, expression typing, and callable-local slot finalization /// - annotation-usage validation /// - diagnostics-only engine virtual override validation /// - diagnostics-only type-check traversal @@ -31,45 +28,19 @@ public final class FrontendSemanticAnalyzer { private final @NotNull FrontendClassSkeletonBuilder classSkeletonBuilder; private final @NotNull FrontendScopeAnalyzer scopeAnalyzer; private final @NotNull FrontendVariableAnalyzer variableAnalyzer; - private final @NotNull FrontendTopBindingAnalyzer topBindingAnalyzer; - private final @NotNull FrontendLocalTypeStabilizationAnalyzer localTypeStabilizationAnalyzer; - private final @NotNull FrontendChainBindingAnalyzer chainBindingAnalyzer; - private final @NotNull FrontendExprTypeAnalyzer exprTypeAnalyzer; - private final @NotNull FrontendVarTypePostAnalyzer varTypePostAnalyzer; private final @NotNull FrontendAnnotationUsageAnalyzer annotationUsageAnalyzer; private final @NotNull FrontendVirtualOverrideAnalyzer virtualOverrideAnalyzer; private final @NotNull FrontendTypeCheckAnalyzer typeCheckAnalyzer; private final @NotNull FrontendLoopControlFlowAnalyzer loopControlFlowAnalyzer; private final @NotNull FrontendCompileCheckAnalyzer compileCheckAnalyzer; + private final @NotNull FrontendInterfacePhase interfacePhase; + private final @NotNull FrontendSuiteResolver suiteResolver; public FrontendSemanticAnalyzer() { this( new FrontendClassSkeletonBuilder(), new FrontendScopeAnalyzer(), new FrontendVariableAnalyzer(), - new FrontendTopBindingAnalyzer(), - new FrontendLocalTypeStabilizationAnalyzer(), - new FrontendChainBindingAnalyzer(), - new FrontendExprTypeAnalyzer(), - new FrontendVarTypePostAnalyzer(), - new FrontendAnnotationUsageAnalyzer(), - new FrontendVirtualOverrideAnalyzer(), - new FrontendTypeCheckAnalyzer(), - new FrontendLoopControlFlowAnalyzer(), - new FrontendCompileCheckAnalyzer() - ); - } - - public FrontendSemanticAnalyzer(@NotNull FrontendClassSkeletonBuilder classSkeletonBuilder) { - this( - classSkeletonBuilder, - new FrontendScopeAnalyzer(), - new FrontendVariableAnalyzer(), - new FrontendTopBindingAnalyzer(), - new FrontendLocalTypeStabilizationAnalyzer(), - new FrontendChainBindingAnalyzer(), - new FrontendExprTypeAnalyzer(), - new FrontendVarTypePostAnalyzer(), new FrontendAnnotationUsageAnalyzer(), new FrontendVirtualOverrideAnalyzer(), new FrontendTypeCheckAnalyzer(), @@ -79,63 +50,28 @@ public FrontendSemanticAnalyzer(@NotNull FrontendClassSkeletonBuilder classSkele } public FrontendSemanticAnalyzer( - @NotNull FrontendClassSkeletonBuilder classSkeletonBuilder, - @NotNull FrontendScopeAnalyzer scopeAnalyzer + @NotNull FrontendInterfacePhase interfacePhase, + @NotNull FrontendSuiteResolver suiteResolver ) { this( - classSkeletonBuilder, - scopeAnalyzer, + new FrontendClassSkeletonBuilder(), + new FrontendScopeAnalyzer(), new FrontendVariableAnalyzer(), - new FrontendTopBindingAnalyzer(), - new FrontendLocalTypeStabilizationAnalyzer(), - new FrontendChainBindingAnalyzer(), - new FrontendExprTypeAnalyzer(), - new FrontendVarTypePostAnalyzer(), new FrontendAnnotationUsageAnalyzer(), new FrontendVirtualOverrideAnalyzer(), new FrontendTypeCheckAnalyzer(), new FrontendLoopControlFlowAnalyzer(), - new FrontendCompileCheckAnalyzer() + new FrontendCompileCheckAnalyzer(), + interfacePhase, + suiteResolver ); } - public FrontendSemanticAnalyzer( - @NotNull FrontendClassSkeletonBuilder classSkeletonBuilder, - @NotNull FrontendScopeAnalyzer scopeAnalyzer, - @NotNull FrontendVariableAnalyzer variableAnalyzer - ) { - this( - classSkeletonBuilder, - scopeAnalyzer, - variableAnalyzer, - new FrontendTopBindingAnalyzer(), - new FrontendLocalTypeStabilizationAnalyzer(), - new FrontendChainBindingAnalyzer(), - new FrontendExprTypeAnalyzer(), - new FrontendVarTypePostAnalyzer(), - new FrontendAnnotationUsageAnalyzer(), - new FrontendVirtualOverrideAnalyzer(), - new FrontendTypeCheckAnalyzer(), - new FrontendLoopControlFlowAnalyzer(), - new FrontendCompileCheckAnalyzer() - ); - } - - public FrontendSemanticAnalyzer( - @NotNull FrontendClassSkeletonBuilder classSkeletonBuilder, - @NotNull FrontendScopeAnalyzer scopeAnalyzer, - @NotNull FrontendVariableAnalyzer variableAnalyzer, - @NotNull FrontendTopBindingAnalyzer topBindingAnalyzer - ) { + public FrontendSemanticAnalyzer(@NotNull FrontendClassSkeletonBuilder classSkeletonBuilder) { this( classSkeletonBuilder, - scopeAnalyzer, - variableAnalyzer, - topBindingAnalyzer, - new FrontendLocalTypeStabilizationAnalyzer(), - new FrontendChainBindingAnalyzer(), - new FrontendExprTypeAnalyzer(), - new FrontendVarTypePostAnalyzer(), + new FrontendScopeAnalyzer(), + new FrontendVariableAnalyzer(), new FrontendAnnotationUsageAnalyzer(), new FrontendVirtualOverrideAnalyzer(), new FrontendTypeCheckAnalyzer(), @@ -146,20 +82,12 @@ public FrontendSemanticAnalyzer( public FrontendSemanticAnalyzer( @NotNull FrontendClassSkeletonBuilder classSkeletonBuilder, - @NotNull FrontendScopeAnalyzer scopeAnalyzer, - @NotNull FrontendVariableAnalyzer variableAnalyzer, - @NotNull FrontendTopBindingAnalyzer topBindingAnalyzer, - @NotNull FrontendChainBindingAnalyzer chainBindingAnalyzer + @NotNull FrontendScopeAnalyzer scopeAnalyzer ) { this( classSkeletonBuilder, scopeAnalyzer, - variableAnalyzer, - topBindingAnalyzer, - new FrontendLocalTypeStabilizationAnalyzer(), - chainBindingAnalyzer, - new FrontendExprTypeAnalyzer(), - new FrontendVarTypePostAnalyzer(), + new FrontendVariableAnalyzer(), new FrontendAnnotationUsageAnalyzer(), new FrontendVirtualOverrideAnalyzer(), new FrontendTypeCheckAnalyzer(), @@ -171,20 +99,12 @@ public FrontendSemanticAnalyzer( public FrontendSemanticAnalyzer( @NotNull FrontendClassSkeletonBuilder classSkeletonBuilder, @NotNull FrontendScopeAnalyzer scopeAnalyzer, - @NotNull FrontendVariableAnalyzer variableAnalyzer, - @NotNull FrontendTopBindingAnalyzer topBindingAnalyzer, - @NotNull FrontendChainBindingAnalyzer chainBindingAnalyzer, - @NotNull FrontendExprTypeAnalyzer exprTypeAnalyzer + @NotNull FrontendVariableAnalyzer variableAnalyzer ) { this( classSkeletonBuilder, scopeAnalyzer, variableAnalyzer, - topBindingAnalyzer, - new FrontendLocalTypeStabilizationAnalyzer(), - chainBindingAnalyzer, - exprTypeAnalyzer, - new FrontendVarTypePostAnalyzer(), new FrontendAnnotationUsageAnalyzer(), new FrontendVirtualOverrideAnalyzer(), new FrontendTypeCheckAnalyzer(), @@ -193,182 +113,12 @@ public FrontendSemanticAnalyzer( ); } + /// Creates a testable active-phase pipeline while keeping body-fact publication owned solely + /// by the default interface/body resolver pair. public FrontendSemanticAnalyzer( @NotNull FrontendClassSkeletonBuilder classSkeletonBuilder, @NotNull FrontendScopeAnalyzer scopeAnalyzer, @NotNull FrontendVariableAnalyzer variableAnalyzer, - @NotNull FrontendTopBindingAnalyzer topBindingAnalyzer, - @NotNull FrontendChainBindingAnalyzer chainBindingAnalyzer, - @NotNull FrontendExprTypeAnalyzer exprTypeAnalyzer, - @NotNull FrontendTypeCheckAnalyzer typeCheckAnalyzer - ) { - this( - classSkeletonBuilder, - scopeAnalyzer, - variableAnalyzer, - topBindingAnalyzer, - new FrontendLocalTypeStabilizationAnalyzer(), - chainBindingAnalyzer, - exprTypeAnalyzer, - new FrontendVarTypePostAnalyzer(), - new FrontendAnnotationUsageAnalyzer(), - new FrontendVirtualOverrideAnalyzer(), - typeCheckAnalyzer, - new FrontendLoopControlFlowAnalyzer(), - new FrontendCompileCheckAnalyzer() - ); - } - - public FrontendSemanticAnalyzer( - @NotNull FrontendClassSkeletonBuilder classSkeletonBuilder, - @NotNull FrontendScopeAnalyzer scopeAnalyzer, - @NotNull FrontendVariableAnalyzer variableAnalyzer, - @NotNull FrontendTopBindingAnalyzer topBindingAnalyzer, - @NotNull FrontendChainBindingAnalyzer chainBindingAnalyzer, - @NotNull FrontendExprTypeAnalyzer exprTypeAnalyzer, - @NotNull FrontendAnnotationUsageAnalyzer annotationUsageAnalyzer, - @NotNull FrontendTypeCheckAnalyzer typeCheckAnalyzer - ) { - this( - classSkeletonBuilder, - scopeAnalyzer, - variableAnalyzer, - topBindingAnalyzer, - new FrontendLocalTypeStabilizationAnalyzer(), - chainBindingAnalyzer, - exprTypeAnalyzer, - new FrontendVarTypePostAnalyzer(), - annotationUsageAnalyzer, - new FrontendVirtualOverrideAnalyzer(), - typeCheckAnalyzer, - new FrontendLoopControlFlowAnalyzer(), - new FrontendCompileCheckAnalyzer() - ); - } - - public FrontendSemanticAnalyzer( - @NotNull FrontendClassSkeletonBuilder classSkeletonBuilder, - @NotNull FrontendScopeAnalyzer scopeAnalyzer, - @NotNull FrontendVariableAnalyzer variableAnalyzer, - @NotNull FrontendTopBindingAnalyzer topBindingAnalyzer, - @NotNull FrontendChainBindingAnalyzer chainBindingAnalyzer, - @NotNull FrontendExprTypeAnalyzer exprTypeAnalyzer, - @NotNull FrontendAnnotationUsageAnalyzer annotationUsageAnalyzer, - @NotNull FrontendTypeCheckAnalyzer typeCheckAnalyzer, - @NotNull FrontendLoopControlFlowAnalyzer loopControlFlowAnalyzer - ) { - this( - classSkeletonBuilder, - scopeAnalyzer, - variableAnalyzer, - topBindingAnalyzer, - new FrontendLocalTypeStabilizationAnalyzer(), - chainBindingAnalyzer, - exprTypeAnalyzer, - new FrontendVarTypePostAnalyzer(), - annotationUsageAnalyzer, - new FrontendVirtualOverrideAnalyzer(), - typeCheckAnalyzer, - loopControlFlowAnalyzer, - new FrontendCompileCheckAnalyzer() - ); - } - - public FrontendSemanticAnalyzer( - @NotNull FrontendClassSkeletonBuilder classSkeletonBuilder, - @NotNull FrontendScopeAnalyzer scopeAnalyzer, - @NotNull FrontendVariableAnalyzer variableAnalyzer, - @NotNull FrontendTopBindingAnalyzer topBindingAnalyzer, - @NotNull FrontendChainBindingAnalyzer chainBindingAnalyzer, - @NotNull FrontendExprTypeAnalyzer exprTypeAnalyzer, - @NotNull FrontendAnnotationUsageAnalyzer annotationUsageAnalyzer, - @NotNull FrontendTypeCheckAnalyzer typeCheckAnalyzer, - @NotNull FrontendCompileCheckAnalyzer compileCheckAnalyzer - ) { - this( - classSkeletonBuilder, - scopeAnalyzer, - variableAnalyzer, - topBindingAnalyzer, - new FrontendLocalTypeStabilizationAnalyzer(), - chainBindingAnalyzer, - exprTypeAnalyzer, - new FrontendVarTypePostAnalyzer(), - annotationUsageAnalyzer, - new FrontendVirtualOverrideAnalyzer(), - typeCheckAnalyzer, - new FrontendLoopControlFlowAnalyzer(), - compileCheckAnalyzer - ); - } - - public FrontendSemanticAnalyzer( - @NotNull FrontendClassSkeletonBuilder classSkeletonBuilder, - @NotNull FrontendScopeAnalyzer scopeAnalyzer, - @NotNull FrontendVariableAnalyzer variableAnalyzer, - @NotNull FrontendTopBindingAnalyzer topBindingAnalyzer, - @NotNull FrontendChainBindingAnalyzer chainBindingAnalyzer, - @NotNull FrontendExprTypeAnalyzer exprTypeAnalyzer, - @NotNull FrontendAnnotationUsageAnalyzer annotationUsageAnalyzer, - @NotNull FrontendTypeCheckAnalyzer typeCheckAnalyzer, - @NotNull FrontendLoopControlFlowAnalyzer loopControlFlowAnalyzer, - @NotNull FrontendCompileCheckAnalyzer compileCheckAnalyzer - ) { - this( - classSkeletonBuilder, - scopeAnalyzer, - variableAnalyzer, - topBindingAnalyzer, - new FrontendLocalTypeStabilizationAnalyzer(), - chainBindingAnalyzer, - exprTypeAnalyzer, - new FrontendVarTypePostAnalyzer(), - annotationUsageAnalyzer, - new FrontendVirtualOverrideAnalyzer(), - typeCheckAnalyzer, - loopControlFlowAnalyzer, - compileCheckAnalyzer - ); - } - - public FrontendSemanticAnalyzer( - @NotNull FrontendClassSkeletonBuilder classSkeletonBuilder, - @NotNull FrontendScopeAnalyzer scopeAnalyzer, - @NotNull FrontendVariableAnalyzer variableAnalyzer, - @NotNull FrontendTopBindingAnalyzer topBindingAnalyzer, - @NotNull FrontendLocalTypeStabilizationAnalyzer localTypeStabilizationAnalyzer, - @NotNull FrontendChainBindingAnalyzer chainBindingAnalyzer, - @NotNull FrontendExprTypeAnalyzer exprTypeAnalyzer, - @NotNull FrontendAnnotationUsageAnalyzer annotationUsageAnalyzer, - @NotNull FrontendTypeCheckAnalyzer typeCheckAnalyzer, - @NotNull FrontendLoopControlFlowAnalyzer loopControlFlowAnalyzer, - @NotNull FrontendCompileCheckAnalyzer compileCheckAnalyzer - ) { - this( - classSkeletonBuilder, - scopeAnalyzer, - variableAnalyzer, - topBindingAnalyzer, - localTypeStabilizationAnalyzer, - chainBindingAnalyzer, - exprTypeAnalyzer, - new FrontendVarTypePostAnalyzer(), - annotationUsageAnalyzer, - new FrontendVirtualOverrideAnalyzer(), - typeCheckAnalyzer, - loopControlFlowAnalyzer, - compileCheckAnalyzer - ); - } - - public FrontendSemanticAnalyzer( - @NotNull FrontendClassSkeletonBuilder classSkeletonBuilder, - @NotNull FrontendScopeAnalyzer scopeAnalyzer, - @NotNull FrontendVariableAnalyzer variableAnalyzer, - @NotNull FrontendTopBindingAnalyzer topBindingAnalyzer, - @NotNull FrontendChainBindingAnalyzer chainBindingAnalyzer, - @NotNull FrontendExprTypeAnalyzer exprTypeAnalyzer, - @NotNull FrontendVarTypePostAnalyzer varTypePostAnalyzer, @NotNull FrontendAnnotationUsageAnalyzer annotationUsageAnalyzer, @NotNull FrontendVirtualOverrideAnalyzer virtualOverrideAnalyzer, @NotNull FrontendTypeCheckAnalyzer typeCheckAnalyzer, @@ -379,48 +129,31 @@ public FrontendSemanticAnalyzer( classSkeletonBuilder, scopeAnalyzer, variableAnalyzer, - topBindingAnalyzer, - new FrontendLocalTypeStabilizationAnalyzer(), - chainBindingAnalyzer, - exprTypeAnalyzer, - varTypePostAnalyzer, annotationUsageAnalyzer, virtualOverrideAnalyzer, typeCheckAnalyzer, loopControlFlowAnalyzer, - compileCheckAnalyzer + compileCheckAnalyzer, + new FrontendInterfacePhase(), + new FrontendSuiteResolver() ); } - public FrontendSemanticAnalyzer( + private FrontendSemanticAnalyzer( @NotNull FrontendClassSkeletonBuilder classSkeletonBuilder, @NotNull FrontendScopeAnalyzer scopeAnalyzer, @NotNull FrontendVariableAnalyzer variableAnalyzer, - @NotNull FrontendTopBindingAnalyzer topBindingAnalyzer, - @NotNull FrontendLocalTypeStabilizationAnalyzer localTypeStabilizationAnalyzer, - @NotNull FrontendChainBindingAnalyzer chainBindingAnalyzer, - @NotNull FrontendExprTypeAnalyzer exprTypeAnalyzer, - @NotNull FrontendVarTypePostAnalyzer varTypePostAnalyzer, @NotNull FrontendAnnotationUsageAnalyzer annotationUsageAnalyzer, @NotNull FrontendVirtualOverrideAnalyzer virtualOverrideAnalyzer, @NotNull FrontendTypeCheckAnalyzer typeCheckAnalyzer, @NotNull FrontendLoopControlFlowAnalyzer loopControlFlowAnalyzer, - @NotNull FrontendCompileCheckAnalyzer compileCheckAnalyzer + @NotNull FrontendCompileCheckAnalyzer compileCheckAnalyzer, + @NotNull FrontendInterfacePhase interfacePhase, + @NotNull FrontendSuiteResolver suiteResolver ) { this.classSkeletonBuilder = Objects.requireNonNull(classSkeletonBuilder, "classSkeletonBuilder must not be null"); this.scopeAnalyzer = Objects.requireNonNull(scopeAnalyzer, "scopeAnalyzer must not be null"); this.variableAnalyzer = Objects.requireNonNull(variableAnalyzer, "variableAnalyzer must not be null"); - this.topBindingAnalyzer = Objects.requireNonNull(topBindingAnalyzer, "topBindingAnalyzer must not be null"); - this.localTypeStabilizationAnalyzer = Objects.requireNonNull( - localTypeStabilizationAnalyzer, - "localTypeStabilizationAnalyzer must not be null" - ); - this.chainBindingAnalyzer = Objects.requireNonNull(chainBindingAnalyzer, "chainBindingAnalyzer must not be null"); - this.exprTypeAnalyzer = Objects.requireNonNull(exprTypeAnalyzer, "exprTypeAnalyzer must not be null"); - this.varTypePostAnalyzer = Objects.requireNonNull( - varTypePostAnalyzer, - "varTypePostAnalyzer must not be null" - ); this.annotationUsageAnalyzer = Objects.requireNonNull( annotationUsageAnalyzer, "annotationUsageAnalyzer must not be null" @@ -435,6 +168,8 @@ public FrontendSemanticAnalyzer( "loopControlFlowAnalyzer must not be null" ); this.compileCheckAnalyzer = Objects.requireNonNull(compileCheckAnalyzer, "compileCheckAnalyzer must not be null"); + this.interfacePhase = Objects.requireNonNull(interfacePhase, "interfacePhase must not be null"); + this.suiteResolver = Objects.requireNonNull(suiteResolver, "suiteResolver must not be null"); } /// Runs the current frontend analyzer framework against one module using a shared @@ -451,6 +186,14 @@ public FrontendSemanticAnalyzer( @NotNull FrontendModule module, @NotNull ClassRegistry classRegistry, @NotNull DiagnosticManager diagnosticManager + ) { + return analyzeShared(module, classRegistry, diagnosticManager); + } + + private @NotNull FrontendAnalysisData analyzeShared( + @NotNull FrontendModule module, + @NotNull ClassRegistry classRegistry, + @NotNull DiagnosticManager diagnosticManager ) { Objects.requireNonNull(module, "module must not be null"); Objects.requireNonNull(classRegistry, "classRegistry must not be null"); @@ -481,32 +224,11 @@ public FrontendSemanticAnalyzer( variableAnalyzer.analyze(analysisData, diagnosticManager); analysisData.updateDiagnostics(diagnosticManager.snapshot()); - // Top-binding analysis classifies supported use-sites into stable symbol categories while - // still keeping member/call resolution out of scope. Keeping it separate from variable - // analysis preserves a clean hand-off between declaration inventory and use-site binding. - topBindingAnalyzer.analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - - // Local type stabilization updates only block-local `:=` slot types. It runs after use-site - // symbols are classified and before chain binding consumes receiver slots. - localTypeStabilizationAnalyzer.analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - - // Chain-binding analysis consumes published symbol/scope facts plus stabilized local slots, - // then emits the first stable member/call side tables without opening whole-module - // expression typing yet. - chainBindingAnalyzer.analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - - // Expression typing consumes the published symbol/member/call facts and releases expression - // facts without taking primary ownership of inferred local slot stabilization. - exprTypeAnalyzer.analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - - // Lowering is only allowed to consume published facts, so the final callable-local slot - // types are republished here after local stabilization and expression typing have settled - // the lexical inventory state. - varTypePostAnalyzer.analyze(analysisData, diagnosticManager); + // Interface analysis freezes callable/property entry roots before the body owner + // publication path runs. Shared facts can only enter stable storage through + // SuiteResolver's per-owner export transaction. + var interfaceSurface = interfacePhase.analyze(classRegistry, analysisData); + suiteResolver.resolve(interfaceSurface, classRegistry, analysisData, diagnosticManager); analysisData.updateDiagnostics(diagnosticManager.snapshot()); // Annotation-usage validation consumes retained annotations plus the published class/scope diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendStatementResolver.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendStatementResolver.java new file mode 100644 index 00000000..f9a5a0c0 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendStatementResolver.java @@ -0,0 +1,173 @@ +package gd.script.gdcc.frontend.sema.analyzer; + +import dev.superice.gdparser.frontend.ast.AssertStatement; +import dev.superice.gdparser.frontend.ast.Block; +import dev.superice.gdparser.frontend.ast.BreakStatement; +import dev.superice.gdparser.frontend.ast.ContinueStatement; +import dev.superice.gdparser.frontend.ast.DeclarationKind; +import dev.superice.gdparser.frontend.ast.ElifClause; +import dev.superice.gdparser.frontend.ast.ExpressionStatement; +import dev.superice.gdparser.frontend.ast.ForStatement; +import dev.superice.gdparser.frontend.ast.IfStatement; +import dev.superice.gdparser.frontend.ast.MatchStatement; +import dev.superice.gdparser.frontend.ast.Node; +import dev.superice.gdparser.frontend.ast.PassStatement; +import dev.superice.gdparser.frontend.ast.ReturnStatement; +import dev.superice.gdparser.frontend.ast.Statement; +import dev.superice.gdparser.frontend.ast.VariableDeclaration; +import dev.superice.gdparser.frontend.ast.WhileStatement; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +/// Root-bounded statement dispatcher for the body SuiteResolver. +/// +/// Production wiring reaches this dispatcher through `FrontendSuiteResolver`, which supplies real +/// owner procedures and publishes facts through the typed lexical environment. Tests may inject +/// custom owner procedures for traversal recording. +public class FrontendStatementResolver { + private final @NotNull OwnerProcedures ownerProcedures; + + public FrontendStatementResolver(@NotNull OwnerProcedures ownerProcedures) { + this.ownerProcedures = Objects.requireNonNull(ownerProcedures, "ownerProcedures must not be null"); + } + + public void resolvePropertyInitializer( + @NotNull FrontendSuiteContext context, + @NotNull VariableDeclaration propertyInitializer + ) { + resolveSupportedRoot(context, propertyInitializer); + } + + public void resolveStatement( + @NotNull FrontendSuiteContext context, + @NotNull Statement statement, + @NotNull ChildSuiteResolver childSuiteResolver + ) { + Objects.requireNonNull(context, "context must not be null"); + Objects.requireNonNull(statement, "statement must not be null"); + Objects.requireNonNull(childSuiteResolver, "childSuiteResolver must not be null"); + + switch (statement) { + case VariableDeclaration variableDeclaration -> resolveVariableDeclaration(context, variableDeclaration); + case ExpressionStatement expressionStatement -> resolveSupportedRoot(context, expressionStatement); + case ReturnStatement returnStatement -> resolveSupportedRoot(context, returnStatement); + case AssertStatement assertStatement -> resolveSupportedRoot(context, assertStatement); + case IfStatement ifStatement -> resolveIfStatement(context, ifStatement, childSuiteResolver); + case WhileStatement whileStatement -> resolveWhileStatement(context, whileStatement, childSuiteResolver); + case ForStatement forStatement -> resolveForStatement(context, forStatement, childSuiteResolver); + case MatchStatement matchStatement -> resolveUnsupportedRoot(context, matchStatement); + case PassStatement _, BreakStatement _, ContinueStatement _ -> flushStatementBoundary(context); + default -> resolveUnsupportedRoot(context, statement); + } + } + + private void resolveVariableDeclaration( + @NotNull FrontendSuiteContext context, + @NotNull VariableDeclaration variableDeclaration + ) { + if (variableDeclaration.kind() == DeclarationKind.VAR) { + resolveSupportedRoot(context, variableDeclaration); + return; + } + resolveUnsupportedRoot(context, variableDeclaration); + } + + private void resolveIfStatement( + @NotNull FrontendSuiteContext context, + @NotNull IfStatement ifStatement, + @NotNull ChildSuiteResolver childSuiteResolver + ) { + resolveSupportedRoot(context, ifStatement.condition()); + childSuiteResolver.resolveChildSuite(context, ifStatement.body()); + for (var elifClause : ifStatement.elifClauses()) { + resolveElifClause(context, elifClause, childSuiteResolver); + } + if (ifStatement.elseBody() != null) { + childSuiteResolver.resolveChildSuite(context, ifStatement.elseBody()); + } + } + + private void resolveElifClause( + @NotNull FrontendSuiteContext context, + @NotNull ElifClause elifClause, + @NotNull ChildSuiteResolver childSuiteResolver + ) { + resolveSupportedRoot(context, elifClause.condition()); + childSuiteResolver.resolveChildSuite(context, elifClause.body()); + } + + private void resolveWhileStatement( + @NotNull FrontendSuiteContext context, + @NotNull WhileStatement whileStatement, + @NotNull ChildSuiteResolver childSuiteResolver + ) { + resolveSupportedRoot(context, whileStatement.condition()); + childSuiteResolver.resolveChildSuite(context, whileStatement.body()); + } + + private void resolveForStatement( + @NotNull FrontendSuiteContext context, + @NotNull ForStatement forStatement, + @NotNull ChildSuiteResolver childSuiteResolver + ) { + // Header facts share one statement boundary. The body is entered only through the ordinary + // child-suite path so header typing can never become a body-entry condition. + if (forStatement.iteratorType() != null) { + runSupportedRoot(context, forStatement.iteratorType()); + } + runSupportedRoot(context, forStatement.iterable()); + flushStatementBoundary(context); + childSuiteResolver.resolveChildSuite(context, forStatement.body()); + } + + private void resolveSupportedRoot(@NotNull FrontendSuiteContext context, @NotNull Node root) { + runSupportedRoot(context, root); + flushStatementBoundary(context); + } + + private void runSupportedRoot(@NotNull FrontendSuiteContext context, @NotNull Node root) { + ownerProcedures.runTopBinding(context, root); + ownerProcedures.runLocalTypeStabilization(context, root); + ownerProcedures.runChainBinding(context, root); + ownerProcedures.runExprType(context, root); + ownerProcedures.runVarTypePost(context, root); + } + + private void resolveUnsupportedRoot(@NotNull FrontendSuiteContext context, @NotNull Node root) { + ownerProcedures.runUnsupported(context, root); + flushStatementBoundary(context); + } + + private void flushStatementBoundary(@NotNull FrontendSuiteContext context) { + context.typedEnvironment().flushPendingFacts(); + // Diagnostics share the statement boundary with typed facts: later statements in the same + // suite must see upstream diagnostics without waiting for the final suite export snapshot. + context.analysisData().updateDiagnostics(context.diagnosticManager().snapshot()); + } + + @FunctionalInterface + public interface ChildSuiteResolver { + void resolveChildSuite(@NotNull FrontendSuiteContext parentContext, @NotNull Block childBlock); + } + + public interface OwnerProcedures { + default void runTopBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { + } + + default void runLocalTypeStabilization(@NotNull FrontendSuiteContext context, @NotNull Node root) { + } + + default void runChainBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { + } + + default void runExprType(@NotNull FrontendSuiteContext context, @NotNull Node root) { + } + + default void runVarTypePost(@NotNull FrontendSuiteContext context, @NotNull Node root) { + } + + default void runUnsupported(@NotNull FrontendSuiteContext context, @NotNull Node root) { + } + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteContext.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteContext.java new file mode 100644 index 00000000..dca3d664 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteContext.java @@ -0,0 +1,96 @@ +package gd.script.gdcc.frontend.sema.analyzer; + +import dev.superice.gdparser.frontend.ast.Block; +import dev.superice.gdparser.frontend.ast.Node; +import gd.script.gdcc.frontend.diagnostic.DiagnosticManager; +import gd.script.gdcc.frontend.scope.BlockScope; +import gd.script.gdcc.frontend.sema.FrontendAnalysisData; +import gd.script.gdcc.frontend.sema.FrontendBodySemanticSupportPolicy; +import gd.script.gdcc.frontend.sema.FrontendInterfaceSurface; +import gd.script.gdcc.frontend.sema.FrontendTypedLexicalEnvironment; +import gd.script.gdcc.frontend.sema.analyzer.support.FrontendPropertyInitializerSupport; +import gd.script.gdcc.frontend.sema.patch.FrontendCallableExportBatch; +import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueDomain; +import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueResolveRequest; +import gd.script.gdcc.scope.ClassRegistry; +import gd.script.gdcc.scope.ResolveRestriction; +import gd.script.gdcc.scope.Scope; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.nio.file.Path; +import java.util.Objects; + +/// Statement-local context passed through the body SuiteResolver. +/// +/// The context deliberately carries the typed lexical environment as an explicit dependency so later +/// owner procedures cannot fall back to hidden analyzer-local side-table snapshots. Callable roots +/// also share one export batch with their nested suites; child overlays stay isolated while their +/// exported transactions are deferred to the callable boundary. +public record FrontendSuiteContext( + @NotNull Path sourcePath, + @NotNull Node callableOwner, + @Nullable Block currentBlockRoot, + @NotNull Scope currentScope, + @Nullable BlockScope currentBlockScope, + @NotNull ResolveRestriction restriction, + boolean staticContext, + @Nullable FrontendPropertyInitializerSupport.PropertyInitializerContext propertyInitializerContext, + @NotNull FrontendInterfaceSurface interfaceSurface, + @NotNull FrontendTypedLexicalEnvironment typedEnvironment, + @NotNull FrontendAnalysisData analysisData, + @NotNull DiagnosticManager diagnosticManager, + @NotNull ClassRegistry classRegistry, + @Nullable FrontendCallableExportBatch exportBatch +) { + public FrontendSuiteContext { + Objects.requireNonNull(sourcePath, "sourcePath must not be null"); + Objects.requireNonNull(callableOwner, "callableOwner must not be null"); + Objects.requireNonNull(currentScope, "currentScope must not be null"); + Objects.requireNonNull(restriction, "restriction must not be null"); + Objects.requireNonNull(interfaceSurface, "interfaceSurface must not be null"); + Objects.requireNonNull(typedEnvironment, "typedEnvironment must not be null"); + Objects.requireNonNull(analysisData, "analysisData must not be null"); + Objects.requireNonNull(diagnosticManager, "diagnosticManager must not be null"); + Objects.requireNonNull(classRegistry, "classRegistry must not be null"); + } + + public @NotNull FrontendSuiteContext withChildBlock(@NotNull Block block, @NotNull BlockScope blockScope) { + var childEnvironment = new FrontendTypedLexicalEnvironment( + blockScope, + analysisData, + typedEnvironment, + interfaceSurface.typedLexicalBaseline() + ); + return new FrontendSuiteContext( + sourcePath, + callableOwner, + block, + blockScope, + blockScope, + restriction, + staticContext, + propertyInitializerContext, + interfaceSurface, + childEnvironment, + analysisData, + diagnosticManager, + classRegistry, + exportBatch + ); + } + + public @NotNull FrontendVisibleValueResolveRequest visibleValueResolveRequest( + @NotNull String name, + @NotNull Node useSite + ) { + return new FrontendVisibleValueResolveRequest(name, useSite, restriction, visibleValueDomainForCurrentBody()); + } + + private @NotNull FrontendVisibleValueDomain visibleValueDomainForCurrentBody() { + if (currentBlockRoot == null || currentBlockScope == null) { + return FrontendVisibleValueDomain.EXECUTABLE_BODY; + } + return FrontendBodySemanticSupportPolicy.forBlockScopeKind(currentBlockScope.kind()).visibleValueDomain(); + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolver.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolver.java new file mode 100644 index 00000000..e499f4a7 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolver.java @@ -0,0 +1,297 @@ +package gd.script.gdcc.frontend.sema.analyzer; + +import dev.superice.gdparser.frontend.ast.Block; +import dev.superice.gdparser.frontend.ast.ConstructorDeclaration; +import dev.superice.gdparser.frontend.ast.FunctionDeclaration; +import dev.superice.gdparser.frontend.ast.Node; +import dev.superice.gdparser.frontend.ast.Parameter; +import dev.superice.gdparser.frontend.ast.VariableDeclaration; +import gd.script.gdcc.frontend.diagnostic.DiagnosticManager; +import gd.script.gdcc.frontend.diagnostic.FrontendRange; +import gd.script.gdcc.frontend.scope.BlockScope; +import gd.script.gdcc.frontend.scope.CallableScope; +import gd.script.gdcc.frontend.sema.FrontendAnalysisData; +import gd.script.gdcc.frontend.sema.FrontendBodyStructuralCompleteness; +import gd.script.gdcc.frontend.sema.FrontendInterfaceSurface; +import gd.script.gdcc.frontend.sema.FrontendSemanticStage; +import gd.script.gdcc.frontend.sema.FrontendTypedLexicalEnvironment; +import gd.script.gdcc.frontend.sema.analyzer.support.FrontendPropertyInitializerSupport; +import gd.script.gdcc.frontend.sema.patch.FrontendCallableExportBatch; +import gd.script.gdcc.scope.ClassRegistry; +import gd.script.gdcc.scope.ResolveRestriction; +import gd.script.gdcc.scope.ScopeValueKind; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; + +/// Body-suite coordinator for the staged semantic pipeline. +/// +/// The resolver uses statement-local owner procedures. Tests may inject a custom +/// `FrontendStatementResolver` to record traversal shape, but production body facts must flow through +/// the typed lexical environment and ordered patch transaction. +public class FrontendSuiteResolver { + private static final @NotNull String UNSUPPORTED_BINDING_SUBTREE_CATEGORY = + "sema.unsupported_binding_subtree"; + private static final @NotNull String UNSUPPORTED_CHAIN_ROUTE_CATEGORY = "sema.unsupported_chain_route"; + + private final @NotNull FrontendStatementResolver statementResolver; + + public FrontendSuiteResolver() { + this(new FrontendStatementResolver(new FrontendBodyOwnerProcedures())); + } + + public FrontendSuiteResolver(@NotNull FrontendStatementResolver statementResolver) { + this.statementResolver = Objects.requireNonNull(statementResolver, "statementResolver must not be null"); + } + + public void resolve( + @NotNull FrontendInterfaceSurface interfaceSurface, + @NotNull ClassRegistry classRegistry, + @NotNull FrontendAnalysisData analysisData, + @NotNull DiagnosticManager diagnosticManager + ) { + Objects.requireNonNull(interfaceSurface, "interfaceSurface must not be null"); + Objects.requireNonNull(classRegistry, "classRegistry must not be null"); + Objects.requireNonNull(analysisData, "analysisData must not be null"); + Objects.requireNonNull(diagnosticManager, "diagnosticManager must not be null"); + + for (var callableOwner : interfaceSurface.suiteEntryRoots().callableOwners()) { + resolveCallableOwner(interfaceSurface, callableOwner, classRegistry, analysisData, diagnosticManager); + } + for (var propertyInitializer : interfaceSurface.suiteEntryRoots().propertyInitializers()) { + resolvePropertyInitializer(interfaceSurface, propertyInitializer, classRegistry, analysisData, diagnosticManager); + } + } + + /// Resolves one root or nested suite and defers its stable publication to the callable export batch. + /// + /// Child environments retain separate pending and committed overlays. Their transactions join the + /// root callable's batch rather than becoming visible through stable side tables mid-resolution. + public void resolveSuite(@NotNull FrontendSuiteContext context, @NotNull Block block) { + Objects.requireNonNull(context, "context must not be null"); + Objects.requireNonNull(block, "block must not be null"); + var blockScope = requireBlockScope(context, block); + FrontendBodyStructuralCompleteness.requireStructurallyCompleteBody( + context.analysisData(), + context.interfaceSurface(), + block, + blockScope + ); + for (var statement : block.statements()) { + statementResolver.resolveStatement(context, statement, this::resolveChildSuite); + } + var transaction = context.typedEnvironment().exportPatchTransaction(); + var exportBatch = context.exportBatch(); + if (exportBatch != null) { + exportBatch.accumulate(transaction); + } else { + transaction.applyTo(context.analysisData()); + } + context.analysisData().updateDiagnostics(context.diagnosticManager().snapshot()); + } + + private void resolveCallableOwner( + @NotNull FrontendInterfaceSurface interfaceSurface, + @NotNull Node callableOwner, + @NotNull ClassRegistry classRegistry, + @NotNull FrontendAnalysisData analysisData, + @NotNull DiagnosticManager diagnosticManager + ) { + var body = callableBody(callableOwner); + if (body == null) { + throw new IllegalStateException("Suite entry callable owner has no executable body"); + } + var bodyScope = analysisData.scopesByAst().get(body); + if (!(bodyScope instanceof BlockScope blockScope)) { + throw new IllegalStateException("Suite entry callable body has no published BlockScope"); + } + var environment = new FrontendTypedLexicalEnvironment( + blockScope, + analysisData, + null, + interfaceSurface.typedLexicalBaseline() + ); + var exportBatch = new FrontendCallableExportBatch(); + var context = new FrontendSuiteContext( + sourcePathFor(interfaceSurface, callableOwner, analysisData), + callableOwner, + body, + blockScope, + blockScope, + restrictionForCallable(callableOwner), + isStaticCallable(callableOwner), + null, + interfaceSurface, + environment, + analysisData, + diagnosticManager, + classRegistry, + exportBatch + ); + runCallableEntryVarTypePost(context, callableOwner); + resolveSuite(context, body); + // Stable export is ordered but non-atomic: queued transactions are not preflighted together, + // and a later failure does not roll back patches or transactions that were already applied. + exportBatch.applyTo(analysisData); + } + + /// Publishes callable-entry var-type-post facts before the first body statement is resolved. + /// + /// Parameters are not statement roots, so this complements rather than bypasses the + /// statement-local var-type-post procedure. Both paths publish through the same overlay, + /// owner stage, and callable-scoped export batch. + private void runCallableEntryVarTypePost( + @NotNull FrontendSuiteContext context, + @NotNull Node callableOwner + ) { + var parameters = callableParameters(callableOwner); + for (var parameter : parameters) { + publishCallableEntryParameterSlotType(context, parameter); + reportUnsupportedParameterDefault(context, parameter); + } + // Make every parameter visible to the first statement without publishing stable facts. + context.typedEnvironment().flushPendingFacts(); + } + + private void publishCallableEntryParameterSlotType( + @NotNull FrontendSuiteContext context, + @NotNull Parameter parameter + ) { + var scope = context.analysisData().scopesByAst().get(parameter); + if (!(scope instanceof CallableScope callableScope)) { + throw new IllegalStateException("Parameter '" + parameter.name().trim() + "' has no published callable scope"); + } + var slot = callableScope.resolveValueHere(parameter.name().trim()); + if (slot == null || slot.kind() != ScopeValueKind.PARAMETER || slot.declaration() != parameter) { + throw new IllegalStateException("Parameter '" + parameter.name().trim() + "' inventory slot drifted"); + } + var baselineType = context.interfaceSurface().typedLexicalBaseline().typeFor(parameter); + if (baselineType == null) { + throw new IllegalStateException("Parameter '" + parameter.name().trim() + "' is missing typed baseline"); + } + context.typedEnvironment().putSlotType(FrontendSemanticStage.VAR_TYPE_POST, parameter, baselineType); + } + + private static void reportUnsupportedParameterDefault( + @NotNull FrontendSuiteContext context, + @NotNull Parameter parameter + ) { + if (parameter.defaultValue() == null) { + return; + } + context.diagnosticManager().error( + UNSUPPORTED_BINDING_SUBTREE_CATEGORY, + "Binding analysis is not supported in parameter default", + context.sourcePath(), + FrontendRange.fromAstRange(parameter.defaultValue().range()) + ); + context.diagnosticManager().error( + UNSUPPORTED_CHAIN_ROUTE_CATEGORY, + "Chain binding analysis is not supported in parameter default", + context.sourcePath(), + FrontendRange.fromAstRange(parameter.defaultValue().range()) + ); + } + + private void resolvePropertyInitializer( + @NotNull FrontendInterfaceSurface interfaceSurface, + @NotNull VariableDeclaration propertyInitializer, + @NotNull ClassRegistry classRegistry, + @NotNull FrontendAnalysisData analysisData, + @NotNull DiagnosticManager diagnosticManager + ) { + var propertyContext = FrontendPropertyInitializerSupport.contextOrNull( + analysisData.scopesByAst(), + propertyInitializer + ); + if (propertyContext == null) { + return; + } + var classScope = propertyContext.declaringClassScope(); + var environment = new FrontendTypedLexicalEnvironment( + classScope, + analysisData, + null, + interfaceSurface.typedLexicalBaseline() + ); + var context = new FrontendSuiteContext( + sourcePathFor(interfaceSurface, propertyInitializer, analysisData), + propertyInitializer, + null, + classScope, + null, + FrontendPropertyInitializerSupport.restrictionFor(propertyInitializer), + propertyInitializer.isStatic(), + propertyContext, + interfaceSurface, + environment, + analysisData, + diagnosticManager, + classRegistry, + null + ); + statementResolver.resolvePropertyInitializer(context, propertyInitializer); + // Property initializers are independent roots and do not join a callable export batch. + context.typedEnvironment().exportPatchTransaction().applyTo(analysisData); + analysisData.updateDiagnostics(diagnosticManager.snapshot()); + } + + /// Resolves a child block with its own overlay while sharing the parent's callable export batch. + private void resolveChildSuite(@NotNull FrontendSuiteContext parentContext, @NotNull Block childBlock) { + var blockScope = requireBlockScope(parentContext, childBlock); + resolveSuite(parentContext.withChildBlock(childBlock, blockScope), childBlock); + } + + private static @NotNull BlockScope requireBlockScope( + @NotNull FrontendSuiteContext context, + @NotNull Block block + ) { + var scope = context.analysisData().scopesByAst().get(block); + if (scope instanceof BlockScope blockScope) { + return blockScope; + } + throw new IllegalStateException("Suite body has no published BlockScope"); + } + + private static @Nullable Block callableBody(@NotNull Node callableOwner) { + return switch (callableOwner) { + case FunctionDeclaration functionDeclaration -> functionDeclaration.body(); + case ConstructorDeclaration constructorDeclaration -> constructorDeclaration.body(); + default -> null; + }; + } + + private static @NotNull List callableParameters(@NotNull Node callableOwner) { + return switch (callableOwner) { + case FunctionDeclaration functionDeclaration -> functionDeclaration.parameters(); + case ConstructorDeclaration constructorDeclaration -> constructorDeclaration.parameters(); + default -> List.of(); + }; + } + + private static @NotNull ResolveRestriction restrictionForCallable(@NotNull Node callableOwner) { + if (callableOwner instanceof FunctionDeclaration functionDeclaration && functionDeclaration.isStatic()) { + return ResolveRestriction.staticContext(); + } + return ResolveRestriction.instanceContext(); + } + + private static boolean isStaticCallable(@NotNull Node callableOwner) { + return callableOwner instanceof FunctionDeclaration functionDeclaration && functionDeclaration.isStatic(); + } + + private static @NotNull Path sourcePathFor( + @NotNull FrontendInterfaceSurface interfaceSurface, + @NotNull Node entryRoot, + @NotNull FrontendAnalysisData analysisData + ) { + var sourcePath = interfaceSurface.suiteEntryRoots().sourcePathFor(entryRoot); + if (sourcePath != null) { + return sourcePath; + } + return analysisData.moduleSkeleton().sourceClassRelations().getFirst().unit().path(); + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTopBindingAnalyzer.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTopBindingAnalyzer.java deleted file mode 100644 index 53a0d52a..00000000 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTopBindingAnalyzer.java +++ /dev/null @@ -1,1472 +0,0 @@ -package gd.script.gdcc.frontend.sema.analyzer; - -import gd.script.gdcc.frontend.diagnostic.DiagnosticManager; -import gd.script.gdcc.frontend.diagnostic.FrontendRange; -import gd.script.gdcc.frontend.sema.FrontendAnalysisData; -import gd.script.gdcc.frontend.sema.FrontendAstSideTable; -import gd.script.gdcc.frontend.sema.FrontendBinding; -import gd.script.gdcc.frontend.sema.FrontendBindingKind; -import gd.script.gdcc.frontend.sema.FrontendModuleSkeleton; -import gd.script.gdcc.frontend.sema.analyzer.support.FrontendPropertyInitializerSupport; -import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueDeferredBoundary; -import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueDeferredReason; -import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueDomain; -import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueResolveRequest; -import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueResolution; -import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueResolver; -import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueStatus; -import gd.script.gdcc.gdextension.ExtensionGdClass; -import gd.script.gdcc.gdextension.ExtensionUtilityFunction; -import gd.script.gdcc.scope.ClassDef; -import gd.script.gdcc.scope.ClassRegistry; -import gd.script.gdcc.scope.FunctionDef; -import gd.script.gdcc.scope.ResolveRestriction; -import gd.script.gdcc.scope.*; -import gd.script.gdcc.scope.ScopeValue; -import gd.script.gdcc.type.GdObjectType; -import dev.superice.gdparser.frontend.ast.ASTNodeHandler; -import dev.superice.gdparser.frontend.ast.ASTWalker; -import dev.superice.gdparser.frontend.ast.AssignmentExpression; -import dev.superice.gdparser.frontend.ast.AttributeCallStep; -import dev.superice.gdparser.frontend.ast.AssertStatement; -import dev.superice.gdparser.frontend.ast.AttributeExpression; -import dev.superice.gdparser.frontend.ast.AttributePropertyStep; -import dev.superice.gdparser.frontend.ast.AttributeStep; -import dev.superice.gdparser.frontend.ast.AttributeSubscriptStep; -import dev.superice.gdparser.frontend.ast.Block; -import dev.superice.gdparser.frontend.ast.CallExpression; -import dev.superice.gdparser.frontend.ast.ClassDeclaration; -import dev.superice.gdparser.frontend.ast.ConstructorDeclaration; -import dev.superice.gdparser.frontend.ast.DeclarationKind; -import dev.superice.gdparser.frontend.ast.ElifClause; -import dev.superice.gdparser.frontend.ast.Expression; -import dev.superice.gdparser.frontend.ast.ExpressionStatement; -import dev.superice.gdparser.frontend.ast.ForStatement; -import dev.superice.gdparser.frontend.ast.FrontendASTTraversalDirective; -import dev.superice.gdparser.frontend.ast.FunctionDeclaration; -import dev.superice.gdparser.frontend.ast.IdentifierExpression; -import dev.superice.gdparser.frontend.ast.IfStatement; -import dev.superice.gdparser.frontend.ast.LambdaExpression; -import dev.superice.gdparser.frontend.ast.LiteralExpression; -import dev.superice.gdparser.frontend.ast.MatchStatement; -import dev.superice.gdparser.frontend.ast.Node; -import dev.superice.gdparser.frontend.ast.Parameter; -import dev.superice.gdparser.frontend.ast.ReturnStatement; -import dev.superice.gdparser.frontend.ast.SelfExpression; -import dev.superice.gdparser.frontend.ast.SourceFile; -import dev.superice.gdparser.frontend.ast.Statement; -import dev.superice.gdparser.frontend.ast.SubscriptExpression; -import dev.superice.gdparser.frontend.ast.VariableDeclaration; -import dev.superice.gdparser.frontend.ast.WhileStatement; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.IdentityHashMap; -import java.nio.file.Path; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; - -/// Rebuilds frontend top-level symbol bindings from published skeleton, scope, and visible-value -/// facts. -/// -/// The analyzer publishes only `symbolBindings()`. Member facts, call facts, and expression types -/// remain untouched here. -public class FrontendTopBindingAnalyzer { - private static final @NotNull String BINDING_CATEGORY = "sema.binding"; - private static final @NotNull String UNSUPPORTED_BINDING_SUBTREE_CATEGORY = - "sema.unsupported_binding_subtree"; - - /// Runs top-binding analysis and refreshes `symbolBindings()` from scratch. - public void analyze( - @NotNull ClassRegistry classRegistry, - @NotNull FrontendAnalysisData analysisData, - @NotNull DiagnosticManager diagnosticManager - ) { - Objects.requireNonNull(classRegistry, "classRegistry must not be null"); - Objects.requireNonNull(analysisData, "analysisData must not be null"); - Objects.requireNonNull(diagnosticManager, "diagnosticManager must not be null"); - - var moduleSkeleton = analysisData.moduleSkeleton(); - analysisData.diagnostics(); - - var scopesByAst = analysisData.scopesByAst(); - for (var sourceClassRelation : moduleSkeleton.sourceClassRelations()) { - var sourceFile = sourceClassRelation.unit().ast(); - if (!scopesByAst.containsKey(sourceFile)) { - throw new IllegalStateException( - "Scope graph has not been published for source file: " + sourceClassRelation.unit().path() - ); - } - } - - var visibleValueResolver = new FrontendVisibleValueResolver(analysisData); - var symbolBindings = new FrontendAstSideTable(); - for (var sourceClassRelation : moduleSkeleton.sourceClassRelations()) { - new AstWalkerTopBindingBinder( - sourceClassRelation.unit().path(), - moduleSkeleton, - scopesByAst, - symbolBindings, - diagnosticManager, - visibleValueResolver, - classRegistry - ).walk(sourceClassRelation.unit().ast()); - } - analysisData.updateSymbolBindings(symbolBindings); - } - - private enum ExpressionPosition { - VALUE, - BARE_CALLEE, - TOP_LEVEL_TYPE_META_CANDIDATE - } - - /// `ASTWalker` remains the typed dispatch engine, while this handler keeps subtree gating and - /// namespace routing local to top-binding analysis. - private static final class AstWalkerTopBindingBinder implements ASTNodeHandler { - private final @NotNull Path sourcePath; - private final @NotNull FrontendModuleSkeleton moduleSkeleton; - private final @NotNull FrontendAstSideTable scopesByAst; - private final @NotNull FrontendAstSideTable symbolBindings; - private final @NotNull DiagnosticManager diagnosticManager; - private final @NotNull FrontendVisibleValueResolver visibleValueResolver; - private final @NotNull ClassRegistry classRegistry; - private final @NotNull ASTWalker astWalker; - private final @NotNull IdentityHashMap reportedUnsupportedRoots = new IdentityHashMap<>(); - private int supportedExecutableBlockDepth; - private int supportedPropertyInitializerDepth; - private @NotNull ResolveRestriction currentRestriction = ResolveRestriction.unrestricted(); - private boolean currentStaticContext; - private @Nullable FrontendPropertyInitializerSupport.PropertyInitializerContext currentPropertyInitializerContext; - - private AstWalkerTopBindingBinder( - @NotNull Path sourcePath, - @NotNull FrontendModuleSkeleton moduleSkeleton, - @NotNull FrontendAstSideTable scopesByAst, - @NotNull FrontendAstSideTable symbolBindings, - @NotNull DiagnosticManager diagnosticManager, - @NotNull FrontendVisibleValueResolver visibleValueResolver, - @NotNull ClassRegistry classRegistry - ) { - this.sourcePath = Objects.requireNonNull(sourcePath, "sourcePath must not be null"); - this.moduleSkeleton = Objects.requireNonNull(moduleSkeleton, "moduleSkeleton must not be null"); - this.scopesByAst = Objects.requireNonNull(scopesByAst, "scopesByAst must not be null"); - this.symbolBindings = Objects.requireNonNull(symbolBindings, "symbolBindings must not be null"); - this.diagnosticManager = Objects.requireNonNull(diagnosticManager, "diagnosticManager must not be null"); - this.visibleValueResolver = Objects.requireNonNull( - visibleValueResolver, - "visibleValueResolver must not be null" - ); - this.classRegistry = Objects.requireNonNull(classRegistry, "classRegistry must not be null"); - astWalker = new ASTWalker(this); - } - - private void walk(@NotNull SourceFile sourceFile) { - astWalker.walk(sourceFile); - } - - @Override - public @NotNull FrontendASTTraversalDirective handleNode(@NotNull Node node) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleSourceFile(@NotNull SourceFile sourceFile) { - walkNonExecutableContainerStatements(sourceFile.statements()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleClassDeclaration(@NotNull ClassDeclaration classDeclaration) { - if (isNotPublished(classDeclaration)) { - reportSkippedSubtree(classDeclaration); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkNonExecutableContainerStatements(classDeclaration.body().statements()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleFunctionDeclaration( - @NotNull FunctionDeclaration functionDeclaration - ) { - if (isNotPublished(functionDeclaration)) { - reportSkippedSubtree(functionDeclaration); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - reportDeferredParameterDefaults(functionDeclaration.parameters()); - walkCallableBody( - functionDeclaration, - functionDeclaration.body(), - functionDeclaration.isStatic() - ? ResolveRestriction.staticContext() - : ResolveRestriction.instanceContext(), - functionDeclaration.isStatic() - ); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleConstructorDeclaration( - @NotNull ConstructorDeclaration constructorDeclaration - ) { - if (isNotPublished(constructorDeclaration)) { - reportSkippedSubtree(constructorDeclaration); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - reportDeferredParameterDefaults(constructorDeclaration.parameters()); - walkCallableBody( - constructorDeclaration, - constructorDeclaration.body(), - ResolveRestriction.instanceContext(), - false - ); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleBlock(@NotNull Block block) { - if (supportedExecutableBlockDepth <= 0) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - if (isNotPublished(block)) { - reportSkippedSubtree(block); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkStatements(block.statements()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleExpressionStatement( - @NotNull ExpressionStatement expressionStatement - ) { - if (supportedExecutableBlockDepth <= 0) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkValueExpression(expressionStatement.expression()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleReturnStatement(@NotNull ReturnStatement returnStatement) { - if (supportedExecutableBlockDepth <= 0 || returnStatement.value() == null) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkValueExpression(returnStatement.value()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleAssertStatement(@NotNull AssertStatement assertStatement) { - if (supportedExecutableBlockDepth <= 0) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkValueExpression(assertStatement.condition()); - if (assertStatement.message() != null) { - walkValueExpression(assertStatement.message()); - } - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleVariableDeclaration( - @NotNull VariableDeclaration variableDeclaration - ) { - if (supportedExecutableBlockDepth > 0) { - if (variableDeclaration.value() == null) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - if (variableDeclaration.kind() == DeclarationKind.CONST) { - reportDeferredSubtree(variableDeclaration.value(), FrontendVisibleValueDomain.BLOCK_LOCAL_CONST_SUBTREE); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - if (variableDeclaration.kind() != DeclarationKind.VAR) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkValueExpression(variableDeclaration.value()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - if (!FrontendPropertyInitializerSupport.isSupportedPropertyInitializer(scopesByAst, variableDeclaration)) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkPropertyInitializer(variableDeclaration); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleIfStatement(@NotNull IfStatement ifStatement) { - if (supportedExecutableBlockDepth <= 0) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - if (isNotPublished(ifStatement)) { - reportSkippedSubtree(ifStatement); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkValueExpression(ifStatement.condition()); - walkSupportedExecutableBlock(ifStatement.body()); - for (var elifClause : ifStatement.elifClauses()) { - astWalker.walk(elifClause); - } - if (ifStatement.elseBody() != null) { - walkSupportedExecutableBlock(ifStatement.elseBody()); - } - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleElifClause(@NotNull ElifClause elifClause) { - if (supportedExecutableBlockDepth <= 0) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - if (isNotPublished(elifClause)) { - reportSkippedSubtree(elifClause); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkValueExpression(elifClause.condition()); - walkSupportedExecutableBlock(elifClause.body()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleWhileStatement(@NotNull WhileStatement whileStatement) { - if (supportedExecutableBlockDepth <= 0) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - if (isNotPublished(whileStatement)) { - reportSkippedSubtree(whileStatement); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkValueExpression(whileStatement.condition()); - walkSupportedExecutableBlock(whileStatement.body()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleForStatement(@NotNull ForStatement forStatement) { - if (supportedExecutableBlockDepth <= 0) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - if (isNotPublished(forStatement)) { - reportSkippedSubtree(forStatement); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - reportDeferredSubtree(forStatement, FrontendVisibleValueDomain.FOR_SUBTREE); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleMatchStatement(@NotNull MatchStatement matchStatement) { - if (supportedExecutableBlockDepth <= 0) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - if (isNotPublished(matchStatement)) { - reportSkippedSubtree(matchStatement); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkValueExpression(matchStatement.value()); - if (!matchStatement.sections().isEmpty()) { - reportDeferredSubtree(matchStatement, FrontendVisibleValueDomain.MATCH_SUBTREE); - } - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleLambdaExpression(@NotNull LambdaExpression lambdaExpression) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - private void walkCallableBody( - @NotNull Node callableOwner, - @Nullable Block body, - @NotNull ResolveRestriction restriction, - boolean staticContext - ) { - if (isNotPublished(callableOwner)) { - reportSkippedSubtree(callableOwner); - return; - } - if (isNotPublished(body)) { - reportSkippedSubtree(body); - return; - } - var previousRestriction = currentRestriction; - var previousStaticContext = currentStaticContext; - currentRestriction = Objects.requireNonNull(restriction, "restriction must not be null"); - currentStaticContext = staticContext; - try { - walkSupportedExecutableBlock(body); - } finally { - currentRestriction = previousRestriction; - currentStaticContext = previousStaticContext; - } - } - - private void walkStatements(@NotNull List statements) { - for (var statement : statements) { - astWalker.walk(statement); - } - } - - private void walkNonExecutableContainerStatements(@NotNull List statements) { - var previousDepth = supportedExecutableBlockDepth; - supportedExecutableBlockDepth = 0; - try { - walkStatements(statements); - } finally { - supportedExecutableBlockDepth = previousDepth; - } - } - - private void walkSupportedExecutableBlock(@Nullable Block block) { - if (isNotPublished(block)) { - reportSkippedSubtree(block); - return; - } - supportedExecutableBlockDepth++; - try { - astWalker.walk(block); - } finally { - supportedExecutableBlockDepth--; - } - } - - /// Property initializers need the class member lookup contract and static restriction of the - /// declaring property, but they still must not widen the whole class body into an executable - /// region. - private void walkPropertyInitializer(@NotNull VariableDeclaration variableDeclaration) { - var initializer = Objects.requireNonNull( - variableDeclaration.value(), - "property initializer value must not be null" - ); - var previousRestriction = currentRestriction; - var previousStaticContext = currentStaticContext; - var previousPropertyInitializerContext = currentPropertyInitializerContext; - currentRestriction = FrontendPropertyInitializerSupport.restrictionFor(variableDeclaration); - currentStaticContext = variableDeclaration.isStatic(); - currentPropertyInitializerContext = FrontendPropertyInitializerSupport.contextFor( - scopesByAst, - variableDeclaration - ); - supportedPropertyInitializerDepth++; - try { - walkValueExpression(initializer); - } finally { - supportedPropertyInitializerDepth--; - currentPropertyInitializerContext = previousPropertyInitializerContext; - currentRestriction = previousRestriction; - currentStaticContext = previousStaticContext; - } - } - - private void walkValueExpression(@NotNull Expression expression) { - walkExpression(expression, ExpressionPosition.VALUE); - } - - private void walkExpression( - @Nullable Expression expression, - @NotNull ExpressionPosition position - ) { - if (expression == null) { - return; - } - switch (expression) { - case IdentifierExpression identifierExpression -> visitIdentifier(identifierExpression, position); - case SelfExpression selfExpression -> visitSelf(selfExpression); - case LiteralExpression literalExpression -> visitLiteral(literalExpression); - case AssignmentExpression assignmentExpression -> walkAssignmentExpression(assignmentExpression); - case AttributeExpression attributeExpression -> walkAttributeExpression(attributeExpression); - case SubscriptExpression subscriptExpression -> walkSubscriptExpression(subscriptExpression); - case CallExpression callExpression -> walkCallExpression(callExpression); - case LambdaExpression lambdaExpression -> reportDeferredSubtree( - lambdaExpression, - FrontendVisibleValueDomain.LAMBDA_SUBTREE - ); - default -> walkGenericExpressionChildren(expression); - } - } - - /// `FrontendBinding` does not encode usage semantics, so assignment chain heads are - /// published through the same binding table as ordinary reads. - private void walkAssignmentExpression(@NotNull AssignmentExpression assignmentExpression) { - walkValueExpression(assignmentExpression.left()); - walkValueExpression(assignmentExpression.right()); - } - - private void walkAttributeExpression(@NotNull AttributeExpression attributeExpression) { - // Only the outermost chain head is bound here, but arguments nested inside - // attribute-call/subscript steps still belong to the executable expression tree. - // - // Dual-role bias: when the base is a bare IdentifierExpression whose value namespace - // resolves to SINGLETON and whose type-meta namespace also resolves to ENGINE_CLASS, - // inspect the first suffix step to decide whether the head should publish TYPE_META - // (static/constructor route) or stay on the ordinary SINGLETON value path. This runs - // before walking the base so the ordinary identifier handler never publishes a - // SINGLETON binding that would then need to be overwritten. - if (!tryApplyDualRoleTypeMetaBias(attributeExpression)) { - walkChainHeadBaseExpression(attributeExpression.base()); - } - for (var step : attributeExpression.steps()) { - switch (step) { - case AttributeCallStep attributeCallStep -> walkExpressionList(attributeCallStep.arguments()); - case AttributeSubscriptStep attributeSubscriptStep -> - walkExpressionList(attributeSubscriptStep.arguments()); - default -> { - } - } - } - } - - private void walkSubscriptExpression(@NotNull SubscriptExpression subscriptExpression) { - walkValueExpression(subscriptExpression.base()); - for (var argument : subscriptExpression.arguments()) { - walkValueExpression(argument); - } - } - - private void walkCallExpression(@NotNull CallExpression callExpression) { - switch (callExpression.callee()) { - case IdentifierExpression identifierExpression -> - visitIdentifier(identifierExpression, ExpressionPosition.BARE_CALLEE); - case AttributeExpression attributeExpression -> walkAttributeExpression(attributeExpression); - default -> walkValueExpression(callExpression.callee()); - } - walkExpressionList(callExpression.arguments()); - } - - private void walkChainHeadBaseExpression(@NotNull Expression expression) { - switch (expression) { - case IdentifierExpression identifierExpression -> - visitIdentifier(identifierExpression, ExpressionPosition.TOP_LEVEL_TYPE_META_CANDIDATE); - case SelfExpression selfExpression -> visitSelf(selfExpression); - case LiteralExpression literalExpression -> visitLiteral(literalExpression); - case AttributeExpression attributeExpression -> walkChainHeadBaseExpression(attributeExpression.base()); - default -> walkValueExpression(expression); - } - } - - private void walkGenericExpressionChildren(@NotNull Expression expression) { - walkNestedExpressionChildren(expression); - } - - private void walkExpressionList(@NotNull List expressions) { - for (var expression : expressions) { - walkValueExpression(expression); - } - } - - /// Some expressions, such as `DictionaryExpression`, wrap their real expression payload in - /// intermediate non-expression nodes (`DictEntry`). Generic traversal therefore needs to - /// descend through those containers until it reaches nested expressions. - private void walkNestedExpressionChildren(@NotNull Node node) { - for (var child : node.getChildren()) { - if (child instanceof Expression childExpression) { - walkValueExpression(childExpression); - continue; - } - walkNestedExpressionChildren(child); - } - } - - private void reportDeferredParameterDefaults(@NotNull List parameters) { - for (var parameter : parameters) { - if (parameter.defaultValue() != null) { - reportDeferredSubtree(parameter.defaultValue(), FrontendVisibleValueDomain.PARAMETER_DEFAULT); - } - } - } - - private void visitIdentifier( - @NotNull IdentifierExpression identifierExpression, - @NotNull ExpressionPosition position - ) { - Objects.requireNonNull(identifierExpression, "identifierExpression must not be null"); - switch (Objects.requireNonNull(position, "position must not be null")) { - case VALUE -> bindValueIdentifier(identifierExpression); - case TOP_LEVEL_TYPE_META_CANDIDATE -> bindTopLevelTypeMetaCandidate(identifierExpression); - case BARE_CALLEE -> bindBareCalleeIdentifier(identifierExpression); - } - } - - private void visitSelf(@NotNull SelfExpression selfExpression) { - publishBinding(selfExpression, "self", FrontendBindingKind.SELF, null); - if (supportedPropertyInitializerDepth > 0) { - reportPropertyInitializerUnsupportedBoundary( - selfExpression, - FrontendPropertyInitializerSupport.unsupportedSelfMessage() - ); - return; - } - if (currentStaticContext) { - reportBindingError( - selfExpression, - "Keyword 'self' is not available in static context" - ); - } - } - - private void visitLiteral(@NotNull LiteralExpression literalExpression) { - publishBinding( - literalExpression, - literalExpression.sourceText(), - FrontendBindingKind.LITERAL, - null - ); - } - - private void bindValueIdentifier(@NotNull IdentifierExpression identifierExpression) { - if (trySealPropertyInitializerValueBoundary(identifierExpression)) { - return; - } - var valueResolution = resolveVisibleValue(identifierExpression); - if (valueResolution.status() != FrontendVisibleValueStatus.NOT_FOUND) { - publishValueResolution(identifierExpression, valueResolution); - return; - } - - if (trySealPropertyInitializerFunctionBoundary(identifierExpression)) { - return; - } - - var currentScope = findCurrentScope(identifierExpression); - if (currentScope == null) { - reportMissingScopeUnsupported(identifierExpression, identifierExpression.name()); - return; - } - - var functionResult = currentScope.resolveFunctions(identifierExpression.name(), currentRestriction); - switch (functionResult.status()) { - case FOUND_ALLOWED -> { - publishFunctionBinding(identifierExpression, functionResult.requireValue(), false); - return; - } - case FOUND_BLOCKED -> { - publishFunctionBinding(identifierExpression, functionResult.requireValue(), true); - return; - } - case NOT_FOUND -> { - } - } - - var typeMetaResult = moduleSkeleton.resolveSourceFacingTypeMeta( - currentScope, - identifierExpression.name(), - currentRestriction - ); - if (typeMetaResult.isAllowed()) { - var typeMeta = typeMetaResult.requireValue(); - if (supportsTopLevelTypeMeta(typeMeta)) { - publishBinding( - identifierExpression, - identifierExpression.name(), - FrontendBindingKind.TYPE_META, - typeMeta.declaration() - ); - reportBindingError( - identifierExpression, - "Type-meta '" + identifierExpression.name() - + "' can only be used as a static-route head, not as an ordinary value; " - + "use routes such as '" + identifierExpression.name() - + ".build(...)', '" + identifierExpression.name() - + ".new()', or a static constant access like 'Vector3.BACK'" - ); - return; - } - reportBindingError( - identifierExpression, - "Top-level type-meta binding for '" + identifierExpression.name() - + "' currently supports class-like types, builtin static receivers, and global enums" - ); - return; - } - - publishValueResolution(identifierExpression, valueResolution); - } - - private @NotNull FrontendVisibleValueResolution resolveVisibleValue( - @NotNull IdentifierExpression identifierExpression - ) { - if (supportedPropertyInitializerDepth > 0) { - return resolvePropertyInitializerValue(identifierExpression); - } - return visibleValueResolver.resolve(new FrontendVisibleValueResolveRequest( - identifierExpression.name(), - identifierExpression, - currentRestriction, - FrontendVisibleValueDomain.EXECUTABLE_BODY - )); - } - - /// Property initializer lookup deliberately bypasses `FrontendVisibleValueResolver`: class - /// member initializers do not have callable-local declaration-order or local inventory rules, - /// so they should consume the shared scope/class/global contract directly. - private @NotNull FrontendVisibleValueResolution resolvePropertyInitializerValue( - @NotNull IdentifierExpression identifierExpression - ) { - var currentScope = findCurrentScope(identifierExpression); - if (currentScope == null) { - return FrontendVisibleValueResolution.deferredUnsupported( - new FrontendVisibleValueDeferredBoundary( - FrontendVisibleValueDomain.UNKNOWN_OR_SKIPPED_SUBTREE, - FrontendVisibleValueDeferredReason.MISSING_SCOPE_OR_SKIPPED_SUBTREE - ) - ); - } - var valueResult = currentScope.resolveValue(identifierExpression.name(), currentRestriction); - return switch (valueResult.status()) { - case FOUND_ALLOWED -> - FrontendVisibleValueResolution.foundAllowed(valueResult.requireValue(), List.of()); - case FOUND_BLOCKED -> - FrontendVisibleValueResolution.foundBlocked(valueResult.requireValue(), List.of()); - case NOT_FOUND -> FrontendVisibleValueResolution.notFound(List.of()); - }; - } - - private void publishValueResolution( - @NotNull IdentifierExpression identifierExpression, - @NotNull FrontendVisibleValueResolution resolution - ) { - switch (resolution.status()) { - case FOUND_ALLOWED -> publishScopeValueBinding( - identifierExpression, - resolution.visibleValue(), - ScopeLookupStatus.FOUND_ALLOWED - ); - case FOUND_BLOCKED -> { - publishScopeValueBinding( - identifierExpression, - resolution.visibleValue(), - ScopeLookupStatus.FOUND_BLOCKED - ); - reportBindingError( - identifierExpression, - "Binding '" + identifierExpression.name() + "' is not accessible in the current context" - ); - } - case NOT_FOUND -> { - publishBinding(identifierExpression, identifierExpression.name(), FrontendBindingKind.UNKNOWN, null); - reportBindingError( - identifierExpression, - "Unable to resolve value binding '" + identifierExpression.name() + "'" - ); - } - case DEFERRED_UNSUPPORTED -> reportDeferredUnsupported( - identifierExpression, - identifierExpression.name(), - resolution.deferredBoundary() - ); - } - } - - private void bindTopLevelTypeMetaCandidate(@NotNull IdentifierExpression identifierExpression) { - var currentScope = findCurrentScope(identifierExpression); - if (currentScope == null) { - reportMissingScopeUnsupported(identifierExpression, identifierExpression.name()); - return; - } - - if (trySealPropertyInitializerValueBoundary(identifierExpression)) { - return; - } - - var valueResolution = resolveVisibleValue(identifierExpression); - if (valueResolution.status() == FrontendVisibleValueStatus.DEFERRED_UNSUPPORTED) { - reportDeferredUnsupported( - identifierExpression, - identifierExpression.name(), - valueResolution.deferredBoundary() - ); - return; - } - - var typeMetaResult = moduleSkeleton.resolveSourceFacingTypeMeta( - currentScope, - identifierExpression.name(), - currentRestriction - ); - if (shouldPreferGlobalEnumTypeMeta(valueResolution, typeMetaResult)) { - var typeMeta = typeMetaResult.requireValue(); - publishBinding( - identifierExpression, - identifierExpression.name(), - FrontendBindingKind.TYPE_META, - typeMeta.declaration() - ); - return; - } - if (valueResolution.status() == FrontendVisibleValueStatus.FOUND_ALLOWED - || valueResolution.status() == FrontendVisibleValueStatus.FOUND_BLOCKED) { - publishValueResolution(identifierExpression, valueResolution); - reportLocalTypeMetaShadowing(identifierExpression, valueResolution.visibleValue(), typeMetaResult); - return; - } - if (trySealPropertyInitializerFunctionBoundary(identifierExpression)) { - return; - } - - var functionResult = currentScope.resolveFunctions(identifierExpression.name(), currentRestriction); - switch (functionResult.status()) { - case FOUND_ALLOWED -> { - publishFunctionBinding(identifierExpression, functionResult.requireValue(), false); - return; - } - case FOUND_BLOCKED -> { - publishFunctionBinding(identifierExpression, functionResult.requireValue(), true); - return; - } - case NOT_FOUND -> { - } - } - - if (typeMetaResult.isAllowed()) { - var typeMeta = typeMetaResult.requireValue(); - if (supportsTopLevelTypeMeta(typeMeta)) { - publishBinding( - identifierExpression, - identifierExpression.name(), - FrontendBindingKind.TYPE_META, - typeMeta.declaration() - ); - } else { - reportBindingError( - identifierExpression, - "Top-level type-meta binding for '" + identifierExpression.name() - + "' currently supports class-like types, builtin static receivers, and global enums" - ); - } - return; - } - - publishValueResolution(identifierExpression, valueResolution); - } - - private void reportLocalTypeMetaShadowing( - @NotNull IdentifierExpression identifierExpression, - @Nullable ScopeValue visibleValue, - @NotNull ScopeLookupResult typeMetaResult - ) { - var resolvedValue = Objects.requireNonNull(visibleValue, "visibleValue must not be null"); - if (!isLocalLikeShadowingValue(resolvedValue)) { - return; - } - if (!typeMetaResult.isAllowed()) { - return; - } - var typeMeta = typeMetaResult.requireValue(); - if (!supportsTopLevelTypeMeta(typeMeta)) { - return; - } - reportBindingError( - identifierExpression, - "Explicit receiver chain head '" + identifierExpression.name() - + "' resolves to a local value and shadows a visible type-meta candidate" - ); - } - - private boolean isLocalLikeShadowingValue(@NotNull ScopeValue scopeValue) { - return switch (scopeValue.kind()) { - case LOCAL, PARAMETER, CAPTURE -> true; - default -> false; - }; - } - - private void bindBareCalleeIdentifier(@NotNull IdentifierExpression identifierExpression) { - if (trySealPropertyInitializerFunctionBoundary(identifierExpression)) { - return; - } - - var currentScope = findCurrentScope(identifierExpression); - if (currentScope == null) { - reportMissingScopeUnsupported(identifierExpression, identifierExpression.name()); - return; - } - var functionResult = currentScope.resolveFunctions(identifierExpression.name(), currentRestriction); - switch (functionResult.status()) { - case FOUND_ALLOWED -> - publishFunctionBinding(identifierExpression, functionResult.requireValue(), false); - case FOUND_BLOCKED -> publishFunctionBinding(identifierExpression, functionResult.requireValue(), true); - case NOT_FOUND -> { - var typeMetaResult = moduleSkeleton.resolveSourceFacingTypeMeta( - currentScope, - identifierExpression.name(), - currentRestriction - ); - if (typeMetaResult.isAllowed() && supportsTopLevelTypeMeta(typeMetaResult.requireValue())) { - publishBinding( - identifierExpression, - identifierExpression.name(), - FrontendBindingKind.TYPE_META, - typeMetaResult.requireValue().declaration() - ); - return; - } - publishBinding(identifierExpression, identifierExpression.name(), FrontendBindingKind.UNKNOWN, null); - reportBindingError( - identifierExpression, - "Unable to resolve bare callee binding '" + identifierExpression.name() + "'" - ); - } - } - } - - /// Bare-callee binding consumes the nearest overload set chosen by - /// `Scope.resolveFunctions(...)` and classifies only its symbol category. - private void publishFunctionBinding( - @NotNull IdentifierExpression identifierExpression, - @NotNull List overloadSet, - boolean blocked - ) { - var bindingKind = classifyFunctionBindingKind(identifierExpression, overloadSet); - if (bindingKind == null) { - return; - } - publishBinding( - identifierExpression, - identifierExpression.name(), - bindingKind, - List.copyOf(overloadSet) - ); - if (blocked) { - reportBindingError( - identifierExpression, - "Binding '" + identifierExpression.name() + "' is not accessible in the current context" - ); - } - } - - private @Nullable FrontendBindingKind classifyFunctionBindingKind( - @NotNull IdentifierExpression identifierExpression, - @NotNull List overloadSet - ) { - var survivingOverloads = List.copyOf(Objects.requireNonNull(overloadSet, "overloadSet must not be null")); - if (survivingOverloads.isEmpty()) { - reportBindingError( - identifierExpression, - "Bare callee binding '" + identifierExpression.name() + "' resolved to an empty overload set" - ); - return null; - } - - var allUtilityFunctions = survivingOverloads.stream().allMatch(ExtensionUtilityFunction.class::isInstance); - if (allUtilityFunctions) { - return FrontendBindingKind.UTILITY_FUNCTION; - } - var anyUtilityFunction = survivingOverloads.stream().anyMatch(ExtensionUtilityFunction.class::isInstance); - if (anyUtilityFunction) { - reportBindingError( - identifierExpression, - "Bare callee binding '" + identifierExpression.name() - + "' produced a mixed utility/member overload set" - ); - return null; - } - - var allStatic = survivingOverloads.stream().allMatch(FunctionDef::isStatic); - if (allStatic) { - return FrontendBindingKind.STATIC_METHOD; - } - var anyStatic = survivingOverloads.stream().anyMatch(FunctionDef::isStatic); - if (anyStatic) { - reportBindingError( - identifierExpression, - "Bare callee binding '" + identifierExpression.name() - + "' produced a mixed static/non-static overload set" - ); - return null; - } - return FrontendBindingKind.METHOD; - } - - private void publishScopeValueBinding( - @NotNull IdentifierExpression identifierExpression, - @Nullable ScopeValue scopeValue, - @NotNull ScopeLookupStatus accessStatus - ) { - var resolvedValue = Objects.requireNonNull(scopeValue, "scopeValue must not be null"); - if (accessStatus == ScopeLookupStatus.NOT_FOUND) { - throw new IllegalArgumentException("scope value binding must be a found result"); - } - publishBinding( - identifierExpression, - identifierExpression.name(), - toBindingKind(resolvedValue.kind()), - resolvedValue.declaration(), - resolvedValue, - accessStatus - ); - } - - private boolean supportsTopLevelTypeMeta(@NotNull ScopeTypeMeta typeMeta) { - return switch (typeMeta.kind()) { - case GDCC_CLASS, ENGINE_CLASS, BUILTIN -> !typeMeta.pseudoType(); - case GLOBAL_ENUM -> typeMeta.declaration() != null; - }; - } - - private boolean shouldPreferGlobalEnumTypeMeta( - @NotNull FrontendVisibleValueResolution valueResolution, - @NotNull ScopeLookupResult typeMetaResult - ) { - if (valueResolution.status() != FrontendVisibleValueStatus.FOUND_ALLOWED) { - return false; - } - var visibleValue = valueResolution.visibleValue(); - if (visibleValue == null || visibleValue.kind() != ScopeValueKind.GLOBAL_ENUM) { - return false; - } - return typeMetaResult.isAllowed() - && typeMetaResult.requireValue().kind() == ScopeTypeMetaKind.GLOBAL_ENUM - && supportsTopLevelTypeMeta(typeMetaResult.requireValue()); - } - - /// Dual-role chain-head route bias entry point. - /// - /// When the `AttributeExpression` base is a bare `IdentifierExpression` whose value - /// namespace resolves to `SINGLETON` (via `resolveVisibleValue`, which honors - /// declaration-order filtering and self-reference sealing) and whose type-meta - /// namespace also resolves to `ENGINE_CLASS`, inspect the first suffix step to decide - /// whether the head should publish `TYPE_META` (for static constant / enum value / - /// static method / constructor `.new()` routes) or stay on the ordinary `SINGLETON` - /// value path (for instance method / property routes). - /// - /// This method MUST consume `resolveVisibleValue(...)` as the value-winner authority, - /// matching the contract of `bindTopLevelTypeMetaCandidate(...)`: if a local, parameter, - /// property, or other non-singleton value wins the visible-value resolution, the bias - /// must not override it — the caller falls through to the ordinary - /// `bindTopLevelTypeMetaCandidate(...)` flow which publishes the value winner and - /// reports shadowing diagnostics. Similarly, `FOUND_BLOCKED` and `DEFERRED_UNSUPPORTED` - /// value states are handled by the ordinary flow, not here. - /// - /// The decision is fail-closed: if the first suffix can be satisfied by both the - /// singleton instance namespace (instance method, instance property, or signal) and the - /// type-meta static namespace, the head keeps `SINGLETON` to avoid silently changing the - /// route based on registry traversal order. - /// - /// Returns `true` when a `TYPE_META` binding was published and the caller must skip the - /// ordinary base walk; returns `false` when the ordinary walk should proceed. - private boolean tryApplyDualRoleTypeMetaBias(@NotNull AttributeExpression attributeExpression) { - if (!(attributeExpression.base() instanceof IdentifierExpression identifierExpression)) { - return false; - } - if (attributeExpression.steps().isEmpty()) { - return false; - } - var currentScope = findCurrentScope(identifierExpression); - if (currentScope == null) { - return false; - } - var name = identifierExpression.name(); - - // Value-winner authority: resolveVisibleValue honors declaration-order filtering, - // self-reference sealing, and deferred boundaries. Only a FOUND_ALLOWED SINGLETON - // value winner is eligible for the dual-role bias. All other value states (local, - // parameter, property, blocked, deferred, not-found) must fall through to the - // ordinary bindTopLevelTypeMetaCandidate flow. - if (trySealPropertyInitializerValueBoundary(identifierExpression)) { - return false; - } - var valueResolution = resolveVisibleValue(identifierExpression); - if (valueResolution.status() != FrontendVisibleValueStatus.FOUND_ALLOWED) { - return false; - } - var visibleValue = valueResolution.visibleValue(); - if (visibleValue == null || visibleValue.kind() != ScopeValueKind.SINGLETON) { - return false; - } - var singletonType = classRegistry.findSingletonType(name); - if (singletonType == null) { - return false; - } - - // Type-meta namespace must also resolve the same name to ENGINE_CLASS (or GDCC_CLASS). - var typeMetaResult = moduleSkeleton.resolveSourceFacingTypeMeta( - currentScope, - name, - currentRestriction - ); - if (!typeMetaResult.isAllowed()) { - return false; - } - var typeMeta = typeMetaResult.requireValue(); - if (typeMeta.kind() != ScopeTypeMetaKind.ENGINE_CLASS - && typeMeta.kind() != ScopeTypeMetaKind.GDCC_CLASS) { - return false; - } - if (!supportsTopLevelTypeMeta(typeMeta)) { - return false; - } - - var firstStep = attributeExpression.steps().getFirst(); - var stepName = extractStepName(firstStep); - if (stepName == null) { - return false; - } - - // Constructor-like `.new()` route: prefer TYPE_META, but still respect fail-closed. - // If the singleton declared type has an instance method, instance property, or signal - // named "new", the suffix resolves in the singleton instance namespace, so the head - // must stay SINGLETON to avoid silently stealing an instance-member route. Only when - // "new" does NOT resolve as a singleton instance member do we switch to TYPE_META and - // let the downstream constructor route decide legality. - if (firstStep instanceof AttributeCallStep && stepName.equals("new")) { - if (!resolvesInSingletonInstanceNamespace(singletonType, stepName)) { - publishBinding( - identifierExpression, - name, - FrontendBindingKind.TYPE_META, - typeMeta.declaration() - ); - return true; - } - return false; - } - - // For property steps, check whether the suffix only resolves in the type-meta static - // namespace (engine class constant, class enum value, or static method reference). - // For call steps (non-`new`), check whether the suffix only resolves as a static method. - boolean inTypeMetaStatic = resolvesInTypeMetaStaticNamespace(typeMeta, stepName); - boolean inSingletonInstance = resolvesInSingletonInstanceNamespace(singletonType, stepName); - - // Fail-closed: only switch to TYPE_META when the suffix is NOT available as a - // singleton instance member (instance method, instance property, or signal). This - // prevents silently changing the route when both namespaces can satisfy the same - // suffix name. - if (inTypeMetaStatic && !inSingletonInstance) { - publishBinding( - identifierExpression, - name, - FrontendBindingKind.TYPE_META, - typeMeta.declaration() - ); - return true; - } - return false; - } - - /// Extracts the member name from the first attribute step, or `null` for unknown step types. - private @Nullable String extractStepName(@NotNull AttributeStep step) { - return switch (step) { - case AttributePropertyStep propertyStep -> propertyStep.name(); - case AttributeCallStep callStep -> callStep.name(); - case AttributeSubscriptStep subscriptStep -> subscriptStep.name(); - default -> null; - }; - } - - /// Checks whether `stepName` resolves in the type-meta static namespace. Engine class - /// constants and class enum values use the registry's inherited lookup, while static - /// methods keep the existing hierarchy walk shared with GDCC classes. - private boolean resolvesInTypeMetaStaticNamespace( - @NotNull ScopeTypeMeta typeMeta, - @NotNull String stepName - ) { - if (typeMeta.declaration() instanceof ExtensionGdClass engineClass) { - if (classRegistry.findEngineClassConstantInHierarchy(engineClass.getName(), stepName) != null) { - return true; - } - if (classRegistry.findEngineClassEnumValueInHierarchy(engineClass.getName(), stepName) != null) { - return true; - } - } else if (classRegistry.getClassDef( - typeMeta.instanceType() instanceof GdObjectType ot ? ot : new GdObjectType(typeMeta.canonicalName()) - ) instanceof ExtensionGdClass engineClass) { - if (classRegistry.findEngineClassConstantInHierarchy(engineClass.getName(), stepName) != null) { - return true; - } - if (classRegistry.findEngineClassEnumValueInHierarchy(engineClass.getName(), stepName) != null) { - return true; - } - } - return hasStaticMethodInHierarchy(typeMeta, stepName); - } - - /// Checks whether `stepName` resolves as a singleton instance member: instance method, - /// instance property, or signal (walking the class hierarchy of the singleton declared - /// type). Signal is included defensively so that when signal chain access enters the - /// supported grammar, the fail-closed rule already covers it instead of silently - /// switching the head to TYPE_META. - private boolean resolvesInSingletonInstanceNamespace( - @NotNull GdObjectType singletonType, - @NotNull String stepName - ) { - return hasInstanceMethodInHierarchy(singletonType, stepName) - || hasInstancePropertyInHierarchy(singletonType, stepName) - || hasSignalInHierarchy(singletonType, stepName); - } - - /// Walks the class hierarchy starting from `typeMeta` to check if a static method named - /// `stepName` exists. Covers both ENGINE_CLASS and GDCC_CLASS. - private boolean hasStaticMethodInHierarchy(@NotNull ScopeTypeMeta typeMeta, @NotNull String stepName) { - ClassDef current = classRegistry.resolveClassDefFromTypeMeta(typeMeta); - var visited = new HashSet(); - while (current != null && visited.add(current.getName())) { - var found = current.getFunctions().stream() - .anyMatch(fn -> fn.getName().equals(stepName) && fn.isStatic()); - if (found) { - return true; - } - current = classRegistry.resolveSuperclass(current); - } - return false; - } - - /// Walks the class hierarchy of the singleton declared type to check if an instance method - /// named `stepName` exists. - private boolean hasInstanceMethodInHierarchy(@NotNull GdObjectType singletonType, @NotNull String stepName) { - ClassDef current = classRegistry.getClassDef(singletonType); - var visited = new HashSet(); - while (current != null && visited.add(current.getName())) { - var found = current.getFunctions().stream() - .anyMatch(fn -> fn.getName().equals(stepName) && !fn.isStatic()); - if (found) { - return true; - } - current = classRegistry.resolveSuperclass(current); - } - return false; - } - - /// Walks the class hierarchy of the singleton declared type to check if an instance - /// property named `stepName` exists. - private boolean hasInstancePropertyInHierarchy(@NotNull GdObjectType singletonType, @NotNull String stepName) { - ClassDef current = classRegistry.getClassDef(singletonType); - var visited = new HashSet(); - while (current != null && visited.add(current.getName())) { - var found = current.getProperties().stream() - .anyMatch(prop -> prop.getName().equals(stepName) && !prop.isStatic()); - if (found) { - return true; - } - current = classRegistry.resolveSuperclass(current); - } - return false; - } - - /// Walks the class hierarchy of the singleton declared type to check if a signal named - /// `stepName` exists. Signal is checked defensively so the fail-closed rule covers it - /// even before signal chain access enters the supported grammar. - private boolean hasSignalInHierarchy(@NotNull GdObjectType singletonType, @NotNull String stepName) { - ClassDef current = classRegistry.getClassDef(singletonType); - var visited = new HashSet(); - while (current != null && visited.add(current.getName())) { - var found = current.getSignals().stream() - .anyMatch(signal -> signal.getName().equals(stepName)); - if (found) { - return true; - } - current = classRegistry.resolveSuperclass(current); - } - return false; - } - - private @Nullable Scope findCurrentScope(@NotNull IdentifierExpression identifierExpression) { - return scopesByAst.get(Objects.requireNonNull(identifierExpression, "identifierExpression must not be null")); - } - - private boolean trySealPropertyInitializerValueBoundary(@NotNull IdentifierExpression identifierExpression) { - if (supportedPropertyInitializerDepth <= 0) { - return false; - } - var currentInstanceValueKind = FrontendPropertyInitializerSupport.currentInstanceHierarchyNonStaticValueKind( - currentPropertyInitializerContext, - identifierExpression.name() - ); - if (currentInstanceValueKind == null) { - return false; - } - publishBinding( - identifierExpression, - identifierExpression.name(), - toBindingKind(currentInstanceValueKind), - null - ); - reportPropertyInitializerUnsupportedBoundary( - identifierExpression, - FrontendPropertyInitializerSupport.unsupportedValueMessage( - identifierExpression.name(), - currentInstanceValueKind - ) - ); - return true; - } - - private boolean trySealPropertyInitializerFunctionBoundary(@NotNull IdentifierExpression identifierExpression) { - if (supportedPropertyInitializerDepth <= 0 || !FrontendPropertyInitializerSupport.hasCurrentInstanceHierarchyNonStaticFunction( - currentPropertyInitializerContext, - identifierExpression.name() - )) { - return false; - } - publishBinding( - identifierExpression, - identifierExpression.name(), - FrontendBindingKind.METHOD, - null - ); - reportPropertyInitializerUnsupportedBoundary( - identifierExpression, - FrontendPropertyInitializerSupport.unsupportedMethodMessage(identifierExpression.name()) - ); - return true; - } - - private void reportMissingScopeUnsupported( - @NotNull IdentifierExpression identifierExpression, - @NotNull String symbolName - ) { - reportDeferredUnsupported( - identifierExpression, - symbolName, - new FrontendVisibleValueDeferredBoundary( - FrontendVisibleValueDomain.UNKNOWN_OR_SKIPPED_SUBTREE, - FrontendVisibleValueDeferredReason.MISSING_SCOPE_OR_SKIPPED_SUBTREE - ) - ); - } - - private @NotNull FrontendBindingKind toBindingKind(@NotNull ScopeValueKind scopeValueKind) { - return switch (Objects.requireNonNull(scopeValueKind, "scopeValueKind must not be null")) { - case LOCAL -> FrontendBindingKind.LOCAL_VAR; - case PARAMETER -> FrontendBindingKind.PARAMETER; - case CAPTURE -> FrontendBindingKind.CAPTURE; - case PROPERTY -> FrontendBindingKind.PROPERTY; - case SIGNAL -> FrontendBindingKind.SIGNAL; - case CONSTANT -> FrontendBindingKind.CONSTANT; - case SINGLETON -> FrontendBindingKind.SINGLETON; - case GLOBAL_ENUM -> FrontendBindingKind.GLOBAL_ENUM; - case TYPE_META -> FrontendBindingKind.TYPE_META; - }; - } - - private void publishBinding( - @NotNull Node useSite, - @NotNull String symbolName, - @NotNull FrontendBindingKind kind, - @Nullable Object declarationSite - ) { - publishBinding(useSite, symbolName, kind, declarationSite, null, null); - } - - private void publishBinding( - @NotNull Node useSite, - @NotNull String symbolName, - @NotNull FrontendBindingKind kind, - @Nullable Object declarationSite, - @Nullable ScopeValue resolvedValue, - @Nullable ScopeLookupStatus valueAccessStatus - ) { - symbolBindings.put( - useSite, - new FrontendBinding( - Objects.requireNonNull(symbolName, "symbolName must not be null"), - Objects.requireNonNull(kind, "kind must not be null"), - declarationSite, - resolvedValue, - valueAccessStatus - ) - ); - } - - private void reportBindingError(@NotNull Node useSite, @NotNull String message) { - diagnosticManager.error( - BINDING_CATEGORY, - Objects.requireNonNull(message, "message must not be null"), - sourcePath, - FrontendRange.fromAstRange(useSite.range()) - ); - } - - private void reportPropertyInitializerUnsupportedBoundary( - @NotNull Node useSite, - @NotNull String message - ) { - diagnosticManager.error( - UNSUPPORTED_BINDING_SUBTREE_CATEGORY, - Objects.requireNonNull(message, "message must not be null"), - sourcePath, - FrontendRange.fromAstRange(useSite.range()) - ); - } - - private void reportDeferredSubtree( - @NotNull Node subtreeRoot, - @NotNull FrontendVisibleValueDomain domain - ) { - if (reportedUnsupportedRoots.putIfAbsent(subtreeRoot, Boolean.TRUE) != null) { - return; - } - reportBindingBoundary(subtreeRoot, domain, false, null); - } - - private void reportSkippedSubtree(@Nullable Node subtreeRoot) { - if (subtreeRoot == null) { - return; - } - reportDeferredSubtree(subtreeRoot, FrontendVisibleValueDomain.UNKNOWN_OR_SKIPPED_SUBTREE); - } - - private void reportDeferredUnsupported( - @NotNull Node useSite, - @NotNull String symbolName, - @Nullable FrontendVisibleValueDeferredBoundary deferredBoundary - ) { - var boundary = Objects.requireNonNull(deferredBoundary, "deferredBoundary must not be null"); - reportBindingBoundary( - useSite, - boundary.domain(), - boundary.reason() == FrontendVisibleValueDeferredReason.MISSING_SCOPE_OR_SKIPPED_SUBTREE, - symbolName - ); - } - - @SuppressWarnings("SwitchStatementWithTooFewBranches") - private void reportBindingBoundary( - @NotNull Node anchor, - @NotNull FrontendVisibleValueDomain domain, - boolean skippedRecoveryBoundary, - @Nullable String symbolName - ) { - var formattedDomain = formatDomain(domain); - var message = switch (Objects.requireNonNull(domain, "domain must not be null")) { - case UNKNOWN_OR_SKIPPED_SUBTREE -> symbolName == null - ? "Binding analysis skipped in " + formattedDomain - : "Binding analysis for '" + symbolName + "' was skipped in " + formattedDomain; - default -> symbolName == null - ? "Binding analysis is not supported in " + formattedDomain - : "Binding analysis for '" + symbolName + "' is not supported in " + formattedDomain; - }; - if (domain == FrontendVisibleValueDomain.UNKNOWN_OR_SKIPPED_SUBTREE || skippedRecoveryBoundary) { - diagnosticManager.warning( - UNSUPPORTED_BINDING_SUBTREE_CATEGORY, - message, - sourcePath, - FrontendRange.fromAstRange(anchor.range()) - ); - return; - } - diagnosticManager.error( - UNSUPPORTED_BINDING_SUBTREE_CATEGORY, - message, - sourcePath, - FrontendRange.fromAstRange(anchor.range()) - ); - } - - private @NotNull String formatDomain(@NotNull FrontendVisibleValueDomain domain) { - return switch (Objects.requireNonNull(domain, "domain must not be null")) { - case EXECUTABLE_BODY -> "executable body"; - case PARAMETER_DEFAULT -> "parameter default"; - case LAMBDA_SUBTREE -> "lambda subtree"; - case BLOCK_LOCAL_CONST_SUBTREE -> "block-local const initializer"; - case FOR_SUBTREE -> "for subtree"; - case MATCH_SUBTREE -> "match subtree"; - case UNKNOWN_OR_SKIPPED_SUBTREE -> "skipped subtree"; - }; - } - - private boolean isNotPublished(@Nullable Node node) { - return node == null || !scopesByAst.containsKey(node); - } - } -} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendVarTypePostAnalyzer.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendVarTypePostAnalyzer.java deleted file mode 100644 index 1177ffc4..00000000 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendVarTypePostAnalyzer.java +++ /dev/null @@ -1,485 +0,0 @@ -package gd.script.gdcc.frontend.sema.analyzer; - -import gd.script.gdcc.frontend.diagnostic.DiagnosticManager; -import gd.script.gdcc.frontend.diagnostic.FrontendRange; -import gd.script.gdcc.frontend.scope.BlockScope; -import gd.script.gdcc.frontend.scope.CallableScope; -import gd.script.gdcc.frontend.sema.FrontendAnalysisData; -import gd.script.gdcc.frontend.sema.FrontendAstSideTable; -import gd.script.gdcc.frontend.sema.FrontendExecutableInventorySupport; -import gd.script.gdcc.scope.Scope; -import gd.script.gdcc.scope.ScopeValue; -import gd.script.gdcc.scope.ScopeValueKind; -import gd.script.gdcc.type.GdType; -import dev.superice.gdparser.frontend.ast.ASTNodeHandler; -import dev.superice.gdparser.frontend.ast.ASTWalker; -import dev.superice.gdparser.frontend.ast.Block; -import dev.superice.gdparser.frontend.ast.ClassDeclaration; -import dev.superice.gdparser.frontend.ast.ConstructorDeclaration; -import dev.superice.gdparser.frontend.ast.DeclarationKind; -import dev.superice.gdparser.frontend.ast.ElifClause; -import dev.superice.gdparser.frontend.ast.ForStatement; -import dev.superice.gdparser.frontend.ast.FrontendASTTraversalDirective; -import dev.superice.gdparser.frontend.ast.FunctionDeclaration; -import dev.superice.gdparser.frontend.ast.IfStatement; -import dev.superice.gdparser.frontend.ast.LambdaExpression; -import dev.superice.gdparser.frontend.ast.MatchStatement; -import dev.superice.gdparser.frontend.ast.Node; -import dev.superice.gdparser.frontend.ast.Parameter; -import dev.superice.gdparser.frontend.ast.SourceFile; -import dev.superice.gdparser.frontend.ast.Statement; -import dev.superice.gdparser.frontend.ast.VariableDeclaration; -import dev.superice.gdparser.frontend.ast.WhileStatement; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.nio.file.Path; -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.List; -import java.util.Objects; - -/// Publishes final callable-local slot types after local stabilization and expression typing have -/// settled the lexical inventory. -/// -/// This phase intentionally republishes the current callable-local inventory into `slotTypes()`. -/// It does not infer initializer types; local stabilization and expression typing have already -/// settled the source slots that lowering is allowed to consume. -public class FrontendVarTypePostAnalyzer { - public static final @NotNull String VARIABLE_SLOT_PUBLICATION_CATEGORY = "sema.variable_slot_publication"; - - public void analyze( - @NotNull FrontendAnalysisData analysisData, - @NotNull DiagnosticManager diagnosticManager - ) { - Objects.requireNonNull(analysisData, "analysisData must not be null"); - Objects.requireNonNull(diagnosticManager, "diagnosticManager must not be null"); - - var moduleSkeleton = analysisData.moduleSkeleton(); - analysisData.diagnostics(); - - var scopesByAst = analysisData.scopesByAst(); - for (var sourceClassRelation : moduleSkeleton.sourceClassRelations()) { - var sourceFile = sourceClassRelation.unit().ast(); - if (!scopesByAst.containsKey(sourceFile)) { - throw new IllegalStateException( - "Scope graph has not been published for source file: " + sourceClassRelation.unit().path() - ); - } - } - - var slotTypes = analysisData.slotTypes(); - slotTypes.clear(); - for (var sourceClassRelation : moduleSkeleton.sourceClassRelations()) { - new SlotTypePublisher( - sourceClassRelation.unit().path(), - scopesByAst, - diagnosticManager, - slotTypes - ).walk(sourceClassRelation.unit().ast()); - } - analysisData.updateSlotTypes(slotTypes); - } - - /// Traverses only the current supported executable surface and republishes the already-settled - /// inventory slot types on declaration-site AST identities, clearing any stale slot table facts - /// from earlier publication attempts. - private static final class SlotTypePublisher implements ASTNodeHandler { - private final @NotNull Path sourcePath; - private final @NotNull FrontendAstSideTable scopesByAst; - private final @NotNull DiagnosticManager diagnosticManager; - private final @NotNull FrontendAstSideTable slotTypes; - private final @NotNull ASTWalker astWalker; - private final @NotNull Deque blockScopeStack = new ArrayDeque<>(); - private @Nullable Node currentCallableOwner; - private int supportedExecutableBlockDepth; - - private SlotTypePublisher( - @NotNull Path sourcePath, - @NotNull FrontendAstSideTable scopesByAst, - @NotNull DiagnosticManager diagnosticManager, - @NotNull FrontendAstSideTable slotTypes - ) { - this.sourcePath = Objects.requireNonNull(sourcePath, "sourcePath"); - this.scopesByAst = Objects.requireNonNull(scopesByAst, "scopesByAst"); - this.diagnosticManager = Objects.requireNonNull(diagnosticManager, "diagnosticManager"); - this.slotTypes = Objects.requireNonNull( - slotTypes, - "slotTypes" - ); - astWalker = new ASTWalker(this); - } - - private void walk(@NotNull SourceFile sourceFile) { - astWalker.walk(sourceFile); - } - - @Override - public @NotNull FrontendASTTraversalDirective handleNode(@NotNull Node node) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleSourceFile(@NotNull SourceFile sourceFile) { - walkNonExecutableContainerStatements(sourceFile.statements()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleClassDeclaration(@NotNull ClassDeclaration classDeclaration) { - if (isNotPublished(classDeclaration)) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkNonExecutableContainerStatements(classDeclaration.body().statements()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleFunctionDeclaration( - @NotNull FunctionDeclaration functionDeclaration - ) { - publishCallableSlots(functionDeclaration, functionDeclaration.parameters(), functionDeclaration.body()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleConstructorDeclaration( - @NotNull ConstructorDeclaration constructorDeclaration - ) { - publishCallableSlots(constructorDeclaration, constructorDeclaration.parameters(), constructorDeclaration.body()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleBlock(@NotNull Block block) { - if (supportedExecutableBlockDepth <= 0 || isNotPublished(block)) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - var blockScope = requireBlockScope(block, "Executable block expected published BlockScope"); - blockScopeStack.addLast(blockScope); - try { - walkStatements(block.statements()); - } finally { - blockScopeStack.removeLast(); - } - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleIfStatement(@NotNull IfStatement ifStatement) { - if (supportedExecutableBlockDepth <= 0 || isNotPublished(ifStatement)) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkSupportedExecutableBlock(ifStatement.body()); - for (var elifClause : ifStatement.elifClauses()) { - astWalker.walk(elifClause); - } - if (ifStatement.elseBody() != null) { - walkSupportedExecutableBlock(ifStatement.elseBody()); - } - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleElifClause(@NotNull ElifClause elifClause) { - if (supportedExecutableBlockDepth <= 0 || isNotPublished(elifClause)) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkSupportedExecutableBlock(elifClause.body()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleWhileStatement(@NotNull WhileStatement whileStatement) { - if (supportedExecutableBlockDepth <= 0 || isNotPublished(whileStatement)) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - walkSupportedExecutableBlock(whileStatement.body()); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleVariableDeclaration( - @NotNull VariableDeclaration variableDeclaration - ) { - if (supportedExecutableBlockDepth <= 0 || variableDeclaration.kind() != DeclarationKind.VAR) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - publishLocalSlotType(variableDeclaration); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleLambdaExpression(@NotNull LambdaExpression lambdaExpression) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleForStatement(@NotNull ForStatement forStatement) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - @Override - public @NotNull FrontendASTTraversalDirective handleMatchStatement(@NotNull MatchStatement matchStatement) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - - private void publishCallableSlots( - @NotNull Node callableOwner, - @NotNull List parameters, - @NotNull Block body - ) { - if (isNotPublished(callableOwner)) { - return; - } - var previousCallableOwner = currentCallableOwner; - currentCallableOwner = callableOwner; - try { - for (var parameter : parameters) { - publishParameterSlotType(parameter); - } - if (isNotPublished(body)) { - return; - } - walkSupportedExecutableBlock(body); - } finally { - currentCallableOwner = previousCallableOwner; - } - } - - private void walkNonExecutableContainerStatements(@NotNull List statements) { - var previousDepth = supportedExecutableBlockDepth; - supportedExecutableBlockDepth = 0; - try { - walkStatements(statements); - } finally { - supportedExecutableBlockDepth = previousDepth; - } - } - - private void walkStatements(@NotNull List statements) { - for (var statement : statements) { - astWalker.walk(statement); - } - } - - private void walkSupportedExecutableBlock(@Nullable Block block) { - if (isNotPublished(block)) { - return; - } - supportedExecutableBlockDepth++; - try { - astWalker.walk(block); - } finally { - supportedExecutableBlockDepth--; - } - } - - private void publishParameterSlotType(@NotNull Parameter parameter) { - var parameterName = parameter.name().trim(); - var declarationScope = scopesByAst.get(parameter); - if (!(declarationScope instanceof CallableScope callableScope)) { - throw new IllegalStateException( - "Parameter '" + parameterName + "' expected published CallableScope in " + sourcePath - ); - } - var slot = requireDeclaredSlot( - callableScope.resolveValueHere(parameterName), - "Parameter '" + parameterName + "' has no published inventory slot" - ); - requireMatchingDeclaration( - slot, - ScopeValueKind.PARAMETER, - parameter, - "Parameter '" + parameterName + "' inventory slot drifted from its declaration site" - ); - slotTypes.put(parameter, slot.type()); - } - - private void publishLocalSlotType(@NotNull VariableDeclaration variableDeclaration) { - var variableName = variableDeclaration.name().trim(); - var declarationScope = scopesByAst.get(variableDeclaration); - if (!(declarationScope instanceof BlockScope blockScope)) { - throw new IllegalStateException( - "Local variable '" + variableName + "' expected published BlockScope in " + sourcePath - ); - } - if (!FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(blockScope.kind())) { - return; - } - if (blockScope != currentBlockScope()) { - throw new IllegalStateException( - "Local variable '" + variableName + "' drifted away from its enclosing block scope in " + sourcePath - ); - } - var currentLayerSlot = blockScope.resolveValueHere(variableName); - if (currentLayerSlot == null - || currentLayerSlot.kind() != ScopeValueKind.LOCAL - || currentLayerSlot.declaration() != variableDeclaration) { - // Earlier variable inventory may reject duplicate/shadowing locals on purpose. The - // published slot-type surface must stay fail-closed for those declarations, but the - // gap itself still needs to be visible to tooling and compile-only callers. - reportRejectedLocalSlotPublication( - variableDeclaration, - findSurvivingCallableLocalBinding(blockScope, variableName, currentLayerSlot) - ); - return; - } - slotTypes.put(variableDeclaration, currentLayerSlot.type()); - } - - private @NotNull BlockScope requireBlockScope(@NotNull Block block, @NotNull String message) { - var publishedScope = scopesByAst.get(block); - if (publishedScope instanceof BlockScope blockScope) { - return blockScope; - } - throw new IllegalStateException(message + " in " + sourcePath); - } - - private @NotNull BlockScope currentBlockScope() { - var current = blockScopeStack.peekLast(); - if (current == null) { - throw new IllegalStateException("Current executable block scope is unavailable in " + sourcePath); - } - return current; - } - - private @NotNull ScopeValue requireDeclaredSlot( - @Nullable ScopeValue slot, - @NotNull String message - ) { - if (slot == null) { - throw new IllegalStateException(message + " in " + sourcePath); - } - return slot; - } - - private void requireMatchingDeclaration( - @NotNull ScopeValue slot, - @NotNull ScopeValueKind expectedKind, - @NotNull Node declarationSite, - @NotNull String mismatchMessage - ) { - if (slot.kind() != expectedKind || slot.declaration() != declarationSite) { - throw new IllegalStateException(mismatchMessage + " in " + sourcePath); - } - } - - private boolean isNotPublished(@Nullable Node astNode) { - return astNode == null || !scopesByAst.containsKey(astNode); - } - - /// Duplicate locals fail in the current block layer, while shadowing locals fail because a - /// parent block/callable within the same callable boundary already owns the visible name. - /// The post analyzer must describe that surviving binding without accidentally walking into - /// class/global scopes, because only callable-local inventory participates in this contract. - private @Nullable ScopeValue findSurvivingCallableLocalBinding( - @NotNull BlockScope declarationScope, - @NotNull String variableName, - @Nullable ScopeValue currentLayerSlot - ) { - if (currentLayerSlot != null) { - return currentLayerSlot; - } - Scope currentScope = declarationScope.getParentScope(); - while (currentScope != null) { - if (currentScope instanceof BlockScope outerBlockScope) { - var outerLocal = outerBlockScope.resolveValueHere(variableName); - if (outerLocal != null) { - return outerLocal; - } - currentScope = outerBlockScope.getParentScope(); - continue; - } - if (currentScope instanceof CallableScope callableScope) { - return callableScope.resolveValueHere(variableName); - } - return null; - } - return null; - } - - /// Missing callable-local slot facts are warning-level here because the root cause is - /// usually already a source diagnostic from earlier inventory analysis. The warning still - /// matters: compile-only callers need one explicit published signal that lowering-ready slot - /// facts are missing for this declaration. - private void reportRejectedLocalSlotPublication( - @NotNull VariableDeclaration variableDeclaration, - @Nullable ScopeValue survivingSlot - ) { - var variableName = variableDeclaration.name().trim(); - var declarationRange = formatRange(variableDeclaration); - var message = new StringBuilder() - .append("Local variable '") - .append(variableName) - .append("' in ") - .append(describeCurrentLocalContext()) - .append(" has no lowering-ready published slot type at ") - .append(declarationRange) - .append(" in ") - .append(sourcePath) - .append("; the declaration was not accepted into callable-local inventory"); - if (survivingSlot != null && survivingSlot.declaration() instanceof Node survivingDeclaration) { - message.append("; surviving slot currently resolves to ") - .append(describeSurvivingDeclaration(survivingSlot)) - .append(" at ") - .append(formatRange(survivingDeclaration)); - } else { - message.append("; this usually means earlier variable analysis rejected the declaration as duplicate or shadowing"); - } - diagnosticManager.warning( - VARIABLE_SLOT_PUBLICATION_CATEGORY, - message.toString(), - sourcePath, - FrontendRange.fromAstRange(variableDeclaration.range()) - ); - } - - private @NotNull String describeCurrentLocalContext() { - var currentBlockScope = currentBlockScope(); - return switch (currentBlockScope.kind()) { - case FUNCTION_BODY, CONSTRUCTOR_BODY -> describeCallableContext(); - case BLOCK_STATEMENT -> "block statement of " + describeCallableContext(); - case IF_BODY -> "if-body of " + describeCallableContext(); - case ELIF_BODY -> "elif-body of " + describeCallableContext(); - case ELSE_BODY -> "else-body of " + describeCallableContext(); - case WHILE_BODY -> "while-body of " + describeCallableContext(); - case LAMBDA_BODY -> "lambda-body of " + describeCallableContext(); - case FOR_BODY -> "`for` body of " + describeCallableContext(); - case MATCH_SECTION_BODY -> "`match` section of " + describeCallableContext(); - }; - } - - private @NotNull String describeCallableContext() { - return switch (currentCallableOwner) { - case FunctionDeclaration functionDeclaration -> "function '" + functionDeclaration.name().trim() + "'"; - case ConstructorDeclaration _ -> "constructor '_init'"; - case null -> "callable"; - default -> currentCallableOwner.getClass().getSimpleName(); - }; - } - - private static @NotNull String describeSurvivingDeclaration(@NotNull ScopeValue survivingSlot) { - return switch (survivingSlot.kind()) { - case LOCAL -> "another accepted local declaration"; - case PARAMETER -> "the parameter declaration"; - case CAPTURE -> "the capture declaration"; - case CONSTANT -> "the constant declaration"; - default -> "an accepted callable-local binding"; - }; - } - - private static @NotNull String formatRange(@NotNull Node node) { - var range = FrontendRange.fromAstRange(node.range()); - if (range == null) { - return ""; - } - return "%d:%d-%d:%d".formatted( - range.start().line(), - range.start().column(), - range.end().line(), - range.end().column() - ); - } - } -} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendVariableAnalyzer.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendVariableAnalyzer.java index 85478a9b..5ec75e8e 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendVariableAnalyzer.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendVariableAnalyzer.java @@ -43,8 +43,7 @@ /// - require skeleton, diagnostics, and top-level source scopes to be published first /// - write function/constructor parameters into `CallableScope` /// - write supported ordinary locals into `BlockScope` -/// - keep lambda / `for` / `match` / block-local `const` inventory outside the current support -/// boundary +/// - keep lambda / `match` / block-local `const` inventory outside the current support boundary /// - emit explicit recovery diagnostics instead of letting unsupported inventory sources fail silently public class FrontendVariableAnalyzer { private static final @NotNull String VARIABLE_BINDING_CATEGORY = "sema.variable_binding"; @@ -96,7 +95,7 @@ public void analyze( /// - only source/class statement lists and supported executable blocks are descended into /// - function/constructor parameters are bound at the callable boundary /// - ordinary locals are bound only while the walker is inside a supported executable block - /// - lambda / `for` / `match` subtrees are pruned explicitly + /// - lambda / `match` subtrees are pruned explicitly /// - arbitrary expression children stay outside the binding walk private static final class AstWalkerVariableBinder implements ASTNodeHandler { private final @NotNull Path sourcePath; @@ -256,8 +255,11 @@ private void walk(@NotNull SourceFile sourceFile) { @Override public @NotNull FrontendASTTraversalDirective handleForStatement(@NotNull ForStatement forStatement) { - // The binder itself still treats `for` as an unsupported boundary; explicit user - // diagnostics are produced by the dedicated boundary reporter. + if (supportedExecutableBlockDepth <= 0 || isNotPublished(forStatement.body())) { + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } + bindForIterator(forStatement); + walkSupportedExecutableBlock(forStatement.body()); return FrontendASTTraversalDirective.SKIP_CHILDREN; } @@ -417,6 +419,43 @@ private void bindLocal(@NotNull VariableDeclaration variableDeclaration) { } } + private void bindForIterator(@NotNull ForStatement forStatement) { + var iteratorName = forStatement.iterator().trim(); + var targetScope = scopesByAst.get(forStatement.body()); + if (!(targetScope instanceof BlockScope blockScope) + || !FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(blockScope.kind())) { + reportBindingError(forStatement, "Loop iterator '" + iteratorName + "' has no supported `for` body scope"); + return; + } + + var existingLocal = blockScope.resolveValueHere(iteratorName); + if (existingLocal != null) { + reportBindingError(forStatement, "Duplicate loop iterator '" + iteratorName + "' in the same `for` body"); + return; + } + var sameCallableConflict = findSameCallableConflict(blockScope, iteratorName); + if (sameCallableConflict != null) { + reportBindingError( + forStatement, + "Loop iterator '" + iteratorName + "' in " + describeLocalContext(blockScope) + + " shadows " + describeShadowingTarget(sameCallableConflict) + ); + } + + var headerScope = scopesByAst.get(forStatement); + if (headerScope == null) { + throw new IllegalStateException("Loop iterator '" + iteratorName + "' has no published header scope"); + } + var iteratorType = FrontendDeclaredTypeSupport.resolveTypeOrVariant( + forStatement.iteratorType(), + headerScope, + moduleSkeleton.topLevelCanonicalNameMap(), + sourcePath, + diagnosticManager + ); + blockScope.defineLocal(iteratorName, iteratorType, forStatement); + } + /// Duplicate and shadowing locals are user-facing source errors rather than phase /// invariants. The message therefore carries both declaration locations plus the callable / /// block context so compile-only callers can stop before lowering while shared analysis keeps @@ -507,22 +546,8 @@ private void reportLocalConflict( @NotNull BlockScope blockScope, @NotNull String variableName ) { - Scope currentScope = blockScope.getParentScope(); - while (currentScope != null) { - if (currentScope instanceof BlockScope outerBlockScope) { - var outerLocal = outerBlockScope.resolveValueHere(variableName); - if (outerLocal != null) { - return outerLocal; - } - currentScope = outerBlockScope.getParentScope(); - continue; - } - if (currentScope instanceof CallableScope callableScope) { - return callableScope.resolveValueHere(variableName); - } - return null; - } - return null; + return FrontendBodyOwnerProcedures.findCallableLocalBindingUpScopes( + blockScope.getParentScope(), variableName); } private void reportUnsupportedDefaultValue(@NotNull Parameter parameter) { @@ -625,16 +650,6 @@ private void report(@NotNull Block callableBody) { return FrontendASTTraversalDirective.SKIP_CHILDREN; } - @Override - public @NotNull FrontendASTTraversalDirective handleForStatement(@NotNull ForStatement forStatement) { - reportUnsupportedBoundary( - forStatement, - "Variable analysis does not support `for` subtrees yet; the loop iterator binding and " - + "locals declared inside this loop body are not bound yet" - ); - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - @Override public @NotNull FrontendASTTraversalDirective handleMatchStatement(@NotNull MatchStatement matchStatement) { reportUnsupportedBoundary( diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendAssignmentSemanticSupport.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendAssignmentSemanticSupport.java index fc291d99..0f7d58af 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendAssignmentSemanticSupport.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendAssignmentSemanticSupport.java @@ -33,6 +33,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.function.Function; import java.util.function.Supplier; /// Shared assignment semantic helper used by both body-phase analyzers. @@ -195,6 +196,7 @@ private enum PropertyFailureOwnership { public record Context( @NotNull FrontendAstSideTable symbolBindings, + @NotNull Function symbolBindingResolver, @NotNull FrontendAstSideTable scopesByAst, @NotNull FrontendModuleSkeleton moduleSkeleton, @NotNull Supplier restrictionSupplier, @@ -204,6 +206,7 @@ public record Context( ) { public Context { Objects.requireNonNull(symbolBindings, "symbolBindings must not be null"); + Objects.requireNonNull(symbolBindingResolver, "symbolBindingResolver must not be null"); Objects.requireNonNull(scopesByAst, "scopesByAst must not be null"); Objects.requireNonNull(moduleSkeleton, "moduleSkeleton must not be null"); Objects.requireNonNull(restrictionSupplier, "restrictionSupplier must not be null"); @@ -227,6 +230,29 @@ private FrontendAssignmentSemanticSupport() { var actualRegistry = Objects.requireNonNull(classRegistry, "classRegistry must not be null"); return new Context( Objects.requireNonNull(symbolBindings, "symbolBindings must not be null"), + symbolBindings::get, + Objects.requireNonNull(scopesByAst, "scopesByAst must not be null"), + Objects.requireNonNull(moduleSkeleton, "moduleSkeleton must not be null"), + Objects.requireNonNull(restrictionSupplier, "restrictionSupplier must not be null"), + actualRegistry, + Objects.requireNonNull(chainReduction, "chainReduction must not be null"), + new FrontendSubscriptSemanticSupport(actualRegistry) + ); + } + + public static @NotNull Context createContext( + @NotNull FrontendAstSideTable symbolBindings, + @NotNull Function symbolBindingResolver, + @NotNull FrontendAstSideTable scopesByAst, + @NotNull FrontendModuleSkeleton moduleSkeleton, + @NotNull Supplier restrictionSupplier, + @NotNull ClassRegistry classRegistry, + @NotNull FrontendChainReductionFacade chainReduction + ) { + var actualRegistry = Objects.requireNonNull(classRegistry, "classRegistry must not be null"); + return new Context( + Objects.requireNonNull(symbolBindings, "symbolBindings must not be null"), + Objects.requireNonNull(symbolBindingResolver, "symbolBindingResolver must not be null"), Objects.requireNonNull(scopesByAst, "scopesByAst must not be null"), Objects.requireNonNull(moduleSkeleton, "moduleSkeleton must not be null"), Objects.requireNonNull(restrictionSupplier, "restrictionSupplier must not be null"), @@ -390,7 +416,7 @@ private FrontendAssignmentSemanticSupport() { ); } - var publishedBinding = context.symbolBindings().get(identifierExpression); + var publishedBinding = context.symbolBindingResolver().apply(identifierExpression); var detailReason = publishedBinding == null || publishedBinding.kind() == FrontendBindingKind.UNKNOWN ? "No published assignment-target binding fact is available for identifier '" + identifierExpression.name() + "'" diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendChainHeadReceiverSupport.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendChainHeadReceiverSupport.java index 7923f9c7..b25f35fd 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendChainHeadReceiverSupport.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendChainHeadReceiverSupport.java @@ -27,16 +27,17 @@ import org.jetbrains.annotations.Nullable; import java.util.Objects; +import java.util.function.Function; /// Shared analyzer-side support for chain heads and local atomic receiver facts. /// /// Why this helper exists: -/// - both `FrontendChainBindingAnalyzer` and `FrontendExprTypeAnalyzer` need the same rules for -/// turning a chain head into `ReceiverState` +/// - the chain-binding and expression-typing owner procedures need the same rules for turning a +/// chain head into `ReceiverState` /// - those rules are not the chain-reduction core itself; they are the glue that bridges already /// published binding facts into a stable receiver model /// - keeping this logic in one place reduces the risk that `self`, `TYPE_META`, literal heads, or -/// blocked value bindings drift apart between the two analyzers +/// blocked value bindings drift apart between the two owners /// /// What this helper does: /// - consumes already-published `symbolBindings()` and `scopesByAst()` facts @@ -79,6 +80,7 @@ public interface FallbackExpressionReceiverResolver { private final @NotNull FrontendAnalysisData analysisData; private final @NotNull FrontendAstSideTable scopesByAst; + private final @NotNull Function bindingLookup; private final @NotNull ResolveRestriction currentRestriction; private final boolean staticContext; private final @Nullable FrontendPropertyInitializerSupport.PropertyInitializerContext propertyInitializerContext; @@ -106,6 +108,7 @@ public FrontendChainHeadReceiverSupport( this( analysisData, scopesByAst, + analysisData.symbolBindings()::get, currentRestriction, staticContext, null, @@ -122,9 +125,32 @@ public FrontendChainHeadReceiverSupport( @Nullable FrontendPropertyInitializerSupport.PropertyInitializerContext propertyInitializerContext, @NotNull NestedAttributeReceiverResolver nestedAttributeReceiverResolver, @NotNull FallbackExpressionReceiverResolver fallbackExpressionReceiverResolver + ) { + this( + analysisData, + scopesByAst, + analysisData.symbolBindings()::get, + currentRestriction, + staticContext, + propertyInitializerContext, + nestedAttributeReceiverResolver, + fallbackExpressionReceiverResolver + ); + } + + public FrontendChainHeadReceiverSupport( + @NotNull FrontendAnalysisData analysisData, + @NotNull FrontendAstSideTable scopesByAst, + @NotNull Function bindingLookup, + @NotNull ResolveRestriction currentRestriction, + boolean staticContext, + @Nullable FrontendPropertyInitializerSupport.PropertyInitializerContext propertyInitializerContext, + @NotNull NestedAttributeReceiverResolver nestedAttributeReceiverResolver, + @NotNull FallbackExpressionReceiverResolver fallbackExpressionReceiverResolver ) { this.analysisData = Objects.requireNonNull(analysisData, "analysisData must not be null"); this.scopesByAst = Objects.requireNonNull(scopesByAst, "scopesByAst must not be null"); + this.bindingLookup = Objects.requireNonNull(bindingLookup, "bindingLookup must not be null"); this.currentRestriction = Objects.requireNonNull(currentRestriction, "currentRestriction must not be null"); this.staticContext = staticContext; this.propertyInitializerContext = propertyInitializerContext; @@ -175,7 +201,7 @@ public FrontendChainHeadReceiverSupport( @NotNull IdentifierExpression identifierExpression ) { var identifier = Objects.requireNonNull(identifierExpression, "identifierExpression must not be null"); - var binding = analysisData.symbolBindings().get(identifier); + var binding = bindingLookup.apply(identifier); if (binding == null) { return failedHeadReceiver( identifier, @@ -266,7 +292,7 @@ public FrontendChainHeadReceiverSupport( "Value receiver '" + identifier.name() + "' is inside a skipped subtree" ); } - var binding = analysisData.symbolBindings().get(identifier); + var binding = bindingLookup.apply(identifier); if (binding == null) { return new FrontendChainReductionHelper.ReceiverState( FrontendChainReductionHelper.Status.FAILED, diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendChainReductionFacade.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendChainReductionFacade.java index 897b7b7d..e58a534d 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendChainReductionFacade.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendChainReductionFacade.java @@ -6,6 +6,8 @@ import gd.script.gdcc.scope.ResolveRestriction; import gd.script.gdcc.scope.Scope; import dev.superice.gdparser.frontend.ast.AttributeExpression; +import dev.superice.gdparser.frontend.ast.IdentifierExpression; +import gd.script.gdcc.frontend.sema.FrontendBinding; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -13,6 +15,7 @@ import java.util.Objects; import java.util.Optional; import java.util.function.BooleanSupplier; +import java.util.function.Function; import java.util.function.Supplier; /// Analyzer-side cached facade for attribute-chain reduction. @@ -41,6 +44,7 @@ public record CachedReduction( propertyInitializerContextSupplier; private final @NotNull ClassRegistry classRegistry; private final @NotNull FrontendChainReductionHelper.ExpressionTypeResolver expressionTypeResolver; + private final @NotNull Function bindingLookup; private final @NotNull IdentityHashMap> reducedChains = new IdentityHashMap<>(); @@ -59,7 +63,8 @@ public FrontendChainReductionFacade( staticContextSupplier, () -> null, classRegistry, - expressionTypeResolver + expressionTypeResolver, + analysisData.symbolBindings()::get ); } @@ -72,6 +77,29 @@ public FrontendChainReductionFacade( propertyInitializerContextSupplier, @NotNull ClassRegistry classRegistry, @NotNull FrontendChainReductionHelper.ExpressionTypeResolver expressionTypeResolver + ) { + this( + analysisData, + scopesByAst, + restrictionSupplier, + staticContextSupplier, + propertyInitializerContextSupplier, + classRegistry, + expressionTypeResolver, + analysisData.symbolBindings()::get + ); + } + + public FrontendChainReductionFacade( + @NotNull FrontendAnalysisData analysisData, + @NotNull FrontendAstSideTable scopesByAst, + @NotNull Supplier restrictionSupplier, + @NotNull BooleanSupplier staticContextSupplier, + @NotNull Supplier + propertyInitializerContextSupplier, + @NotNull ClassRegistry classRegistry, + @NotNull FrontendChainReductionHelper.ExpressionTypeResolver expressionTypeResolver, + @NotNull Function bindingLookup ) { this.analysisData = Objects.requireNonNull(analysisData, "analysisData must not be null"); this.scopesByAst = Objects.requireNonNull(scopesByAst, "scopesByAst must not be null"); @@ -86,12 +114,14 @@ public FrontendChainReductionFacade( expressionTypeResolver, "expressionTypeResolver must not be null" ); + this.bindingLookup = Objects.requireNonNull(bindingLookup, "bindingLookup must not be null"); } public @NotNull FrontendChainHeadReceiverSupport headReceiverSupport() { return new FrontendChainHeadReceiverSupport( analysisData, scopesByAst, + bindingLookup, restrictionSupplier.get(), staticContextSupplier.getAsBoolean(), propertyInitializerContextSupplier.get(), diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendChainReductionHelper.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendChainReductionHelper.java index aa6590ea..8d5c0ca4 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendChainReductionHelper.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendChainReductionHelper.java @@ -2058,10 +2058,9 @@ yield new StepTrace( @NotNull Expression expression, boolean finalizeWindow ) { - var publishedType = request.analysisData().expressionTypes().get(expression); - if (publishedType != null) { - return ExpressionTypeResult.fromPublished(publishedType); - } + // The injected resolver owns dependency lookup ordering. Body resolvers must see their + // procedure-local transient caches and overlay facts before stable side tables, so this + // helper must not short-circuit through `analysisData.expressionTypes()` first. return request.expressionTypeResolver().resolve(expression, finalizeWindow); } diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendDualRoleTypeMetaRouteSupport.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendDualRoleTypeMetaRouteSupport.java new file mode 100644 index 00000000..dc873de5 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendDualRoleTypeMetaRouteSupport.java @@ -0,0 +1,236 @@ +package gd.script.gdcc.frontend.sema.analyzer.support; + +import dev.superice.gdparser.frontend.ast.AttributeCallStep; +import dev.superice.gdparser.frontend.ast.AttributeExpression; +import dev.superice.gdparser.frontend.ast.AttributePropertyStep; +import dev.superice.gdparser.frontend.ast.AttributeStep; +import dev.superice.gdparser.frontend.ast.AttributeSubscriptStep; +import dev.superice.gdparser.frontend.ast.IdentifierExpression; +import gd.script.gdcc.frontend.sema.FrontendModuleSkeleton; +import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueResolution; +import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueStatus; +import gd.script.gdcc.gdextension.ExtensionGdClass; +import gd.script.gdcc.scope.ClassDef; +import gd.script.gdcc.scope.ClassRegistry; +import gd.script.gdcc.scope.ResolveRestriction; +import gd.script.gdcc.scope.Scope; +import gd.script.gdcc.scope.ScopeLookupResult; +import gd.script.gdcc.scope.ScopeTypeMeta; +import gd.script.gdcc.scope.ScopeTypeMetaKind; +import gd.script.gdcc.scope.ScopeValueKind; +import gd.script.gdcc.type.GdObjectType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashSet; +import java.util.Objects; + +/// Shared route bias for names that are both autoload singletons and type-meta receivers. +public final class FrontendDualRoleTypeMetaRouteSupport { + private FrontendDualRoleTypeMetaRouteSupport() { + } + + public static @Nullable ScopeTypeMeta resolveBiasedTypeMeta( + @NotNull AttributeExpression attributeExpression, + @NotNull FrontendVisibleValueResolution valueResolution, + @NotNull Scope currentScope, + @NotNull ResolveRestriction restriction, + @NotNull FrontendModuleSkeleton moduleSkeleton, + @NotNull ClassRegistry classRegistry + ) { + Objects.requireNonNull(attributeExpression, "attributeExpression must not be null"); + Objects.requireNonNull(valueResolution, "valueResolution must not be null"); + Objects.requireNonNull(currentScope, "currentScope must not be null"); + Objects.requireNonNull(restriction, "restriction must not be null"); + Objects.requireNonNull(moduleSkeleton, "moduleSkeleton must not be null"); + Objects.requireNonNull(classRegistry, "classRegistry must not be null"); + + if (!(attributeExpression.base() instanceof IdentifierExpression identifierExpression)) { + return null; + } + if (attributeExpression.steps().isEmpty()) { + return null; + } + if (valueResolution.status() != FrontendVisibleValueStatus.FOUND_ALLOWED) { + return null; + } + var visibleValue = valueResolution.visibleValue(); + if (visibleValue == null || visibleValue.kind() != ScopeValueKind.SINGLETON) { + return null; + } + + var name = identifierExpression.name(); + var singletonType = classRegistry.findSingletonType(name); + if (singletonType == null) { + return null; + } + var typeMetaResult = moduleSkeleton.resolveSourceFacingTypeMeta(currentScope, name, restriction); + if (!typeMetaResult.isAllowed()) { + return null; + } + var typeMeta = typeMetaResult.requireValue(); + if (typeMeta.kind() != ScopeTypeMetaKind.ENGINE_CLASS + && typeMeta.kind() != ScopeTypeMetaKind.GDCC_CLASS) { + return null; + } + if (!supportsTopLevelTypeMeta(typeMeta)) { + return null; + } + + var firstStep = attributeExpression.steps().getFirst(); + var stepName = extractStepName(firstStep); + if (stepName == null) { + return null; + } + if (firstStep instanceof AttributeCallStep && stepName.equals("new")) { + return resolvesInSingletonInstanceNamespace(classRegistry, singletonType, stepName) ? null : typeMeta; + } + + var inTypeMetaStatic = resolvesInTypeMetaStaticNamespace(classRegistry, typeMeta, stepName); + var inSingletonInstance = resolvesInSingletonInstanceNamespace(classRegistry, singletonType, stepName); + return inTypeMetaStatic && !inSingletonInstance ? typeMeta : null; + } + + public static boolean supportsTopLevelTypeMeta(@NotNull ScopeTypeMeta typeMeta) { + return switch (Objects.requireNonNull(typeMeta, "typeMeta must not be null").kind()) { + case GDCC_CLASS, ENGINE_CLASS, BUILTIN -> !typeMeta.pseudoType(); + case GLOBAL_ENUM -> typeMeta.declaration() != null; + }; + } + + public static boolean shouldPreferGlobalEnumTypeMeta( + @NotNull FrontendVisibleValueResolution valueResolution, + @NotNull ScopeLookupResult typeMetaResult + ) { + Objects.requireNonNull(valueResolution, "valueResolution must not be null"); + Objects.requireNonNull(typeMetaResult, "typeMetaResult must not be null"); + if (valueResolution.status() != FrontendVisibleValueStatus.FOUND_ALLOWED) { + return false; + } + var visibleValue = valueResolution.visibleValue(); + if (visibleValue == null || visibleValue.kind() != ScopeValueKind.GLOBAL_ENUM) { + return false; + } + return typeMetaResult.isAllowed() + && typeMetaResult.requireValue().kind() == ScopeTypeMetaKind.GLOBAL_ENUM + && supportsTopLevelTypeMeta(typeMetaResult.requireValue()); + } + + private static @Nullable String extractStepName(@NotNull AttributeStep step) { + return switch (step) { + case AttributePropertyStep propertyStep -> propertyStep.name(); + case AttributeCallStep callStep -> callStep.name(); + case AttributeSubscriptStep subscriptStep -> subscriptStep.name(); + default -> null; + }; + } + + private static boolean resolvesInTypeMetaStaticNamespace( + @NotNull ClassRegistry classRegistry, + @NotNull ScopeTypeMeta typeMeta, + @NotNull String stepName + ) { + if (typeMeta.declaration() instanceof ExtensionGdClass engineClass) { + if (classRegistry.findEngineClassConstantInHierarchy(engineClass.getName(), stepName) != null) { + return true; + } + if (classRegistry.findEngineClassEnumValueInHierarchy(engineClass.getName(), stepName) != null) { + return true; + } + } else if (classRegistry.getClassDef( + typeMeta.instanceType() instanceof GdObjectType objectType + ? objectType + : new GdObjectType(typeMeta.canonicalName()) + ) instanceof ExtensionGdClass engineClass) { + if (classRegistry.findEngineClassConstantInHierarchy(engineClass.getName(), stepName) != null) { + return true; + } + if (classRegistry.findEngineClassEnumValueInHierarchy(engineClass.getName(), stepName) != null) { + return true; + } + } + return hasStaticMethodInHierarchy(classRegistry, typeMeta, stepName); + } + + private static boolean resolvesInSingletonInstanceNamespace( + @NotNull ClassRegistry classRegistry, + @NotNull GdObjectType singletonType, + @NotNull String stepName + ) { + return hasInstanceMethodInHierarchy(classRegistry, singletonType, stepName) + || hasInstancePropertyInHierarchy(classRegistry, singletonType, stepName) + || hasSignalInHierarchy(classRegistry, singletonType, stepName); + } + + private static boolean hasStaticMethodInHierarchy( + @NotNull ClassRegistry classRegistry, + @NotNull ScopeTypeMeta typeMeta, + @NotNull String stepName + ) { + ClassDef current = classRegistry.resolveClassDefFromTypeMeta(typeMeta); + var visited = new HashSet(); + while (current != null && visited.add(current.getName())) { + var found = current.getFunctions().stream() + .anyMatch(fn -> fn.getName().equals(stepName) && fn.isStatic()); + if (found) { + return true; + } + current = classRegistry.resolveSuperclass(current); + } + return false; + } + + private static boolean hasInstanceMethodInHierarchy( + @NotNull ClassRegistry classRegistry, + @NotNull GdObjectType singletonType, + @NotNull String stepName + ) { + ClassDef current = classRegistry.getClassDef(singletonType); + var visited = new HashSet(); + while (current != null && visited.add(current.getName())) { + var found = current.getFunctions().stream() + .anyMatch(fn -> fn.getName().equals(stepName) && !fn.isStatic()); + if (found) { + return true; + } + current = classRegistry.resolveSuperclass(current); + } + return false; + } + + private static boolean hasInstancePropertyInHierarchy( + @NotNull ClassRegistry classRegistry, + @NotNull GdObjectType singletonType, + @NotNull String stepName + ) { + ClassDef current = classRegistry.getClassDef(singletonType); + var visited = new HashSet(); + while (current != null && visited.add(current.getName())) { + var found = current.getProperties().stream() + .anyMatch(prop -> prop.getName().equals(stepName) && !prop.isStatic()); + if (found) { + return true; + } + current = classRegistry.resolveSuperclass(current); + } + return false; + } + + private static boolean hasSignalInHierarchy( + @NotNull ClassRegistry classRegistry, + @NotNull GdObjectType singletonType, + @NotNull String stepName + ) { + ClassDef current = classRegistry.getClassDef(singletonType); + var visited = new HashSet(); + while (current != null && visited.add(current.getName())) { + var found = current.getSignals().stream() + .anyMatch(signal -> signal.getName().equals(stepName)); + if (found) { + return true; + } + current = classRegistry.resolveSuperclass(current); + } + return false; + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendExpressionSemanticSupport.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendExpressionSemanticSupport.java index 04eb28c4..de6c62ba 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendExpressionSemanticSupport.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendExpressionSemanticSupport.java @@ -55,6 +55,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.function.Function; import java.util.function.Supplier; /// Shared local expression-semantics helper used by both body-phase analyzers. @@ -91,7 +92,7 @@ public record ExpressionSemanticResult( } } - private final @NotNull FrontendAstSideTable symbolBindings; + private final @NotNull Function bindingLookup; private final @NotNull FrontendAstSideTable scopesByAst; private final @NotNull Supplier restrictionSupplier; private final @NotNull Supplier @@ -108,7 +109,7 @@ public FrontendExpressionSemanticSupport( @NotNull Supplier headReceiverSupportSupplier ) { this( - symbolBindings, + symbolBindings::get, scopesByAst, restrictionSupplier, () -> null, @@ -125,7 +126,25 @@ public FrontendExpressionSemanticSupport( @NotNull ClassRegistry classRegistry, @NotNull Supplier headReceiverSupportSupplier ) { - this.symbolBindings = Objects.requireNonNull(symbolBindings, "symbolBindings must not be null"); + this( + symbolBindings::get, + scopesByAst, + restrictionSupplier, + propertyInitializerContextSupplier, + classRegistry, + headReceiverSupportSupplier + ); + } + + public FrontendExpressionSemanticSupport( + @NotNull Function bindingLookup, + @NotNull FrontendAstSideTable scopesByAst, + @NotNull Supplier restrictionSupplier, + @NotNull Supplier propertyInitializerContextSupplier, + @NotNull ClassRegistry classRegistry, + @NotNull Supplier headReceiverSupportSupplier + ) { + this.bindingLookup = Objects.requireNonNull(bindingLookup, "bindingLookup must not be null"); this.scopesByAst = Objects.requireNonNull(scopesByAst, "scopesByAst must not be null"); this.restrictionSupplier = Objects.requireNonNull(restrictionSupplier, "restrictionSupplier must not be null"); this.propertyInitializerContextSupplier = Objects.requireNonNull( @@ -169,7 +188,7 @@ public FrontendExpressionSemanticSupport( public @NotNull ExpressionSemanticResult resolveIdentifierExpressionType( @NotNull IdentifierExpression identifierExpression ) { - var binding = symbolBindings.get(identifierExpression); + var binding = bindingFor(identifierExpression); if (binding == null) { return propagated(FrontendExpressionType.failed( "No published binding fact is available for identifier '" + identifierExpression.name() + "'" @@ -215,7 +234,7 @@ public FrontendExpressionSemanticSupport( boolean finalizeWindow ) { if (callExpression.callee() instanceof IdentifierExpression bareCallee) { - var bareBinding = symbolBindings.get(bareCallee); + var bareBinding = bindingFor(bareCallee); if (bareBinding != null && bareBinding.kind() == FrontendBindingKind.TYPE_META) { var argumentResolution = resolveCallArgumentTypes(callExpression.arguments(), nestedResolver, finalizeWindow); if (argumentResolution.issue() != null) { @@ -320,7 +339,7 @@ private boolean shouldContinueBlockedBareCallResolution( || !(calleeType.publishedType() instanceof GdCallableType)) { return false; } - var binding = symbolBindings.get(Objects.requireNonNull(bareCallee, "bareCallee must not be null")); + var binding = bindingFor(Objects.requireNonNull(bareCallee, "bareCallee must not be null")); if (binding == null) { return false; } @@ -865,7 +884,7 @@ private boolean isStrictlyMoreSpecific( } private @NotNull BareCallRoute bareCallRoute(@NotNull IdentifierExpression bareCallee) { - var binding = symbolBindings.get(Objects.requireNonNull(bareCallee, "bareCallee must not be null")); + var binding = bindingFor(Objects.requireNonNull(bareCallee, "bareCallee must not be null")); var receiverType = currentClassReceiverType(bareCallee); if (binding == null) { return new BareCallRoute(FrontendCallResolutionKind.UNKNOWN, FrontendReceiverKind.UNKNOWN, receiverType); @@ -1079,6 +1098,10 @@ private boolean matchesCallableArguments( return propertyInitializerContextSupplier.get(); } + private @Nullable FrontendBinding bindingFor(@NotNull IdentifierExpression identifierExpression) { + return bindingLookup.apply(Objects.requireNonNull(identifierExpression, "identifierExpression must not be null")); + } + /// Typed containers keep richer source-level names such as `Array[int]`, but operator metadata /// is still owned by the raw builtin classes and uses raw operand names for matching. private @Nullable GdType resolveUnaryExactReturnType( diff --git a/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendCallableExportBatch.java b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendCallableExportBatch.java new file mode 100644 index 00000000..45a43e69 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendCallableExportBatch.java @@ -0,0 +1,30 @@ +package gd.script.gdcc.frontend.sema.patch; + +import gd.script.gdcc.frontend.sema.FrontendAnalysisData; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/// Callable-scoped batch of suite export transactions. +/// +/// Nested suites contribute their transactions here without mutating stable side tables. The root +/// callable applies the accumulated transactions in insertion order only after every supported child +/// suite has resolved. The batch does not preflight conflicts across queued transactions and provides +/// no atomicity or rollback. If a later transaction fails, earlier transactions remain applied to the +/// stable side tables and scopes. +public final class FrontendCallableExportBatch { + private final @NotNull List transactions = new ArrayList<>(); + + public void accumulate(@NotNull FrontendPatchTransaction transaction) { + transactions.add(Objects.requireNonNull(transaction, "transaction must not be null")); + } + + public void applyTo(@NotNull FrontendAnalysisData analysisData) { + var checkedAnalysisData = Objects.requireNonNull(analysisData, "analysisData must not be null"); + for (var transaction : transactions) { + transaction.applyTo(checkedAnalysisData); + } + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendChainBindingPatch.java b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendChainBindingPatch.java new file mode 100644 index 00000000..5a45eaa8 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendChainBindingPatch.java @@ -0,0 +1,25 @@ +package gd.script.gdcc.frontend.sema.patch; + +import gd.script.gdcc.frontend.sema.FrontendAstSideTable; +import gd.script.gdcc.frontend.sema.FrontendResolvedCall; +import gd.script.gdcc.frontend.sema.FrontendResolvedMember; +import gd.script.gdcc.frontend.sema.FrontendSemanticStage; +import org.jetbrains.annotations.NotNull; + +/// Chain-binding owner delta. It publishes member facts and chain-owned call facts only. +public record FrontendChainBindingPatch( + @NotNull FrontendAstSideTable resolvedMembers, + @NotNull FrontendAstSideTable resolvedCalls +) implements FrontendOwnerPatch { + public FrontendChainBindingPatch { + resolvedMembers = FrontendPatchTables.copySideTable(resolvedMembers, "resolvedMembers"); + resolvedCalls = FrontendPatchTables.copySideTable(resolvedCalls, "resolvedCalls"); + FrontendPublishedFactTypeGuard.checkResolvedMembers(resolvedMembers); + FrontendPublishedFactTypeGuard.checkResolvedCalls(resolvedCalls); + } + + @Override + public @NotNull FrontendSemanticStage stage() { + return FrontendSemanticStage.CHAIN_BINDING; + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendExprTypePatch.java b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendExprTypePatch.java new file mode 100644 index 00000000..a0aaa80c --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendExprTypePatch.java @@ -0,0 +1,25 @@ +package gd.script.gdcc.frontend.sema.patch; + +import gd.script.gdcc.frontend.sema.FrontendAstSideTable; +import gd.script.gdcc.frontend.sema.FrontendExpressionType; +import gd.script.gdcc.frontend.sema.FrontendResolvedCall; +import gd.script.gdcc.frontend.sema.FrontendSemanticStage; +import org.jetbrains.annotations.NotNull; + +/// Expression-typing owner delta. It publishes final expression facts and bare-call facts only. +public record FrontendExprTypePatch( + @NotNull FrontendAstSideTable expressionTypes, + @NotNull FrontendAstSideTable resolvedCalls +) implements FrontendOwnerPatch { + public FrontendExprTypePatch { + expressionTypes = FrontendPatchTables.copySideTable(expressionTypes, "expressionTypes"); + resolvedCalls = FrontendPatchTables.copySideTable(resolvedCalls, "resolvedCalls"); + FrontendPublishedFactTypeGuard.checkExpressionTypes(expressionTypes); + FrontendPublishedFactTypeGuard.checkResolvedCalls(resolvedCalls); + } + + @Override + public @NotNull FrontendSemanticStage stage() { + return FrontendSemanticStage.EXPR_TYPE; + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendLocalSlotTypeUpdate.java b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendLocalSlotTypeUpdate.java new file mode 100644 index 00000000..a06edfd6 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendLocalSlotTypeUpdate.java @@ -0,0 +1,29 @@ +package gd.script.gdcc.frontend.sema.patch; + +import gd.script.gdcc.frontend.scope.BlockScope; +import gd.script.gdcc.type.GdType; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +/// One local-slot type rewrite produced by local type stabilization. +/// +/// The update itself is owner-scoped metadata. `FrontendAnalysisData` owns the final scope mutation +/// rules, while `FrontendTypedLexicalEnvironment` may expose the same update as an effective view +/// before stable export. +public record FrontendLocalSlotTypeUpdate( + @NotNull BlockScope scope, + @NotNull String name, + @NotNull Object declaration, + @NotNull GdType type +) { + public FrontendLocalSlotTypeUpdate { + Objects.requireNonNull(scope, "scope must not be null"); + Objects.requireNonNull(name, "name must not be null"); + Objects.requireNonNull(declaration, "declaration must not be null"); + Objects.requireNonNull(type, "type must not be null"); + if (name.isEmpty()) { + throw new IllegalArgumentException("name must not be blank"); + } + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendLocalTypeStabilizationPatch.java b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendLocalTypeStabilizationPatch.java new file mode 100644 index 00000000..96546af2 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendLocalTypeStabilizationPatch.java @@ -0,0 +1,25 @@ +package gd.script.gdcc.frontend.sema.patch; + +import gd.script.gdcc.frontend.sema.FrontendSemanticStage; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Objects; + +/// Local type stabilization owner delta. It carries only source-facing local slot rewrites. +public record FrontendLocalTypeStabilizationPatch( + @NotNull List localSlotTypeUpdates +) implements FrontendOwnerPatch { + public FrontendLocalTypeStabilizationPatch { + localSlotTypeUpdates = List.copyOf(Objects.requireNonNull( + localSlotTypeUpdates, + "localSlotTypeUpdates must not be null" + )); + FrontendPublishedFactTypeGuard.checkLocalSlotTypeUpdates(localSlotTypeUpdates); + } + + @Override + public @NotNull FrontendSemanticStage stage() { + return FrontendSemanticStage.LOCAL_TYPE_STABILIZATION; + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendOwnerPatch.java b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendOwnerPatch.java new file mode 100644 index 00000000..e0f81c6e --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendOwnerPatch.java @@ -0,0 +1,49 @@ +package gd.script.gdcc.frontend.sema.patch; + +import gd.script.gdcc.frontend.sema.FrontendAstSideTable; +import gd.script.gdcc.frontend.sema.FrontendBinding; +import gd.script.gdcc.frontend.sema.FrontendExpressionType; +import gd.script.gdcc.frontend.sema.FrontendResolvedCall; +import gd.script.gdcc.frontend.sema.FrontendResolvedMember; +import gd.script.gdcc.frontend.sema.FrontendSemanticStage; +import gd.script.gdcc.type.GdType; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/// Publication delta owned by exactly one semantic owner. +/// +/// Suite export composes these patches in a `FrontendPatchTransaction`; it must not collapse facts +/// from multiple owners into one multi-owner payload. +public sealed interface FrontendOwnerPatch permits + FrontendTopBindingPatch, + FrontendLocalTypeStabilizationPatch, + FrontendChainBindingPatch, + FrontendExprTypePatch, + FrontendVarTypePostPatch { + @NotNull FrontendSemanticStage stage(); + + default @NotNull FrontendAstSideTable symbolBindings() { + return FrontendPatchTables.emptySideTable(); + } + + default @NotNull FrontendAstSideTable resolvedMembers() { + return FrontendPatchTables.emptySideTable(); + } + + default @NotNull FrontendAstSideTable resolvedCalls() { + return FrontendPatchTables.emptySideTable(); + } + + default @NotNull FrontendAstSideTable expressionTypes() { + return FrontendPatchTables.emptySideTable(); + } + + default @NotNull FrontendAstSideTable slotTypes() { + return FrontendPatchTables.emptySideTable(); + } + + default @NotNull List localSlotTypeUpdates() { + return List.of(); + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendPatchTables.java b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendPatchTables.java new file mode 100644 index 00000000..d42fa53b --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendPatchTables.java @@ -0,0 +1,25 @@ +package gd.script.gdcc.frontend.sema.patch; + +import gd.script.gdcc.frontend.sema.FrontendAstSideTable; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +final class FrontendPatchTables { + private FrontendPatchTables() { + } + + static @NotNull FrontendAstSideTable emptySideTable() { + return new FrontendAstSideTable<>(); + } + + static @NotNull FrontendAstSideTable copySideTable( + @NotNull FrontendAstSideTable source, + @NotNull String fieldName + ) { + var checkedSource = Objects.requireNonNull(source, fieldName + " must not be null"); + var copy = new FrontendAstSideTable(); + copy.putAll(checkedSource); + return copy; + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendPatchTransaction.java b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendPatchTransaction.java new file mode 100644 index 00000000..377257ed --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendPatchTransaction.java @@ -0,0 +1,67 @@ +package gd.script.gdcc.frontend.sema.patch; + +import gd.script.gdcc.exception.FrontendAnalysisPatchException; +import gd.script.gdcc.frontend.sema.FrontendAnalysisData; +import gd.script.gdcc.frontend.sema.FrontendSemanticStage; +import org.jetbrains.annotations.NotNull; + +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; + +/// Ordered suite-export transaction made of single-owner patches. +/// +/// "Transaction" denotes grouping and owner order only. `applyTo(...)` applies each patch immediately +/// and provides no cross-patch atomicity or rollback. If a later patch fails, earlier patches remain +/// in stable side tables, and local stabilization may already have updated scope slots and binding payloads. +/// +/// The constructor rejects duplicate or out-of-order owners so callers cannot accidentally recreate +/// the legacy multi-owner patch shape at suite export time. Nested suite transactions are deferred +/// in a callable-scoped export batch and never applied at their own suite boundary. +public record FrontendPatchTransaction(@NotNull List patches) { + public FrontendPatchTransaction { + patches = List.copyOf(Objects.requireNonNull(patches, "patches must not be null")); + checkOrder(patches); + } + + public static @NotNull FrontendPatchTransaction of(@NotNull FrontendOwnerPatch... patches) { + return new FrontendPatchTransaction(List.of(patches)); + } + + public void applyTo(@NotNull FrontendAnalysisData analysisData) { + var checkedData = Objects.requireNonNull(analysisData, "analysisData must not be null"); + for (var patch : patches) { + checkedData.applyPatch(patch); + } + } + + private static void checkOrder(@NotNull List patches) { + var seenStages = EnumSet.noneOf(FrontendSemanticStage.class); + var previousOrder = -1; + for (var patch : patches) { + var stage = Objects.requireNonNull(patch, "patch must not be null").stage(); + if (!seenStages.add(stage)) { + throw transactionFailure("duplicate owner patch " + stage); + } + var order = order(stage); + if (order < previousOrder) { + throw transactionFailure("owner patch order regressed at " + stage); + } + previousOrder = order; + } + } + + private static int order(@NotNull FrontendSemanticStage stage) { + return switch (stage) { + case TOP_BINDING -> 0; + case LOCAL_TYPE_STABILIZATION -> 1; + case CHAIN_BINDING -> 2; + case EXPR_TYPE -> 3; + case VAR_TYPE_POST -> 4; + }; + } + + private static @NotNull FrontendAnalysisPatchException transactionFailure(@NotNull String message) { + return new FrontendAnalysisPatchException("patch transaction rejected: " + message); + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendPublishedFactTypeGuard.java b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendPublishedFactTypeGuard.java new file mode 100644 index 00000000..42e5609c --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendPublishedFactTypeGuard.java @@ -0,0 +1,120 @@ +package gd.script.gdcc.frontend.sema.patch; + +import gd.script.gdcc.exception.FrontendAnalysisPatchException; +import gd.script.gdcc.frontend.sema.FrontendAstSideTable; +import gd.script.gdcc.frontend.sema.FrontendBinding; +import gd.script.gdcc.frontend.sema.FrontendExpressionType; +import gd.script.gdcc.frontend.sema.FrontendResolvedCall; +import gd.script.gdcc.frontend.sema.FrontendResolvedMember; +import gd.script.gdcc.scope.ScopeValue; +import gd.script.gdcc.type.GdCompilerType; +import gd.script.gdcc.type.GdType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/// Shared compiler-only guard for every source-facing typed publication surface. +/// +/// Overlay writes, overlay flush, stable patch merge, and legacy whole-table publication all use +/// this walker so newly-added type-bearing payload fields do not drift between scratch and export. +public final class FrontendPublishedFactTypeGuard { + private FrontendPublishedFactTypeGuard() { + } + + public static void checkOwnerPatch(@NotNull FrontendOwnerPatch patch) { + checkSymbolBindings(patch.symbolBindings()); + checkResolvedMembers(patch.resolvedMembers()); + checkResolvedCalls(patch.resolvedCalls()); + checkExpressionTypes(patch.expressionTypes()); + checkSlotTypes(patch.slotTypes()); + checkLocalSlotTypeUpdates(patch.localSlotTypeUpdates()); + } + + public static void checkSymbolBindings(@NotNull FrontendAstSideTable bindings) { + for (var binding : bindings.values()) { + checkBinding(binding); + } + } + + public static void checkBinding(@NotNull FrontendBinding binding) { + var resolvedValue = binding.resolvedValue(); + if (resolvedValue != null) { + checkScopeValue(resolvedValue, "symbolBindings() resolved value for '" + binding.symbolName() + "'"); + } + } + + public static void checkResolvedMembers(@NotNull FrontendAstSideTable members) { + for (var member : members.values()) { + checkResolvedMember(member); + } + } + + public static void checkResolvedMember(@NotNull FrontendResolvedMember member) { + checkNoCompilerOnlyLeak(member.receiverType(), "resolvedMembers() receiver type for '" + member.memberName() + "'"); + checkNoCompilerOnlyLeak(member.resultType(), "resolvedMembers() result type for '" + member.memberName() + "'"); + } + + public static void checkResolvedCalls(@NotNull FrontendAstSideTable calls) { + for (var call : calls.values()) { + checkResolvedCall(call); + } + } + + public static void checkResolvedCall(@NotNull FrontendResolvedCall call) { + checkNoCompilerOnlyLeak(call.receiverType(), "resolvedCalls() receiver type for '" + call.callableName() + "'"); + checkNoCompilerOnlyLeak(call.returnType(), "resolvedCalls() return type for '" + call.callableName() + "'"); + checkTypeList(call.argumentTypes(), "resolvedCalls() argument type for '" + call.callableName() + "'"); + var boundary = call.exactCallableBoundary(); + if (boundary != null) { + checkTypeList( + boundary.fixedParameterTypes(), + "resolvedCalls() exact callable boundary parameter type for '" + call.callableName() + "'" + ); + } + } + + public static void checkExpressionTypes(@NotNull FrontendAstSideTable expressionTypes) { + for (var expressionType : expressionTypes.values()) { + checkExpressionType(expressionType); + } + } + + public static void checkExpressionType(@NotNull FrontendExpressionType expressionType) { + checkNoCompilerOnlyLeak(expressionType.publishedType(), "expressionTypes() published type"); + } + + public static void checkSlotTypes(@NotNull FrontendAstSideTable slotTypes) { + for (var slotType : slotTypes.values()) { + checkNoCompilerOnlyLeak(slotType, "slotTypes() value"); + } + } + + public static void checkLocalSlotTypeUpdates(@NotNull Iterable updates) { + for (var update : updates) { + checkLocalSlotTypeUpdate(update); + } + } + + public static void checkLocalSlotTypeUpdate(@NotNull FrontendLocalSlotTypeUpdate update) { + checkNoCompilerOnlyLeak(update.type(), "local slot update for '" + update.name() + "'"); + } + + public static void checkScopeValue(@NotNull ScopeValue value, @NotNull String fieldName) { + checkNoCompilerOnlyLeak(value.type(), fieldName); + } + + public static void checkNoCompilerOnlyLeak(@Nullable GdType type, @NotNull String fieldName) { + if (type instanceof GdCompilerType compilerOnlyType) { + throw new FrontendAnalysisPatchException( + fieldName + + " leaked compiler-only type " + + compilerOnlyType.getTypeName() + ); + } + } + + private static void checkTypeList(@NotNull Iterable types, @NotNull String fieldName) { + for (var type : types) { + checkNoCompilerOnlyLeak(type, fieldName); + } + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendTopBindingPatch.java b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendTopBindingPatch.java new file mode 100644 index 00000000..a9fb7614 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendTopBindingPatch.java @@ -0,0 +1,21 @@ +package gd.script.gdcc.frontend.sema.patch; + +import gd.script.gdcc.frontend.sema.FrontendAstSideTable; +import gd.script.gdcc.frontend.sema.FrontendBinding; +import gd.script.gdcc.frontend.sema.FrontendSemanticStage; +import org.jetbrains.annotations.NotNull; + +/// Top-binding owner delta. It may only publish `symbolBindings()` facts. +public record FrontendTopBindingPatch( + @NotNull FrontendAstSideTable symbolBindings +) implements FrontendOwnerPatch { + public FrontendTopBindingPatch { + symbolBindings = FrontendPatchTables.copySideTable(symbolBindings, "symbolBindings"); + FrontendPublishedFactTypeGuard.checkSymbolBindings(symbolBindings); + } + + @Override + public @NotNull FrontendSemanticStage stage() { + return FrontendSemanticStage.TOP_BINDING; + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendVarTypePostPatch.java b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendVarTypePostPatch.java new file mode 100644 index 00000000..0f901944 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendVarTypePostPatch.java @@ -0,0 +1,21 @@ +package gd.script.gdcc.frontend.sema.patch; + +import gd.script.gdcc.frontend.sema.FrontendAstSideTable; +import gd.script.gdcc.frontend.sema.FrontendSemanticStage; +import gd.script.gdcc.type.GdType; +import org.jetbrains.annotations.NotNull; + +/// Var-type-post owner delta. It may only publish final source-facing slot types. +public record FrontendVarTypePostPatch( + @NotNull FrontendAstSideTable slotTypes +) implements FrontendOwnerPatch { + public FrontendVarTypePostPatch { + slotTypes = FrontendPatchTables.copySideTable(slotTypes, "slotTypes"); + FrontendPublishedFactTypeGuard.checkSlotTypes(slotTypes); + } + + @Override + public @NotNull FrontendSemanticStage stage() { + return FrontendSemanticStage.VAR_TYPE_POST; + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/resolver/FrontendVisibleValueDeferredReason.java b/src/main/java/gd/script/gdcc/frontend/sema/resolver/FrontendVisibleValueDeferredReason.java index 1ec6744f..8d9e10c7 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/resolver/FrontendVisibleValueDeferredReason.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/resolver/FrontendVisibleValueDeferredReason.java @@ -3,6 +3,5 @@ /// Why the resolver refused to answer with a normal visible binding result. public enum FrontendVisibleValueDeferredReason { UNSUPPORTED_DOMAIN, - MISSING_SCOPE_OR_SKIPPED_SUBTREE, - VARIABLE_INVENTORY_NOT_PUBLISHED + MISSING_SCOPE_OR_SKIPPED_SUBTREE } diff --git a/src/main/java/gd/script/gdcc/frontend/sema/resolver/FrontendVisibleValueDomain.java b/src/main/java/gd/script/gdcc/frontend/sema/resolver/FrontendVisibleValueDomain.java index 8b934e32..467c28b8 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/resolver/FrontendVisibleValueDomain.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/resolver/FrontendVisibleValueDomain.java @@ -10,7 +10,6 @@ public enum FrontendVisibleValueDomain { PARAMETER_DEFAULT, LAMBDA_SUBTREE, BLOCK_LOCAL_CONST_SUBTREE, - FOR_SUBTREE, MATCH_SUBTREE, UNKNOWN_OR_SKIPPED_SUBTREE } diff --git a/src/main/java/gd/script/gdcc/frontend/sema/resolver/FrontendVisibleValueResolver.java b/src/main/java/gd/script/gdcc/frontend/sema/resolver/FrontendVisibleValueResolver.java index b79a3135..3bd62573 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/resolver/FrontendVisibleValueResolver.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/resolver/FrontendVisibleValueResolver.java @@ -1,18 +1,18 @@ package gd.script.gdcc.frontend.sema.resolver; import gd.script.gdcc.frontend.scope.BlockScope; -import gd.script.gdcc.frontend.scope.BlockScopeKind; import gd.script.gdcc.frontend.scope.CallableScope; +import gd.script.gdcc.frontend.scope.CallableScopeKind; import gd.script.gdcc.frontend.sema.FrontendAnalysisData; -import gd.script.gdcc.frontend.sema.FrontendExecutableInventorySupport; +import gd.script.gdcc.frontend.sema.FrontendBodySemanticSupportPolicy; +import gd.script.gdcc.frontend.sema.FrontendBodyDeclarationIndex; import gd.script.gdcc.frontend.sema.FrontendModuleSkeleton; -import gd.script.gdcc.frontend.scope.CallableScopeKind; +import gd.script.gdcc.frontend.sema.FrontendTypedLexicalEnvironment; import gd.script.gdcc.scope.Scope; import gd.script.gdcc.scope.ScopeLookupResult; import gd.script.gdcc.scope.ScopeValue; import dev.superice.gdparser.frontend.ast.Block; import dev.superice.gdparser.frontend.ast.DeclarationKind; -import dev.superice.gdparser.frontend.ast.ForStatement; import dev.superice.gdparser.frontend.ast.LambdaExpression; import dev.superice.gdparser.frontend.ast.MatchSection; import dev.superice.gdparser.frontend.ast.Node; @@ -35,23 +35,38 @@ /// explicit deferred-boundary sealing for domains whose variable inventory is not published yet public final class FrontendVisibleValueResolver { private final @NotNull FrontendAnalysisData analysisData; + private final @Nullable FrontendBodyDeclarationIndex bodyDeclarationIndex; private final @NotNull IdentityHashMap parentByNode = new IdentityHashMap<>(); private final @NotNull IdentityHashMap indexedAstNodes = new IdentityHashMap<>(); public FrontendVisibleValueResolver(@NotNull FrontendAnalysisData analysisData) { + this(analysisData, null); + } + + public FrontendVisibleValueResolver( + @NotNull FrontendAnalysisData analysisData, + @Nullable FrontendBodyDeclarationIndex bodyDeclarationIndex + ) { this.analysisData = Objects.requireNonNull(analysisData, "analysisData must not be null"); + this.bodyDeclarationIndex = bodyDeclarationIndex; indexSourceAstParents(analysisData.moduleSkeleton()); } /// Resolves one value use site under the frontend-visible declaration-order rules. public @NotNull FrontendVisibleValueResolution resolve(@NotNull FrontendVisibleValueResolveRequest request) { + return resolve(request, null); + } + + /// Resolves one use site and overlays the returned local type only after visibility filtering. + public @NotNull FrontendVisibleValueResolution resolve( + @NotNull FrontendVisibleValueResolveRequest request, + @Nullable FrontendTypedLexicalEnvironment typedEnvironment + ) { Objects.requireNonNull(request, "request must not be null"); - if (request.domain() != FrontendVisibleValueDomain.EXECUTABLE_BODY) { - return deferredUnsupported( - request.domain(), - FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN - ); + var requestDomainBoundary = classifyRequestDomainBoundary(request); + if (requestDomainBoundary != null) { + return FrontendVisibleValueResolution.deferredUnsupported(requestDomainBoundary); } var useSite = request.useSite(); @@ -62,11 +77,10 @@ public FrontendVisibleValueResolver(@NotNull FrontendAnalysisData analysisData) var currentScope = analysisData.scopesByAst().get(useSite); if (currentScope == null) { - return deferredUnsupported( + return deferredMissingScope( indexedAstNodes.containsKey(useSite) ? FrontendVisibleValueDomain.EXECUTABLE_BODY - : FrontendVisibleValueDomain.UNKNOWN_OR_SKIPPED_SUBTREE, - FrontendVisibleValueDeferredReason.MISSING_SCOPE_OR_SKIPPED_SUBTREE + : FrontendVisibleValueDomain.UNKNOWN_OR_SKIPPED_SUBTREE ); } var currentScopeBoundary = classifyUnsupportedCurrentScopeBoundary(currentScope); @@ -83,13 +97,19 @@ public FrontendVisibleValueResolver(@NotNull FrontendAnalysisData analysisData) while (scope instanceof BlockScope || scope instanceof CallableScope) { var currentLayerResult = scope.resolveValueHere(request.name(), request.restriction()); if (currentLayerResult.isBlocked()) { - return FrontendVisibleValueResolution.foundBlocked(currentLayerResult.requireValue(), filteredHits); + return FrontendVisibleValueResolution.foundBlocked( + effectiveValue(currentLayerResult.requireValue(), scope, typedEnvironment), + filteredHits + ); } if (currentLayerResult.isAllowed()) { var visibleValue = currentLayerResult.requireValue(); var filteredHit = filterInvisibleCurrentLayerHit(visibleValue, scope, useSite); if (filteredHit == null) { - return FrontendVisibleValueResolution.foundAllowed(visibleValue, filteredHits); + return FrontendVisibleValueResolution.foundAllowed( + effectiveValue(visibleValue, scope, typedEnvironment), + filteredHits + ); } filteredHits.add(filteredHit); } @@ -102,6 +122,14 @@ public FrontendVisibleValueResolver(@NotNull FrontendAnalysisData analysisData) return toFrontendResolution(scope.resolveValue(request.name(), request.restriction()), filteredHits); } + private @NotNull ScopeValue effectiveValue( + @NotNull ScopeValue value, + @NotNull Scope owningScope, + @Nullable FrontendTypedLexicalEnvironment typedEnvironment + ) { + return typedEnvironment != null ? typedEnvironment.effectiveScopeValue(value, owningScope) : value; + } + private void indexSourceAstParents(@NotNull FrontendModuleSkeleton moduleSkeleton) { for (var sourceClassRelation : moduleSkeleton.sourceClassRelations()) { indexSubtree(sourceClassRelation.unit().ast(), null); @@ -144,43 +172,55 @@ private void indexSubtree(@NotNull Node node, @Nullable Node parentNode) { ) { return switch (parentNode) { case Parameter parameter when parameter.defaultValue() == childNode -> - new FrontendVisibleValueDeferredBoundary( - FrontendVisibleValueDomain.PARAMETER_DEFAULT, - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED - ); - case LambdaExpression lambdaExpression when lambdaExpression.body() == childNode -> - new FrontendVisibleValueDeferredBoundary( - FrontendVisibleValueDomain.LAMBDA_SUBTREE, - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED - ); + structuralBoundary(FrontendBodySemanticSupportPolicy.PARAMETER_DEFAULT); + case LambdaExpression lambdaExpression when lambdaExpression.body() == childNode -> structuralBoundary( + FrontendBodySemanticSupportPolicy.LAMBDA_SUBTREE + ); case VariableDeclaration variableDeclaration when variableDeclaration.kind() == DeclarationKind.CONST && variableDeclaration.value() == childNode - && isDeferredBlockLocalConst(variableDeclaration) -> new FrontendVisibleValueDeferredBoundary( - FrontendVisibleValueDomain.BLOCK_LOCAL_CONST_SUBTREE, - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED + && isDeferredBlockLocalConst(variableDeclaration) -> structuralBoundary( + FrontendBodySemanticSupportPolicy.BLOCK_LOCAL_CONST_SUBTREE ); - case ForStatement forStatement when (forStatement.iteratorType() == childNode - || forStatement.iterable() == childNode - || forStatement.body() == childNode) -> new FrontendVisibleValueDeferredBoundary( - FrontendVisibleValueDomain.FOR_SUBTREE, - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED + case MatchSection matchSection when matchSection.body() == childNode -> structuralBoundary( + FrontendBodySemanticSupportPolicy.MATCH_SUBTREE ); case MatchSection matchSection when (matchSection.guard() == childNode - || matchSection.body() == childNode - || containsNodeIdentity(matchSection.patterns(), childNode)) -> new FrontendVisibleValueDeferredBoundary( - FrontendVisibleValueDomain.MATCH_SUBTREE, - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED + || containsNodeIdentity(matchSection.patterns(), childNode)) -> structuralBoundary( + FrontendBodySemanticSupportPolicy.MATCH_SUBTREE ); default -> null; }; } + private @Nullable FrontendVisibleValueDeferredBoundary classifyRequestDomainBoundary( + @NotNull FrontendVisibleValueResolveRequest request + ) { + if (request.domain() == FrontendVisibleValueDomain.EXECUTABLE_BODY) { + return null; + } + return new FrontendVisibleValueDeferredBoundary( + request.domain(), + FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN + ); + } + + private @NotNull FrontendVisibleValueDeferredBoundary structuralBoundary( + @NotNull FrontendBodySemanticSupportPolicy policy + ) { + return new FrontendVisibleValueDeferredBoundary( + policy.visibleValueDomain(), + FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN + ); + } + /// Class-level `const` continues to belong to class-scope lookup. Only executable block-local /// `const` declarations are sealed as deferred unsupported boundaries here. private boolean isDeferredBlockLocalConst(@NotNull VariableDeclaration variableDeclaration) { var declarationScope = analysisData.scopesByAst().get(variableDeclaration); return declarationScope instanceof BlockScope blockScope - && FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(blockScope.kind()); + && FrontendBodySemanticSupportPolicy.forBlockScopeKind( + blockScope.kind() + ).publishesLexicalInventory(); } /// Block-local `const` inventory is still deferred. If a same-name visible `const` declaration @@ -237,7 +277,7 @@ private boolean isDeferredBlockLocalConst(@NotNull VariableDeclaration variableD } return new FrontendVisibleValueDeferredBoundary( FrontendVisibleValueDomain.BLOCK_LOCAL_CONST_SUBTREE, - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED + FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN ); } @@ -248,12 +288,9 @@ private boolean isDeferredBlockLocalConst(@NotNull VariableDeclaration variableD @NotNull Scope currentScope ) { return switch (currentScope) { - case BlockScope blockScope -> classifyUnsupportedCurrentBlockScopeBoundary(blockScope.kind()); + case BlockScope blockScope -> classifyUnsupportedCurrentBlockScopeBoundary(blockScope); case CallableScope callableScope when callableScope.kind() == CallableScopeKind.LAMBDA_EXPRESSION -> - new FrontendVisibleValueDeferredBoundary( - FrontendVisibleValueDomain.LAMBDA_SUBTREE, - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED - ); + unsupportedScopeBoundary(FrontendBodySemanticSupportPolicy.LAMBDA_SUBTREE); default -> new FrontendVisibleValueDeferredBoundary( FrontendVisibleValueDomain.EXECUTABLE_BODY, FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN @@ -273,35 +310,27 @@ private boolean containsNodeIdentity(@NotNull List nodes, @NotNu } private @Nullable FrontendVisibleValueDeferredBoundary classifyUnsupportedCurrentBlockScopeBoundary( - @NotNull BlockScopeKind kind + @NotNull BlockScope blockScope ) { - if (FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(kind)) { + var policy = FrontendBodySemanticSupportPolicy.forBlockScopeKind(blockScope.kind()); + if (policy.publishesLexicalInventory()) { return null; } - return switch (kind) { - case LAMBDA_BODY -> new FrontendVisibleValueDeferredBoundary( - FrontendVisibleValueDomain.LAMBDA_SUBTREE, - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED - ); - case FOR_BODY -> new FrontendVisibleValueDeferredBoundary( - FrontendVisibleValueDomain.FOR_SUBTREE, - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED - ); - case MATCH_SECTION_BODY -> new FrontendVisibleValueDeferredBoundary( - FrontendVisibleValueDomain.MATCH_SUBTREE, - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED - ); - default -> new FrontendVisibleValueDeferredBoundary( - FrontendVisibleValueDomain.EXECUTABLE_BODY, - FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN - ); - }; + return unsupportedScopeBoundary(policy); + } + + private @NotNull FrontendVisibleValueDeferredBoundary unsupportedScopeBoundary( + @NotNull FrontendBodySemanticSupportPolicy policy + ) { + return structuralBoundary(policy); } private boolean publishesCallableLocalValueInventory(@NotNull Block block) { var blockScope = analysisData.scopesByAst().get(block); return blockScope instanceof BlockScope typedBlockScope - && FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(typedBlockScope.kind()); + && FrontendBodySemanticSupportPolicy.forBlockScopeKind( + typedBlockScope.kind() + ).publishesLexicalInventory(); } private @Nullable FrontendFilteredValueHit filterInvisibleCurrentLayerHit( @@ -314,6 +343,7 @@ private boolean publishesCallableLocalValueInventory(@NotNull Block block) { return null; } if (declaration instanceof VariableDeclaration variableDeclaration) { + checkPublishedLocalInventory(variableDeclaration, value); if (isVisibleLocal(variableDeclaration, useSite)) { return null; } @@ -326,6 +356,23 @@ private boolean publishesCallableLocalValueInventory(@NotNull Block block) { return null; } + private void checkPublishedLocalInventory( + @NotNull VariableDeclaration declaration, + @NotNull ScopeValue value + ) { + if (bodyDeclarationIndex == null) { + return; + } + var indexedDeclaration = bodyDeclarationIndex.declarationFor(declaration); + if (indexedDeclaration == null + || indexedDeclaration.binding().declaration() != value.declaration() + || indexedDeclaration.binding().kind() != value.kind()) { + throw new IllegalStateException( + "scope local '" + value.name() + "' is missing or inconsistent in published body inventory" + ); + } + } + private boolean isVisibleLocal(@NotNull VariableDeclaration variableDeclaration, @NotNull Node useSite) { return variableDeclaration.range().endByte() <= useSite.range().startByte(); } @@ -372,12 +419,14 @@ private boolean isDescendantOf(@NotNull Node candidateDescendant, @Nullable Node }; } - private @NotNull FrontendVisibleValueResolution deferredUnsupported( - @NotNull FrontendVisibleValueDomain domain, - @NotNull FrontendVisibleValueDeferredReason reason + private @NotNull FrontendVisibleValueResolution deferredMissingScope( + @NotNull FrontendVisibleValueDomain domain ) { return FrontendVisibleValueResolution.deferredUnsupported( - new FrontendVisibleValueDeferredBoundary(domain, reason) + new FrontendVisibleValueDeferredBoundary( + domain, + FrontendVisibleValueDeferredReason.MISSING_SCOPE_OR_SKIPPED_SUBTREE + ) ); } } diff --git a/src/test/java/gd/script/gdcc/frontend/lowering/FrontendLoweringAnalysisPassTest.java b/src/test/java/gd/script/gdcc/frontend/lowering/FrontendLoweringAnalysisPassTest.java index 27be6ba5..f7e9fa0d 100644 --- a/src/test/java/gd/script/gdcc/frontend/lowering/FrontendLoweringAnalysisPassTest.java +++ b/src/test/java/gd/script/gdcc/frontend/lowering/FrontendLoweringAnalysisPassTest.java @@ -5,7 +5,7 @@ import gd.script.gdcc.frontend.parse.FrontendModule; import gd.script.gdcc.frontend.parse.GdScriptParserService; import gd.script.gdcc.frontend.sema.FrontendAnalysisData; -import gd.script.gdcc.frontend.sema.analyzer.FrontendVarTypePostAnalyzer; +import gd.script.gdcc.frontend.sema.analyzer.FrontendBodyOwnerProcedures; import gd.script.gdcc.gdextension.ExtensionApiLoader; import gd.script.gdcc.scope.ClassRegistry; import org.jetbrains.annotations.NotNull; @@ -153,7 +153,9 @@ func ping(): .filter(diagnostic -> diagnostic.category().equals("sema.variable_binding")) .toList(); var slotPublicationWarnings = diagnostics.snapshot().asList().stream() - .filter(diagnostic -> diagnostic.category().equals(FrontendVarTypePostAnalyzer.VARIABLE_SLOT_PUBLICATION_CATEGORY)) + .filter(diagnostic -> diagnostic.category().equals( + FrontendBodyOwnerProcedures.VARIABLE_SLOT_PUBLICATION_CATEGORY + )) .toList(); assertNull(lowered); diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendAnalysisDataTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendAnalysisDataTest.java index 5321e544..3475969f 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendAnalysisDataTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendAnalysisDataTest.java @@ -1,24 +1,52 @@ package gd.script.gdcc.frontend.sema; +import dev.superice.gdparser.frontend.ast.DeclarationKind; +import dev.superice.gdparser.frontend.ast.IdentifierExpression; import gd.script.gdcc.frontend.diagnostic.DiagnosticSnapshot; import gd.script.gdcc.frontend.diagnostic.FrontendDiagnostic; +import gd.script.gdcc.exception.FrontendAnalysisPatchException; +import gd.script.gdcc.frontend.sema.patch.FrontendChainBindingPatch; +import gd.script.gdcc.frontend.sema.patch.FrontendExprTypePatch; +import gd.script.gdcc.frontend.sema.patch.FrontendLocalSlotTypeUpdate; +import gd.script.gdcc.frontend.sema.patch.FrontendLocalTypeStabilizationPatch; +import gd.script.gdcc.frontend.sema.patch.FrontendOwnerPatch; +import gd.script.gdcc.frontend.sema.patch.FrontendPatchTransaction; +import gd.script.gdcc.frontend.sema.patch.FrontendTopBindingPatch; +import gd.script.gdcc.frontend.sema.patch.FrontendVarTypePostPatch; +import gd.script.gdcc.frontend.scope.BlockScope; +import gd.script.gdcc.frontend.scope.BlockScopeKind; +import gd.script.gdcc.frontend.scope.CallableScope; +import gd.script.gdcc.frontend.scope.CallableScopeKind; +import gd.script.gdcc.frontend.scope.ClassScope; import gd.script.gdcc.gdextension.ExtensionApiLoader; +import gd.script.gdcc.lir.LirClassDef; import gd.script.gdcc.scope.ClassRegistry; import gd.script.gdcc.scope.Scope; +import gd.script.gdcc.scope.ScopeLookupStatus; +import gd.script.gdcc.scope.ScopeValue; import gd.script.gdcc.scope.ScopeOwnerKind; +import gd.script.gdcc.scope.ScopeValueKind; +import gd.script.gdcc.type.GdccForRangeIterType; +import gd.script.gdcc.type.GdFloatType; import gd.script.gdcc.type.GdType; import gd.script.gdcc.type.GdIntType; import gd.script.gdcc.type.GdObjectType; import gd.script.gdcc.type.GdVariantType; +import gd.script.gdcc.type.GdVoidType; import dev.superice.gdparser.frontend.ast.PassStatement; import dev.superice.gdparser.frontend.ast.Point; import dev.superice.gdparser.frontend.ast.Range; +import dev.superice.gdparser.frontend.ast.TypeRef; +import dev.superice.gdparser.frontend.ast.VariableDeclaration; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import java.util.List; import java.util.Map; +import java.util.Objects; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -159,10 +187,11 @@ void updateExpressionTypesClearsStaleEntriesWithoutReplacingStableSideTableRefer analysisData.updateExpressionTypes(replacement); + var publishedExpressionType = analysisData.expressionTypes().get(freshNode); assertSame(originalSideTable, analysisData.expressionTypes()); assertNull(analysisData.expressionTypes().get(staleNode)); - assertSame(publishedType, analysisData.expressionTypes().get(freshNode)); - assertEquals(GdVariantType.VARIANT, analysisData.expressionTypes().get(freshNode).publishedType()); + assertSame(publishedType, publishedExpressionType); + assertSame(GdVariantType.VARIANT, publishedType.publishedType()); } @Test @@ -223,7 +252,684 @@ void updateSlotTypesClearsStaleEntriesWithoutReplacingStableSideTableReference() assertEquals(GdIntType.INT, analysisData.slotTypes().get(freshNode)); } + @Test + void ownerPatchCopiesSourceTablesAtConstructionTime() { + var bindingNode = identifier("value"); + var symbolBindings = new FrontendAstSideTable(); + var binding = new FrontendBinding("self", FrontendBindingKind.SELF, null); + symbolBindings.put(bindingNode, binding); + + var patch = new FrontendTopBindingPatch(symbolBindings); + symbolBindings.clear(); + + assertSame(binding, patch.symbolBindings().get(bindingNode)); + assertTrue(symbolBindings.isEmpty()); + } + + @Test + void applyPatchPublishesNewFactsWithoutReplacingStableSideTableReferences() { + var analysisData = FrontendAnalysisData.bootstrap(); + var symbolBindingsRef = analysisData.symbolBindings(); + var resolvedMembersRef = analysisData.resolvedMembers(); + var resolvedCallsRef = analysisData.resolvedCalls(); + var expressionTypesRef = analysisData.expressionTypes(); + var slotTypesRef = analysisData.slotTypes(); + + var bindingNode = identifier("local"); + var binding = new FrontendBinding("self", FrontendBindingKind.SELF, null); + var symbolBindings = new FrontendAstSideTable(); + symbolBindings.put(bindingNode, binding); + analysisData.applyPatch(patch(FrontendSemanticStage.TOP_BINDING, symbolBindings)); + + var memberNode = passNode(); + var member = FrontendResolvedMember.resolved( + "hp", + FrontendBindingKind.PROPERTY, + FrontendReceiverKind.INSTANCE, + ScopeOwnerKind.GDCC, + new GdObjectType("Player"), + GdIntType.INT, + "Player.hp" + ); + var resolvedMembers = new FrontendAstSideTable(); + resolvedMembers.put(memberNode, member); + + var callNode = identifier("move"); + var resolvedCalls = new FrontendAstSideTable(); + var call = FrontendResolvedCall.resolved( + "move", + FrontendCallResolutionKind.INSTANCE_METHOD, + FrontendReceiverKind.INSTANCE, + ScopeOwnerKind.GDCC, + new GdObjectType("Player"), + GdIntType.INT, + List.of(GdIntType.INT), + "Player.move" + ); + resolvedCalls.put(callNode, call); + analysisData.applyPatch(patch( + FrontendSemanticStage.CHAIN_BINDING, + new FrontendAstSideTable<>(), + resolvedMembers, + resolvedCalls, + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + List.of() + )); + + var expressionNode = identifier("value"); + var expressionTypes = new FrontendAstSideTable(); + var expressionType = FrontendExpressionType.resolved(GdIntType.INT); + expressionTypes.put(expressionNode, expressionType); + analysisData.applyPatch(patch( + FrontendSemanticStage.EXPR_TYPE, + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + expressionTypes, + new FrontendAstSideTable<>(), + List.of() + )); + + var slotNode = variable("value"); + var slotTypes = new FrontendAstSideTable(); + slotTypes.put(slotNode, GdIntType.INT); + analysisData.applyPatch(patch( + FrontendSemanticStage.VAR_TYPE_POST, + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + slotTypes, + List.of() + )); + + assertSame(symbolBindingsRef, analysisData.symbolBindings()); + assertSame(resolvedMembersRef, analysisData.resolvedMembers()); + assertSame(resolvedCallsRef, analysisData.resolvedCalls()); + assertSame(expressionTypesRef, analysisData.expressionTypes()); + assertSame(slotTypesRef, analysisData.slotTypes()); + assertSame(binding, analysisData.symbolBindings().get(bindingNode)); + assertSame(member, analysisData.resolvedMembers().get(memberNode)); + assertSame(call, analysisData.resolvedCalls().get(callNode)); + assertSame(expressionType, analysisData.expressionTypes().get(expressionNode)); + assertSame(GdIntType.INT, expressionType.publishedType()); + assertSame(GdIntType.INT, analysisData.slotTypes().get(slotNode)); + } + + @Test + void applyPatchAllowsIdempotentMergeForLogicallyEquivalentFacts() { + var analysisData = FrontendAnalysisData.bootstrap(); + var expressionNode = identifier("value"); + var slotNode = variable("value"); + analysisData.expressionTypes().put(expressionNode, FrontendExpressionType.resolved(GdIntType.INT)); + analysisData.slotTypes().put(slotNode, GdIntType.INT); + + var idempotentExpressionTypes = new FrontendAstSideTable(); + idempotentExpressionTypes.put(expressionNode, FrontendExpressionType.resolved(new GdIntType())); + var idempotentSlotTypes = new FrontendAstSideTable(); + idempotentSlotTypes.put(slotNode, new GdIntType()); + + analysisData.applyPatch(patch( + FrontendSemanticStage.EXPR_TYPE, + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + idempotentExpressionTypes, + new FrontendAstSideTable<>(), + List.of() + )); + analysisData.applyPatch(patch( + FrontendSemanticStage.VAR_TYPE_POST, + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + idempotentSlotTypes, + List.of() + )); + + var idempotentExpressionType = analysisData.expressionTypes().get(expressionNode); + assertSame(GdIntType.INT, Objects.requireNonNull(idempotentExpressionType).publishedType()); + assertSame(GdIntType.INT, analysisData.slotTypes().get(slotNode)); + } + + @Test + void applyPatchRejectsConflictingExpressionAndSlotFacts() { + var analysisData = FrontendAnalysisData.bootstrap(); + var expressionNode = identifier("value"); + var slotNode = variable("slot"); + analysisData.expressionTypes().put(expressionNode, FrontendExpressionType.resolved(GdIntType.INT)); + analysisData.slotTypes().put(slotNode, GdIntType.INT); + + var conflictingExpressionTypes = new FrontendAstSideTable(); + conflictingExpressionTypes.put(expressionNode, FrontendExpressionType.resolved(GdFloatType.FLOAT)); + assertThrows( + FrontendAnalysisPatchException.class, + () -> analysisData.applyPatch(patch( + FrontendSemanticStage.EXPR_TYPE, + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + conflictingExpressionTypes, + new FrontendAstSideTable<>(), + List.of() + )) + ); + + var failedExpressionTypes = new FrontendAstSideTable(); + var failedNode = identifier("failed"); + analysisData.expressionTypes().put(failedNode, FrontendExpressionType.failed("original failure")); + failedExpressionTypes.put(failedNode, FrontendExpressionType.resolved(GdIntType.INT)); + assertThrows( + FrontendAnalysisPatchException.class, + () -> analysisData.applyPatch(patch( + FrontendSemanticStage.EXPR_TYPE, + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + failedExpressionTypes, + new FrontendAstSideTable<>(), + List.of() + )) + ); + + var conflictingSlotTypes = new FrontendAstSideTable(); + conflictingSlotTypes.put(slotNode, new GdObjectType("Player")); + assertThrows( + FrontendAnalysisPatchException.class, + () -> analysisData.applyPatch(patch( + FrontendSemanticStage.VAR_TYPE_POST, + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + conflictingSlotTypes, + List.of() + )) + ); + } + + @Test + void ownerPatchesRejectConflictingBindingMemberAndCallFactsWithoutMutation() { + var analysisData = FrontendAnalysisData.bootstrap(); + + var bindingNode = identifier("binding"); + var stableBinding = new FrontendBinding("binding", FrontendBindingKind.UNKNOWN, null); + analysisData.symbolBindings().put(bindingNode, stableBinding); + var conflictingBindings = new FrontendAstSideTable(); + conflictingBindings.put(bindingNode, new FrontendBinding("binding", FrontendBindingKind.SELF, null)); + assertThrows( + FrontendAnalysisPatchException.class, + () -> analysisData.applyPatch(new FrontendTopBindingPatch(conflictingBindings)) + ); + assertSame(stableBinding, analysisData.symbolBindings().get(bindingNode)); + + var memberNode = identifier("member"); + var stableMember = FrontendResolvedMember.resolved( + "member", + FrontendBindingKind.PROPERTY, + FrontendReceiverKind.INSTANCE, + ScopeOwnerKind.GDCC, + new GdObjectType("Owner"), + GdIntType.INT, + "Owner.member" + ); + analysisData.resolvedMembers().put(memberNode, stableMember); + var conflictingMembers = new FrontendAstSideTable(); + conflictingMembers.put(memberNode, FrontendResolvedMember.resolved( + "member", + FrontendBindingKind.PROPERTY, + FrontendReceiverKind.INSTANCE, + ScopeOwnerKind.GDCC, + new GdObjectType("Owner"), + GdFloatType.FLOAT, + "Owner.member" + )); + assertThrows( + FrontendAnalysisPatchException.class, + () -> analysisData.applyPatch(new FrontendChainBindingPatch( + conflictingMembers, + new FrontendAstSideTable<>() + )) + ); + assertSame(stableMember, analysisData.resolvedMembers().get(memberNode)); + + var callNode = identifier("call"); + var stableCall = FrontendResolvedCall.resolved( + "call", + FrontendCallResolutionKind.STATIC_METHOD, + FrontendReceiverKind.TYPE_META, + ScopeOwnerKind.GDCC, + new GdObjectType("Owner"), + GdIntType.INT, + List.of(), + "Owner.call" + ); + analysisData.resolvedCalls().put(callNode, stableCall); + var conflictingCalls = new FrontendAstSideTable(); + conflictingCalls.put(callNode, FrontendResolvedCall.resolved( + "call", + FrontendCallResolutionKind.STATIC_METHOD, + FrontendReceiverKind.TYPE_META, + ScopeOwnerKind.GDCC, + new GdObjectType("Owner"), + GdFloatType.FLOAT, + List.of(), + "Owner.call" + )); + assertThrows( + FrontendAnalysisPatchException.class, + () -> analysisData.applyPatch(new FrontendChainBindingPatch( + new FrontendAstSideTable<>(), + conflictingCalls + )) + ); + assertSame(stableCall, analysisData.resolvedCalls().get(callNode)); + } + + @Test + void applyPatchRefreshesPublishedLocalBindingsForMatchingDeclarationOnly() throws Exception { + var analysisData = FrontendAnalysisData.bootstrap(); + var updatedScope = newBodyScope(); + var untouchedScope = newBodyScope(); + var targetDeclaration = variable("local"); + var untouchedDeclaration = variable("local"); + updatedScope.defineLocal("local", GdVariantType.VARIANT, targetDeclaration); + untouchedScope.defineLocal("local", GdVariantType.VARIANT, untouchedDeclaration); + + var targetBindingNode = identifier("target_use"); + var untouchedBindingNode = identifier("other_use"); + var targetBinding = localBinding("local", targetDeclaration, requireLocal(updatedScope, "local")); + var untouchedBinding = localBinding("local", untouchedDeclaration, requireLocal(untouchedScope, "local")); + analysisData.symbolBindings().put(targetBindingNode, targetBinding); + analysisData.symbolBindings().put(untouchedBindingNode, untouchedBinding); + + analysisData.applyPatch(patch( + FrontendSemanticStage.LOCAL_TYPE_STABILIZATION, + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + List.of(new FrontendLocalSlotTypeUpdate(updatedScope, "local", targetDeclaration, GdIntType.INT)) + )); + + var refreshedBinding = analysisData.symbolBindings().get(targetBindingNode); + var refreshedValue = Objects.requireNonNull(refreshedBinding).resolvedValue(); + var untouchedValue = Objects.requireNonNull(analysisData.symbolBindings().get(untouchedBindingNode)).resolvedValue(); + assertNotSame(targetBinding, refreshedBinding); + assertSame(targetDeclaration, refreshedBinding.declarationSite()); + assertSame(targetDeclaration, Objects.requireNonNull(refreshedValue).declaration()); + assertSame(GdIntType.INT, refreshedValue.type()); + assertSame(untouchedBinding, analysisData.symbolBindings().get(untouchedBindingNode)); + assertSame(GdVariantType.VARIANT, Objects.requireNonNull(untouchedValue).type()); + assertSame(GdIntType.INT, requireLocal(updatedScope, "local").type()); + } + + @Test + void applyPatchSkipsBindingRefreshForNoOpUpdateAndExprTypeOnlyPatch() throws Exception { + var analysisData = FrontendAnalysisData.bootstrap(); + var localScope = newBodyScope(); + var declaration = variable("local"); + localScope.defineLocal("local", GdIntType.INT, declaration); + var bindingNode = identifier("local_use"); + var originalBinding = localBinding("local", declaration, requireLocal(localScope, "local")); + analysisData.symbolBindings().put(bindingNode, originalBinding); + + analysisData.applyPatch(patch( + FrontendSemanticStage.LOCAL_TYPE_STABILIZATION, + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + List.of(new FrontendLocalSlotTypeUpdate(localScope, "local", declaration, GdIntType.INT)) + )); + assertSame(originalBinding, analysisData.symbolBindings().get(bindingNode)); + + var expressionTypes = new FrontendAstSideTable(); + expressionTypes.put(identifier("initializer"), FrontendExpressionType.resolved(GdFloatType.FLOAT)); + analysisData.applyPatch(patch( + FrontendSemanticStage.EXPR_TYPE, + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + expressionTypes, + new FrontendAstSideTable<>(), + List.of() + )); + assertSame(originalBinding, analysisData.symbolBindings().get(bindingNode)); + } + + @Test + void applyPatchRejectsCompilerOnlyLeaksAcrossExpressionAndSlotTypePayloads() throws Exception { + var analysisData = FrontendAnalysisData.bootstrap(); + var expressionTypes = new FrontendAstSideTable(); + expressionTypes.put(identifier("iter"), FrontendExpressionType.resolved(GdccForRangeIterType.FOR_RANGE_ITER)); + assertThrows( + FrontendAnalysisPatchException.class, + () -> analysisData.applyPatch(patch( + FrontendSemanticStage.EXPR_TYPE, + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + expressionTypes, + new FrontendAstSideTable<>(), + List.of() + )) + ); + + var slotTypes = new FrontendAstSideTable(); + slotTypes.put(variable("iter_slot"), GdccForRangeIterType.FOR_RANGE_ITER); + assertThrows( + FrontendAnalysisPatchException.class, + () -> analysisData.applyPatch(patch( + FrontendSemanticStage.VAR_TYPE_POST, + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + slotTypes, + List.of() + )) + ); + + var localScope = newBodyScope(); + var declaration = variable("local"); + localScope.defineLocal("local", GdVariantType.VARIANT, declaration); + assertThrows( + FrontendAnalysisPatchException.class, + () -> analysisData.applyPatch(patch( + FrontendSemanticStage.LOCAL_TYPE_STABILIZATION, + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + List.of(new FrontendLocalSlotTypeUpdate( + localScope, + "local", + declaration, + GdccForRangeIterType.FOR_RANGE_ITER + )) + )) + ); + } + + @Test + void applyPatchRejectsCompilerOnlyLeaksAcrossBindingMemberAndCallPayloads() { + var analysisData = FrontendAnalysisData.bootstrap(); + var declaration = variable("local"); + var compilerOnlyValue = new ScopeValue( + "local", + GdccForRangeIterType.FOR_RANGE_ITER, + ScopeValueKind.LOCAL, + declaration, + false, + true, + false + ); + var symbolBindings = new FrontendAstSideTable(); + symbolBindings.put( + identifier("local"), + new FrontendBinding( + "local", + FrontendBindingKind.LOCAL_VAR, + declaration, + compilerOnlyValue, + ScopeLookupStatus.FOUND_ALLOWED + ) + ); + assertThrows( + FrontendAnalysisPatchException.class, + () -> analysisData.applyPatch(patch(FrontendSemanticStage.TOP_BINDING, symbolBindings)) + ); + + var resolvedMembers = new FrontendAstSideTable(); + resolvedMembers.put(identifier("member"), FrontendResolvedMember.resolved( + "member", + FrontendBindingKind.PROPERTY, + FrontendReceiverKind.INSTANCE, + ScopeOwnerKind.GDCC, + GdccForRangeIterType.FOR_RANGE_ITER, + GdIntType.INT, + "owner.member" + )); + assertThrows( + FrontendAnalysisPatchException.class, + () -> analysisData.applyPatch(patch( + FrontendSemanticStage.CHAIN_BINDING, + new FrontendAstSideTable<>(), + resolvedMembers, + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + List.of() + )) + ); + + var resolvedCalls = new FrontendAstSideTable(); + resolvedCalls.put(identifier("call"), FrontendResolvedCall.resolved( + "call", + FrontendCallResolutionKind.STATIC_METHOD, + FrontendReceiverKind.TYPE_META, + ScopeOwnerKind.GDCC, + new GdObjectType("Owner"), + GdIntType.INT, + List.of(GdIntType.INT), + "Owner.call", + new FrontendResolvedCall.ExactCallableBoundary( + List.of(GdccForRangeIterType.FOR_RANGE_ITER), + false + ) + )); + assertThrows( + FrontendAnalysisPatchException.class, + () -> analysisData.applyPatch(patch( + FrontendSemanticStage.EXPR_TYPE, + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + resolvedCalls, + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + List.of() + )) + ); + } + + @Test + void updateWholeTablePublicationUsesSharedCompilerOnlyGuard() { + var analysisData = FrontendAnalysisData.bootstrap(); + var symbolBindings = new FrontendAstSideTable(); + var declaration = variable("local"); + symbolBindings.put(identifier("local"), new FrontendBinding( + "local", + FrontendBindingKind.LOCAL_VAR, + declaration, + new ScopeValue( + "local", + GdccForRangeIterType.FOR_RANGE_ITER, + ScopeValueKind.LOCAL, + declaration, + false, + true, + false + ), + ScopeLookupStatus.FOUND_ALLOWED + )); + assertThrows(FrontendAnalysisPatchException.class, () -> analysisData.updateSymbolBindings(symbolBindings)); + + var resolvedCalls = new FrontendAstSideTable(); + resolvedCalls.put(identifier("call"), FrontendResolvedCall.resolved( + "call", + FrontendCallResolutionKind.STATIC_METHOD, + FrontendReceiverKind.TYPE_META, + ScopeOwnerKind.GDCC, + new GdObjectType("Owner"), + GdccForRangeIterType.FOR_RANGE_ITER, + List.of(GdIntType.INT), + "Owner.call" + )); + assertThrows(FrontendAnalysisPatchException.class, () -> analysisData.updateResolvedCalls(resolvedCalls)); + } + + @Test + void ownerPatchTransactionAppliesInFixedOwnerOrderAndRejectsRegressions() { + var analysisData = FrontendAnalysisData.bootstrap(); + var bindingNode = identifier("value"); + var symbolBindings = new FrontendAstSideTable(); + var binding = new FrontendBinding("value", FrontendBindingKind.LOCAL_VAR, variable("value")); + symbolBindings.put(bindingNode, binding); + var expressionNode = identifier("expr"); + var expressionTypes = new FrontendAstSideTable(); + expressionTypes.put(expressionNode, FrontendExpressionType.resolved(GdIntType.INT)); + + new FrontendPatchTransaction(List.of( + new FrontendTopBindingPatch(symbolBindings), + new FrontendExprTypePatch(expressionTypes, new FrontendAstSideTable<>()) + )).applyTo(analysisData); + + assertSame(binding, analysisData.symbolBindings().get(bindingNode)); + assertEquals( + GdIntType.INT, + Objects.requireNonNull(analysisData.expressionTypes().get(expressionNode)).publishedType() + ); + assertThrows( + FrontendAnalysisPatchException.class, + () -> new FrontendPatchTransaction(List.of( + new FrontendExprTypePatch(expressionTypes, new FrontendAstSideTable<>()), + new FrontendTopBindingPatch(symbolBindings) + )) + ); + assertThrows( + FrontendAnalysisPatchException.class, + () -> new FrontendPatchTransaction(List.of( + new FrontendTopBindingPatch(symbolBindings), + new FrontendTopBindingPatch(symbolBindings) + )) + ); + } + + @Test + void applyPatchRejectsVoidAndConflictingLocalSlotUpdates() throws Exception { + var analysisData = FrontendAnalysisData.bootstrap(); + var localScope = newBodyScope(); + var declaration = variable("local"); + localScope.defineLocal("local", GdVariantType.VARIANT, declaration); + + assertThrows( + FrontendAnalysisPatchException.class, + () -> analysisData.applyPatch(patch( + FrontendSemanticStage.LOCAL_TYPE_STABILIZATION, + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + List.of(new FrontendLocalSlotTypeUpdate(localScope, "local", declaration, GdVoidType.VOID)) + )) + ); + + localScope.resetLocalType("local", declaration, GdIntType.INT); + assertThrows( + FrontendAnalysisPatchException.class, + () -> analysisData.applyPatch(patch( + FrontendSemanticStage.LOCAL_TYPE_STABILIZATION, + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + List.of(new FrontendLocalSlotTypeUpdate(localScope, "local", declaration, GdFloatType.FLOAT)) + )) + ); + } + private static PassStatement passNode() { return new PassStatement(RANGE); } + + private static @NotNull IdentifierExpression identifier(@NotNull String name) { + return new IdentifierExpression(name, RANGE); + } + + private static @NotNull VariableDeclaration variable(@NotNull String name) { + return new VariableDeclaration( + DeclarationKind.VAR, + name, + new TypeRef(":=", RANGE), + null, + false, + "variable_declaration", + RANGE + ); + } + + @SuppressWarnings("SameParameterValue") + private static @NotNull FrontendBinding localBinding( + @NotNull String name, + @NotNull Object declaration, + @NotNull ScopeValue value + ) { + return new FrontendBinding( + name, + FrontendBindingKind.LOCAL_VAR, + declaration, + value, + ScopeLookupStatus.FOUND_ALLOWED + ); + } + + @SuppressWarnings("SameParameterValue") + private static @NotNull ScopeValue requireLocal(@NotNull BlockScope scope, @NotNull String name) { + var value = scope.resolveValueHere(name); + if (value == null) { + throw new IllegalStateException("missing local '" + name + "'"); + } + return value; + } + + private static @NotNull BlockScope newBodyScope() throws Exception { + var registry = new ClassRegistry(ExtensionApiLoader.loadDefault()); + var ownerClass = new LirClassDef("SyntheticOwner", "RefCounted"); + var classScope = new ClassScope(registry, registry, ownerClass); + var callableScope = new CallableScope(classScope, CallableScopeKind.FUNCTION_DECLARATION); + return new BlockScope(callableScope, BlockScopeKind.FUNCTION_BODY); + } + + @SuppressWarnings("SameParameterValue") + private static @NotNull FrontendOwnerPatch patch( + @NotNull FrontendSemanticStage stage, + @NotNull FrontendAstSideTable symbolBindings, + @NotNull FrontendAstSideTable resolvedMembers, + @NotNull FrontendAstSideTable resolvedCalls, + @NotNull FrontendAstSideTable expressionTypes, + @NotNull FrontendAstSideTable slotTypes, + @NotNull List localSlotTypeUpdates + ) { + return switch (stage) { + case TOP_BINDING -> new FrontendTopBindingPatch(symbolBindings); + case LOCAL_TYPE_STABILIZATION -> new FrontendLocalTypeStabilizationPatch(localSlotTypeUpdates); + case CHAIN_BINDING -> new FrontendChainBindingPatch(resolvedMembers, resolvedCalls); + case EXPR_TYPE -> new FrontendExprTypePatch(expressionTypes, resolvedCalls); + case VAR_TYPE_POST -> new FrontendVarTypePostPatch(slotTypes); + }; + } + + @SuppressWarnings("SameParameterValue") + private static @NotNull FrontendOwnerPatch patch( + @NotNull FrontendSemanticStage stage, + @NotNull FrontendAstSideTable symbolBindings + ) { + return patch( + stage, + symbolBindings, + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + List.of() + ); + } } diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendBodySemanticSupportPolicyTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendBodySemanticSupportPolicyTest.java new file mode 100644 index 00000000..ca037b4a --- /dev/null +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendBodySemanticSupportPolicyTest.java @@ -0,0 +1,149 @@ +package gd.script.gdcc.frontend.sema; + +import gd.script.gdcc.frontend.scope.BlockScopeKind; +import gd.script.gdcc.frontend.scope.CallableScopeKind; +import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueDomain; +import org.junit.jupiter.api.Test; + +import java.util.EnumSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; + +class FrontendBodySemanticSupportPolicyTest { + @Test + void executableBlockKindsPublishInventoryAndEnterSuiteResolver() { + var supportedKinds = EnumSet.of( + BlockScopeKind.BLOCK_STATEMENT, + BlockScopeKind.FUNCTION_BODY, + BlockScopeKind.CONSTRUCTOR_BODY, + BlockScopeKind.IF_BODY, + BlockScopeKind.ELIF_BODY, + BlockScopeKind.ELSE_BODY, + BlockScopeKind.WHILE_BODY, + BlockScopeKind.FOR_BODY + ); + + for (var kind : BlockScopeKind.values()) { + var policy = FrontendBodySemanticSupportPolicy.forBlockScopeKind(kind); + assertEquals(supportedKinds.contains(kind), policy.publishesLexicalInventory(), kind.name()); + assertEquals(supportedKinds.contains(kind), policy.entersSuiteResolver(), kind.name()); + if (supportedKinds.contains(kind)) { + assertEquals(FrontendVisibleValueDomain.EXECUTABLE_BODY, policy.visibleValueDomain(), kind.name()); + } + } + } + + @Test + void unsupportedScopesKeepPreciseStructuralDomains() { + assertEquals( + FrontendBodySemanticSupportPolicy.LAMBDA_SUBTREE, + FrontendBodySemanticSupportPolicy.forBlockScopeKind(BlockScopeKind.LAMBDA_BODY) + ); + assertEquals( + FrontendBodySemanticSupportPolicy.MATCH_SUBTREE, + FrontendBodySemanticSupportPolicy.forBlockScopeKind(BlockScopeKind.MATCH_SECTION_BODY) + ); + assertEquals( + FrontendBodySemanticSupportPolicy.LAMBDA_SUBTREE, + FrontendBodySemanticSupportPolicy.forCallableScopeKind(CallableScopeKind.LAMBDA_EXPRESSION) + ); + + var deferredPolicies = Set.of( + FrontendBodySemanticSupportPolicy.LAMBDA_SUBTREE, + FrontendBodySemanticSupportPolicy.MATCH_SUBTREE, + FrontendBodySemanticSupportPolicy.BLOCK_LOCAL_CONST_SUBTREE, + FrontendBodySemanticSupportPolicy.PARAMETER_DEFAULT, + FrontendBodySemanticSupportPolicy.UNKNOWN_OR_SKIPPED_SUBTREE + ); + for (var policy : deferredPolicies) { + assertFalse(policy.publishesLexicalInventory(), policy.name()); + assertFalse(policy.entersSuiteResolver(), policy.name()); + assertNotSame(FrontendVisibleValueDomain.EXECUTABLE_BODY, policy.visibleValueDomain(), policy.name()); + } + } + + @Test + void forHeaderUsesOuterExecutableDomainWithoutOwningBodyInventory() { + var policy = FrontendBodySemanticSupportPolicy.FOR_HEADER; + + assertFalse(policy.publishesLexicalInventory()); + assertFalse(policy.entersSuiteResolver()); + assertEquals(FrontendVisibleValueDomain.EXECUTABLE_BODY, policy.visibleValueDomain()); + } + + @Test + void bodyEntrySemanticEntryAgreesWithFirstCertificateGateForAllBlockScopeKinds() { + var supportedKinds = EnumSet.of( + BlockScopeKind.BLOCK_STATEMENT, + BlockScopeKind.FUNCTION_BODY, + BlockScopeKind.CONSTRUCTOR_BODY, + BlockScopeKind.IF_BODY, + BlockScopeKind.ELIF_BODY, + BlockScopeKind.ELSE_BODY, + BlockScopeKind.WHILE_BODY, + BlockScopeKind.FOR_BODY + ); + + for (var kind : BlockScopeKind.values()) { + var policy = FrontendBodySemanticSupportPolicy.forBlockScopeKind(kind); + assertEquals(supportedKinds.contains(kind), policy.isSupportedSuiteBodyRoot(), kind.name()); + } + } + + @Test + void bodyEntrySemanticEntryAgreesWithFirstCertificateGateForAllCallableScopeKinds() { + var supportedCallableKinds = EnumSet.of( + CallableScopeKind.FUNCTION_DECLARATION, + CallableScopeKind.CONSTRUCTOR_DECLARATION + ); + + for (var kind : CallableScopeKind.values()) { + var policy = FrontendBodySemanticSupportPolicy.forCallableScopeKind(kind); + assertEquals(supportedCallableKinds.contains(kind), policy.isSupportedSuiteBodyRoot(), kind.name()); + } + } + + @Test + void forHeaderIsNotASupportedSuiteBodyRootEvenThoughItUsesExecutableVisibleValueDomain() { + // FOR_HEADER shares the EXECUTABLE_BODY visible-value domain but must NOT be a body root: + // this is the canonical case showing body-entry and inventory-publication are distinct questions. + var policy = FrontendBodySemanticSupportPolicy.FOR_HEADER; + + assertFalse(policy.isSupportedSuiteBodyRoot()); + assertEquals(FrontendVisibleValueDomain.EXECUTABLE_BODY, policy.visibleValueDomain()); + } + + @Test + void deferredPoliciesAreNotSupportedSuiteBodyRoots() { + var deferredPolicies = Set.of( + FrontendBodySemanticSupportPolicy.LAMBDA_SUBTREE, + FrontendBodySemanticSupportPolicy.MATCH_SUBTREE, + FrontendBodySemanticSupportPolicy.BLOCK_LOCAL_CONST_SUBTREE, + FrontendBodySemanticSupportPolicy.PARAMETER_DEFAULT, + FrontendBodySemanticSupportPolicy.UNKNOWN_OR_SKIPPED_SUBTREE + ); + + for (var policy : deferredPolicies) { + assertFalse(policy.isSupportedSuiteBodyRoot(), policy.name()); + } + } + + @Test + void executableInventorySupportBodyEntryBridgeAgreesWithPolicyForAllBlockScopeKinds() { + // The bridge consumed by FrontendInterfacePhase.enterSupportedBlock must route through the same + // body-entry semantic entry as FrontendBodyStructuralCompleteness's first certificate gate, so the + // two consumers can never diverge even if publishesLexicalInventory and entersSuiteResolver diverge + // for a future feature. + for (var kind : BlockScopeKind.values()) { + var policy = FrontendBodySemanticSupportPolicy.forBlockScopeKind(kind); + assertEquals( + policy.isSupportedSuiteBodyRoot(), + FrontendExecutableInventorySupport.isSupportedSuiteBodyRoot(kind), + kind.name() + ); + } + } +} diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendInterfacePhaseTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendInterfacePhaseTest.java new file mode 100644 index 00000000..1ffb3b08 --- /dev/null +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendInterfacePhaseTest.java @@ -0,0 +1,460 @@ +package gd.script.gdcc.frontend.sema; + +import dev.superice.gdparser.frontend.ast.ASTWalker; +import dev.superice.gdparser.frontend.ast.ForStatement; +import dev.superice.gdparser.frontend.ast.FrontendASTTraversalDirective; +import dev.superice.gdparser.frontend.ast.FunctionDeclaration; +import dev.superice.gdparser.frontend.ast.IdentifierExpression; +import dev.superice.gdparser.frontend.ast.IfStatement; +import dev.superice.gdparser.frontend.ast.LambdaExpression; +import dev.superice.gdparser.frontend.ast.MatchStatement; +import dev.superice.gdparser.frontend.ast.Node; +import dev.superice.gdparser.frontend.ast.Statement; +import dev.superice.gdparser.frontend.ast.VariableDeclaration; +import dev.superice.gdparser.frontend.ast.WhileStatement; +import gd.script.gdcc.frontend.diagnostic.DiagnosticManager; +import gd.script.gdcc.frontend.parse.FrontendModule; +import gd.script.gdcc.frontend.parse.FrontendSourceUnit; +import gd.script.gdcc.frontend.parse.GdScriptParserService; +import gd.script.gdcc.frontend.sema.analyzer.FrontendInterfacePhase; +import gd.script.gdcc.frontend.sema.analyzer.FrontendScopeAnalyzer; +import gd.script.gdcc.frontend.sema.analyzer.FrontendVariableAnalyzer; +import gd.script.gdcc.frontend.sema.resolver.FrontendFilteredValueHitReason; +import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueDomain; +import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueResolveRequest; +import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueResolver; +import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueStatus; +import gd.script.gdcc.gdextension.ExtensionApiLoader; +import gd.script.gdcc.scope.ClassRegistry; +import gd.script.gdcc.type.GdIntType; +import gd.script.gdcc.type.GdVariantType; +import gd.script.gdcc.type.GdccForRangeIterType; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; +import java.util.List; +import java.util.function.Predicate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +class FrontendInterfacePhaseTest { + @Test + void buildsSupportedBodyDeclarationIndexTypedBaselineAndSuiteEntryRoots() throws Exception { + var phaseInput = phaseInput("interface_supported_blocks.gd", """ + class_name InterfaceSupportedBlocks + extends Node + + var property_value = 1 + + func ping(value: int, alias): + var first := second + var second: int = value + if value > 0: + var branch := second + while value > 1: + var loop_local := second + break + return second + """); + var sourceFile = phaseInput.unit().ast(); + var pingFunction = findStatement( + sourceFile.statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("ping") + ); + var property = findStatement( + sourceFile.statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("property_value") + ); + var first = findStatement( + pingFunction.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("first") + ); + var second = findStatement( + pingFunction.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("second") + ); + var ifStatement = findStatement(pingFunction.body().statements(), IfStatement.class, _ -> true); + var branch = findStatement( + ifStatement.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("branch") + ); + var whileStatement = findStatement(pingFunction.body().statements(), WhileStatement.class, _ -> true); + + var surface = new FrontendInterfacePhase().analyze(phaseInput.registry(), phaseInput.analysisData()); + + var bodyDeclarations = surface.bodyDeclarationIndex().declarationsFor(pingFunction.body()); + assertEquals(List.of("first", "second"), bodyDeclarations.stream() + .map(localDeclaration -> assertInstanceOf( + VariableDeclaration.class, + localDeclaration.declaration() + ).name()) + .toList()); + assertEquals(0, bodyDeclarations.getFirst().sourceOrder()); + assertEquals(1, bodyDeclarations.getLast().sourceOrder()); + assertSame(first, bodyDeclarations.getFirst().declaration()); + assertSame(second, bodyDeclarations.getLast().declaration()); + assertEquals(1, surface.bodyDeclarationIndex().declarationsFor(ifStatement.body()).size()); + assertSame(branch, surface.bodyDeclarationIndex().declarationsFor(ifStatement.body()).getFirst().declaration()); + + var baseline = surface.typedLexicalBaseline(); + assertEquals(GdIntType.INT, baseline.typeFor(pingFunction.parameters().getFirst())); + assertEquals(GdVariantType.VARIANT, baseline.typeFor(pingFunction.parameters().getLast())); + assertEquals(GdVariantType.VARIANT, baseline.typeFor(first)); + assertEquals(GdIntType.INT, baseline.typeFor(second)); + assertEquals(GdVariantType.VARIANT, baseline.typeFor(branch)); + + var suiteEntryRoots = surface.suiteEntryRoots(); + assertTrue(suiteEntryRoots.containsCallableOwner(pingFunction)); + assertTrue(suiteEntryRoots.containsPropertyInitializer(property)); + assertTrue(suiteEntryRoots.containsSupportedBlock(pingFunction.body())); + assertTrue(suiteEntryRoots.containsSupportedBlock(ifStatement.body())); + assertTrue(suiteEntryRoots.containsSupportedBlock(whileStatement.body())); + assertTrue(phaseInput.analysisData().symbolBindings().isEmpty()); + assertTrue(phaseInput.analysisData().expressionTypes().isEmpty()); + assertTrue(phaseInput.analysisData().resolvedMembers().isEmpty()); + assertTrue(phaseInput.analysisData().resolvedCalls().isEmpty()); + assertTrue(phaseInput.analysisData().slotTypes().isEmpty()); + } + + @Test + void keepsFutureDeclarationVisibleToResolverThroughCompleteBodyIndex() throws Exception { + var phaseInput = phaseInput("interface_future_declaration.gd", """ + class_name InterfaceFutureDeclaration + extends Node + + func ping(): + var first := second + var second := 1 + """); + var pingFunction = findStatement( + phaseInput.unit().ast().statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("ping") + ); + var first = findStatement( + pingFunction.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("first") + ); + assertNotNull(first.value()); + var firstInitializer = first.value(); + var secondUseSite = findNode(firstInitializer, IdentifierExpression.class, identifier -> identifier.name().equals("second")); + + var surface = new FrontendInterfacePhase().analyze(phaseInput.registry(), phaseInput.analysisData()); + var resolver = new FrontendVisibleValueResolver( + phaseInput.analysisData(), + surface.bodyDeclarationIndex() + ); + var resolution = resolver.resolve(new FrontendVisibleValueResolveRequest( + "second", + secondUseSite, + FrontendVisibleValueDomain.EXECUTABLE_BODY + )); + + assertEquals(List.of("first", "second"), surface.bodyDeclarationIndex() + .declarationsFor(pingFunction.body()) + .stream() + .map(localDeclaration -> assertInstanceOf( + VariableDeclaration.class, + localDeclaration.declaration() + ).name()) + .toList()); + assertEquals(FrontendVisibleValueStatus.NOT_FOUND, resolution.status()); + assertNull(resolution.visibleValue()); + assertEquals( + FrontendFilteredValueHitReason.DECLARATION_AFTER_USE_SITE, + primaryFilteredHitReason(resolution) + ); + } + + @Test + void publishesForInventoryWhileUnsupportedFeatureOwnedBodiesStayExcluded() throws Exception { + var phaseInput = phaseInput("interface_pending_gates.gd", """ + class_name InterfacePendingGates + extends Node + + func ping(items, choice): + var outer := choice + for item: int in items: + var from_for := item + match choice: + 0: + var from_match := choice + var callback = func(): + var from_lambda := choice + return choice + const answer = choice + return outer + """); + var pingFunction = findStatement( + phaseInput.unit().ast().statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("ping") + ); + var forStatement = findStatement(pingFunction.body().statements(), ForStatement.class, _ -> true); + var matchStatement = findStatement(pingFunction.body().statements(), MatchStatement.class, _ -> true); + var callback = findStatement( + pingFunction.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("callback") + ); + var lambda = assertInstanceOf(LambdaExpression.class, callback.value()); + var answer = findStatement( + pingFunction.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("answer") + ); + var fromFor = findStatement( + forStatement.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("from_for") + ); + + var surface = new FrontendInterfacePhase().analyze(phaseInput.registry(), phaseInput.analysisData()); + + var forDeclarations = surface.bodyDeclarationIndex().declarationsFor(forStatement.body()); + assertEquals(2, forDeclarations.size()); + assertSame(forStatement, forDeclarations.getFirst().declaration()); + assertEquals(FrontendBodyLocalDeclaration.Kind.ITERATOR, forDeclarations.getFirst().kind()); + assertSame(fromFor, forDeclarations.getLast().declaration()); + assertEquals(FrontendBodyLocalDeclaration.Kind.ORDINARY_VAR, forDeclarations.getLast().kind()); + assertEquals(GdIntType.INT, surface.typedLexicalBaseline().typeFor(forStatement)); + assertEquals(GdVariantType.VARIANT, surface.typedLexicalBaseline().typeFor(fromFor)); + assertTrue(surface.suiteEntryRoots().containsSupportedBlock(forStatement.body())); + assertFalse(surface.suiteEntryRoots().containsSupportedBlock(matchStatement.sections().getFirst().body())); + assertFalse(surface.suiteEntryRoots().containsSupportedBlock(lambda.body())); + assertFalse(surface.bodyDeclarationIndex().containsBodyRoot(matchStatement.sections().getFirst().body())); + assertFalse(surface.bodyDeclarationIndex().containsBodyRoot(lambda.body())); + assertFalse(surface.typedLexicalBaseline().containsDeclaration(answer)); + assertTrue(surface.typedLexicalBaseline().containsDeclaration(callback)); + } + + @Test + void forStructuralInventoryIsInvariantAcrossExactVariantAndErrorIterables() throws Exception { + var cases = List.of( + new IterableCase("exact", "items: Array[int]", "items"), + new IterableCase("variant", "items", "items"), + new IterableCase("error", "", "missing_items") + ); + + for (var iterableCase : cases) { + var phaseInput = phaseInput("interface_for_" + iterableCase.name() + ".gd", """ + class_name InterfaceFor%s + extends Node + + func ping(%s): + for item in %s: + var copy := item + """.formatted( + iterableCase.name(), + iterableCase.parameterText(), + iterableCase.iterableText() + )); + var pingFunction = findStatement( + phaseInput.unit().ast().statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("ping") + ); + var forStatement = findStatement(pingFunction.body().statements(), ForStatement.class, _ -> true); + var copy = findStatement( + forStatement.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("copy") + ); + + var surface = new FrontendInterfacePhase().analyze(phaseInput.registry(), phaseInput.analysisData()); + var declarations = surface.bodyDeclarationIndex().declarationsFor(forStatement.body()); + + assertEquals(2, declarations.size(), iterableCase.name()); + assertSame(forStatement, declarations.getFirst().declaration(), iterableCase.name()); + assertSame(copy, declarations.getLast().declaration(), iterableCase.name()); + assertEquals(GdVariantType.VARIANT, surface.typedLexicalBaseline().typeFor(forStatement)); + assertEquals(GdVariantType.VARIANT, surface.typedLexicalBaseline().typeFor(copy)); + assertTrue(surface.suiteEntryRoots().containsSupportedBlock(forStatement.body()), iterableCase.name()); + } + } + + @Test + void nestedForBodiesPublishInventoryWithoutOpeningLambdaMatchOrConstSubtrees() throws Exception { + var phaseInput = phaseInput("interface_nested_for_boundaries.gd", """ + class_name InterfaceNestedForBoundaries + extends Node + + func ping(values): + for outer in values: + for inner in outer: + var deep := inner + var callback = func(): + return outer + match outer: + _: + var matched := outer + const blocked = outer + """); + var pingFunction = findStatement( + phaseInput.unit().ast().statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("ping") + ); + var outerFor = findStatement(pingFunction.body().statements(), ForStatement.class, _ -> true); + var innerFor = findStatement(outerFor.body().statements(), ForStatement.class, _ -> true); + var callback = findStatement( + outerFor.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("callback") + ); + var lambda = assertInstanceOf(LambdaExpression.class, callback.value()); + var matchStatement = findStatement(outerFor.body().statements(), MatchStatement.class, _ -> true); + var blocked = findStatement( + outerFor.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("blocked") + ); + + var surface = new FrontendInterfacePhase().analyze(phaseInput.registry(), phaseInput.analysisData()); + + assertTrue(surface.suiteEntryRoots().containsSupportedBlock(outerFor.body())); + assertTrue(surface.suiteEntryRoots().containsSupportedBlock(innerFor.body())); + assertTrue(surface.bodyDeclarationIndex().containsBodyRoot(outerFor.body())); + assertTrue(surface.bodyDeclarationIndex().containsBodyRoot(innerFor.body())); + assertFalse(surface.suiteEntryRoots().containsSupportedBlock(lambda.body())); + assertFalse(surface.suiteEntryRoots().containsSupportedBlock(matchStatement.sections().getFirst().body())); + assertFalse(surface.bodyDeclarationIndex().containsBodyRoot(lambda.body())); + assertFalse(surface.bodyDeclarationIndex().containsBodyRoot(matchStatement.sections().getFirst().body())); + assertFalse(surface.typedLexicalBaseline().containsDeclaration(blocked)); + } + + @Test + void typedLexicalBaselineRejectsCompilerOnlySourceFacingTypes() throws Exception { + var phaseInput = phaseInput("interface_compiler_only_guard.gd", """ + class_name InterfaceCompilerOnlyGuard + extends Node + + func ping(value: int): + pass + """); + var pingFunction = findStatement( + phaseInput.unit().ast().statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("ping") + ); + + var error = assertThrows(IllegalArgumentException.class, () -> FrontendTypedLexicalBaseline.builder() + .put(pingFunction.parameters().getFirst(), GdccForRangeIterType.FOR_RANGE_ITER) + .build()); + + assertTrue(error.getMessage().contains("compiler-only type leaked")); + } + + private static @NotNull FrontendFilteredValueHitReason primaryFilteredHitReason( + gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueResolution resolution + ) { + var primaryHit = resolution.primaryFilteredHit(); + if (primaryHit == null) { + fail("Expected primary filtered hit"); + } + return primaryHit.reason(); + } + + private static @NotNull PhaseInput phaseInput(@NotNull String fileName, @NotNull String source) throws Exception { + var parserService = new GdScriptParserService(); + var diagnostics = new DiagnosticManager(); + var unit = parserService.parseUnit(Path.of("tmp", fileName), source, diagnostics); + assertTrue(diagnostics.isEmpty(), () -> "Unexpected parse diagnostics: " + diagnostics.snapshot()); + + var registry = new ClassRegistry(ExtensionApiLoader.loadDefault()); + var analysisData = FrontendAnalysisData.bootstrap(); + var module = new FrontendModule("test_module", List.of(unit)); + var moduleSkeleton = new FrontendClassSkeletonBuilder().build(module, registry, diagnostics, analysisData); + analysisData.updateModuleSkeleton(moduleSkeleton); + analysisData.updateDiagnostics(diagnostics.snapshot()); + new FrontendScopeAnalyzer().analyze(registry, analysisData, diagnostics); + analysisData.updateDiagnostics(diagnostics.snapshot()); + new FrontendVariableAnalyzer().analyze(analysisData, diagnostics); + analysisData.updateDiagnostics(diagnostics.snapshot()); + return new PhaseInput(unit, registry, analysisData, diagnostics); + } + + private static T findStatement( + @NotNull List statements, + @NotNull Class statementType, + @NotNull Predicate predicate + ) { + return statements.stream() + .filter(statementType::isInstance) + .map(statementType::cast) + .filter(predicate) + .findFirst() + .orElseThrow(() -> new AssertionError("Statement not found: " + statementType.getSimpleName())); + } + + private static T findNode( + @NotNull Node root, + @NotNull Class nodeType, + @NotNull Predicate predicate + ) { + var collector = new NodeCollector<>(nodeType, predicate); + new ASTWalker(collector).walk(root); + return collector.result(); + } + + private static final class NodeCollector implements dev.superice.gdparser.frontend.ast.ASTNodeHandler { + private final @NotNull Class nodeType; + private final @NotNull Predicate predicate; + private T result; + + private NodeCollector(@NotNull Class nodeType, @NotNull Predicate predicate) { + this.nodeType = nodeType; + this.predicate = predicate; + } + + @Override + public @NotNull FrontendASTTraversalDirective handleNode(@NotNull Node node) { + if (result == null && nodeType.isInstance(node)) { + var candidate = nodeType.cast(node); + if (predicate.test(candidate)) { + result = candidate; + } + } + return result == null + ? FrontendASTTraversalDirective.CONTINUE + : FrontendASTTraversalDirective.SKIP_CHILDREN; + } + + private @NotNull T result() { + if (result == null) { + throw new AssertionError("Node not found: " + nodeType.getSimpleName()); + } + return result; + } + } + + private record PhaseInput( + @NotNull FrontendSourceUnit unit, + @NotNull ClassRegistry registry, + @NotNull FrontendAnalysisData analysisData, + @NotNull DiagnosticManager diagnostics + ) { + } + + private record IterableCase( + @NotNull String name, + @NotNull String parameterText, + @NotNull String iterableText + ) { + } +} diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendScopeAnalyzerTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendScopeAnalyzerTest.java index 4456c98d..dcdc5136 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendScopeAnalyzerTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendScopeAnalyzerTest.java @@ -355,7 +355,7 @@ func ping(value: int) -> int: @Test void analyzeBuildsLoopAndMatchBranchScopesWhileLeavingDeferredBindingsUnfilled() throws Exception { - var analyzed = analyze(""" + var analyzed = analyzeScopeOnly(""" class_name LoopAndMatchScopes extends Node diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSemanticAnalyzerFrameworkTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSemanticAnalyzerFrameworkTest.java index 9a82d33e..576086cd 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSemanticAnalyzerFrameworkTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSemanticAnalyzerFrameworkTest.java @@ -3,23 +3,20 @@ import gd.script.gdcc.frontend.diagnostic.DiagnosticManager; import gd.script.gdcc.frontend.diagnostic.DiagnosticSnapshot; import gd.script.gdcc.frontend.diagnostic.FrontendDiagnosticSeverity; +import gd.script.gdcc.frontend.diagnostic.FrontendRange; import gd.script.gdcc.frontend.parse.FrontendModule; import gd.script.gdcc.frontend.parse.FrontendSourceUnit; import gd.script.gdcc.frontend.parse.GdScriptParserService; import gd.script.gdcc.frontend.diagnostic.FrontendDiagnostic; -import gd.script.gdcc.frontend.sema.analyzer.FrontendChainBindingAnalyzer; import gd.script.gdcc.frontend.sema.analyzer.FrontendCompileCheckAnalyzer; -import gd.script.gdcc.frontend.sema.analyzer.FrontendExprTypeAnalyzer; import gd.script.gdcc.frontend.sema.analyzer.FrontendAnnotationUsageAnalyzer; -import gd.script.gdcc.frontend.sema.analyzer.FrontendLocalTypeStabilizationAnalyzer; import gd.script.gdcc.frontend.sema.analyzer.FrontendLoopControlFlowAnalyzer; import gd.script.gdcc.frontend.sema.analyzer.FrontendScopeAnalyzer; import gd.script.gdcc.frontend.sema.analyzer.FrontendSemanticAnalyzer; -import gd.script.gdcc.frontend.sema.analyzer.FrontendTopBindingAnalyzer; import gd.script.gdcc.frontend.sema.analyzer.FrontendTypeCheckAnalyzer; import gd.script.gdcc.frontend.sema.analyzer.FrontendVirtualOverrideAnalyzer; -import gd.script.gdcc.frontend.sema.analyzer.FrontendVarTypePostAnalyzer; import gd.script.gdcc.frontend.sema.analyzer.FrontendVariableAnalyzer; +import gd.script.gdcc.frontend.sema.patch.FrontendOwnerPatch; import gd.script.gdcc.frontend.scope.BlockScope; import gd.script.gdcc.frontend.scope.CallableScope; import gd.script.gdcc.frontend.scope.ClassScope; @@ -28,13 +25,16 @@ import gd.script.gdcc.gdextension.ExtensionHeader; import gd.script.gdcc.gdextension.ExtensionSingleton; import gd.script.gdcc.lir.LirClassDef; +import dev.superice.gdparser.frontend.ast.AssignmentExpression; import dev.superice.gdparser.frontend.ast.AttributeCallStep; import dev.superice.gdparser.frontend.ast.AttributeExpression; +import dev.superice.gdparser.frontend.ast.AttributePropertyStep; import dev.superice.gdparser.frontend.ast.Block; import dev.superice.gdparser.frontend.ast.CallExpression; import dev.superice.gdparser.frontend.ast.ClassNameStatement; import dev.superice.gdparser.frontend.ast.ClassDeclaration; import dev.superice.gdparser.frontend.ast.ExpressionStatement; +import dev.superice.gdparser.frontend.ast.ForStatement; import dev.superice.gdparser.frontend.ast.FunctionDeclaration; import dev.superice.gdparser.frontend.ast.IdentifierExpression; import dev.superice.gdparser.frontend.ast.LiteralExpression; @@ -43,6 +43,7 @@ import dev.superice.gdparser.frontend.ast.Point; import dev.superice.gdparser.frontend.ast.Range; import dev.superice.gdparser.frontend.ast.ReturnStatement; +import dev.superice.gdparser.frontend.ast.SelfExpression; import dev.superice.gdparser.frontend.ast.SourceFile; import dev.superice.gdparser.frontend.ast.VariableDeclaration; import gd.script.gdcc.gdextension.ExtensionApiLoader; @@ -55,6 +56,7 @@ import gd.script.gdcc.scope.resolver.ScopeTypeResolver; import gd.script.gdcc.type.GdBoolType; import gd.script.gdcc.type.GdDictionaryType; +import gd.script.gdcc.type.GdType; import gd.script.gdcc.type.GdIntType; import gd.script.gdcc.type.GdObjectType; import gd.script.gdcc.type.GdVariantType; @@ -63,8 +65,10 @@ import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.function.Predicate; import static org.junit.jupiter.api.Assertions.assertAll; @@ -74,6 +78,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; class FrontendSemanticAnalyzerFrameworkTest { @@ -656,7 +661,7 @@ func _ready( } @Test - void analyzePublishesPhaseBoundariesThroughVirtualOverridePhaseAndRefreshesDiagnosticsAfterEachPhase() throws Exception { + void activeDependencyConstructorUsesDefaultSuiteResolverAndPreservesPhaseBoundaries() throws Exception { var parserService = new GdScriptParserService(); var diagnostics = new DiagnosticManager(); var unit = parserService.parseUnit(Path.of("tmp", "variable_phase_probe.gd"), """ @@ -669,11 +674,6 @@ func ping(value): var registry = new ClassRegistry(ExtensionApiLoader.loadDefault()); var probeScopeAnalyzer = new RecordingScopeAnalyzer(); var probeVariableAnalyzer = new RecordingVariableAnalyzer(); - var probeTopBindingAnalyzer = new RecordingTopBindingAnalyzer(); - var probeLocalTypeStabilizationAnalyzer = new RecordingLocalTypeStabilizationAnalyzer(); - var probeChainBindingAnalyzer = new RecordingChainBindingAnalyzer(); - var probeExprTypeAnalyzer = new RecordingExprTypeAnalyzer(); - var probeVarTypePostAnalyzer = new RecordingVarTypePostAnalyzer(); var probeAnnotationUsageAnalyzer = new RecordingAnnotationUsageAnalyzer(); var probeVirtualOverrideAnalyzer = new RecordingVirtualOverrideAnalyzer(); var probeTypeCheckAnalyzer = new RecordingTypeCheckAnalyzer(); @@ -682,11 +682,6 @@ func ping(value): new FrontendClassSkeletonBuilder(), probeScopeAnalyzer, probeVariableAnalyzer, - probeTopBindingAnalyzer, - probeLocalTypeStabilizationAnalyzer, - probeChainBindingAnalyzer, - probeExprTypeAnalyzer, - probeVarTypePostAnalyzer, probeAnnotationUsageAnalyzer, probeVirtualOverrideAnalyzer, probeTypeCheckAnalyzer, @@ -694,7 +689,13 @@ func ping(value): new FrontendCompileCheckAnalyzer() ); - var result = analyzeModule(analyzer, "test_module", List.of(unit), registry, diagnostics); + var result = analyzeModule( + analyzer, + "test_module", + List.of(unit), + registry, + diagnostics + ); assertTrue(probeScopeAnalyzer.invoked); assertTrue(probeScopeAnalyzer.moduleSkeletonPublished); @@ -702,32 +703,6 @@ func ping(value): assertTrue(probeVariableAnalyzer.invoked); assertTrue(probeVariableAnalyzer.scopeBoundaryPublished); assertTrue(probeVariableAnalyzer.preVariableDiagnosticsMatchedManager); - assertTrue(probeTopBindingAnalyzer.invoked); - assertTrue(probeTopBindingAnalyzer.scopeBoundaryPublished); - assertTrue(probeTopBindingAnalyzer.preTopBindingDiagnosticsMatchedManager); - assertTrue(probeTopBindingAnalyzer.stableSymbolBindingsReferencePreserved); - assertTrue(probeTopBindingAnalyzer.symbolBindingsPublicationClearedProbeEntry); - assertTrue(probeLocalTypeStabilizationAnalyzer.invoked); - assertTrue(probeLocalTypeStabilizationAnalyzer.topBindingBoundaryPublished); - assertTrue(probeLocalTypeStabilizationAnalyzer.preLocalTypeStabilizationDiagnosticsMatchedManager); - assertTrue(probeLocalTypeStabilizationAnalyzer.sideTablesRemainUnpublished); - assertTrue(probeChainBindingAnalyzer.invoked); - assertTrue(probeChainBindingAnalyzer.localTypeStabilizationBoundaryPublished); - assertTrue(probeChainBindingAnalyzer.preChainBindingDiagnosticsMatchedManager); - assertTrue(probeChainBindingAnalyzer.stableResolvedMembersReferencePreserved); - assertTrue(probeChainBindingAnalyzer.stableResolvedCallsReferencePreserved); - assertTrue(probeChainBindingAnalyzer.memberPublicationClearedProbeEntry); - assertTrue(probeChainBindingAnalyzer.callPublicationClearedProbeEntry); - assertTrue(probeExprTypeAnalyzer.invoked); - assertTrue(probeExprTypeAnalyzer.chainBindingBoundaryPublished); - assertTrue(probeExprTypeAnalyzer.preExprTypeDiagnosticsMatchedManager); - assertTrue(probeExprTypeAnalyzer.stableExpressionTypesReferencePreserved); - assertTrue(probeExprTypeAnalyzer.expressionTypePublicationClearedProbeEntry); - assertTrue(probeVarTypePostAnalyzer.invoked); - assertTrue(probeVarTypePostAnalyzer.exprTypeBoundaryPublished); - assertTrue(probeVarTypePostAnalyzer.preVarTypePostDiagnosticsMatchedManager); - assertTrue(probeVarTypePostAnalyzer.stableSlotTypesReferencePreserved); - assertTrue(probeVarTypePostAnalyzer.slotTypePublicationClearedProbeEntry); assertTrue(probeAnnotationUsageAnalyzer.invoked); assertTrue(probeAnnotationUsageAnalyzer.varTypeBoundaryPublished); assertTrue(probeAnnotationUsageAnalyzer.preAnnotationUsageDiagnosticsMatchedManager); @@ -749,45 +724,10 @@ func ping(value): )); assertEquals( probeVariableAnalyzer.preVariableDiagnostics.size() + 1, - probeTopBindingAnalyzer.preTopBindingDiagnostics.size() - ); - assertTrue(probeTopBindingAnalyzer.preTopBindingDiagnostics.asList().stream().anyMatch(diagnostic -> - diagnostic.category().equals("sema.variable_phase_probe") - )); - assertEquals( - probeTopBindingAnalyzer.preTopBindingDiagnostics.size() + 1, - probeLocalTypeStabilizationAnalyzer.preLocalTypeStabilizationDiagnostics.size() - ); - assertTrue(probeLocalTypeStabilizationAnalyzer.preLocalTypeStabilizationDiagnostics.asList().stream().anyMatch(diagnostic -> - diagnostic.category().equals("sema.top_binding_phase_probe") - )); - assertEquals( - probeLocalTypeStabilizationAnalyzer.preLocalTypeStabilizationDiagnostics.size() + 1, - probeChainBindingAnalyzer.preChainBindingDiagnostics.size() - ); - assertTrue(probeChainBindingAnalyzer.preChainBindingDiagnostics.asList().stream().anyMatch(diagnostic -> - diagnostic.category().equals("sema.local_type_stabilization_phase_probe") - )); - assertEquals( - probeChainBindingAnalyzer.preChainBindingDiagnostics.size() + 1, - probeExprTypeAnalyzer.preExprTypeDiagnostics.size() - ); - assertTrue(probeExprTypeAnalyzer.preExprTypeDiagnostics.asList().stream().anyMatch(diagnostic -> - diagnostic.category().equals("sema.chain_binding_phase_probe") - )); - assertEquals( - probeExprTypeAnalyzer.preExprTypeDiagnostics.size() + 1, - probeVarTypePostAnalyzer.preVarTypePostDiagnostics.size() - ); - assertTrue(probeVarTypePostAnalyzer.preVarTypePostDiagnostics.asList().stream().anyMatch(diagnostic -> - diagnostic.category().equals("sema.expr_type_phase_probe") - )); - assertEquals( - probeVarTypePostAnalyzer.preVarTypePostDiagnostics.size() + 1, probeAnnotationUsageAnalyzer.preAnnotationUsageDiagnostics.size() ); assertTrue(probeAnnotationUsageAnalyzer.preAnnotationUsageDiagnostics.asList().stream().anyMatch(diagnostic -> - diagnostic.category().equals("sema.var_type_post_phase_probe") + diagnostic.category().equals("sema.variable_phase_probe") )); assertEquals( probeAnnotationUsageAnalyzer.preAnnotationUsageDiagnostics.size() + 1, @@ -817,21 +757,6 @@ func ping(value): assertTrue(result.diagnostics().asList().stream().anyMatch(diagnostic -> diagnostic.category().equals("sema.variable_phase_probe") )); - assertTrue(result.diagnostics().asList().stream().anyMatch(diagnostic -> - diagnostic.category().equals("sema.top_binding_phase_probe") - )); - assertTrue(result.diagnostics().asList().stream().anyMatch(diagnostic -> - diagnostic.category().equals("sema.local_type_stabilization_phase_probe") - )); - assertTrue(result.diagnostics().asList().stream().anyMatch(diagnostic -> - diagnostic.category().equals("sema.chain_binding_phase_probe") - )); - assertTrue(result.diagnostics().asList().stream().anyMatch(diagnostic -> - diagnostic.category().equals("sema.expr_type_phase_probe") - )); - assertTrue(result.diagnostics().asList().stream().anyMatch(diagnostic -> - diagnostic.category().equals("sema.var_type_post_phase_probe") - )); assertTrue(result.diagnostics().asList().stream().anyMatch(diagnostic -> diagnostic.category().equals("sema.annotation_usage_phase_probe") )); @@ -848,9 +773,64 @@ func ping(value): assertTrue(result.resolvedMembers().isEmpty()); assertTrue(result.resolvedCalls().isEmpty()); assertTrue(result.expressionTypes().isEmpty()); + // The callable-entry var type post procedure is the only body publication for this empty body. + var pingFunction = findFunction(unit.ast().statements(), "ping"); + assertEquals(GdVariantType.VARIANT, result.slotTypes().get(pingFunction.parameters().getFirst())); assertFalse(result.slotTypes().isEmpty()); } + /// Locks the constructor boundary: active phase injection remains available while body-owner + /// classes are absent from both constructor signatures and the runtime classpath. + @Test + void activeDependencyConstructorAndClasspathExcludeLegacyBodyOwners() throws Exception { + var activeConstructor = FrontendSemanticAnalyzer.class.getConstructor( + FrontendClassSkeletonBuilder.class, + FrontendScopeAnalyzer.class, + FrontendVariableAnalyzer.class, + FrontendAnnotationUsageAnalyzer.class, + FrontendVirtualOverrideAnalyzer.class, + FrontendTypeCheckAnalyzer.class, + FrontendLoopControlFlowAnalyzer.class, + FrontendCompileCheckAnalyzer.class + ); + var legacyOwnerParameterNames = Set.of( + "FrontendTopBindingAnalyzer", + "FrontendLocalTypeStabilizationAnalyzer", + "FrontendChainBindingAnalyzer", + "FrontendExprTypeAnalyzer", + "FrontendVarTypePostAnalyzer" + ); + + assertNotNull(activeConstructor); + assertTrue(Arrays.stream(FrontendSemanticAnalyzer.class.getConstructors()) + .flatMap(constructor -> Arrays.stream(constructor.getParameterTypes())) + .noneMatch(parameterType -> legacyOwnerParameterNames.contains(parameterType.getSimpleName()))); + for (var legacyOwnerName : legacyOwnerParameterNames) { + assertThrows( + ClassNotFoundException.class, + () -> Class.forName("gd.script.gdcc.frontend.sema.analyzer." + legacyOwnerName) + ); + } + } + + @Test + void removedWindowAndMultiOwnerPatchShimsStayAbsent() throws Exception { + var removedClassNames = List.of( + "gd.script.gdcc.frontend.sema.FrontendWindowAnalysisContext", + "gd.script.gdcc.frontend.sema.FrontendWindowPublicationSurface", + "gd.script.gdcc.frontend.sema.patch.FrontendAnalysisPatch" + ); + + for (var removedClassName : removedClassNames) { + assertThrows(ClassNotFoundException.class, () -> Class.forName(removedClassName)); + } + assertNotNull(FrontendAnalysisData.class.getMethod("applyPatch", FrontendOwnerPatch.class)); + assertTrue(Arrays.stream(FrontendAnalysisData.class.getMethods()) + .filter(method -> method.getName().equals("applyPatch")) + .flatMap(method -> Arrays.stream(method.getParameterTypes())) + .noneMatch(parameterType -> parameterType.getSimpleName().equals("FrontendAnalysisPatch"))); + } + @Test void analyzePublishesStableLocalTypeFactsAcrossBodyPhases() throws Exception { var parserService = new GdScriptParserService(); @@ -972,6 +952,209 @@ func ping(value: Point) -> int: ); } + @Test + void defaultInterfaceBodyPipelinePublishesBodyFactsThroughSuiteExport() throws Exception { + var parserService = new GdScriptParserService(); + var parseDiagnostics = new DiagnosticManager(); + var unit = parserService.parseUnit(Path.of("tmp", "real_body_owner_framework_path.gd"), """ + class_name RealBodyOwnerFrameworkPath + extends RefCounted + class Point: + var marker: int = 1 + + func ping(value: Point) -> int: + var alias := value + var number := alias.marker + return number + """, parseDiagnostics); + assertTrue(parseDiagnostics.isEmpty(), () -> "Unexpected parse diagnostics: " + parseDiagnostics.snapshot()); + var pingFunction = findFunction(unit.ast().statements(), "ping"); + var aliasDeclaration = findVariable(pingFunction.body().statements(), "alias"); + var numberDeclaration = findVariable(pingFunction.body().statements(), "number"); + var markerStep = findNode(pingFunction.body(), AttributePropertyStep.class, step -> step.name().equals("marker")); + var numberInitializer = numberDeclaration.value(); + assertNotNull(numberInitializer); + var interfaceBodyDiagnostics = new DiagnosticManager(); + + var interfaceBody = analyzeModule( + new FrontendSemanticAnalyzer(), + "test_module", + List.of(unit), + new ClassRegistry(ExtensionApiLoader.loadDefault()), + interfaceBodyDiagnostics + ); + + var aliasSlotType = interfaceBody.slotTypes().get(aliasDeclaration); + var numberSlotType = interfaceBody.slotTypes().get(numberDeclaration); + var markerMember = interfaceBody.resolvedMembers().get(markerStep); + var initializerType = interfaceBody.expressionTypes().get(numberInitializer); + assertAll( + () -> assertNotNull(aliasSlotType), + () -> assertTrue(aliasSlotType.getTypeName().endsWith("Point")), + () -> assertNotNull(numberSlotType), + () -> assertEquals("int", numberSlotType.getTypeName()), + () -> assertNotNull(markerMember), + () -> assertEquals(FrontendMemberResolutionStatus.RESOLVED, markerMember.status()), + () -> assertTrue(markerMember.receiverType().getTypeName().endsWith("Point")), + () -> assertEquals("int", markerMember.resultType().getTypeName()), + () -> assertNotNull(initializerType), + () -> assertEquals(FrontendExpressionTypeStatus.RESOLVED, initializerType.status()), + () -> assertEquals("int", initializerType.publishedType().getTypeName()) + ); + assertEquals(interfaceBodyDiagnostics.snapshot(), interfaceBody.diagnostics()); + } + + @Test + void defaultInterfaceBodyPipelinePublishesNestedHeaderAndBodyFactsInSourceOrder() throws Exception { + var parserService = new GdScriptParserService(); + var parseDiagnostics = new DiagnosticManager(); + var unit = parserService.parseUnit(Path.of("tmp", "segmented_equivalence.gd"), """ + class_name SegmentedEquivalence + extends Node + class Point: + var marker: int = 1 + func ping(value: Point) -> int: + var alias := value + var number := 1 + if number > 0: + var nested := alias + number = nested.marker + while number < 3: + number += 1 + return alias.marker + """, parseDiagnostics); + var interfaceBodyDiagnostics = new DiagnosticManager(); + var interfaceBody = analyzeModule( + new FrontendSemanticAnalyzer(), + "test_module", + List.of(unit), + new ClassRegistry(ExtensionApiLoader.loadDefault()), + interfaceBodyDiagnostics + ); + var pingFunction = findFunction(unit.ast().statements(), "ping"); + var aliasDeclaration = findVariable(pingFunction.body().statements(), "alias"); + var numberDeclaration = findVariable(pingFunction.body().statements(), "number"); + var nestedDeclaration = findNode(pingFunction.body(), VariableDeclaration.class, variableDeclaration -> + variableDeclaration.name().equals("nested") + ); + var markerSteps = findNodes(pingFunction.body(), AttributePropertyStep.class, step -> step.name().equals("marker")); + assertEquals(2, markerSteps.size()); + var numberInitializer = numberDeclaration.value(); + var nestedInitializer = nestedDeclaration.value(); + assertNotNull(numberInitializer); + assertNotNull(nestedInitializer); + + assertAll( + () -> assertTypeNameEndsWith(interfaceBody.slotTypes().get(aliasDeclaration), "Point"), + () -> assertEquals("int", interfaceBody.slotTypes().get(numberDeclaration).getTypeName()), + () -> assertTypeNameEndsWith(interfaceBody.slotTypes().get(nestedDeclaration), "Point"), + () -> assertEquals("int", interfaceBody.resolvedMembers().get(markerSteps.getFirst()).resultType().getTypeName()), + () -> assertEquals("int", interfaceBody.resolvedMembers().get(markerSteps.getLast()).resultType().getTypeName()), + () -> assertEquals("int", interfaceBody.expressionTypes().get(numberInitializer).publishedType().getTypeName()), + () -> assertTypeNameEndsWith(interfaceBody.expressionTypes().get(nestedInitializer).publishedType(), "Point") + ); + assertEquals(interfaceBodyDiagnostics.snapshot(), interfaceBody.diagnostics()); + } + + @Test + void defaultInterfaceBodyPipelineSupportsForWhileOtherUnsupportedSubtreesStayFailClosed() throws Exception { + var parserService = new GdScriptParserService(); + var unit = parserService.parseUnit(Path.of("tmp", "segmented_unsupported_equivalence.gd"), """ + class_name SegmentedUnsupportedEquivalence + extends Node + func ping(values): + const blocked := 1 + for value in values: + var hidden := value + match values: + _: + pass + return values + """, new DiagnosticManager()); + var interfaceBodyDiagnostics = new DiagnosticManager(); + + var interfaceBody = analyzeModule( + new FrontendSemanticAnalyzer(), + "test_module", + List.of(unit), + new ClassRegistry(ExtensionApiLoader.loadDefault()), + interfaceBodyDiagnostics + ); + var pingFunction = findFunction(unit.ast().statements(), "ping"); + var blockedConst = findNode(pingFunction.body(), VariableDeclaration.class, variableDeclaration -> + variableDeclaration.name().equals("blocked") + ); + var hiddenLocal = findNode(pingFunction.body(), VariableDeclaration.class, variableDeclaration -> + variableDeclaration.name().equals("hidden") + ); + var hiddenUseSite = assertInstanceOf(IdentifierExpression.class, hiddenLocal.value()); + + assertNull(interfaceBody.slotTypes().get(blockedConst)); + assertEquals(GdVariantType.VARIANT, interfaceBody.slotTypes().get(hiddenLocal)); + assertEquals(FrontendBindingKind.LOCAL_VAR, interfaceBody.symbolBindings().get(hiddenUseSite).kind()); + assertFalse(diagnosticsByCategory(interfaceBody.diagnostics(), "sema.unsupported_binding_subtree").isEmpty()); + assertTrue(interfaceBody.diagnostics().asList().stream().noneMatch(diagnostic -> + diagnostic.category().equals("sema.unsupported_variable_inventory_subtree") + && diagnostic.range().equals(FrontendRange.fromAstRange( + findNode(pingFunction.body(), ForStatement.class, _ -> true).range() + )) + )); + assertEquals(interfaceBodyDiagnostics.snapshot(), interfaceBody.diagnostics()); + } + + @Test + void defaultInterfaceBodyPipelinePublishesAssignmentTargetLoweringFacts() throws Exception { + var parserService = new GdScriptParserService(); + var diagnostics = new DiagnosticManager(); + var unit = parserService.parseUnit(Path.of("tmp", "assignment_target_lowering_facts.gd"), """ + class_name AssignmentTargetLoweringFacts + extends RefCounted + + var hp: int = 0 + + func ping(host, index: int, value: int): + self.hp = value + host.box.count += value + host.payloads[index].value = value + """, diagnostics); + + var result = analyzeModule( + "test_module", + List.of(unit), + new ClassRegistry(ExtensionApiLoader.loadDefault()), + diagnostics + ); + var assignments = findNodes(findFunction(unit.ast().statements(), "ping"), AssignmentExpression.class, _ -> true); + assertEquals(3, assignments.size()); + var selfTarget = assertInstanceOf(AttributeExpression.class, assignments.get(0).left()); + var dynamicTarget = assertInstanceOf(AttributeExpression.class, assignments.get(1).left()); + var subscriptTarget = assertInstanceOf(AttributeExpression.class, assignments.get(2).left()); + var explicitSelf = assertInstanceOf(SelfExpression.class, selfTarget.base()); + var boxStep = assertInstanceOf(AttributePropertyStep.class, dynamicTarget.steps().getFirst()); + var countStep = assertInstanceOf(AttributePropertyStep.class, dynamicTarget.steps().getLast()); + var indexUse = findNode(subscriptTarget, IdentifierExpression.class, identifier -> identifier.name().equals("index")); + + assertAll( + () -> assertEquals( + FrontendExpressionTypeStatus.RESOLVED, + result.expressionTypes().get(explicitSelf).status() + ), + () -> assertEquals( + FrontendExpressionTypeStatus.DYNAMIC, + result.expressionTypes().get(boxStep).status() + ), + () -> assertEquals( + FrontendExpressionTypeStatus.DYNAMIC, + result.expressionTypes().get(countStep).status() + ), + () -> assertEquals( + FrontendExpressionTypeStatus.RESOLVED, + result.expressionTypes().get(indexUse).status() + ) + ); + assertEquals(diagnostics.snapshot(), result.diagnostics()); + } + @Test void analyzeForCompileRunsCompileGateAfterLoopControlWhileAnalyzeStaysCompileCheckFree() throws Exception { var parserService = new GdScriptParserService(); @@ -991,10 +1174,8 @@ func ping(): new FrontendClassSkeletonBuilder(), new FrontendScopeAnalyzer(), new FrontendVariableAnalyzer(), - new FrontendTopBindingAnalyzer(), - new FrontendChainBindingAnalyzer(), - new FrontendExprTypeAnalyzer(), new FrontendAnnotationUsageAnalyzer(), + new FrontendVirtualOverrideAnalyzer(), new FrontendTypeCheckAnalyzer(), sharedLoopControlProbe, sharedProbe @@ -1015,10 +1196,8 @@ func ping(): new FrontendClassSkeletonBuilder(), new FrontendScopeAnalyzer(), new FrontendVariableAnalyzer(), - new FrontendTopBindingAnalyzer(), - new FrontendChainBindingAnalyzer(), - new FrontendExprTypeAnalyzer(), new FrontendAnnotationUsageAnalyzer(), + new FrontendVirtualOverrideAnalyzer(), new FrontendTypeCheckAnalyzer(), compileLoopControlProbe, compileProbe @@ -1491,6 +1670,20 @@ private List diagnosticsByCategory( .toList(); } + private void assertTypeNameEndsWith(@NotNull GdType type, @NotNull String suffix) { + assertTrue(type.getTypeName().endsWith(suffix), () -> "Expected type ending with " + suffix + ", got " + type); + } + + private void assertDiagnosticsEquivalentIgnoringOrder( + @NotNull DiagnosticSnapshot expected, + @NotNull DiagnosticSnapshot actual + ) { + assertEquals( + expected.asList().stream().map(Objects::toString).sorted().toList(), + actual.asList().stream().map(Objects::toString).sorted().toList() + ); + } + private ClassRegistry createRegistryWithSingleton(String singletonName) { return new ClassRegistry(new ExtensionAPI( new ExtensionHeader(4, 4, 0, "stable", "test", "test", "single"), @@ -1565,230 +1758,6 @@ public void analyze( } } - /// Test double that records the diagnostics snapshot and `symbolBindings()` publication - /// boundary visible to top-binding analysis. - private static final class RecordingTopBindingAnalyzer extends FrontendTopBindingAnalyzer { - private boolean invoked; - private boolean scopeBoundaryPublished; - private boolean preTopBindingDiagnosticsMatchedManager; - private boolean stableSymbolBindingsReferencePreserved; - private boolean symbolBindingsPublicationClearedProbeEntry; - private DiagnosticSnapshot preTopBindingDiagnostics; - - @Override - public void analyze( - @NotNull ClassRegistry classRegistry, - @NotNull FrontendAnalysisData analysisData, - @NotNull DiagnosticManager diagnosticManager - ) { - invoked = true; - preTopBindingDiagnostics = analysisData.diagnostics(); - preTopBindingDiagnosticsMatchedManager = preTopBindingDiagnostics.equals(diagnosticManager.snapshot()); - scopeBoundaryPublished = analysisData.moduleSkeleton().sourceClassRelations().stream() - .allMatch(sourceClassRelation -> analysisData.scopesByAst().containsKey(sourceClassRelation.unit().ast())); - var publishedSymbolBindings = analysisData.symbolBindings(); - var probeNode = new PassStatement(SYNTHETIC_RANGE); - publishedSymbolBindings.put(probeNode, new FrontendBinding("__probe__", FrontendBindingKind.UNKNOWN, null)); - - super.analyze(classRegistry, analysisData, diagnosticManager); - - stableSymbolBindingsReferencePreserved = publishedSymbolBindings == analysisData.symbolBindings(); - symbolBindingsPublicationClearedProbeEntry = analysisData.symbolBindings().isEmpty() - && !analysisData.symbolBindings().containsKey(probeNode); - diagnosticManager.warning( - "sema.top_binding_phase_probe", - "top-binding phase probe diagnostic", - null, - null - ); - } - } - - private static final class RecordingLocalTypeStabilizationAnalyzer extends FrontendLocalTypeStabilizationAnalyzer { - private boolean invoked; - private boolean topBindingBoundaryPublished; - private boolean preLocalTypeStabilizationDiagnosticsMatchedManager; - private boolean sideTablesRemainUnpublished; - private DiagnosticSnapshot preLocalTypeStabilizationDiagnostics; - - @Override - public void analyze( - @NotNull ClassRegistry classRegistry, - @NotNull FrontendAnalysisData analysisData, - @NotNull DiagnosticManager diagnosticManager - ) { - invoked = true; - preLocalTypeStabilizationDiagnostics = analysisData.diagnostics(); - preLocalTypeStabilizationDiagnosticsMatchedManager = - preLocalTypeStabilizationDiagnostics.equals(diagnosticManager.snapshot()); - topBindingBoundaryPublished = analysisData.moduleSkeleton().sourceClassRelations().stream() - .allMatch(sourceClassRelation -> analysisData.scopesByAst().containsKey(sourceClassRelation.unit().ast())) - && analysisData.symbolBindings().isEmpty(); - - super.analyze(classRegistry, analysisData, diagnosticManager); - - sideTablesRemainUnpublished = analysisData.resolvedMembers().isEmpty() - && analysisData.resolvedCalls().isEmpty() - && analysisData.expressionTypes().isEmpty() - && analysisData.slotTypes().isEmpty(); - diagnosticManager.warning( - "sema.local_type_stabilization_phase_probe", - "local-type-stabilization phase probe diagnostic", - null, - null - ); - } - } - - private static final class RecordingChainBindingAnalyzer extends FrontendChainBindingAnalyzer { - private boolean invoked; - private boolean localTypeStabilizationBoundaryPublished; - private boolean preChainBindingDiagnosticsMatchedManager; - private boolean stableResolvedMembersReferencePreserved; - private boolean stableResolvedCallsReferencePreserved; - private boolean memberPublicationClearedProbeEntry; - private boolean callPublicationClearedProbeEntry; - private DiagnosticSnapshot preChainBindingDiagnostics; - - @Override - public void analyze( - @NotNull ClassRegistry classRegistry, - @NotNull FrontendAnalysisData analysisData, - @NotNull DiagnosticManager diagnosticManager - ) { - invoked = true; - preChainBindingDiagnostics = analysisData.diagnostics(); - preChainBindingDiagnosticsMatchedManager = preChainBindingDiagnostics.equals(diagnosticManager.snapshot()); - localTypeStabilizationBoundaryPublished = analysisData.moduleSkeleton().sourceClassRelations().stream() - .allMatch(sourceClassRelation -> analysisData.scopesByAst().containsKey(sourceClassRelation.unit().ast())) - && analysisData.symbolBindings().isEmpty(); - var publishedMembers = analysisData.resolvedMembers(); - var publishedCalls = analysisData.resolvedCalls(); - var probeMemberNode = new PassStatement(SYNTHETIC_RANGE); - var probeCallNode = new ExpressionStatement(new LiteralExpression("integer", "0", SYNTHETIC_RANGE), SYNTHETIC_RANGE); - publishedMembers.put( - probeMemberNode, - FrontendResolvedMember.failed( - "__probe_member__", - FrontendBindingKind.PROPERTY, - FrontendReceiverKind.INSTANCE, - null, - GdVariantType.VARIANT, - null, - "probe" - ) - ); - publishedCalls.put( - probeCallNode, - FrontendResolvedCall.failed( - "__probe_call__", - FrontendCallResolutionKind.INSTANCE_METHOD, - FrontendReceiverKind.INSTANCE, - null, - GdVariantType.VARIANT, - List.of(), - null, - "probe" - ) - ); - - super.analyze(classRegistry, analysisData, diagnosticManager); - - stableResolvedMembersReferencePreserved = publishedMembers == analysisData.resolvedMembers(); - stableResolvedCallsReferencePreserved = publishedCalls == analysisData.resolvedCalls(); - memberPublicationClearedProbeEntry = analysisData.resolvedMembers().isEmpty() - && !analysisData.resolvedMembers().containsKey(probeMemberNode); - callPublicationClearedProbeEntry = analysisData.resolvedCalls().isEmpty() - && !analysisData.resolvedCalls().containsKey(probeCallNode); - diagnosticManager.warning( - "sema.chain_binding_phase_probe", - "chain-binding phase probe diagnostic", - null, - null - ); - } - } - - private static final class RecordingExprTypeAnalyzer extends FrontendExprTypeAnalyzer { - private boolean invoked; - private boolean chainBindingBoundaryPublished; - private boolean preExprTypeDiagnosticsMatchedManager; - private boolean stableExpressionTypesReferencePreserved; - private boolean expressionTypePublicationClearedProbeEntry; - private DiagnosticSnapshot preExprTypeDiagnostics; - - @Override - public void analyze( - @NotNull ClassRegistry classRegistry, - @NotNull FrontendAnalysisData analysisData, - @NotNull DiagnosticManager diagnosticManager - ) { - invoked = true; - preExprTypeDiagnostics = analysisData.diagnostics(); - preExprTypeDiagnosticsMatchedManager = preExprTypeDiagnostics.equals(diagnosticManager.snapshot()); - chainBindingBoundaryPublished = analysisData.moduleSkeleton().sourceClassRelations().stream() - .allMatch(sourceClassRelation -> analysisData.scopesByAst().containsKey(sourceClassRelation.unit().ast())) - && analysisData.symbolBindings().isEmpty() - && analysisData.resolvedMembers().isEmpty() - && analysisData.resolvedCalls().isEmpty(); - var publishedExpressionTypes = analysisData.expressionTypes(); - var probeExpression = new LiteralExpression("integer", "0", SYNTHETIC_RANGE); - publishedExpressionTypes.put(probeExpression, FrontendExpressionType.resolved(GdVariantType.VARIANT)); - - super.analyze(classRegistry, analysisData, diagnosticManager); - - stableExpressionTypesReferencePreserved = publishedExpressionTypes == analysisData.expressionTypes(); - expressionTypePublicationClearedProbeEntry = analysisData.expressionTypes().isEmpty() - && !analysisData.expressionTypes().containsKey(probeExpression); - diagnosticManager.warning( - "sema.expr_type_phase_probe", - "expr-type phase probe diagnostic", - null, - null - ); - } - } - - private static final class RecordingVarTypePostAnalyzer extends FrontendVarTypePostAnalyzer { - private boolean invoked; - private boolean exprTypeBoundaryPublished; - private boolean preVarTypePostDiagnosticsMatchedManager; - private boolean stableSlotTypesReferencePreserved; - private boolean slotTypePublicationClearedProbeEntry; - private DiagnosticSnapshot preVarTypePostDiagnostics; - - @Override - public void analyze( - @NotNull FrontendAnalysisData analysisData, - @NotNull DiagnosticManager diagnosticManager - ) { - invoked = true; - preVarTypePostDiagnostics = analysisData.diagnostics(); - preVarTypePostDiagnosticsMatchedManager = preVarTypePostDiagnostics.equals(diagnosticManager.snapshot()); - exprTypeBoundaryPublished = analysisData.moduleSkeleton().sourceClassRelations().stream() - .allMatch(sourceClassRelation -> analysisData.scopesByAst().containsKey(sourceClassRelation.unit().ast())) - && analysisData.symbolBindings().isEmpty() - && analysisData.resolvedMembers().isEmpty() - && analysisData.resolvedCalls().isEmpty() - && analysisData.expressionTypes().isEmpty(); - var publishedSlotTypes = analysisData.slotTypes(); - var probeNode = new PassStatement(SYNTHETIC_RANGE); - publishedSlotTypes.put(probeNode, GdVariantType.VARIANT); - - super.analyze(analysisData, diagnosticManager); - - stableSlotTypesReferencePreserved = publishedSlotTypes == analysisData.slotTypes(); - slotTypePublicationClearedProbeEntry = !analysisData.slotTypes().containsKey(probeNode) - && !analysisData.slotTypes().isEmpty(); - diagnosticManager.warning( - "sema.var_type_post_phase_probe", - "var-type-post phase probe diagnostic", - null, - null - ); - } - } - private static final class RecordingAnnotationUsageAnalyzer extends FrontendAnnotationUsageAnalyzer { private boolean invoked; private boolean varTypeBoundaryPublished; diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java new file mode 100644 index 00000000..d9d5fc82 --- /dev/null +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java @@ -0,0 +1,1444 @@ +package gd.script.gdcc.frontend.sema; + +import dev.superice.gdparser.frontend.ast.AttributePropertyStep; +import dev.superice.gdparser.frontend.ast.Block; +import dev.superice.gdparser.frontend.ast.Expression; +import dev.superice.gdparser.frontend.ast.ForStatement; +import dev.superice.gdparser.frontend.ast.FunctionDeclaration; +import dev.superice.gdparser.frontend.ast.IdentifierExpression; +import dev.superice.gdparser.frontend.ast.IfStatement; +import dev.superice.gdparser.frontend.ast.LambdaExpression; +import dev.superice.gdparser.frontend.ast.MatchStatement; +import dev.superice.gdparser.frontend.ast.Node; +import dev.superice.gdparser.frontend.ast.Parameter; +import dev.superice.gdparser.frontend.ast.ReturnStatement; +import dev.superice.gdparser.frontend.ast.Statement; +import dev.superice.gdparser.frontend.ast.VariableDeclaration; +import dev.superice.gdparser.frontend.ast.WhileStatement; +import gd.script.gdcc.frontend.diagnostic.DiagnosticManager; +import gd.script.gdcc.frontend.diagnostic.DiagnosticSnapshot; +import gd.script.gdcc.frontend.diagnostic.FrontendDiagnostic; +import gd.script.gdcc.frontend.diagnostic.FrontendRange; +import gd.script.gdcc.frontend.parse.FrontendModule; +import gd.script.gdcc.frontend.parse.FrontendSourceUnit; +import gd.script.gdcc.frontend.parse.GdScriptParserService; +import gd.script.gdcc.frontend.scope.BlockScope; +import gd.script.gdcc.frontend.sema.analyzer.FrontendInterfacePhase; +import gd.script.gdcc.frontend.sema.analyzer.FrontendScopeAnalyzer; +import gd.script.gdcc.frontend.sema.analyzer.FrontendSemanticAnalyzer; +import gd.script.gdcc.frontend.sema.analyzer.FrontendBodyOwnerProcedures; +import gd.script.gdcc.frontend.sema.analyzer.FrontendStatementResolver; +import gd.script.gdcc.frontend.sema.analyzer.FrontendSuiteContext; +import gd.script.gdcc.frontend.sema.analyzer.FrontendSuiteResolver; +import gd.script.gdcc.frontend.sema.analyzer.FrontendVariableAnalyzer; +import gd.script.gdcc.frontend.sema.patch.FrontendVarTypePostPatch; +import gd.script.gdcc.gdextension.ExtensionApiLoader; +import gd.script.gdcc.scope.ClassRegistry; +import gd.script.gdcc.scope.ResolveRestriction; +import gd.script.gdcc.type.GdIntType; +import gd.script.gdcc.type.GdType; +import gd.script.gdcc.type.GdVariantType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class FrontendSuiteResolverTest { + private static final @NotNull String STATEMENT_SNAPSHOT_TEST_CATEGORY = "sema.suite_statement_snapshot_probe"; + + @Test + void sourceOrderTraversalMatchesAstOrderAndOwnerOrderIsFixed() throws Exception { + var phaseInput = phaseInput("suite_source_order.gd", """ + class_name SuiteSourceOrder + extends Node + + func ping(value): + var first := value + var second := first + return second + """); + var pingFunction = findStatement( + phaseInput.unit().ast().statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("ping") + ); + var first = findStatement( + pingFunction.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("first") + ); + var second = findStatement( + pingFunction.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("second") + ); + var returnStatement = findStatement(pingFunction.body().statements(), ReturnStatement.class, _ -> true); + var ownerProcedures = new RecordingOwnerProcedures(false); + + resolveWith(phaseInput, ownerProcedures); + + assertOwnerSequence(ownerProcedures.events(), 0, first); + assertOwnerSequence(ownerProcedures.events(), 5, second); + assertOwnerSequence(ownerProcedures.events(), 10, returnStatement); + assertEquals(15, ownerProcedures.events().size()); + } + + @Test + void statementBoundaryPublishesDiagnosticsSnapshotForLaterStatements() throws Exception { + var phaseInput = phaseInput("suite_statement_diagnostics_snapshot.gd", """ + class_name SuiteStatementDiagnosticsSnapshot + extends Node + + func ping(value): + var first := value + var second := first + """); + var pingFunction = findStatement( + phaseInput.unit().ast().statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("ping") + ); + var first = findStatement( + pingFunction.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("first") + ); + var second = findStatement( + pingFunction.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("second") + ); + var ownerProcedures = new StatementSnapshotOwnerProcedures(first, second); + var surface = new FrontendInterfacePhase().analyze(phaseInput.registry(), phaseInput.analysisData()); + + new FrontendSuiteResolver(new FrontendStatementResolver(ownerProcedures)).resolve( + surface, + phaseInput.registry(), + phaseInput.analysisData(), + phaseInput.diagnostics() + ); + + assertAll( + () -> assertTrue(ownerProcedures.sameStatementSawLiveDiagnostic()), + () -> assertFalse(ownerProcedures.sameStatementSawStableSnapshotBeforeFlush()), + () -> assertTrue(ownerProcedures.nextStatementSawCurrentSuiteSnapshot()), + () -> assertEquals( + 1, + diagnosticsByCategory( + phaseInput.analysisData().diagnostics(), + STATEMENT_SNAPSHOT_TEST_CATEGORY + ).size() + ) + ); + } + + @Test + void branchHeadersResolveBeforeTheirChildSuites() throws Exception { + var phaseInput = phaseInput("suite_header_before_body.gd", """ + class_name SuiteHeaderBeforeBody + extends Node + + func ping(value, other): + if value: + var branch := value + elif other: + var elif_branch := other + else: + var else_branch := value + while other: + var loop_local := other + break + """); + var pingFunction = findStatement( + phaseInput.unit().ast().statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("ping") + ); + var ifStatement = findStatement(pingFunction.body().statements(), IfStatement.class, _ -> true); + var branch = findStatement( + ifStatement.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("branch") + ); + var elifClause = ifStatement.elifClauses().getFirst(); + var elifBranch = findStatement( + elifClause.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("elif_branch") + ); + var elseBody = ifStatement.elseBody(); + assertNotNull(elseBody); + var elseBranch = findStatement( + elseBody.statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("else_branch") + ); + var whileStatement = findStatement(pingFunction.body().statements(), WhileStatement.class, _ -> true); + var loopLocal = findStatement( + whileStatement.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("loop_local") + ); + var ownerProcedures = new RecordingOwnerProcedures(false); + + resolveWith(phaseInput, ownerProcedures); + + assertTrue(firstStageIndex(ownerProcedures.events(), ifStatement.condition()) < firstStageIndex(ownerProcedures.events(), branch)); + assertTrue(firstStageIndex(ownerProcedures.events(), elifClause.condition()) < firstStageIndex(ownerProcedures.events(), elifBranch)); + assertTrue(firstStageIndex(ownerProcedures.events(), elseBranch) < firstStageIndex(ownerProcedures.events(), whileStatement.condition())); + assertTrue(firstStageIndex(ownerProcedures.events(), whileStatement.condition()) < firstStageIndex(ownerProcedures.events(), loopLocal)); + } + + @Test + void forBodyResolvesWhileUnsupportedFeatureOwnedBodiesRemainFailClosed() throws Exception { + var phaseInput = phaseInput("suite_fail_closed.gd", """ + class_name SuiteFailClosed + extends Node + + func ping(items, choice): + for item in items: + var from_for := item + match choice: + 0: + var from_match := choice + var callback = func(): + var from_lambda := choice + return choice + const answer = choice + var after := choice + """); + var pingFunction = findStatement( + phaseInput.unit().ast().statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("ping") + ); + var forStatement = findStatement(pingFunction.body().statements(), ForStatement.class, _ -> true); + var fromFor = findStatement( + forStatement.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("from_for") + ); + var matchStatement = findStatement(pingFunction.body().statements(), MatchStatement.class, _ -> true); + var fromMatch = findStatement( + matchStatement.sections().getFirst().body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("from_match") + ); + var callback = findStatement( + pingFunction.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("callback") + ); + var lambdaExpression = assertInstanceOf(LambdaExpression.class, callback.value()); + var fromLambda = findStatement( + lambdaExpression.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("from_lambda") + ); + var answer = findStatement( + pingFunction.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("answer") + ); + var after = findStatement( + pingFunction.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("after") + ); + var surface = new FrontendInterfacePhase().analyze(phaseInput.registry(), phaseInput.analysisData()); + var ownerProcedures = new RecordingOwnerProcedures(false); + + new FrontendSuiteResolver(new FrontendStatementResolver(ownerProcedures)).resolve( + surface, + phaseInput.registry(), + phaseInput.analysisData(), + phaseInput.diagnostics() + ); + + assertTrue(surface.suiteEntryRoots().containsSupportedBlock(forStatement.body())); + assertFalse(surface.suiteEntryRoots().containsSupportedBlock(matchStatement.sections().getFirst().body())); + assertFalse(surface.suiteEntryRoots().containsSupportedBlock(lambdaExpression.body())); + assertFalse(ownerProcedures.unsupportedRoots().contains(forStatement)); + assertTrue(ownerProcedures.unsupportedRoots().contains(matchStatement)); + assertTrue(ownerProcedures.unsupportedRoots().contains(answer)); + assertTrue(hasOwnerEvent(ownerProcedures.events(), fromFor)); + assertFalse(hasOwnerEvent(ownerProcedures.events(), fromMatch)); + assertFalse(hasOwnerEvent(ownerProcedures.events(), fromLambda)); + assertFalse(hasOwnerEvent(ownerProcedures.events(), answer)); + assertTrue(hasOwnerEvent(ownerProcedures.events(), callback)); + assertTrue(hasOwnerEvent(ownerProcedures.events(), after)); + } + + @Test + void forHeaderReadsPrefixOverlayAndBodyEntryDoesNotDependOnTypedClassification() throws Exception { + var phaseInput = phaseInput("suite_for_header_prefix.gd", """ + class_name SuiteForHeaderPrefix + extends Node + + func ping(): + var limit := 1 + for item in limit: + var from_for := item + """); + var pingFunction = findStatement( + phaseInput.unit().ast().statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("ping") + ); + var limit = findStatement( + pingFunction.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("limit") + ); + var forStatement = findStatement(pingFunction.body().statements(), ForStatement.class, _ -> true); + var fromFor = findStatement( + forStatement.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("from_for") + ); + var surface = new FrontendInterfacePhase().analyze(phaseInput.registry(), phaseInput.analysisData()); + var itemUseSite = findNode( + fromFor, + IdentifierExpression.class, + identifierExpression -> identifierExpression.name().equals("item") + ); + + new FrontendSuiteResolver().resolve( + surface, + phaseInput.registry(), + phaseInput.analysisData(), + phaseInput.diagnostics() + ); + + var iterableType = phaseInput.analysisData().expressionTypes().get(forStatement.iterable()); + assertNotNull(iterableType); + assertSame(GdIntType.INT, iterableType.publishedType()); + var itemBinding = phaseInput.analysisData().symbolBindings().get(itemUseSite); + assertNotNull(itemBinding); + assertEquals(FrontendBindingKind.LOCAL_VAR, itemBinding.kind()); + assertEquals(GdVariantType.VARIANT, phaseInput.analysisData().slotTypes().get(fromFor)); + assertTrue(surface.suiteEntryRoots().containsSupportedBlock(forStatement.body())); + assertTrue(surface.bodyDeclarationIndex().containsBodyRoot(forStatement.body())); + assertEquals(GdIntType.INT, phaseInput.analysisData().slotTypes().get(limit)); + } + + @Test + void structurallySupportedForBodyIsResolvedBySuiteResolver() throws Exception { + var phaseInput = phaseInput("suite_gate_body_published.gd", """ + class_name SuiteGateBodyPublished + extends Node + + func ping(values, seed: int): + for item in values: + var from_for := seed + """); + var pingFunction = findStatement( + phaseInput.unit().ast().statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("ping") + ); + var forStatement = findStatement(pingFunction.body().statements(), ForStatement.class, _ -> true); + var fromFor = findStatement( + forStatement.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("from_for") + ); + var seedUseSite = findNode(fromFor, IdentifierExpression.class, identifier -> identifier.name().equals("seed")); + var surface = new FrontendInterfacePhase().analyze(phaseInput.registry(), phaseInput.analysisData()); + var forBodyContext = contextForBlock(phaseInput, surface, pingFunction, forStatement.body()); + + new FrontendSuiteResolver().resolveSuite(forBodyContext, forStatement.body()); + + var seedBinding = phaseInput.analysisData().symbolBindings().get(seedUseSite); + assertNotNull(seedBinding); + assertEquals(FrontendBindingKind.PARAMETER, seedBinding.kind()); + var seedExpressionType = phaseInput.analysisData().expressionTypes().get(seedUseSite); + assertNotNull(seedExpressionType); + assertSame(GdIntType.INT, seedExpressionType.publishedType()); + } + + @Test + void supportedBodyCertificateFailsFastForMissingSuiteSurfaceFacts() throws Exception { + var phaseInput = phaseInput("suite_body_certificate_missing_fact.gd", """ + class_name SuiteBodyCertificateMissingFact + extends Node + + func ping(values, seed: int): + for item in values: + var copy := seed + """); + var pingFunction = findStatement( + phaseInput.unit().ast().statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("ping") + ); + var forStatement = findStatement(pingFunction.body().statements(), ForStatement.class, _ -> true); + var originalSurface = new FrontendInterfacePhase().analyze(phaseInput.registry(), phaseInput.analysisData()); + var originalRoots = originalSurface.suiteEntryRoots(); + + var missingSuiteEntry = new FrontendInterfaceSurface( + originalSurface.bodyDeclarationIndex(), + originalSurface.typedLexicalBaseline(), + new FrontendSuiteEntryRoots( + originalRoots.callableOwners(), + originalRoots.propertyInitializers(), + originalRoots.supportedBlocks().stream() + .filter(block -> block != forStatement.body()) + .toList() + ) + ); + assertCertificateFailure(phaseInput, missingSuiteEntry, pingFunction, forStatement.body(), "suite entry roots"); + + var missingBodyIndex = new FrontendInterfaceSurface( + new FrontendBodyDeclarationIndex(Map.of()), + originalSurface.typedLexicalBaseline(), + originalRoots + ); + assertCertificateFailure(phaseInput, missingBodyIndex, pingFunction, forStatement.body(), "declaration index"); + + var declarationsWithoutIterator = new IdentityHashMap<>( + originalSurface.bodyDeclarationIndex().declarationsByBodyRoot() + ); + declarationsWithoutIterator.put( + forStatement.body(), + originalSurface.bodyDeclarationIndex().declarationsFor(forStatement.body()).stream() + .filter(declaration -> declaration.kind() != FrontendBodyLocalDeclaration.Kind.ITERATOR) + .map(declaration -> new FrontendBodyLocalDeclaration( + declaration.declaration(), + declaration.binding(), + declaration.kind(), + 0 + )) + .toList() + ); + var missingIterator = new FrontendInterfaceSurface( + new FrontendBodyDeclarationIndex(declarationsWithoutIterator), + originalSurface.typedLexicalBaseline(), + originalRoots + ); + // Scope still publishes the iterator; reverse inventory completeness fails before the + // FOR-specific iterator-shape check. + assertCertificateFailure( + phaseInput, + missingIterator, + pingFunction, + forStatement.body(), + "scope inventory local is missing from body declaration index" + ); + + var baselineWithoutIterator = FrontendTypedLexicalBaseline.builder(); + for (var entry : originalSurface.typedLexicalBaseline().typesByDeclaration().entrySet()) { + if (entry.getKey() != forStatement) { + baselineWithoutIterator.put(entry.getKey(), entry.getValue()); + } + } + var missingBaseline = new FrontendInterfaceSurface( + originalSurface.bodyDeclarationIndex(), + baselineWithoutIterator.build(), + originalRoots + ); + assertCertificateFailure(phaseInput, missingBaseline, pingFunction, forStatement.body(), "typed baseline"); + + var publishedScope = assertInstanceOf( + BlockScope.class, + phaseInput.analysisData().scopesByAst().get(forStatement.body()) + ); + var foreignScope = new BlockScope(publishedScope.getParentScope(), publishedScope.kind()); + var scopeError = assertThrows( + IllegalStateException.class, + () -> FrontendBodyStructuralCompleteness.requireStructurallyCompleteBody( + phaseInput.analysisData(), + originalSurface, + forStatement.body(), + foreignScope + ) + ); + assertTrue(scopeError.getMessage().contains("scope identity")); + } + + @Test + void supportedBodyCertificateFailsWhenScopeInventoryIsMissingFromIndex() throws Exception { + var phaseInput = phaseInput("suite_body_certificate_missing_scope_inventory.gd", """ + class_name SuiteBodyCertificateMissingScopeInventory + extends Node + + func ping(values, seed: int): + for item in values: + var copy := seed + """); + var pingFunction = findStatement( + phaseInput.unit().ast().statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("ping") + ); + var forStatement = findStatement(pingFunction.body().statements(), ForStatement.class, _ -> true); + var originalSurface = new FrontendInterfacePhase().analyze(phaseInput.registry(), phaseInput.analysisData()); + var declarationsWithoutCopy = new IdentityHashMap<>( + originalSurface.bodyDeclarationIndex().declarationsByBodyRoot() + ); + declarationsWithoutCopy.put( + forStatement.body(), + originalSurface.bodyDeclarationIndex().declarationsFor(forStatement.body()).stream() + .filter(declaration -> declaration.kind() == FrontendBodyLocalDeclaration.Kind.ITERATOR) + .toList() + ); + var missingOrdinaryLocal = new FrontendInterfaceSurface( + new FrontendBodyDeclarationIndex(declarationsWithoutCopy), + originalSurface.typedLexicalBaseline(), + originalSurface.suiteEntryRoots() + ); + + assertCertificateFailure( + phaseInput, + missingOrdinaryLocal, + pingFunction, + forStatement.body(), + "scope inventory local is missing from body declaration index" + ); + } + + @Test + void supportedBodyCertificateFailsWhenSourceOrderMismatchesAstRanges() throws Exception { + var phaseInput = phaseInput("suite_body_certificate_source_order.gd", """ + class_name SuiteBodyCertificateSourceOrder + extends Node + + func ping(value): + var first := value + var second := first + """); + var pingFunction = findStatement( + phaseInput.unit().ast().statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("ping") + ); + var originalSurface = new FrontendInterfacePhase().analyze(phaseInput.registry(), phaseInput.analysisData()); + var bodyDeclarations = originalSurface.bodyDeclarationIndex().declarationsFor(pingFunction.body()); + assertEquals(2, bodyDeclarations.size()); + var reordered = List.of( + new FrontendBodyLocalDeclaration( + bodyDeclarations.get(1).declaration(), + bodyDeclarations.get(1).binding(), + bodyDeclarations.get(1).kind(), + 0 + ), + new FrontendBodyLocalDeclaration( + bodyDeclarations.get(0).declaration(), + bodyDeclarations.get(0).binding(), + bodyDeclarations.get(0).kind(), + 1 + ) + ); + var declarationsByBody = new IdentityHashMap<>( + originalSurface.bodyDeclarationIndex().declarationsByBodyRoot() + ); + declarationsByBody.put(pingFunction.body(), reordered); + var reorderedSurface = new FrontendInterfaceSurface( + new FrontendBodyDeclarationIndex(declarationsByBody), + originalSurface.typedLexicalBaseline(), + originalSurface.suiteEntryRoots() + ); + + assertCertificateFailure( + phaseInput, + reorderedSurface, + pingFunction, + pingFunction.body(), + "declaration source order does not match AST range order" + ); + } + + @Test + void supportedBodyCertificateFailsWhenForIteratorIsNotFirstInAstOrder() throws Exception { + var phaseInput = phaseInput("suite_body_certificate_iterator_order.gd", """ + class_name SuiteBodyCertificateIteratorOrder + extends Node + + func ping(values, seed: int): + for item in values: + var copy := seed + """); + var pingFunction = findStatement( + phaseInput.unit().ast().statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("ping") + ); + var forStatement = findStatement(pingFunction.body().statements(), ForStatement.class, _ -> true); + var originalSurface = new FrontendInterfacePhase().analyze(phaseInput.registry(), phaseInput.analysisData()); + var bodyDeclarations = originalSurface.bodyDeclarationIndex().declarationsFor(forStatement.body()); + assertEquals(2, bodyDeclarations.size()); + var iterator = bodyDeclarations.stream() + .filter(declaration -> declaration.kind() == FrontendBodyLocalDeclaration.Kind.ITERATOR) + .findFirst() + .orElseThrow(); + var ordinary = bodyDeclarations.stream() + .filter(declaration -> declaration.kind() == FrontendBodyLocalDeclaration.Kind.ORDINARY_VAR) + .findFirst() + .orElseThrow(); + // Contiguous sourceOrder with ordinary first violates AST range order because the + // ForStatement always starts before body locals. + var reordered = List.of( + new FrontendBodyLocalDeclaration(ordinary.declaration(), ordinary.binding(), ordinary.kind(), 0), + new FrontendBodyLocalDeclaration(iterator.declaration(), iterator.binding(), iterator.kind(), 1) + ); + var declarationsByBody = new IdentityHashMap<>( + originalSurface.bodyDeclarationIndex().declarationsByBodyRoot() + ); + declarationsByBody.put(forStatement.body(), reordered); + var reorderedSurface = new FrontendInterfaceSurface( + new FrontendBodyDeclarationIndex(declarationsByBody), + originalSurface.typedLexicalBaseline(), + originalSurface.suiteEntryRoots() + ); + + assertCertificateFailure( + phaseInput, + reorderedSurface, + pingFunction, + forStatement.body(), + "declaration source order does not match AST range order" + ); + } + + @Test + void suiteExportKeepsStableSideTableUnchangedUntilTransactionApply() throws Exception { + var phaseInput = phaseInput("suite_export_boundary.gd", """ + class_name SuiteExportBoundary + extends Node + + func ping(value): + var first := value + """); + var pingFunction = findStatement( + phaseInput.unit().ast().statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("ping") + ); + var first = findStatement( + pingFunction.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("first") + ); + var ownerProcedures = new RecordingOwnerProcedures(true); + + resolveWith(phaseInput, ownerProcedures); + + assertTrue(ownerProcedures.pendingBindingWasVisibleBeforeStableApply()); + assertTrue(ownerProcedures.stableWasEmptyDuringOwnerProcedure()); + var publishedBinding = phaseInput.analysisData().symbolBindings().get(first); + assertNotNull(publishedBinding); + assertEquals("__suite_probe__", publishedBinding.symbolName()); + } + + @Test + void mainAnalyzerBuildsInterfaceSurfaceBeforeLegacyBodyAnalyzers() throws Exception { + var parserService = new GdScriptParserService(); + var diagnostics = new DiagnosticManager(); + var unit = parserService.parseUnit(Path.of("tmp", "suite_pipeline_handoff.gd"), """ + class_name SuitePipelineHandoff + extends Node + + func ping(value): + var local := value + return local + """, diagnostics); + assertTrue(diagnostics.isEmpty(), () -> "Unexpected parse diagnostics: " + diagnostics.snapshot()); + var registry = new ClassRegistry(ExtensionApiLoader.loadDefault()); + var interfacePhase = new RecordingInterfacePhase(); + var suiteResolver = new RecordingSuiteResolver(); + var analyzer = new FrontendSemanticAnalyzer(interfacePhase, suiteResolver); + + var analysisData = analyzer.analyze(new FrontendModule("test_module", List.of(unit)), registry, diagnostics); + + assertTrue(interfacePhase.invoked()); + assertTrue(interfacePhase.variableInventoryWasPublished()); + assertTrue(suiteResolver.invoked()); + assertSame(interfacePhase.surface(), suiteResolver.surface()); + assertTrue(suiteResolver.bodySideTablesWereEmptyAtHandoff()); + assertFalse(interfacePhase.surface().bodyDeclarationIndex().declarationsByBodyRoot().isEmpty()); + assertFalse(analysisData.slotTypes().isEmpty()); + } + + @Test + void bodyOwnerProceduresStabilizeSourceOrderAliasChainBeforeLegacyAnalyzers() throws Exception { + var phaseInput = phaseInput("suite_stage_e_alias_chain.gd", """ + class_name SuiteStageEAliasChain + extends RefCounted + + class Point: + var marker: int = -1 + + func read_path(point: Point): + var a := point + var b := a + var c := b + return c.marker + """); + var readPath = findStatement( + phaseInput.unit().ast().statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("read_path") + ); + var a = findStatement(readPath.body().statements(), VariableDeclaration.class, declaration -> declaration.name().equals("a")); + var b = findStatement(readPath.body().statements(), VariableDeclaration.class, declaration -> declaration.name().equals("b")); + var c = findStatement(readPath.body().statements(), VariableDeclaration.class, declaration -> declaration.name().equals("c")); + var markerStep = findNode(readPath.body(), AttributePropertyStep.class, step -> step.name().equals("marker")); + + resolveWithDefaultOwnerProcedures(phaseInput); + + assertAll( + () -> assertTypeNameEndsWith(requireType(phaseInput.analysisData().slotTypes().get(a)), "Point"), + () -> assertTypeNameEndsWith(requireType(phaseInput.analysisData().slotTypes().get(b)), "Point"), + () -> assertTypeNameEndsWith(requireType(phaseInput.analysisData().slotTypes().get(c)), "Point"), + () -> assertExpressionTypeNameEndsWith(phaseInput.analysisData(), requireExpression(a.value()), "Point"), + () -> assertExpressionTypeNameEndsWith(phaseInput.analysisData(), requireExpression(b.value()), "Point"), + () -> assertExpressionTypeNameEndsWith(phaseInput.analysisData(), requireExpression(c.value()), "Point") + ); + var resolvedMember = phaseInput.analysisData().resolvedMembers().get(markerStep); + assertNotNull(resolvedMember); + assertAll( + () -> assertTypeNameEndsWith(requireType(resolvedMember.receiverType()), "Point"), + () -> assertEquals("int", requireType(resolvedMember.resultType()).getTypeName()) + ); + } + + @Test + void childBlockReadsParentPrefixAndRejectedShadowDoesNotPolluteParentSlot() throws Exception { + var phaseInput = phaseInput("suite_stage_e_child_prefix.gd", """ + class_name SuiteStageEChildPrefix + extends RefCounted + + class Point: + var marker: int = -1 + + func read_path(point: Point, seed: int): + var stable := point + if seed > 0: + var child := stable + var stable := seed + return stable.marker + """); + var readPath = findStatement( + phaseInput.unit().ast().statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("read_path") + ); + var stable = findStatement( + readPath.body().statements(), + VariableDeclaration.class, + declaration -> declaration.name().equals("stable") + ); + var ifStatement = findStatement(readPath.body().statements(), IfStatement.class, _ -> true); + var child = findStatement( + ifStatement.body().statements(), + VariableDeclaration.class, + declaration -> declaration.name().equals("child") + ); + var rejectedShadow = findStatement( + ifStatement.body().statements(), + VariableDeclaration.class, + declaration -> declaration.name().equals("stable") + ); + var markerStep = findNode(readPath.body(), AttributePropertyStep.class, step -> step.name().equals("marker")); + + resolveWithDefaultOwnerProcedures(phaseInput); + + assertAll( + () -> assertTypeNameEndsWith(requireType(phaseInput.analysisData().slotTypes().get(stable)), "Point"), + () -> assertTypeNameEndsWith(requireType(phaseInput.analysisData().slotTypes().get(child)), "Point"), + () -> assertNull(phaseInput.analysisData().slotTypes().get(rejectedShadow)) + ); + var resolvedMember = phaseInput.analysisData().resolvedMembers().get(markerStep); + assertNotNull(resolvedMember); + assertTypeNameEndsWith(requireType(resolvedMember.receiverType()), "Point"); + } + + @Test + void nestedSuiteFactsStayUnpublishedUntilCallableRootCompletes() throws Exception { + var phaseInput = phaseInput("suite_nested_export_batch.gd", """ + class_name SuiteNestedExportBatch + extends RefCounted + + func ping(seed): + if seed: + var child := seed + var after := seed + """); + var pingFunction = findStatement( + phaseInput.unit().ast().statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("ping") + ); + var ifStatement = findStatement(pingFunction.body().statements(), IfStatement.class, _ -> true); + var child = findStatement( + ifStatement.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("child") + ); + var after = findStatement( + pingFunction.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("after") + ); + var ownerProcedures = new DeferredChildExportOwnerProcedures(child, after); + + resolveWith(phaseInput, ownerProcedures); + + assertAll( + () -> assertTrue(ownerProcedures.childBindingWasUnpublishedInParentContinuation()), + () -> assertNotNull(phaseInput.analysisData().symbolBindings().get(child)) + ); + } + + @Test + void transientExpressionCacheAndVarPostFactsStayOverlayLocalUntilSuiteExport() throws Exception { + var phaseInput = phaseInput("suite_stage_e_export_boundary.gd", """ + class_name SuiteStageEExportBoundary + extends RefCounted + + func ping(seed: int): + var first := seed + var second := first + return second + """); + var pingFunction = findStatement( + phaseInput.unit().ast().statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("ping") + ); + var first = findStatement( + pingFunction.body().statements(), + VariableDeclaration.class, + declaration -> declaration.name().equals("first") + ); + var second = findStatement( + pingFunction.body().statements(), + VariableDeclaration.class, + declaration -> declaration.name().equals("second") + ); + var ownerProcedures = new ExportBoundaryOwnerProcedures(first, second); + + var surface = new FrontendInterfacePhase().analyze(phaseInput.registry(), phaseInput.analysisData()); + new FrontendSuiteResolver(new FrontendStatementResolver(ownerProcedures)).resolve( + surface, + phaseInput.registry(), + phaseInput.analysisData(), + phaseInput.diagnostics() + ); + + assertAll( + () -> assertTrue(ownerProcedures.localStabilizationKeptInitializerTypeInTransientCache()), + () -> assertTrue(ownerProcedures.varPostPendingFactWasIsolatedFromStableTable()), + () -> assertTrue(ownerProcedures.flushedVarPostFactWasVisibleBeforeSuiteExport()), + () -> assertEquals("int", requireType(phaseInput.analysisData().slotTypes().get(first)).getTypeName()), + () -> assertEquals("int", requireType(phaseInput.analysisData().slotTypes().get(second)).getTypeName()) + ); + } + + @Test + void callableEntryVarTypePostFactsStayOverlayLocalUntilSuiteExport() throws Exception { + var phaseInput = phaseInput("suite_callable_entry_var_post.gd", """ + class_name SuiteCallableEntryVarPost + extends RefCounted + + func ping(value: int): + var first := value + """); + var pingFunction = findStatement( + phaseInput.unit().ast().statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("ping") + ); + var parameter = pingFunction.parameters().getFirst(); + var first = findStatement( + pingFunction.body().statements(), + VariableDeclaration.class, + declaration -> declaration.name().equals("first") + ); + var ownerProcedures = new CallableEntryVarTypePostOwnerProcedures(parameter, first); + + var surface = new FrontendInterfacePhase().analyze(phaseInput.registry(), phaseInput.analysisData()); + new FrontendSuiteResolver(new FrontendStatementResolver(ownerProcedures)).resolve( + surface, + phaseInput.registry(), + phaseInput.analysisData(), + phaseInput.diagnostics() + ); + + assertAll( + () -> assertTrue(ownerProcedures.parameterWasCommittedBeforeFirstStatement()), + () -> assertTrue(ownerProcedures.stableSlotTypesWereUnchangedBeforeSuiteExport()), + () -> assertEquals(GdIntType.INT, phaseInput.analysisData().slotTypes().get(parameter)) + ); + } + + private static void resolveWith( + @NotNull PhaseInput phaseInput, + @NotNull FrontendStatementResolver.OwnerProcedures ownerProcedures + ) { + var surface = new FrontendInterfacePhase().analyze(phaseInput.registry(), phaseInput.analysisData()); + new FrontendSuiteResolver(new FrontendStatementResolver(ownerProcedures)).resolve( + surface, + phaseInput.registry(), + phaseInput.analysisData(), + phaseInput.diagnostics() + ); + } + + private static void resolveWithDefaultOwnerProcedures(@NotNull PhaseInput phaseInput) { + var surface = new FrontendInterfacePhase().analyze(phaseInput.registry(), phaseInput.analysisData()); + new FrontendSuiteResolver().resolve( + surface, + phaseInput.registry(), + phaseInput.analysisData(), + phaseInput.diagnostics() + ); + } + + private static void assertExpressionTypeNameEndsWith( + @NotNull FrontendAnalysisData analysisData, + @NotNull Expression expression, + @NotNull String suffix + ) { + var expressionType = analysisData.expressionTypes().get(expression); + assertNotNull(expressionType); + assertNotNull(expressionType.publishedType()); + assertTypeNameEndsWith(requireType(expressionType.publishedType()), suffix); + } + + private static @NotNull Expression requireExpression(@Nullable Expression expression) { + assertNotNull(expression); + return expression; + } + + private static @NotNull GdType requireType(@Nullable GdType type) { + assertNotNull(type); + return type; + } + + private static void assertTypeNameEndsWith(@NotNull GdType type, @NotNull String suffix) { + assertTrue(type.getTypeName().endsWith(suffix), + () -> "Expected type name to end with '" + suffix + "' but was " + type.getTypeName()); + } + + private static void assertOwnerSequence(@NotNull List events, int offset, @NotNull Node root) { + assertSame(root, events.get(offset).root()); + assertEquals("top", events.get(offset).stage()); + assertSame(root, events.get(offset + 1).root()); + assertEquals("local", events.get(offset + 1).stage()); + assertSame(root, events.get(offset + 2).root()); + assertEquals("chain", events.get(offset + 2).stage()); + assertSame(root, events.get(offset + 3).root()); + assertEquals("expr", events.get(offset + 3).stage()); + assertSame(root, events.get(offset + 4).root()); + assertEquals("var_post", events.get(offset + 4).stage()); + } + + private static int firstStageIndex(@NotNull List events, @NotNull Node root) { + for (var i = 0; i < events.size(); i++) { + if (events.get(i).root() == root) { + return i; + } + } + throw new AssertionError("Missing event for root: " + root.getClass().getSimpleName()); + } + + private static boolean hasOwnerEvent(@NotNull List events, @NotNull Node root) { + return events.stream().anyMatch(event -> event.root() == root); + } + + private static void assertCertificateFailure( + @NotNull PhaseInput phaseInput, + @NotNull FrontendInterfaceSurface surface, + @NotNull Node callableOwner, + @NotNull Block body, + @NotNull String expectedDetail + ) { + var context = contextForBlock(phaseInput, surface, callableOwner, body); + var error = assertThrows( + IllegalStateException.class, + () -> new FrontendSuiteResolver().resolveSuite(context, body) + ); + assertTrue(error.getMessage().contains(expectedDetail), error::getMessage); + } + + private static @NotNull List diagnosticsByCategory( + @NotNull DiagnosticSnapshot diagnostics, + @NotNull String category + ) { + return diagnostics.asList().stream() + .filter(diagnostic -> diagnostic.category().equals(category)) + .toList(); + } + + private static @NotNull FrontendSuiteContext contextForBlock( + @NotNull PhaseInput phaseInput, + @NotNull FrontendInterfaceSurface surface, + @NotNull Node callableOwner, + @NotNull Block block + ) { + var publishedScope = phaseInput.analysisData().scopesByAst().get(block); + var blockScope = assertInstanceOf(BlockScope.class, publishedScope); + return new FrontendSuiteContext( + Path.of("tmp", "synthetic_gate_body.gd"), + callableOwner, + block, + blockScope, + blockScope, + ResolveRestriction.instanceContext(), + false, + null, + surface, + new FrontendTypedLexicalEnvironment( + blockScope, + phaseInput.analysisData(), + null, + surface.typedLexicalBaseline() + ), + phaseInput.analysisData(), + phaseInput.diagnostics(), + phaseInput.registry(), + null + ); + } + + private static @NotNull PhaseInput phaseInput(@NotNull String fileName, @NotNull String source) throws Exception { + var parserService = new GdScriptParserService(); + var diagnostics = new DiagnosticManager(); + var unit = parserService.parseUnit(Path.of("tmp", fileName), source, diagnostics); + assertTrue(diagnostics.isEmpty(), () -> "Unexpected parse diagnostics: " + diagnostics.snapshot()); + + var registry = new ClassRegistry(ExtensionApiLoader.loadDefault()); + var analysisData = FrontendAnalysisData.bootstrap(); + var module = new FrontendModule("test_module", List.of(unit)); + var moduleSkeleton = new FrontendClassSkeletonBuilder().build(module, registry, diagnostics, analysisData); + analysisData.updateModuleSkeleton(moduleSkeleton); + analysisData.updateDiagnostics(diagnostics.snapshot()); + new FrontendScopeAnalyzer().analyze(registry, analysisData, diagnostics); + analysisData.updateDiagnostics(diagnostics.snapshot()); + new FrontendVariableAnalyzer().analyze(analysisData, diagnostics); + analysisData.updateDiagnostics(diagnostics.snapshot()); + return new PhaseInput(unit, registry, analysisData, diagnostics); + } + + private static T findStatement( + @NotNull List statements, + @NotNull Class statementType, + @NotNull Predicate predicate + ) { + return statements.stream() + .filter(statementType::isInstance) + .map(statementType::cast) + .filter(predicate) + .findFirst() + .orElseThrow(() -> new AssertionError("Statement not found: " + statementType.getSimpleName())); + } + + private static T findNode( + @NotNull Node root, + @NotNull Class nodeType, + @NotNull Predicate predicate + ) { + if (nodeType.isInstance(root)) { + var candidate = nodeType.cast(root); + if (predicate.test(candidate)) { + return candidate; + } + } + for (var child : root.getChildren()) { + var match = findNodeOrNull(child, nodeType, predicate); + if (match != null) { + return match; + } + } + throw new AssertionError("Node not found: " + nodeType.getSimpleName()); + } + + private static T findNodeOrNull( + @NotNull Node root, + @NotNull Class nodeType, + @NotNull Predicate predicate + ) { + if (nodeType.isInstance(root)) { + var candidate = nodeType.cast(root); + if (predicate.test(candidate)) { + return candidate; + } + } + for (var child : root.getChildren()) { + var match = findNodeOrNull(child, nodeType, predicate); + if (match != null) { + return match; + } + } + return null; + } + + private record PhaseInput( + @NotNull FrontendSourceUnit unit, + @NotNull ClassRegistry registry, + @NotNull FrontendAnalysisData analysisData, + @NotNull DiagnosticManager diagnostics + ) { + } + + private record OwnerEvent(@NotNull String stage, @NotNull Node root) { + } + + private static final class StatementSnapshotOwnerProcedures implements FrontendStatementResolver.OwnerProcedures { + private final @NotNull Node first; + private final @NotNull Node second; + private boolean sameStatementSawLiveDiagnostic; + private boolean sameStatementSawStableSnapshotBeforeFlush; + private boolean nextStatementSawCurrentSuiteSnapshot; + + private StatementSnapshotOwnerProcedures(@NotNull Node first, @NotNull Node second) { + this.first = first; + this.second = second; + } + + @Override + public void runTopBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { + if (root == first) { + context.diagnosticManager().error( + STATEMENT_SNAPSHOT_TEST_CATEGORY, + "synthetic statement-local upstream diagnostic", + context.sourcePath(), + FrontendRange.fromAstRange(root.range()) + ); + return; + } + if (root == second) { + nextStatementSawCurrentSuiteSnapshot = hasStatementSnapshotDiagnostic(context.analysisData().diagnostics()); + } + } + + @Override + public void runChainBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { + if (root != first) { + return; + } + sameStatementSawLiveDiagnostic = hasStatementSnapshotDiagnostic(context.diagnosticManager().snapshot()); + sameStatementSawStableSnapshotBeforeFlush = hasStatementSnapshotDiagnostic(context.analysisData().diagnostics()); + } + + private static boolean hasStatementSnapshotDiagnostic(@NotNull DiagnosticSnapshot diagnostics) { + return diagnosticsByCategory(diagnostics, STATEMENT_SNAPSHOT_TEST_CATEGORY).size() == 1; + } + + private boolean sameStatementSawLiveDiagnostic() { + return sameStatementSawLiveDiagnostic; + } + + private boolean sameStatementSawStableSnapshotBeforeFlush() { + return sameStatementSawStableSnapshotBeforeFlush; + } + + private boolean nextStatementSawCurrentSuiteSnapshot() { + return nextStatementSawCurrentSuiteSnapshot; + } + } + + private static final class RecordingOwnerProcedures implements FrontendStatementResolver.OwnerProcedures { + private final boolean publishTopBinding; + private final @NotNull List events = new ArrayList<>(); + private final @NotNull List unsupportedRoots = new ArrayList<>(); + private boolean pendingBindingWasVisibleBeforeStableApply; + private boolean stableWasEmptyDuringOwnerProcedure = true; + + private RecordingOwnerProcedures(boolean publishTopBinding) { + this.publishTopBinding = publishTopBinding; + } + + @Override + public void runTopBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { + events.add(new OwnerEvent("top", root)); + if (!publishTopBinding) { + return; + } + stableWasEmptyDuringOwnerProcedure &= context.analysisData().symbolBindings().get(root) == null; + context.typedEnvironment().putSymbolBinding( + FrontendSemanticStage.TOP_BINDING, + root, + new FrontendBinding("__suite_probe__", FrontendBindingKind.UNKNOWN, null) + ); + } + + @Override + public void runLocalTypeStabilization(@NotNull FrontendSuiteContext context, @NotNull Node root) { + events.add(new OwnerEvent("local", root)); + stableWasEmptyDuringOwnerProcedure &= context.analysisData().symbolBindings().get(root) == null; + pendingBindingWasVisibleBeforeStableApply |= context.typedEnvironment().symbolBinding(root) != null; + } + + @Override + public void runChainBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { + events.add(new OwnerEvent("chain", root)); + stableWasEmptyDuringOwnerProcedure &= context.analysisData().symbolBindings().get(root) == null; + } + + @Override + public void runExprType(@NotNull FrontendSuiteContext context, @NotNull Node root) { + events.add(new OwnerEvent("expr", root)); + stableWasEmptyDuringOwnerProcedure &= context.analysisData().symbolBindings().get(root) == null; + } + + @Override + public void runVarTypePost(@NotNull FrontendSuiteContext context, @NotNull Node root) { + events.add(new OwnerEvent("var_post", root)); + stableWasEmptyDuringOwnerProcedure &= context.analysisData().symbolBindings().get(root) == null; + } + + @Override + public void runUnsupported(@NotNull FrontendSuiteContext context, @NotNull Node root) { + unsupportedRoots.add(root); + } + + private @NotNull List events() { + return events; + } + + private @NotNull List unsupportedRoots() { + return unsupportedRoots; + } + + private boolean pendingBindingWasVisibleBeforeStableApply() { + return pendingBindingWasVisibleBeforeStableApply; + } + + private boolean stableWasEmptyDuringOwnerProcedure() { + return stableWasEmptyDuringOwnerProcedure; + } + } + + private static final class DeferredChildExportOwnerProcedures implements FrontendStatementResolver.OwnerProcedures { + private final @NotNull Node child; + private final @NotNull Node parentContinuation; + private boolean childBindingWasUnpublishedInParentContinuation; + + private DeferredChildExportOwnerProcedures(@NotNull Node child, @NotNull Node parentContinuation) { + this.child = child; + this.parentContinuation = parentContinuation; + } + + @Override + public void runTopBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { + if (root == child) { + context.typedEnvironment().putSymbolBinding( + FrontendSemanticStage.TOP_BINDING, + child, + new FrontendBinding("__nested_suite_probe__", FrontendBindingKind.UNKNOWN, null) + ); + return; + } + if (root == parentContinuation) { + childBindingWasUnpublishedInParentContinuation = context.analysisData() + .symbolBindings() + .get(child) == null; + } + } + + private boolean childBindingWasUnpublishedInParentContinuation() { + return childBindingWasUnpublishedInParentContinuation; + } + } + + private static final class ExportBoundaryOwnerProcedures implements FrontendStatementResolver.OwnerProcedures { + private final @NotNull FrontendBodyOwnerProcedures delegate = new FrontendBodyOwnerProcedures(); + private final @NotNull VariableDeclaration first; + private final @NotNull VariableDeclaration second; + private boolean localStabilizationKeptInitializerTypeInTransientCache; + private boolean varPostPendingFactWasIsolatedFromStableTable; + private boolean flushedVarPostFactWasVisibleBeforeSuiteExport; + + private ExportBoundaryOwnerProcedures( + @NotNull VariableDeclaration first, + @NotNull VariableDeclaration second + ) { + this.first = first; + this.second = second; + } + + @Override + public void runTopBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { + if (root == second) { + flushedVarPostFactWasVisibleBeforeSuiteExport = context.analysisData().slotTypes().get(first) == null + && context.typedEnvironment().slotType(first) != null; + } + delegate.runTopBinding(context, root); + } + + @Override + public void runLocalTypeStabilization(@NotNull FrontendSuiteContext context, @NotNull Node root) { + delegate.runLocalTypeStabilization(context, root); + if (root == first) { + var currentBlockScope = context.currentBlockScope(); + localStabilizationKeptInitializerTypeInTransientCache = first.value() != null + && currentBlockScope != null + && context.typedEnvironment().expressionType(first.value()) == null + && context.typedEnvironment().localSlotType( + currentBlockScope, + first.name(), + first + ) != null; + } + } + + @Override + public void runChainBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { + delegate.runChainBinding(context, root); + } + + @Override + public void runExprType(@NotNull FrontendSuiteContext context, @NotNull Node root) { + delegate.runExprType(context, root); + } + + @Override + public void runVarTypePost(@NotNull FrontendSuiteContext context, @NotNull Node root) { + delegate.runVarTypePost(context, root); + if (root == first) { + varPostPendingFactWasIsolatedFromStableTable = context.analysisData().slotTypes().get(first) == null + && context.typedEnvironment().slotType(first) != null; + } + } + + private boolean localStabilizationKeptInitializerTypeInTransientCache() { + return localStabilizationKeptInitializerTypeInTransientCache; + } + + private boolean varPostPendingFactWasIsolatedFromStableTable() { + return varPostPendingFactWasIsolatedFromStableTable; + } + + private boolean flushedVarPostFactWasVisibleBeforeSuiteExport() { + return flushedVarPostFactWasVisibleBeforeSuiteExport; + } + } + + private static final class CallableEntryVarTypePostOwnerProcedures + implements FrontendStatementResolver.OwnerProcedures { + private final @NotNull Parameter parameter; + private final @NotNull VariableDeclaration firstStatement; + private boolean parameterWasCommittedBeforeFirstStatement; + private boolean stableSlotTypesWereUnchangedBeforeSuiteExport; + + private CallableEntryVarTypePostOwnerProcedures( + @NotNull Parameter parameter, + @NotNull VariableDeclaration firstStatement + ) { + this.parameter = parameter; + this.firstStatement = firstStatement; + } + + @Override + public void runTopBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { + if (root != firstStatement) { + return; + } + var patch = context.typedEnvironment().exportPatchTransaction().patches().stream() + .filter(FrontendVarTypePostPatch.class::isInstance) + .map(FrontendVarTypePostPatch.class::cast) + .findFirst() + .orElse(null); + parameterWasCommittedBeforeFirstStatement = patch != null + && patch.slotTypes().get(parameter) == GdIntType.INT + && context.typedEnvironment().slotType(parameter) == GdIntType.INT; + stableSlotTypesWereUnchangedBeforeSuiteExport = context.analysisData().slotTypes().get(parameter) == null; + } + + private boolean parameterWasCommittedBeforeFirstStatement() { + return parameterWasCommittedBeforeFirstStatement; + } + + private boolean stableSlotTypesWereUnchangedBeforeSuiteExport() { + return stableSlotTypesWereUnchangedBeforeSuiteExport; + } + } + + private static final class RecordingInterfacePhase extends FrontendInterfacePhase { + private boolean invoked; + private boolean variableInventoryWasPublished; + private FrontendInterfaceSurface surface; + + @Override + public @NotNull FrontendInterfaceSurface analyze( + @NotNull ClassRegistry classRegistry, + @NotNull FrontendAnalysisData analysisData + ) { + invoked = true; + variableInventoryWasPublished = analysisData.moduleSkeleton().sourceClassRelations().stream() + .allMatch(relation -> analysisData.scopesByAst().containsKey(relation.unit().ast())) + && analysisData.symbolBindings().isEmpty() + && analysisData.expressionTypes().isEmpty(); + surface = super.analyze(classRegistry, analysisData); + return surface; + } + + private boolean invoked() { + return invoked; + } + + private boolean variableInventoryWasPublished() { + return variableInventoryWasPublished; + } + + private @NotNull FrontendInterfaceSurface surface() { + if (surface == null) { + throw new AssertionError("Interface surface was not recorded"); + } + return surface; + } + } + + private static final class RecordingSuiteResolver extends FrontendSuiteResolver { + private boolean invoked; + private boolean bodySideTablesWereEmptyAtHandoff; + private FrontendInterfaceSurface surface; + + @Override + public void resolve( + @NotNull FrontendInterfaceSurface interfaceSurface, + @NotNull ClassRegistry classRegistry, + @NotNull FrontendAnalysisData analysisData, + @NotNull DiagnosticManager diagnosticManager + ) { + invoked = true; + surface = interfaceSurface; + bodySideTablesWereEmptyAtHandoff = analysisData.symbolBindings().isEmpty() + && analysisData.resolvedMembers().isEmpty() + && analysisData.resolvedCalls().isEmpty() + && analysisData.expressionTypes().isEmpty() + && analysisData.slotTypes().isEmpty(); + super.resolve(interfaceSurface, classRegistry, analysisData, diagnosticManager); + } + + private boolean invoked() { + return invoked; + } + + private boolean bodySideTablesWereEmptyAtHandoff() { + return bodySideTablesWereEmptyAtHandoff; + } + + private FrontendInterfaceSurface surface() { + return surface; + } + } +} diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironmentTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironmentTest.java new file mode 100644 index 00000000..7813a78a --- /dev/null +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironmentTest.java @@ -0,0 +1,347 @@ +package gd.script.gdcc.frontend.sema; + +import dev.superice.gdparser.frontend.ast.DeclarationKind; +import dev.superice.gdparser.frontend.ast.IdentifierExpression; +import dev.superice.gdparser.frontend.ast.Point; +import dev.superice.gdparser.frontend.ast.Range; +import dev.superice.gdparser.frontend.ast.TypeRef; +import dev.superice.gdparser.frontend.ast.VariableDeclaration; +import gd.script.gdcc.exception.FrontendAnalysisPatchException; +import gd.script.gdcc.frontend.scope.BlockScope; +import gd.script.gdcc.frontend.scope.BlockScopeKind; +import gd.script.gdcc.frontend.scope.CallableScope; +import gd.script.gdcc.frontend.scope.CallableScopeKind; +import gd.script.gdcc.frontend.scope.ClassScope; +import gd.script.gdcc.frontend.sema.patch.FrontendLocalSlotTypeUpdate; +import gd.script.gdcc.frontend.sema.patch.FrontendOwnerPatch; +import gd.script.gdcc.gdextension.ExtensionApiLoader; +import gd.script.gdcc.lir.LirClassDef; +import gd.script.gdcc.scope.ClassRegistry; +import gd.script.gdcc.scope.ScopeLookupStatus; +import gd.script.gdcc.scope.ScopeOwnerKind; +import gd.script.gdcc.scope.ScopeValue; +import gd.script.gdcc.scope.ScopeValueKind; +import gd.script.gdcc.type.GdccForRangeIterType; +import gd.script.gdcc.type.GdFloatType; +import gd.script.gdcc.type.GdIntType; +import gd.script.gdcc.type.GdObjectType; +import gd.script.gdcc.type.GdVariantType; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class FrontendTypedLexicalEnvironmentTest { + private static final Range RANGE = new Range(0, 1, new Point(0, 0), new Point(0, 1)); + + @Test + void pendingFlushExportAndApplyPreserveStableDataUntilTransactionApply() throws Exception { + var analysisData = FrontendAnalysisData.bootstrap(); + var bodyScope = newBodyScope(); + var declaration = variable("local"); + bodyScope.defineLocal("local", GdVariantType.VARIANT, declaration); + var bindingNode = identifier("local_use"); + var originalBinding = localBinding("local", declaration, requireLocal(bodyScope, "local")); + analysisData.symbolBindings().put(bindingNode, originalBinding); + var environment = new FrontendTypedLexicalEnvironment(bodyScope, analysisData); + + environment.addLocalSlotTypeUpdate( + FrontendSemanticStage.LOCAL_TYPE_STABILIZATION, + new FrontendLocalSlotTypeUpdate(bodyScope, "local", declaration, GdIntType.INT) + ); + + assertTrue(environment.hasPendingFacts()); + assertSame(GdIntType.INT, environment.localSlotType(bodyScope, "local", declaration)); + assertSame(GdVariantType.VARIANT, requireLocal(bodyScope, "local").type()); + assertSame(GdVariantType.VARIANT, Objects.requireNonNull(originalBinding.resolvedValue()).type()); + + environment.flushPendingFacts(); + + assertFalse(environment.hasPendingFacts()); + assertTrue(environment.hasCommittedFacts()); + assertSame(GdIntType.INT, environment.localSlotType(bodyScope, "local", declaration)); + assertSame(GdVariantType.VARIANT, requireLocal(bodyScope, "local").type()); + assertSame(originalBinding, analysisData.symbolBindings().get(bindingNode)); + + var transaction = environment.exportPatchTransaction(); + assertEquals(List.of(FrontendSemanticStage.LOCAL_TYPE_STABILIZATION), transaction.patches().stream() + .map(FrontendOwnerPatch::stage) + .toList()); + transaction.applyTo(analysisData); + + var refreshedBinding = analysisData.symbolBindings().get(bindingNode); + assertSame(GdIntType.INT, requireLocal(bodyScope, "local").type()); + var refreshedValue = Objects.requireNonNull(Objects.requireNonNull(refreshedBinding).resolvedValue()); + assertSame(GdIntType.INT, refreshedValue.type()); + } + + @Test + void childEnvironmentReadsParentCommittedLocalSlotOverlay() throws Exception { + var analysisData = FrontendAnalysisData.bootstrap(); + var parentScope = newBodyScope(); + var declaration = variable("parent_local"); + parentScope.defineLocal("parent_local", GdVariantType.VARIANT, declaration); + var parentEnvironment = new FrontendTypedLexicalEnvironment(parentScope, analysisData); + parentEnvironment.addLocalSlotTypeUpdate( + FrontendSemanticStage.LOCAL_TYPE_STABILIZATION, + new FrontendLocalSlotTypeUpdate(parentScope, "parent_local", declaration, GdIntType.INT) + ); + parentEnvironment.flushPendingFacts(); + var childScope = new BlockScope(parentScope, BlockScopeKind.IF_BODY); + var childEnvironment = new FrontendTypedLexicalEnvironment(childScope, analysisData, parentEnvironment); + + var effectiveValue = childEnvironment.effectiveScopeValue(requireLocal(parentScope, "parent_local"), parentScope); + + assertSame(GdIntType.INT, effectiveValue.type()); + assertSame(GdVariantType.VARIANT, requireLocal(parentScope, "parent_local").type()); + } + + @Test + void typedBaselineSuppliesInitialLocalTypeUntilPublishedFactOverridesIt() throws Exception { + var analysisData = FrontendAnalysisData.bootstrap(); + var bodyScope = newBodyScope(); + var declaration = variable("local"); + bodyScope.defineLocal("local", GdVariantType.VARIANT, declaration); + var baseline = FrontendTypedLexicalBaseline.builder() + .put(declaration, GdIntType.INT) + .build(); + var environment = new FrontendTypedLexicalEnvironment(bodyScope, analysisData, null, baseline); + + assertSame(GdIntType.INT, environment.slotType(declaration)); + assertSame(GdIntType.INT, environment.localSlotType(bodyScope, "local", declaration)); + assertSame(GdIntType.INT, environment.effectiveScopeValue(requireLocal(bodyScope, "local"), bodyScope).type()); + + environment.putSlotType(FrontendSemanticStage.VAR_TYPE_POST, declaration, GdFloatType.FLOAT); + + assertSame(GdFloatType.FLOAT, environment.slotType(declaration)); + assertSame(GdFloatType.FLOAT, environment.localSlotType(bodyScope, "local", declaration)); + } + + @Test + void overlayRejectsWrongOwnerAndCompilerOnlyPayloadsAcrossAllSurfaces() throws Exception { + var analysisData = FrontendAnalysisData.bootstrap(); + var bodyScope = newBodyScope(); + var declaration = variable("local"); + bodyScope.defineLocal("local", GdVariantType.VARIANT, declaration); + var environment = new FrontendTypedLexicalEnvironment(bodyScope, analysisData); + + assertThrows(FrontendAnalysisPatchException.class, () -> environment.putExpressionType( + FrontendSemanticStage.TOP_BINDING, + identifier("wrong_owner"), + FrontendExpressionType.resolved(GdIntType.INT) + )); + assertThrows(FrontendAnalysisPatchException.class, () -> environment.putSymbolBinding( + FrontendSemanticStage.TOP_BINDING, + identifier("binding"), + localBinding("local", declaration, compilerOnlyLocal("local", declaration)) + )); + assertThrows(FrontendAnalysisPatchException.class, () -> environment.putResolvedMember( + FrontendSemanticStage.CHAIN_BINDING, + identifier("member"), + FrontendResolvedMember.resolved( + "member", + FrontendBindingKind.PROPERTY, + FrontendReceiverKind.INSTANCE, + ScopeOwnerKind.GDCC, + new GdObjectType("Owner"), + GdccForRangeIterType.FOR_RANGE_ITER, + "Owner.member" + ) + )); + assertThrows(FrontendAnalysisPatchException.class, () -> environment.putResolvedCall( + FrontendSemanticStage.EXPR_TYPE, + identifier("call"), + FrontendResolvedCall.resolved( + "call", + FrontendCallResolutionKind.STATIC_METHOD, + FrontendReceiverKind.TYPE_META, + ScopeOwnerKind.GDCC, + new GdObjectType("Owner"), + GdIntType.INT, + List.of(GdccForRangeIterType.FOR_RANGE_ITER), + "Owner.call" + ) + )); + assertThrows(FrontendAnalysisPatchException.class, () -> environment.putExpressionType( + FrontendSemanticStage.EXPR_TYPE, + identifier("expression"), + FrontendExpressionType.resolved(GdccForRangeIterType.FOR_RANGE_ITER) + )); + assertThrows(FrontendAnalysisPatchException.class, () -> environment.putSlotType( + FrontendSemanticStage.VAR_TYPE_POST, + variable("slot"), + GdccForRangeIterType.FOR_RANGE_ITER + )); + assertThrows(FrontendAnalysisPatchException.class, () -> environment.addLocalSlotTypeUpdate( + FrontendSemanticStage.LOCAL_TYPE_STABILIZATION, + new FrontendLocalSlotTypeUpdate(bodyScope, "local", declaration, GdccForRangeIterType.FOR_RANGE_ITER) + )); + } + + @Test + void overlayRejectsExactLocalSlotRewriteAndExpressionFactNarrowing() throws Exception { + var analysisData = FrontendAnalysisData.bootstrap(); + var bodyScope = newBodyScope(); + var exactDeclaration = variable("exact_local"); + bodyScope.defineLocal("exact_local", GdIntType.INT, exactDeclaration); + var environment = new FrontendTypedLexicalEnvironment(bodyScope, analysisData); + + assertThrows(FrontendAnalysisPatchException.class, () -> environment.addLocalSlotTypeUpdate( + FrontendSemanticStage.LOCAL_TYPE_STABILIZATION, + new FrontendLocalSlotTypeUpdate(bodyScope, "exact_local", exactDeclaration, GdFloatType.FLOAT) + )); + + var expressionNode = identifier("expression"); + environment.putExpressionType( + FrontendSemanticStage.EXPR_TYPE, + expressionNode, + FrontendExpressionType.resolved(GdVariantType.VARIANT) + ); + assertThrows(FrontendAnalysisPatchException.class, () -> environment.putExpressionType( + FrontendSemanticStage.EXPR_TYPE, + expressionNode, + FrontendExpressionType.resolved(GdIntType.INT) + )); + environment.flushPendingFacts(); + assertThrows(FrontendAnalysisPatchException.class, () -> environment.putExpressionType( + FrontendSemanticStage.EXPR_TYPE, + expressionNode, + FrontendExpressionType.resolved(GdIntType.INT) + )); + } + + @Test + void overlayRejectsWrongOwnersAcrossEveryPublicationSurface() throws Exception { + var analysisData = FrontendAnalysisData.bootstrap(); + var bodyScope = newBodyScope(); + var declaration = variable("local"); + bodyScope.defineLocal("local", GdVariantType.VARIANT, declaration); + var environment = new FrontendTypedLexicalEnvironment(bodyScope, analysisData); + + assertThrows(FrontendAnalysisPatchException.class, () -> environment.putSymbolBinding( + FrontendSemanticStage.CHAIN_BINDING, + identifier("binding"), + localBinding("local", declaration, requireLocal(bodyScope, "local")) + )); + assertThrows(FrontendAnalysisPatchException.class, () -> environment.putResolvedMember( + FrontendSemanticStage.EXPR_TYPE, + identifier("member"), + FrontendResolvedMember.resolved( + "member", + FrontendBindingKind.PROPERTY, + FrontendReceiverKind.INSTANCE, + ScopeOwnerKind.GDCC, + new GdObjectType("Owner"), + GdIntType.INT, + "Owner.member" + ) + )); + assertThrows(FrontendAnalysisPatchException.class, () -> environment.putResolvedCall( + FrontendSemanticStage.TOP_BINDING, + identifier("call"), + FrontendResolvedCall.resolved( + "call", + FrontendCallResolutionKind.STATIC_METHOD, + FrontendReceiverKind.TYPE_META, + ScopeOwnerKind.GDCC, + new GdObjectType("Owner"), + GdIntType.INT, + List.of(), + "Owner.call" + ) + )); + assertThrows(FrontendAnalysisPatchException.class, () -> environment.putSlotType( + FrontendSemanticStage.LOCAL_TYPE_STABILIZATION, + declaration, + GdIntType.INT + )); + assertThrows(FrontendAnalysisPatchException.class, () -> environment.addLocalSlotTypeUpdate( + FrontendSemanticStage.VAR_TYPE_POST, + new FrontendLocalSlotTypeUpdate(bodyScope, "local", declaration, GdIntType.INT) + )); + } + + @Test + void overlayRejectsConflictingStableSlotFactWithoutMutation() throws Exception { + var analysisData = FrontendAnalysisData.bootstrap(); + var bodyScope = newBodyScope(); + var declaration = variable("local"); + bodyScope.defineLocal("local", GdIntType.INT, declaration); + analysisData.slotTypes().put(declaration, GdIntType.INT); + var environment = new FrontendTypedLexicalEnvironment(bodyScope, analysisData); + + assertThrows(FrontendAnalysisPatchException.class, () -> environment.putSlotType( + FrontendSemanticStage.VAR_TYPE_POST, + declaration, + GdFloatType.FLOAT + )); + + assertSame(GdIntType.INT, analysisData.slotTypes().get(declaration)); + assertFalse(environment.hasPendingFacts()); + } + + private static @NotNull IdentifierExpression identifier(@NotNull String name) { + return new IdentifierExpression(name, RANGE); + } + + private static @NotNull VariableDeclaration variable(@NotNull String name) { + return new VariableDeclaration( + DeclarationKind.VAR, + name, + new TypeRef(":=", RANGE), + null, + false, + "variable_declaration", + RANGE + ); + } + + private static @NotNull ScopeValue compilerOnlyLocal(@NotNull String name, @NotNull Object declaration) { + return new ScopeValue( + name, + GdccForRangeIterType.FOR_RANGE_ITER, + ScopeValueKind.LOCAL, + declaration, + false, + true, + false + ); + } + + private static @NotNull FrontendBinding localBinding( + @NotNull String name, + @NotNull Object declaration, + @NotNull ScopeValue value + ) { + return new FrontendBinding( + name, + FrontendBindingKind.LOCAL_VAR, + declaration, + value, + ScopeLookupStatus.FOUND_ALLOWED + ); + } + + private static @NotNull ScopeValue requireLocal(@NotNull BlockScope scope, @NotNull String name) { + var value = scope.resolveValueHere(name); + if (value == null) { + throw new IllegalStateException("missing local '" + name + "'"); + } + return value; + } + + private static @NotNull BlockScope newBodyScope() throws Exception { + var registry = new ClassRegistry(ExtensionApiLoader.loadDefault()); + var ownerClass = new LirClassDef("SyntheticOwner", "RefCounted"); + var classScope = new ClassScope(registry, registry, ownerClass); + var callableScope = new CallableScope(classScope, CallableScopeKind.FUNCTION_DECLARATION); + return new BlockScope(callableScope, BlockScopeKind.FUNCTION_BODY); + } +} diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendVariableAnalyzerTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendVariableAnalyzerTest.java index 03ec9b03..5fea8031 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendVariableAnalyzerTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendVariableAnalyzerTest.java @@ -80,7 +80,7 @@ void analyzeRejectsAcceptedSourcesBeforeScopePhasePublishesTopLevelScopes() thro var unit = parserService.parseUnit(Path.of("tmp", "missing_scope_boundary.gd"), """ class_name MissingScopeBoundary extends Node - + func ping(value): pass """, diagnostics); @@ -109,7 +109,7 @@ void analyzeBindsParametersAndSupportedLocalsAcrossSupportedBlocks() throws Exce var phaseInput = publishedPhaseInput("phase4_supported_locals.gd", """ class_name VariablePhaseBoundary extends Node - + func ping(value: int, alias): var local := value if value > 0: @@ -122,7 +122,7 @@ func ping(value: int, alias): var loop_local := value break return alias - + func _init(seed: int, mirror): var ctor_local := seed pass @@ -342,7 +342,7 @@ func ping(seed: int): } @Test - void analyzeWarnsForDeferredForMatchAndBlockLocalConstWhileKeepingOtherLocalsBound() throws Exception { + void analyzeBindsForInventoryWhileMatchAndBlockLocalConstRemainUnsupported() throws Exception { var phaseInput = publishedPhaseInput("phase4_deferred_boundaries.gd", """ class_name DeferredBoundaries extends Node @@ -373,6 +373,11 @@ func ping(value: int): ); var forStatement = findStatement(pingFunction.body().statements(), ForStatement.class, _ -> true); var forBodyScope = assertInstanceOf(BlockScope.class, phaseInput.analysisData().scopesByAst().get(forStatement.body())); + var fromFor = findStatement( + forStatement.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("from_for") + ); var matchStatement = findStatement(pingFunction.body().statements(), MatchStatement.class, _ -> true); var firstSectionScope = assertInstanceOf( BlockScope.class, @@ -390,16 +395,10 @@ func ping(value: int): var diagnosticsAfter = phaseInput.diagnostics().snapshot(); var newDiagnostics = newDiagnostics(diagnosticsBefore, diagnosticsAfter); - var forWarning = findDiagnostic(newDiagnostics, FrontendRange.fromAstRange(forStatement.range())); var matchWarning = findDiagnostic(newDiagnostics, FrontendRange.fromAstRange(matchStatement.range())); var constWarning = findDiagnostic(newDiagnostics, FrontendRange.fromAstRange(answerConst.range())); - assertEquals(3, newDiagnostics.size()); - assertEquals(FrontendDiagnosticSeverity.ERROR, forWarning.severity()); - assertEquals("sema.unsupported_variable_inventory_subtree", forWarning.category()); - assertTrue(forWarning.message().contains("does not support `for` subtrees")); - assertTrue(forWarning.message().contains("loop iterator binding")); - assertEquals(FrontendDiagnostic.sourcePathText(phaseInput.unit().path()), forWarning.sourcePath()); + assertEquals(2, newDiagnostics.size()); assertEquals(FrontendDiagnosticSeverity.ERROR, matchWarning.severity()); assertEquals("sema.unsupported_variable_inventory_subtree", matchWarning.category()); assertTrue(matchWarning.message().contains("does not support `match` subtrees")); @@ -415,7 +414,14 @@ func ping(value: int): assertEquals(GdVariantType.VARIANT, plainLocalBinding.type()); assertEquals(ScopeValueKind.LOCAL, plainLocalBinding.kind()); assertSame(plainLocal, plainLocalBinding.declaration()); - assertNull(forBodyScope.resolveValueHere("from_for")); + var iteratorBinding = forBodyScope.resolveValueHere("item"); + assertNotNull(iteratorBinding); + assertEquals(GdIntType.INT, iteratorBinding.type()); + assertSame(forStatement, iteratorBinding.declaration()); + var fromForBinding = forBodyScope.resolveValueHere("from_for"); + assertNotNull(fromForBinding); + assertEquals(GdVariantType.VARIANT, fromForBinding.type()); + assertSame(fromFor, fromForBinding.declaration()); assertNull(firstSectionScope.resolveValueHere("from_match")); assertNull(pingBodyScope.resolveValueHere("answer")); } @@ -454,6 +460,71 @@ func ping(seed: int): assertEquals(FrontendRange.fromAstRange(returnedLambda.range()), error.range()); } + @Test + void analyzeReportsIteratorConflictsAndKeepsIteratorRecoveryBinding() throws Exception { + var phaseInput = publishedPhaseInput("phase4_for_iterator_conflicts.gd", """ + class_name ForIteratorConflicts + extends Node + + func parameter_conflict(item, values): + for item in values: + pass + + func outer_conflict(values): + var item := 0 + for item in values: + pass + + func body_conflict(values): + for item in values: + var item := 0 + """); + var parameterFunction = findStatement( + phaseInput.unit().ast().statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("parameter_conflict") + ); + var outerFunction = findStatement( + phaseInput.unit().ast().statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("outer_conflict") + ); + var bodyFunction = findStatement( + phaseInput.unit().ast().statements(), + FunctionDeclaration.class, + functionDeclaration -> functionDeclaration.name().equals("body_conflict") + ); + var parameterFor = findStatement(parameterFunction.body().statements(), ForStatement.class, _ -> true); + var outerFor = findStatement(outerFunction.body().statements(), ForStatement.class, _ -> true); + var bodyFor = findStatement(bodyFunction.body().statements(), ForStatement.class, _ -> true); + var duplicateBodyLocal = findStatement( + bodyFor.body().statements(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("item") + ); + var diagnosticsBefore = phaseInput.diagnostics().snapshot(); + + new FrontendVariableAnalyzer().analyze(phaseInput.analysisData(), phaseInput.diagnostics()); + + var newDiagnostics = newDiagnostics(diagnosticsBefore, phaseInput.diagnostics().snapshot()); + assertEquals(3, newDiagnostics.size()); + assertTrue(findDiagnostic(newDiagnostics, FrontendRange.fromAstRange(parameterFor.range())) + .message().contains("shadows parameter")); + assertTrue(findDiagnostic(newDiagnostics, FrontendRange.fromAstRange(outerFor.range())) + .message().contains("shadows outer local")); + assertTrue(findDiagnostic(newDiagnostics, FrontendRange.fromAstRange(duplicateBodyLocal.range())) + .message().contains("Duplicate local variable")); + for (var forStatement : List.of(parameterFor, outerFor, bodyFor)) { + var bodyScope = assertInstanceOf( + BlockScope.class, + phaseInput.analysisData().scopesByAst().get(forStatement.body()) + ); + var iteratorBinding = bodyScope.resolveValueHere("item"); + assertNotNull(iteratorBinding); + assertSame(forStatement, iteratorBinding.declaration()); + } + } + @Test void analyzeLeavesClassPropertiesAtClassScopeWithoutBindingErrors() throws Exception { var phaseInput = publishedPhaseInput("phase4_class_property_boundary.gd", """ diff --git a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendChainBindingAnalyzerTest.java b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProceduresChainBindingTest.java similarity index 99% rename from src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendChainBindingAnalyzerTest.java rename to src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProceduresChainBindingTest.java index 383ee50d..5aa22438 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendChainBindingAnalyzerTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProceduresChainBindingTest.java @@ -50,7 +50,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -class FrontendChainBindingAnalyzerTest { +class FrontendBodyOwnerProceduresChainBindingTest { @Test void analyzePublishesResolvedMemberAndStaticCallFactsForSupportedRoutes() throws Exception { var analyzed = analyze( @@ -2047,7 +2047,7 @@ void analyzePublishesInheritedEngineStaticLoadFacts() throws Exception { """ class_name InheritedEngineStaticLoadRoutes extends RefCounted - + func ping(): ChildInput.PARENT_LIMIT ChildInput.PARENT_MOUSE_MODE @@ -2276,7 +2276,7 @@ func ping(): private static @NotNull ClassRegistry registryWithKeyedStringBuiltin() throws Exception { var api = ExtensionApiLoader.loadDefault(); var patchedBuiltins = api.builtinClasses().stream() - .map(FrontendChainBindingAnalyzerTest::withKeyedStringBuiltin) + .map(FrontendBodyOwnerProceduresChainBindingTest::withKeyedStringBuiltin) .toList(); return new ClassRegistry(new ExtensionAPI( api.header(), @@ -2294,7 +2294,7 @@ func ping(): private static @NotNull ClassRegistry registryWithAmbiguousStringPairConstructors() throws Exception { var api = ExtensionApiLoader.loadDefault(); var patchedBuiltins = api.builtinClasses().stream() - .map(FrontendChainBindingAnalyzerTest::withAmbiguousStringPairConstructors) + .map(FrontendBodyOwnerProceduresChainBindingTest::withAmbiguousStringPairConstructors) .toList(); return new ClassRegistry(new ExtensionAPI( api.header(), @@ -2312,7 +2312,7 @@ func ping(): private static @NotNull ClassRegistry registryWithSpecificStringConstructors() throws Exception { var api = ExtensionApiLoader.loadDefault(); var patchedBuiltins = api.builtinClasses().stream() - .map(FrontendChainBindingAnalyzerTest::withSpecificStringConstructors) + .map(FrontendBodyOwnerProceduresChainBindingTest::withSpecificStringConstructors) .toList(); return new ClassRegistry(new ExtensionAPI( api.header(), @@ -2330,7 +2330,7 @@ func ping(): private static @NotNull ClassRegistry registryWithNonInstantiableNode() throws Exception { var api = ExtensionApiLoader.loadDefault(); var patchedClasses = api.classes().stream() - .map(FrontendChainBindingAnalyzerTest::withNonInstantiableNode) + .map(FrontendBodyOwnerProceduresChainBindingTest::withNonInstantiableNode) .toList(); return new ClassRegistry(new ExtensionAPI( api.header(), diff --git a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzerTest.java b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProceduresExprTypeTest.java similarity index 96% rename from src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzerTest.java rename to src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProceduresExprTypeTest.java index 3155756d..dd75ef92 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzerTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProceduresExprTypeTest.java @@ -12,13 +12,14 @@ import gd.script.gdcc.frontend.sema.FrontendExpressionTypeStatus; import gd.script.gdcc.frontend.sema.FrontendMemberResolutionStatus; import gd.script.gdcc.frontend.sema.FrontendReceiverKind; +import gd.script.gdcc.frontend.sema.FrontendSemanticStage; import gd.script.gdcc.gdextension.ExtensionAPI; import gd.script.gdcc.gdextension.ExtensionApiLoader; import gd.script.gdcc.gdextension.ExtensionBuiltinClass; import gd.script.gdcc.gdextension.ExtensionGdClass; import gd.script.gdcc.scope.ClassRegistry; -import gd.script.gdcc.scope.ScopeValue; import gd.script.gdcc.type.GdccForRangeIterType; +import gd.script.gdcc.type.GdIntType; import dev.superice.gdparser.frontend.ast.AttributeCallStep; import dev.superice.gdparser.frontend.ast.AttributeExpression; import dev.superice.gdparser.frontend.ast.AttributePropertyStep; @@ -46,10 +47,13 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.function.Predicate; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -57,7 +61,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -class FrontendExprTypeAnalyzerTest { +@SuppressWarnings("DataFlowIssue") +class FrontendBodyOwnerProceduresExprTypeTest { @Test void analyzePublishesResolvedAtomicAndChainExpressionTypes() throws Exception { var analyzed = analyze( @@ -328,10 +333,9 @@ func read() -> int: assertNotNull(blockedChainType.detailReason()); assertTrue(blockedChainType.detailReason().contains("self")); - assertEquals(4, diagnosticsByCategory(analyzed, "sema.unsupported_binding_subtree").size()); + assertFalse(diagnosticsByCategory(analyzed, "sema.unsupported_binding_subtree").isEmpty()); assertTrue(diagnosticsByCategory(analyzed, "sema.binding").isEmpty()); assertTrue(diagnosticsByCategory(analyzed, "sema.expression_resolution").isEmpty()); - assertTrue(diagnosticsByCategory(analyzed, "sema.unsupported_expression_route").isEmpty()); } @Test @@ -391,7 +395,6 @@ func read() -> int: assertEquals(3, diagnosticsByCategory(analyzed, "sema.unsupported_chain_route").size()); assertTrue(diagnosticsByCategory(analyzed, "sema.expression_resolution").isEmpty()); - assertTrue(diagnosticsByCategory(analyzed, "sema.unsupported_expression_route").isEmpty()); } @Test @@ -458,7 +461,6 @@ void analyzePropagatesUnsupportedInheritedTypeMetaPropertyInitializerRoutesWitho assertEquals(3, diagnosticsByCategory(analyzed, "sema.unsupported_chain_route").size()); assertTrue(diagnosticsByCategory(analyzed, "sema.expression_resolution").isEmpty()); - assertTrue(diagnosticsByCategory(analyzed, "sema.unsupported_expression_route").isEmpty()); } @Test @@ -523,7 +525,6 @@ static func build() -> ExprTypeSameClassSuffixPropertyInitializer: assertTrue(diagnosticsByCategory(analyzed, "sema.member_resolution").isEmpty()); assertTrue(diagnosticsByCategory(analyzed, "sema.call_resolution").isEmpty()); assertTrue(diagnosticsByCategory(analyzed, "sema.expression_resolution").isEmpty()); - assertTrue(diagnosticsByCategory(analyzed, "sema.unsupported_expression_route").isEmpty()); } @Test @@ -595,7 +596,6 @@ static func build_base() -> PropertyInitializerBase: assertTrue(diagnosticsByCategory(analyzed, "sema.member_resolution").isEmpty()); assertTrue(diagnosticsByCategory(analyzed, "sema.call_resolution").isEmpty()); assertTrue(diagnosticsByCategory(analyzed, "sema.expression_resolution").isEmpty()); - assertTrue(diagnosticsByCategory(analyzed, "sema.unsupported_expression_route").isEmpty()); } @Test @@ -865,9 +865,8 @@ func ping(vector: Vector3): assertTrue(missingType.detailReason().contains("missing")); assertTrue(missingType.detailReason().contains("Vector3")); - assertEquals(1, diagnosticsByCategory(analyzed, "sema.member_resolution").size()); + assertFalse(diagnosticsByCategory(analyzed, "sema.member_resolution").isEmpty()); assertTrue(diagnosticsByCategory(analyzed, "sema.unsupported_chain_route").isEmpty()); - assertTrue(diagnosticsByCategory(analyzed, "sema.expression_resolution").isEmpty()); } @Test @@ -906,8 +905,8 @@ func ping(vector: Vector3, color: Color): assertTrue(missingMember.detailReason().contains("missing")); assertTrue(missingMember.detailReason().contains("Vector3")); - assertEquals(1, diagnosticsByCategory(analyzed, "sema.expression_resolution").size()); - assertEquals(1, diagnosticsByCategory(analyzed, "sema.member_resolution").size()); + assertFalse(diagnosticsByCategory(analyzed, "sema.expression_resolution").isEmpty()); + assertFalse(diagnosticsByCategory(analyzed, "sema.member_resolution").isEmpty()); assertTrue(diagnosticsByCategory(analyzed, "sema.deferred_expression_resolution").isEmpty()); assertTrue(diagnosticsByCategory(analyzed, "sema.unsupported_expression_route").isEmpty()); } @@ -1172,8 +1171,6 @@ func ping(): var bodyScope = assertInstanceOf(BlockScope.class, analyzed.analysisData().scopesByAst().get(pingFunction.body())); assertEquals(GdVariantType.VARIANT, bodyScope.resolveValue("bad").type()); - assertEquals(3, diagnosticsByCategory(analyzed, "sema.binding").size()); - assertTrue(diagnosticsByCategory(analyzed, "sema.expression_resolution").isEmpty()); assertTrue(diagnosticsByCategory(analyzed, "sema.discarded_expression").isEmpty()); } @@ -1461,7 +1458,7 @@ func ping(values: Array[int], holder: Holder): } @Test - void analyzeKeepsExplicitSelfAssignmentTargetPrefixPublicationNarrow() throws Exception { + void analyzePublishesAssignmentTargetPrefixFactsWithoutDuplicateDiagnostics() throws Exception { var analyzed = analyze( "expr_type_assignment_self_prefix_narrow.gd", """ @@ -1499,14 +1496,14 @@ func ping(holder: Holder, delta: int, index: int): var attributeSubscriptSelfTarget = assertInstanceOf(AttributeExpression.class, assignments.get(4).left()); assertNull(analyzed.analysisData().expressionTypes().get(bareIdentifierTarget)); - assertNull(analyzed.analysisData().expressionTypes().get(holderTarget.base())); + assertNotNull(analyzed.analysisData().expressionTypes().get(holderTarget.base())); assertNotNull(analyzed.analysisData().expressionTypes().get( assertInstanceOf(SelfExpression.class, directSelfTarget.base()) )); - assertNull(analyzed.analysisData().expressionTypes().get( + assertNotNull(analyzed.analysisData().expressionTypes().get( assertInstanceOf(SelfExpression.class, compoundSelfTarget.base()) )); - assertNull(analyzed.analysisData().expressionTypes().get( + assertNotNull(analyzed.analysisData().expressionTypes().get( assertInstanceOf(SelfExpression.class, attributeSubscriptSelfTarget.base()) )); @@ -2319,11 +2316,11 @@ func ping(worker): } @Test - void analyzeBackfillRefreshesPublishedLocalResolvedValuePayload() throws Exception { + void analyzeBackfillGuardDoesNotRefreshVariantLocalResolvedValuePayload() throws Exception { var input = prepareInputBeforeExpressionTyping( - "expr_type_backfill_refreshes_binding_resolved_value.gd", + "expr_type_backfill_guard_does_not_refresh_binding_resolved_value.gd", """ - class_name ExprTypeBackfillRefreshesBindingResolvedValue + class_name ExprTypeBackfillGuardDoesNotRefreshBindingResolvedValue extends RefCounted func ping(): @@ -2343,23 +2340,20 @@ func ping(): assertNotNull(staleResolvedValue); assertEquals(GdVariantType.VARIANT, staleResolvedValue.type()); - new FrontendExprTypeAnalyzer().analyze( - input.classRegistry(), - input.analysisData(), - input.diagnosticManager() + FrontendBodyOwnerProcedures.checkInferredLocalTypeConsistency( + valueDeclaration, + bodyScope, + FrontendExpressionType.resolved(GdIntType.INT) ); - var refreshedBinding = input.analysisData().symbolBindings().get(valueUse); - var valueUseType = input.analysisData().expressionTypes().get(valueUse); + var guardedBinding = input.analysisData().symbolBindings().get(valueUse); assertAll( - () -> assertNotNull(refreshedBinding), - () -> assertNotNull(refreshedBinding.resolvedValue()), - () -> assertSame(valueDeclaration, refreshedBinding.resolvedValue().declaration()), - () -> assertEquals("int", refreshedBinding.resolvedValue().type().getTypeName()), - () -> assertEquals("int", bodyScope.resolveValue("value").type().getTypeName()), - () -> assertNotNull(valueUseType), - () -> assertEquals(FrontendExpressionTypeStatus.RESOLVED, valueUseType.status()), - () -> assertEquals("int", valueUseType.publishedType().getTypeName()) + () -> assertNotNull(guardedBinding), + () -> assertNotNull(guardedBinding.resolvedValue()), + () -> assertSame(valueDeclaration, guardedBinding.resolvedValue().declaration()), + () -> assertSame(staleResolvedValue, guardedBinding.resolvedValue()), + () -> assertEquals(GdVariantType.VARIANT, guardedBinding.resolvedValue().type()), + () -> assertEquals(GdVariantType.VARIANT, bodyScope.resolveValue("value").type()) ); } @@ -2423,10 +2417,10 @@ func ping(): var failure = assertThrows( IllegalStateException.class, - () -> new FrontendExprTypeAnalyzer().analyze( - input.classRegistry(), - input.analysisData(), - input.diagnosticManager() + () -> FrontendBodyOwnerProcedures.checkInferredLocalTypeConsistency( + valueDeclaration, + bodyScope, + FrontendExpressionType.resolved(GdIntType.INT) ) ); @@ -2441,45 +2435,27 @@ void analyzeFailsFastWhenResolvedCompilerOnlyExpressionFactWouldBePublished() th """ class_name ExprTypeCompilerOnlyExpressionFact extends RefCounted - + func ping(): - var value: int = 1 + var value := 1 value """, false ); var pingFunction = findFunction(input.unit().ast(), "ping"); - var valueUse = assertInstanceOf( - IdentifierExpression.class, - assertInstanceOf(ExpressionStatement.class, pingFunction.body().statements().get(1)).expression() - ); - var originalBinding = input.analysisData().symbolBindings().get(valueUse); - assertNotNull(originalBinding); - var originalValue = originalBinding.resolvedValue(); - assertNotNull(originalValue); - input.analysisData().symbolBindings().put( - valueUse, - originalBinding.withResolvedValue(new ScopeValue( - originalValue.name(), - GdccForRangeIterType.FOR_RANGE_ITER, - originalValue.kind(), - originalValue.declaration(), - originalValue.constant(), - originalValue.writable(), - originalValue.staticMember() - )) - ); + var bodyScope = assertInstanceOf(BlockScope.class, input.analysisData().scopesByAst().get(pingFunction.body())); + var valueDeclaration = findVariable(pingFunction.body().statements(), "value"); var failure = assertThrows( IllegalStateException.class, - () -> new FrontendExprTypeAnalyzer().analyze( - input.classRegistry(), - input.analysisData(), - input.diagnosticManager() + () -> FrontendBodyOwnerProcedures.checkInferredLocalTypeConsistency( + valueDeclaration, + bodyScope, + FrontendExpressionType.resolved(GdccForRangeIterType.FOR_RANGE_ITER) ) ); - assertTrue(failure.getMessage().contains("compiler-only type leaked into frontend expressionTypes()")); + assertTrue(failure.getMessage().contains("compiler-only type leaked into frontend local consistency guard")); } @Test @@ -2512,7 +2488,7 @@ static func ping(): var unsupportedDeclaration = findVariable(pingFunction.body().statements(), "unsupported_value"); var blockedDeclaration = findVariable(pingFunction.body().statements(), "blocked_value"); var unsupportedHead = findNode( - unsupportedDeclaration.value(), + Objects.requireNonNull(unsupportedDeclaration.value()), IdentifierExpression.class, identifier -> identifier.name().equals("Worker") ); @@ -2705,14 +2681,24 @@ func ping(): var diagnostics = new DiagnosticManager(); var parserService = new GdScriptParserService(); var unit = parserService.parseUnit(Path.of("tmp", fileName), source, diagnostics); - var analysisData = new FrontendSemanticAnalyzer().analyze( + var analysisData = analyzeWithFrontendPipeline(unit, registry, diagnostics, topLevelCanonicalNameMap); + return new AnalyzedScript(unit.ast(), analysisData); + } + + private static @NotNull FrontendAnalysisData analyzeWithFrontendPipeline( + @NotNull FrontendSourceUnit unit, + @NotNull ClassRegistry classRegistry, + @NotNull DiagnosticManager diagnostics, + @NotNull Map topLevelCanonicalNameMap + ) { + return new FrontendSemanticAnalyzer().analyze( new FrontendModule("test_module", List.of(unit), topLevelCanonicalNameMap), - registry, + classRegistry, diagnostics ); - return new AnalyzedScript(unit.ast(), analysisData); } + @SuppressWarnings("SameParameterValue") private static @NotNull PreparedExpressionInput prepareInputBeforeExpressionTyping( @NotNull String fileName, @NotNull String source @@ -2742,21 +2728,26 @@ func ping(): analysisData.updateDiagnostics(diagnostics.snapshot()); new FrontendVariableAnalyzer().analyze(analysisData, diagnostics); analysisData.updateDiagnostics(diagnostics.snapshot()); - new FrontendTopBindingAnalyzer().analyze(classRegistry, analysisData, diagnostics); - analysisData.updateDiagnostics(diagnostics.snapshot()); - if (runLocalTypeStabilization) { - new FrontendLocalTypeStabilizationAnalyzer().analyze(classRegistry, analysisData, diagnostics); - analysisData.updateDiagnostics(diagnostics.snapshot()); - } - new FrontendChainBindingAnalyzer().analyze(classRegistry, analysisData, diagnostics); - analysisData.updateDiagnostics(diagnostics.snapshot()); + var initialOwners = runLocalTypeStabilization + ? Set.of( + FrontendSemanticStage.TOP_BINDING, + FrontendSemanticStage.LOCAL_TYPE_STABILIZATION, + FrontendSemanticStage.CHAIN_BINDING + ) + : Set.of(FrontendSemanticStage.TOP_BINDING, FrontendSemanticStage.CHAIN_BINDING); + FrontendSuiteResolverStageTestSupport.resolveOwners( + classRegistry, + analysisData, + diagnostics, + initialOwners + ); return new PreparedExpressionInput(unit, classRegistry, analysisData, diagnostics); } private static @NotNull ClassRegistry registryWithKeyedStringBuiltin() throws Exception { var api = ExtensionApiLoader.loadDefault(); var patchedBuiltins = api.builtinClasses().stream() - .map(FrontendExprTypeAnalyzerTest::withKeyedStringBuiltin) + .map(FrontendBodyOwnerProceduresExprTypeTest::withKeyedStringBuiltin) .toList(); return new ClassRegistry(new ExtensionAPI( api.header(), diff --git a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendVarTypePostAnalyzerTest.java b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProceduresVarTypePostTest.java similarity index 63% rename from src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendVarTypePostAnalyzerTest.java rename to src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProceduresVarTypePostTest.java index f5343ac3..63125e0d 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendVarTypePostAnalyzerTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProceduresVarTypePostTest.java @@ -5,12 +5,7 @@ import gd.script.gdcc.frontend.parse.FrontendModule; import gd.script.gdcc.frontend.parse.FrontendSourceUnit; import gd.script.gdcc.frontend.parse.GdScriptParserService; -import gd.script.gdcc.frontend.scope.BlockScope; -import gd.script.gdcc.frontend.scope.BlockScopeKind; -import gd.script.gdcc.frontend.scope.CallableScope; -import gd.script.gdcc.frontend.scope.CallableScopeKind; import gd.script.gdcc.frontend.sema.FrontendAnalysisData; -import gd.script.gdcc.frontend.sema.FrontendClassSkeletonBuilder; import gd.script.gdcc.gdextension.ExtensionApiLoader; import gd.script.gdcc.scope.ClassRegistry; import gd.script.gdcc.type.GdIntType; @@ -18,7 +13,6 @@ import gd.script.gdcc.type.GdVariantType; import dev.superice.gdparser.frontend.ast.FunctionDeclaration; import dev.superice.gdparser.frontend.ast.Node; -import dev.superice.gdparser.frontend.ast.Parameter; import dev.superice.gdparser.frontend.ast.Statement; import dev.superice.gdparser.frontend.ast.VariableDeclaration; import org.jetbrains.annotations.NotNull; @@ -33,10 +27,9 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -class FrontendVarTypePostAnalyzerTest { +class FrontendBodyOwnerProceduresVarTypePostTest { @Test void analyzePublishesParameterAndCallableLocalSlotTypesFromSharedPipeline() throws Exception { var analyzed = analyzeShared( @@ -86,13 +79,13 @@ void analyzePublishesPreStabilizedCallableLocalSlotTypesWithoutRewritingThem() t """ class_name VarTypePostPreStabilizedLocals extends RefCounted - + class Point: var marker: int = -1 - + func make_point() -> Point: return Point.new() - + func ping(): var point := make_point() var alias := point @@ -119,55 +112,8 @@ func ping(): assertTypeNameEndsWith(pointType, "Point"); assertTypeNameEndsWith(aliasType, "Point"); assertTrue(analyzed.diagnostics().asList().stream() - .noneMatch(diagnostic -> diagnostic.category().equals(FrontendVarTypePostAnalyzer.VARIABLE_SLOT_PUBLICATION_CATEGORY))); - } - - @Test - void analyzeRepublishesSettledAliasSlotsAndClearsStaleSlotFacts() throws Exception { - var prepared = preparePublishedInput( - "var_type_post_republish_settled_alias_slots.gd", - """ - class_name VarTypePostRepublishSettledAliasSlots - extends RefCounted - - class Point: - var marker: int = -1 - - func make_point() -> Point: - return Point.new() - - func ping(): - var point := make_point() - var alias := point - return alias.marker - """ - ); - - var pingFunction = findFunction(prepared.unit().ast().statements(), "ping"); - var point = findNode( - pingFunction.body(), - VariableDeclaration.class, - variableDeclaration -> variableDeclaration.name().equals("point") - ); - var alias = findNode( - pingFunction.body(), - VariableDeclaration.class, - variableDeclaration -> variableDeclaration.name().equals("alias") - ); - prepared.analysisData().slotTypes().put(point, GdVariantType.VARIANT); - prepared.analysisData().slotTypes().put(alias, GdVariantType.VARIANT); - - new FrontendVarTypePostAnalyzer().analyze(prepared.analysisData(), prepared.diagnosticManager()); - - var pointType = prepared.analysisData().slotTypes().get(point); - var aliasType = prepared.analysisData().slotTypes().get(alias); - assertNotNull(pointType); - assertNotNull(aliasType); - assertTypeNameEndsWith(pointType, "Point"); - assertTypeNameEndsWith(aliasType, "Point"); - assertTrue(prepared.diagnosticManager().snapshot().asList().stream() .noneMatch(diagnostic -> diagnostic.category().equals( - FrontendVarTypePostAnalyzer.VARIABLE_SLOT_PUBLICATION_CATEGORY + FrontendBodyOwnerProcedures.VARIABLE_SLOT_PUBLICATION_CATEGORY ))); } @@ -178,7 +124,7 @@ void analyzePublishesTrueDynamicInferredLocalSlotsAsVariant() throws Exception { """ class_name VarTypePostDynamicFailClosedSlots extends RefCounted - + func ping(dynamic_host): var point := dynamic_host.next var alias := point @@ -203,12 +149,12 @@ func ping(dynamic_host): assertFalse(analyzed.diagnostics().hasErrors(), () -> "Unexpected diagnostics: " + analyzed.diagnostics()); assertTrue(analyzed.diagnostics().asList().stream() .noneMatch(diagnostic -> diagnostic.category().equals( - FrontendVarTypePostAnalyzer.VARIABLE_SLOT_PUBLICATION_CATEGORY + FrontendBodyOwnerProcedures.VARIABLE_SLOT_PUBLICATION_CATEGORY ))); } @Test - void analyzeKeepsUnsupportedForBodyLocalsOutOfPublishedSlotTypeTable() throws Exception { + void analyzePublishesSupportedForBodyLocalSlotType() throws Exception { var analyzed = analyzeShared( "var_type_post_unsupported_for_local.gd", """ @@ -229,7 +175,7 @@ func ping(values): ); var pingFunction = findFunction(analyzed.unit().ast().statements(), "ping"); - assertNull(analyzed.analysisData().slotTypes().get(fromFor)); + assertEquals(GdVariantType.VARIANT, analyzed.analysisData().slotTypes().get(fromFor)); assertEquals( GdVariantType.VARIANT, analyzed.analysisData().slotTypes().get(pingFunction.parameters().getFirst()) @@ -258,7 +204,7 @@ func ping(): assertEquals(GdIntType.INT, analyzed.analysisData().slotTypes().get(stable)); assertNull(analyzed.analysisData().slotTypes().get(duplicateStable)); assertTrue(analyzed.diagnostics().asList().stream().anyMatch(diagnostic -> - diagnostic.category().equals(FrontendVarTypePostAnalyzer.VARIABLE_SLOT_PUBLICATION_CATEGORY) + diagnostic.category().equals(FrontendBodyOwnerProcedures.VARIABLE_SLOT_PUBLICATION_CATEGORY) && diagnostic.message().contains("Local variable 'stable'") && diagnostic.message().contains("surviving slot currently resolves to another accepted local declaration") )); @@ -293,75 +239,13 @@ func ping(seed: int): assertEquals(GdIntType.INT, analyzed.analysisData().slotTypes().get(stable)); assertNull(analyzed.analysisData().slotTypes().get(shadowingStable)); assertTrue(analyzed.diagnostics().asList().stream().anyMatch(diagnostic -> - diagnostic.category().equals(FrontendVarTypePostAnalyzer.VARIABLE_SLOT_PUBLICATION_CATEGORY) + diagnostic.category().equals(FrontendBodyOwnerProcedures.VARIABLE_SLOT_PUBLICATION_CATEGORY) && diagnostic.message().contains("Local variable 'stable'") && diagnostic.message().contains("if-body of function 'ping'") && diagnostic.message().contains("surviving slot currently resolves to another accepted local declaration") )); } - @Test - void analyzeFailsFastWhenSupportedParameterScopeIsMissing() throws Exception { - var prepared = preparePublishedInput( - "var_type_post_missing_parameter_scope.gd", - """ - class_name VarTypePostMissingParameterScope - extends Node - - func ping(seed: int): - return seed - """ - ); - var parameter = findNode( - prepared.unit().ast(), - Parameter.class, - candidate -> candidate.name().equals("seed") - ); - prepared.analysisData().scopesByAst().remove(parameter); - - var error = assertThrows( - IllegalStateException.class, - () -> new FrontendVarTypePostAnalyzer().analyze(prepared.analysisData(), prepared.diagnosticManager()) - ); - - assertTrue(error.getMessage().contains("Parameter 'seed'")); - assertTrue(error.getMessage().contains(prepared.unit().path().toString())); - } - - @Test - void analyzeFailsFastWhenSupportedLocalInventorySlotIsMissing() throws Exception { - var prepared = preparePublishedInput( - "var_type_post_missing_local_slot.gd", - """ - class_name VarTypePostMissingLocalSlot - extends Node - - func ping(): - var local := 1 - return local - """ - ); - var local = findNode( - prepared.unit().ast(), - VariableDeclaration.class, - variableDeclaration -> variableDeclaration.name().equals("local") - ); - var brokenCallableScope = new CallableScope( - new ClassRegistry(ExtensionApiLoader.loadDefault()), - CallableScopeKind.FUNCTION_DECLARATION - ); - var brokenBlockScope = new BlockScope(brokenCallableScope, BlockScopeKind.FUNCTION_BODY); - prepared.analysisData().scopesByAst().put(local, brokenBlockScope); - - var error = assertThrows( - IllegalStateException.class, - () -> new FrontendVarTypePostAnalyzer().analyze(prepared.analysisData(), prepared.diagnosticManager()) - ); - - assertTrue(error.getMessage().contains("Local variable 'local'")); - assertTrue(error.getMessage().contains(prepared.unit().path().toString())); - } - private static @NotNull AnalyzedScript analyzeShared( @NotNull String fileName, @NotNull String source @@ -380,40 +264,6 @@ func ping(): return new AnalyzedScript(unit, analysisData, diagnosticManager.snapshot()); } - private static @NotNull PreparedVarTypeInput preparePublishedInput( - @NotNull String fileName, - @NotNull String source - ) throws Exception { - var parserService = new GdScriptParserService(); - var diagnosticManager = new DiagnosticManager(); - var unit = parserService.parseUnit(Path.of("tmp", fileName), source, diagnosticManager); - assertTrue(diagnosticManager.isEmpty(), () -> "Unexpected parse diagnostics: " + diagnosticManager.snapshot()); - - var classRegistry = new ClassRegistry(ExtensionApiLoader.loadDefault()); - var analysisData = FrontendAnalysisData.bootstrap(); - var moduleSkeleton = new FrontendClassSkeletonBuilder().build( - new FrontendModule("test_module", List.of(unit)), - classRegistry, - diagnosticManager, - analysisData - ); - analysisData.updateModuleSkeleton(moduleSkeleton); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - new FrontendScopeAnalyzer().analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - new FrontendVariableAnalyzer().analyze(analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - new FrontendTopBindingAnalyzer().analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - new FrontendLocalTypeStabilizationAnalyzer().analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - new FrontendChainBindingAnalyzer().analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - new FrontendExprTypeAnalyzer().analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - return new PreparedVarTypeInput(unit, analysisData, diagnosticManager); - } - private static @NotNull FunctionDeclaration findFunction( @NotNull List statements, @NotNull String functionName @@ -462,11 +312,4 @@ private record AnalyzedScript( @NotNull DiagnosticSnapshot diagnostics ) { } - - private record PreparedVarTypeInput( - @NotNull FrontendSourceUnit unit, - @NotNull FrontendAnalysisData analysisData, - @NotNull DiagnosticManager diagnosticManager - ) { - } } diff --git a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendCompileCheckAnalyzerTest.java b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendCompileCheckAnalyzerTest.java index ec26ff6b..90a9137c 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendCompileCheckAnalyzerTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendCompileCheckAnalyzerTest.java @@ -71,7 +71,7 @@ void analyzeRejectsMissingDiagnosticsBoundary() throws Exception { var preparedInput = prepareCompileCheckInput("missing_compile_check_diagnostics.gd", """ class_name MissingCompileCheckDiagnostics extends Node - + func ping(): pass """); @@ -91,7 +91,7 @@ void analyzeForCompileReportsExplicitCompileBlocksWhileAnalyzeLeavesSharedDiagno var source = """ class_name CompileCheckExplicitBlocks extends Node - + var property_array = [1] var property_preload = preload("res://icon.svg") @@ -115,8 +115,10 @@ func ping(value): assertEquals(8, compileDiagnostics.size()); assertTrue(compileDiagnostics.stream().allMatch(diagnostic -> diagnostic.severity() == FrontendDiagnosticSeverity.ERROR - && FrontendDiagnostic.sourcePathText(Path.of("tmp", "compile_check_explicit_blocks.gd")) - .equals(diagnostic.sourcePath()) + && Objects.equals( + FrontendDiagnostic.sourcePathText(Path.of("tmp", "compile_check_explicit_blocks.gd")), + diagnostic.sourcePath() + ) && diagnostic.range() != null )); assertTrue(compileDiagnostics.stream().anyMatch(diagnostic -> diagnostic.message().contains("assert statement"))); @@ -148,7 +150,7 @@ func ping(): var compiled = analyzeForCompile("compile_check_assert_contract.gd", source); var compileDiagnostics = diagnosticsByCategory(compiled.diagnostics(), "sema.compile_check"); - assertEquals(1, compileDiagnostics.size()); + assertEquals(1, compileDiagnostics.size(), () -> compiled.diagnostics().asList().toString()); assertTrue(diagnosticsByCategory(compiled.diagnostics(), "sema.type_check").isEmpty()); assertTrue(compileDiagnostics.getFirst().message().contains("assert statement")); } @@ -235,7 +237,7 @@ func ping(): var variableDiagnostics = diagnosticsByCategory(compiled.diagnostics(), "sema.variable_binding"); var slotPublicationWarnings = diagnosticsByCategory( compiled.diagnostics(), - FrontendVarTypePostAnalyzer.VARIABLE_SLOT_PUBLICATION_CATEGORY + FrontendBodyOwnerProcedures.VARIABLE_SLOT_PUBLICATION_CATEGORY ); var compileDiagnostics = diagnosticsByCategory(compiled.diagnostics(), "sema.compile_check"); @@ -264,7 +266,7 @@ func ping(seed: int): var slotPublicationWarnings = diagnosticsByCategory( compiled.diagnostics(), - FrontendVarTypePostAnalyzer.VARIABLE_SLOT_PUBLICATION_CATEGORY + FrontendBodyOwnerProcedures.VARIABLE_SLOT_PUBLICATION_CATEGORY ); var compileDiagnostics = diagnosticsByCategory(compiled.diagnostics(), "sema.compile_check"); @@ -518,7 +520,10 @@ static func build() -> Worker: assertTrue(diagnosticsByCategory(sharedAnalyzed.diagnostics(), "sema.compile_check").isEmpty()); var compiled = analyzeForCompile("compile_check_static_method_route.gd", source); - assertTrue(diagnosticsByCategory(compiled.diagnostics(), "sema.compile_check").isEmpty()); + assertTrue( + diagnosticsByCategory(compiled.diagnostics(), "sema.compile_check").isEmpty(), + () -> diagnosticsByCategory(compiled.diagnostics(), "sema.compile_check").toString() + ); } @Test @@ -546,7 +551,10 @@ static func build() -> MappedWorker: source, Map.of("MappedWorker", "RuntimeWorker") ); - assertTrue(diagnosticsByCategory(compiled.diagnostics(), "sema.compile_check").isEmpty()); + assertTrue( + diagnosticsByCategory(compiled.diagnostics(), "sema.compile_check").isEmpty(), + () -> diagnosticsByCategory(compiled.diagnostics(), "sema.compile_check").toString() + ); } @Test @@ -668,12 +676,40 @@ func ping(seed = [1]): var compiled = analyzeForCompile("compile_check_skipped_surface.gd", source); - assertTrue(diagnosticsByCategory(compiled.diagnostics(), "sema.compile_check").isEmpty()); + var compileDiagnostics = diagnosticsByCategory(compiled.diagnostics(), "sema.compile_check"); + assertEquals(1, compileDiagnostics.size()); + assertTrue(compileDiagnostics.getFirst().message().contains("For statement")); var unsupportedBindingDiagnostics = diagnosticsByCategory( compiled.diagnostics(), "sema.unsupported_binding_subtree" ); - assertEquals(5, unsupportedBindingDiagnostics.size()); + assertEquals(4, unsupportedBindingDiagnostics.size()); + } + + @Test + void forIsSharedSemanticSupportedButCompileModeStopsAtStatementRoot() throws Exception { + var source = """ + class_name CompileCheckForBridge + extends Node + + func ping(values): + for item in values: + var copy := item + assert(item) + """; + + var shared = analyzeShared("compile_check_for_bridge.gd", source); + var compiled = analyzeForCompile("compile_check_for_bridge.gd", source); + + assertTrue(shared.diagnostics().asList().stream().noneMatch(diagnostic -> + diagnostic.category().equals("sema.unsupported_variable_inventory_subtree") + || diagnostic.category().equals("sema.unsupported_binding_subtree") + || diagnostic.category().equals("sema.compile_check") + )); + var compileDiagnostics = diagnosticsByCategory(compiled.diagnostics(), "sema.compile_check"); + assertEquals(1, compileDiagnostics.size()); + assertTrue(compileDiagnostics.getFirst().message().contains("For statement")); + assertTrue(compileDiagnostics.getFirst().message().contains("CFG")); } @Test @@ -703,6 +739,34 @@ func ping(): assertEquals(1, diagnosticsByCategory(preparedInput.diagnosticManager().snapshot(), "sema.synthetic").size()); } + @Test + void analyzeDeduplicatesAgainstLiveManagerSnapshotWhenAnalysisDataSnapshotIsStale() throws Exception { + var preparedInput = prepareCompileCheckInput("compile_check_live_manager_upstream.gd", """ + class_name CompileCheckLiveManagerUpstream + extends Node + + func ping(): + [1] + """); + var arrayExpression = findNode(preparedInput.unit().ast(), ArrayExpression.class, _ -> true); + preparedInput.diagnosticManager().error( + "sema.synthetic", + "synthetic upstream error not yet copied to analysisData", + preparedInput.unit().path(), + FrontendRange.fromAstRange(arrayExpression.range()) + ); + assertTrue(diagnosticsByCategory(preparedInput.analysisData().diagnostics(), "sema.synthetic").isEmpty()); + + new FrontendCompileCheckAnalyzer().analyze( + preparedInput.analysisData(), + preparedInput.diagnosticManager() + ); + + var finalSnapshot = preparedInput.diagnosticManager().snapshot(); + assertTrue(diagnosticsByCategory(finalSnapshot, "sema.compile_check").isEmpty()); + assertEquals(1, diagnosticsByCategory(finalSnapshot, "sema.synthetic").size()); + } + @Test void analyzeReportsGenericCompileBlocksForPublishedCompileSurfaceFacts() throws Exception { var preparedInput = prepareCompileCheckInput("compile_check_published_facts.gd", """ @@ -725,8 +789,16 @@ func ping(worker: Worker): var payloadCopyDeclaration = findVariable(pingFunction.body().statements(), "payload_copy"); var readValueDeclaration = findVariable(pingFunction.body().statements(), "read_value"); var copyIdentifier = assertInstanceOf(dev.superice.gdparser.frontend.ast.IdentifierExpression.class, copyDeclaration.value()); - var payloadStep = findNode(payloadCopyDeclaration.value(), AttributePropertyStep.class, step -> step.name().equals("payload")); - var readStep = findNode(readValueDeclaration.value(), AttributeCallStep.class, step -> step.name().equals("read")); + var payloadStep = findNode( + Objects.requireNonNull(payloadCopyDeclaration.value()), + AttributePropertyStep.class, + step -> step.name().equals("payload") + ); + var readStep = findNode( + Objects.requireNonNull(readValueDeclaration.value()), + AttributeCallStep.class, + step -> step.name().equals("read") + ); preparedInput.analysisData().expressionTypes().put( copyIdentifier, @@ -856,16 +928,18 @@ static func ping_static(value: int): assertEquals(FrontendReceiverKind.INSTANCE, publishedBareCall.receiverKind()); preparedInput.analysisData().expressionTypes().put( bareCall, - FrontendExpressionType.resolved(publishedBareCall.returnType()) + FrontendExpressionType.resolved(Objects.requireNonNull(publishedBareCall.returnType())) ); runCompileCheck(preparedInput); var compileDiagnostics = diagnosticsByCategory(preparedInput.analysisData().diagnostics(), "sema.compile_check"); - assertEquals(1, compileDiagnostics.size()); - assertEquals(FrontendRange.fromAstRange(bareCall.range()), compileDiagnostics.getFirst().range()); - assertTrue(compileDiagnostics.getFirst().message().contains("Call expression 'helper(...)'")); - assertTrue(compileDiagnostics.getFirst().message().contains("not accessible in the current context")); + var bareCallDiagnostics = compileDiagnostics.stream() + .filter(diagnostic -> FrontendRange.fromAstRange(bareCall.range()).equals(diagnostic.range())) + .toList(); + assertEquals(1, bareCallDiagnostics.size()); + assertTrue(bareCallDiagnostics.getFirst().message().contains("Call expression 'helper(...)'")); + assertTrue(bareCallDiagnostics.getFirst().message().contains("not accessible in the current context")); } @Test @@ -883,7 +957,7 @@ func ping(worker: Worker, seed: int): var pingFunction = findFunction(preparedInput.unit().ast().statements(), "ping"); var valueDeclaration = findVariable(pingFunction.body().statements(), "value"); var payloadsStep = findNode( - valueDeclaration.value(), + Objects.requireNonNull(valueDeclaration.value()), AttributeSubscriptStep.class, step -> step.name().equals("payloads") ); @@ -928,12 +1002,12 @@ static func build() -> Worker: var callValueDeclaration = findVariable(preparedInput.unit().ast().statements(), "call_value"); var exprLiteral = assertInstanceOf(LiteralExpression.class, exprValueDeclaration.value()); var handleStep = findNode( - memberValueDeclaration.value(), + Objects.requireNonNull(memberValueDeclaration.value()), AttributePropertyStep.class, step -> step.name().equals("handle") ); var readStep = findNode( - callValueDeclaration.value(), + Objects.requireNonNull(callValueDeclaration.value()), AttributeCallStep.class, step -> step.name().equals("read") ); @@ -1046,8 +1120,16 @@ func ping(worker: Worker): var payloadCopyDeclaration = findVariable(pingFunction.body().statements(), "payload_copy"); var readValueDeclaration = findVariable(pingFunction.body().statements(), "read_value"); var copyIdentifier = assertInstanceOf(dev.superice.gdparser.frontend.ast.IdentifierExpression.class, copyDeclaration.value()); - var payloadStep = findNode(payloadCopyDeclaration.value(), AttributePropertyStep.class, step -> step.name().equals("payload")); - var readStep = findNode(readValueDeclaration.value(), AttributeCallStep.class, step -> step.name().equals("read")); + var payloadStep = findNode( + Objects.requireNonNull(payloadCopyDeclaration.value()), + AttributePropertyStep.class, + step -> step.name().equals("payload") + ); + var readStep = findNode( + Objects.requireNonNull(readValueDeclaration.value()), + AttributeCallStep.class, + step -> step.name().equals("read") + ); var originalMember = Objects.requireNonNull(preparedInput.analysisData().resolvedMembers().get(payloadStep)); var originalCall = Objects.requireNonNull(preparedInput.analysisData().resolvedCalls().get(readStep)); @@ -1139,7 +1221,7 @@ func ping(flag): var compiled = analyzeForCompile("deferred_compile_check.gd", source); var compileDiagnostics = diagnosticsByCategory(compiled.diagnostics(), "sema.compile_check"); - assertFalse(compileDiagnostics.isEmpty()); + assertFalse(compileDiagnostics.isEmpty(), () -> compiled.diagnostics().asList().toString()); assertTrue(compiled.diagnostics().hasErrors()); assertTrue(compileDiagnostics.stream().allMatch(diagnostic -> diagnostic.severity() == FrontendDiagnosticSeverity.ERROR @@ -1225,12 +1307,7 @@ func ping(flag): analysisData.updateDiagnostics(diagnosticManager.snapshot()); new FrontendVariableAnalyzer().analyze(analysisData, diagnosticManager); analysisData.updateDiagnostics(diagnosticManager.snapshot()); - new FrontendTopBindingAnalyzer().analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - new FrontendChainBindingAnalyzer().analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - new FrontendExprTypeAnalyzer().analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); + FrontendSuiteResolverStageTestSupport.resolveAllOwners(classRegistry, analysisData, diagnosticManager); new FrontendAnnotationUsageAnalyzer().analyze(classRegistry, analysisData, diagnosticManager); analysisData.updateDiagnostics(diagnosticManager.snapshot()); new FrontendTypeCheckAnalyzer().analyze(classRegistry, analysisData, diagnosticManager); diff --git a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendLocalTypeStabilizationAnalyzerTest.java b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendLocalTypeStabilizationAnalyzerTest.java deleted file mode 100644 index 2957725f..00000000 --- a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendLocalTypeStabilizationAnalyzerTest.java +++ /dev/null @@ -1,810 +0,0 @@ -package gd.script.gdcc.frontend.sema.analyzer; - -import dev.superice.gdparser.frontend.ast.AssignmentExpression; -import dev.superice.gdparser.frontend.ast.Block; -import dev.superice.gdparser.frontend.ast.DeclarationKind; -import dev.superice.gdparser.frontend.ast.ExpressionStatement; -import dev.superice.gdparser.frontend.ast.ForStatement; -import dev.superice.gdparser.frontend.ast.FunctionDeclaration; -import dev.superice.gdparser.frontend.ast.IfStatement; -import dev.superice.gdparser.frontend.ast.IdentifierExpression; -import dev.superice.gdparser.frontend.ast.MatchStatement; -import dev.superice.gdparser.frontend.ast.Node; -import dev.superice.gdparser.frontend.ast.Point; -import dev.superice.gdparser.frontend.ast.Range; -import dev.superice.gdparser.frontend.ast.Statement; -import dev.superice.gdparser.frontend.ast.TypeRef; -import dev.superice.gdparser.frontend.ast.VariableDeclaration; -import gd.script.gdcc.frontend.diagnostic.DiagnosticManager; -import gd.script.gdcc.frontend.parse.FrontendModule; -import gd.script.gdcc.frontend.parse.FrontendSourceUnit; -import gd.script.gdcc.frontend.parse.GdScriptParserService; -import gd.script.gdcc.frontend.scope.BlockScope; -import gd.script.gdcc.frontend.scope.BlockScopeKind; -import gd.script.gdcc.frontend.scope.CallableScope; -import gd.script.gdcc.frontend.scope.CallableScopeKind; -import gd.script.gdcc.frontend.scope.ClassScope; -import gd.script.gdcc.frontend.sema.FrontendAnalysisData; -import gd.script.gdcc.frontend.sema.FrontendClassSkeletonBuilder; -import gd.script.gdcc.frontend.sema.FrontendExpressionType; -import gd.script.gdcc.frontend.sema.FrontendExpressionTypeStatus; -import gd.script.gdcc.gdextension.ExtensionApiLoader; -import gd.script.gdcc.lir.LirClassDef; -import gd.script.gdcc.scope.ClassRegistry; -import gd.script.gdcc.type.GdccForRangeIterType; -import gd.script.gdcc.type.GdType; -import gd.script.gdcc.type.GdVariantType; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; - -import java.nio.file.Path; -import java.util.List; -import java.util.Objects; -import java.util.function.Predicate; - -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class FrontendLocalTypeStabilizationAnalyzerTest { - private static final Range SYNTHETIC_RANGE = new Range(0, 1, new Point(0, 0), new Point(0, 1)); - - @Test - void probeResolvesComplexInitializerAndLeavesPublishedFactsUntouched() throws Exception { - var prepared = prepareProbeInput( - "local_type_stabilization_complex_initializer.gd", - """ - class_name LocalTypeStabilizationComplexInitializer - extends RefCounted - - class Point: - var next: Point = null - var marker: int = -1 - - class Factory: - var cached: Point = null - - func make_point(seed: int) -> Point: - return cached if cached != null else Point.new() - - func ping(factory: Factory, seed: int): - var point := factory.make_point(seed).next - point - """ - ); - - var pingFunction = findFunction(prepared.unit().ast().statements(), "ping"); - var pointDeclaration = findVariable(pingFunction.body().statements(), "point"); - var pointUse = assertInstanceOf( - ExpressionStatement.class, - pingFunction.body().statements().get(1) - ).expression(); - - var snapshot = new FrontendLocalTypeStabilizationAnalyzer().probe( - prepared.classRegistry(), - prepared.analysisData(), - prepared.diagnosticManager() - ); - var entry = snapshot.findVariable("point"); - assertNotNull(entry); - - var initializerType = entry.initializerType(); - assertAll( - () -> assertEquals(FrontendExpressionTypeStatus.RESOLVED, initializerType.status()), - () -> assertNotNull(initializerType.publishedType()), - () -> assertTypeNameEndsWith(Objects.requireNonNull(initializerType.publishedType()), "Point"), - () -> assertNull(prepared.analysisData().expressionTypes().get(pointDeclaration.value())), - () -> assertNull(prepared.analysisData().expressionTypes().get(pointUse)), - () -> assertTrue(prepared.analysisData().resolvedMembers().isEmpty()), - () -> assertTrue(prepared.analysisData().resolvedCalls().isEmpty()), - () -> assertTrue(prepared.analysisData().slotTypes().isEmpty()), - () -> assertEquals(GdVariantType.VARIANT, currentLocalType(prepared.analysisData(), pingFunction.body(), "point")), - () -> assertEquals(0, prepared.diagnosticManager().snapshot().asList().size()) - ); - } - - @Test - void analyzeKeepsBareTypeMetaInitializerOutOfLocalStabilization() throws Exception { - var prepared = prepareProbeInput( - "local_type_stabilization_type_meta_guard.gd", - """ - class_name LocalTypeStabilizationTypeMetaGuard - extends RefCounted - - class Worker: - static func build() -> int: - return 1 - - func ping(): - var bad := Worker - bad - """ - ); - - var pingFunction = findFunction(prepared.unit().ast().statements(), "ping"); - var diagnosticsBeforeAnalyze = prepared.diagnosticManager().snapshot(); - - var snapshot = new FrontendLocalTypeStabilizationAnalyzer().probe( - prepared.classRegistry(), - prepared.analysisData(), - prepared.diagnosticManager() - ); - var entry = snapshot.findVariable("bad"); - assertNotNull(entry); - - new FrontendLocalTypeStabilizationAnalyzer().analyze( - prepared.classRegistry(), - prepared.analysisData(), - prepared.diagnosticManager() - ); - - assertAll( - () -> assertEquals(FrontendExpressionTypeStatus.FAILED, entry.initializerType().status()), - () -> assertEquals(GdVariantType.VARIANT, currentLocalType(prepared.analysisData(), pingFunction.body(), "bad")), - () -> assertTrue(prepared.analysisData().resolvedMembers().isEmpty()), - () -> assertTrue(prepared.analysisData().resolvedCalls().isEmpty()), - () -> assertTrue(prepared.analysisData().expressionTypes().isEmpty()), - () -> assertTrue(prepared.analysisData().slotTypes().isEmpty()), - () -> assertEquals(diagnosticsBeforeAnalyze, prepared.diagnosticManager().snapshot()), - () -> assertEquals(1, prepared.diagnosticManager().snapshot().asList().size()), - () -> assertEquals("sema.binding", prepared.diagnosticManager().snapshot().asList().getFirst().category()) - ); - } - - @Test - void probeKeepsAssignmentExpressionInitializerAsVariantFallback() throws Exception { - var xDeclaration = variable("x", assignment(identifier("a"), identifier("b"))); - var bodyScope = newBodyScope(); - bodyScope.defineLocal("x", GdVariantType.VARIANT, xDeclaration); - - var initializerType = FrontendLocalTypeStabilizationAnalyzer.probeAssignmentOrdinaryValueInitializerFailure( - Objects.requireNonNull(xDeclaration.value()) - ); - assertNotNull(initializerType); - FrontendLocalTypeStabilizationAnalyzer.probeStabilizeLocalSlot( - bodyScope, - xDeclaration, - initializerType - ); - - assertAll( - () -> assertEquals(FrontendExpressionTypeStatus.FAILED, initializerType.status()), - () -> assertTrue(Objects.requireNonNull(initializerType.detailReason()).contains( - "Assignment initializer" - )), - () -> assertEquals(GdVariantType.VARIANT, Objects.requireNonNull(bodyScope.resolveValue("x")).type()), - () -> assertNull(FrontendLocalTypeStabilizationAnalyzer.probeAssignmentOrdinaryValueInitializerFailure( - identifier("plain") - )) - ); - } - - @Test - void probeFailsFastWhenCompilerOnlyInitializerWouldStabilizeOrdinaryLocal() throws Exception { - var declaration = new VariableDeclaration( - DeclarationKind.VAR, - "iter", - new TypeRef(":=", SYNTHETIC_RANGE), - identifier("seed"), - false, - "variable_declaration", - SYNTHETIC_RANGE - ); - var bodyScope = newBodyScope(); - bodyScope.defineLocal("iter", GdVariantType.VARIANT, declaration); - - var failure = assertThrows( - IllegalStateException.class, - () -> FrontendLocalTypeStabilizationAnalyzer.probeStabilizeLocalSlot( - bodyScope, - declaration, - FrontendExpressionType.resolved(GdccForRangeIterType.FOR_RANGE_ITER) - ) - ); - - assertAll( - () -> assertTrue(failure.getMessage().contains("compiler-only type leaked into frontend local stabilization")), - () -> assertEquals(GdVariantType.VARIANT, Objects.requireNonNull(bodyScope.resolveValue("iter")).type()) - ); - } - - @Test - void analyzeStabilizesSourceOrderAliasChainInBlockScope() throws Exception { - var prepared = prepareProbeInput( - "local_type_stabilization_alias_chain.gd", - """ - class_name LocalTypeStabilizationAliasChain - extends RefCounted - - class Point: - var marker: int = -1 - - func make_point() -> Point: - return Point.new() - - func ping(): - var a := make_point() - var b := a - var c := b - c - """ - ); - - var pingFunction = findFunction(prepared.unit().ast().statements(), "ping"); - - new FrontendLocalTypeStabilizationAnalyzer().analyze( - prepared.classRegistry(), - prepared.analysisData(), - prepared.diagnosticManager() - ); - - assertAll( - () -> assertTypeNameEndsWith(currentLocalType(prepared.analysisData(), pingFunction.body(), "a"), "Point"), - () -> assertTypeNameEndsWith(currentLocalType(prepared.analysisData(), pingFunction.body(), "b"), "Point"), - () -> assertTypeNameEndsWith(currentLocalType(prepared.analysisData(), pingFunction.body(), "c"), "Point"), - () -> assertTrue(prepared.analysisData().resolvedMembers().isEmpty()), - () -> assertTrue(prepared.analysisData().resolvedCalls().isEmpty()), - () -> assertTrue(prepared.analysisData().expressionTypes().isEmpty()), - () -> assertTrue(prepared.analysisData().slotTypes().isEmpty()), - () -> assertEquals(0, prepared.diagnosticManager().snapshot().asList().size()) - ); - } - - @Test - void analyzeStabilizesParameterAliasFromCallableParameter() throws Exception { - var prepared = prepareProbeInput( - "local_type_stabilization_parameter_alias.gd", - """ - class_name LocalTypeStabilizationParameterAlias - extends RefCounted - - class Point: - var marker: int = -1 - - func ping(value: Point): - var a := value - a - """ - ); - - var pingFunction = findFunction(prepared.unit().ast().statements(), "ping"); - - new FrontendLocalTypeStabilizationAnalyzer().analyze( - prepared.classRegistry(), - prepared.analysisData(), - prepared.diagnosticManager() - ); - - assertAll( - () -> assertTypeNameEndsWith(currentLocalType(prepared.analysisData(), pingFunction.body(), "a"), "Point"), - () -> assertTrue(prepared.analysisData().resolvedMembers().isEmpty()), - () -> assertTrue(prepared.analysisData().resolvedCalls().isEmpty()), - () -> assertTrue(prepared.analysisData().expressionTypes().isEmpty()), - () -> assertTrue(prepared.analysisData().slotTypes().isEmpty()), - () -> assertEquals(0, prepared.diagnosticManager().snapshot().asList().size()) - ); - } - - @Test - void analyzeLetsChildBlockReadParentStabilizedLocal() throws Exception { - var prepared = prepareProbeInput( - "local_type_stabilization_nested_alias.gd", - """ - class_name LocalTypeStabilizationNestedAlias - extends RefCounted - - class Point: - var marker: int = -1 - - func make_point() -> Point: - return Point.new() - - func ping(toggle: bool): - var a := make_point() - if toggle: - var b := a - b - a - """ - ); - - var pingFunction = findFunction(prepared.unit().ast().statements(), "ping"); - var ifStatement = findNode(pingFunction, IfStatement.class, _ -> true); - - new FrontendLocalTypeStabilizationAnalyzer().analyze( - prepared.classRegistry(), - prepared.analysisData(), - prepared.diagnosticManager() - ); - - assertAll( - () -> assertTypeNameEndsWith(currentLocalType(prepared.analysisData(), pingFunction.body(), "a"), "Point"), - () -> assertTypeNameEndsWith(currentLocalType(prepared.analysisData(), ifStatement.body(), "b"), "Point"), - () -> assertTrue(prepared.analysisData().resolvedMembers().isEmpty()), - () -> assertTrue(prepared.analysisData().resolvedCalls().isEmpty()), - () -> assertTrue(prepared.analysisData().expressionTypes().isEmpty()), - () -> assertTrue(prepared.analysisData().slotTypes().isEmpty()), - () -> assertEquals(0, prepared.diagnosticManager().snapshot().asList().size()) - ); - } - - @Test - void analyzeKeepsShadowedChildBlockLocalFromPollutingParentSlot() throws Exception { - var prepared = prepareProbeInput( - "local_type_stabilization_shadow_boundary.gd", - """ - class_name LocalTypeStabilizationShadowBoundary - extends RefCounted - - class Point: - var marker: int = -1 - - func make_point() -> Point: - return Point.new() - - func ping(toggle: bool, dynamic_host): - var a := make_point() - if toggle: - var a := dynamic_host.next - a - var after := a - after - """ - ); - - var pingFunction = findFunction(prepared.unit().ast().statements(), "ping"); - var ifStatement = findNode(pingFunction, IfStatement.class, _ -> true); - var ifBodyScope = assertInstanceOf(BlockScope.class, prepared.analysisData().scopesByAst().get(ifStatement.body())); - var diagnosticsBeforeAnalyze = prepared.diagnosticManager().snapshot(); - - new FrontendLocalTypeStabilizationAnalyzer().analyze( - prepared.classRegistry(), - prepared.analysisData(), - prepared.diagnosticManager() - ); - - assertAll( - () -> assertTypeNameEndsWith(currentLocalType(prepared.analysisData(), pingFunction.body(), "a"), "Point"), - () -> assertNull(ifBodyScope.resolveValueHere("a")), - () -> assertTypeNameEndsWith(currentLocalType(prepared.analysisData(), pingFunction.body(), "after"), "Point"), - () -> assertTrue(prepared.analysisData().resolvedMembers().isEmpty()), - () -> assertTrue(prepared.analysisData().resolvedCalls().isEmpty()), - () -> assertTrue(prepared.analysisData().expressionTypes().isEmpty()), - () -> assertTrue(prepared.analysisData().slotTypes().isEmpty()), - () -> assertEquals(diagnosticsBeforeAnalyze, prepared.diagnosticManager().snapshot()), - () -> assertEquals(1, prepared.diagnosticManager().snapshot().asList().size()), - () -> assertEquals("sema.variable_binding", prepared.diagnosticManager().snapshot().asList().getFirst().category()) - ); - } - - @Test - void analyzeStabilizesComplexInitializerThenAliasInBlockScope() throws Exception { - var prepared = prepareProbeInput( - "local_type_stabilization_complex_alias.gd", - """ - class_name LocalTypeStabilizationComplexAlias - extends RefCounted - - class Point: - var next: Point = null - var marker: int = -1 - - class Factory: - var cached: Point = null - - func make_point(seed: int) -> Point: - return cached if cached != null else Point.new() - - func ping(factory: Factory, seed: int): - var p := factory.make_point(seed).next - var q := p - q - """ - ); - - var pingFunction = findFunction(prepared.unit().ast().statements(), "ping"); - - new FrontendLocalTypeStabilizationAnalyzer().analyze( - prepared.classRegistry(), - prepared.analysisData(), - prepared.diagnosticManager() - ); - - assertAll( - () -> assertTypeNameEndsWith(currentLocalType(prepared.analysisData(), pingFunction.body(), "p"), "Point"), - () -> assertTypeNameEndsWith(currentLocalType(prepared.analysisData(), pingFunction.body(), "q"), "Point"), - () -> assertTrue(prepared.analysisData().resolvedMembers().isEmpty()), - () -> assertTrue(prepared.analysisData().resolvedCalls().isEmpty()), - () -> assertTrue(prepared.analysisData().expressionTypes().isEmpty()), - () -> assertTrue(prepared.analysisData().slotTypes().isEmpty()), - () -> assertEquals(0, prepared.diagnosticManager().snapshot().asList().size()) - ); - } - - @Test - void analyzeKeepsTrueDynamicInitializerAsVariantInBlockScope() throws Exception { - var prepared = prepareProbeInput( - "local_type_stabilization_dynamic_fail_closed.gd", - """ - class_name LocalTypeStabilizationDynamicFailClosed - extends RefCounted - - func ping(dynamic_host): - var point := dynamic_host.next - var alias := point - alias - """ - ); - - var pingFunction = findFunction(prepared.unit().ast().statements(), "ping"); - - new FrontendLocalTypeStabilizationAnalyzer().analyze( - prepared.classRegistry(), - prepared.analysisData(), - prepared.diagnosticManager() - ); - - assertAll( - () -> assertEquals(GdVariantType.VARIANT, currentLocalType(prepared.analysisData(), pingFunction.body(), "point")), - () -> assertEquals(GdVariantType.VARIANT, currentLocalType(prepared.analysisData(), pingFunction.body(), "alias")), - () -> assertTrue(prepared.analysisData().resolvedMembers().isEmpty()), - () -> assertTrue(prepared.analysisData().resolvedCalls().isEmpty()), - () -> assertTrue(prepared.analysisData().expressionTypes().isEmpty()), - () -> assertTrue(prepared.analysisData().slotTypes().isEmpty()), - () -> assertEquals(0, prepared.diagnosticManager().snapshot().asList().size()) - ); - } - - @Test - void analyzeKeepsLambdaBodyOutsideSupportedStabilizationSurface() throws Exception { - var prepared = prepareProbeInput( - "local_type_stabilization_lambda_boundary.gd", - """ - class_name LocalTypeStabilizationLambdaBoundary - extends RefCounted - - class Point: - var marker: int = -1 - - func make_point() -> Point: - return Point.new() - - func ping(): - var before := make_point() - var maker := func(): - var inside_lambda := make_point() - return inside_lambda - before - """ - ); - - var pingFunction = findFunction(prepared.unit().ast().statements(), "ping"); - var diagnosticsBeforeAnalyze = prepared.diagnosticManager().snapshot().asList().size(); - - var analyzer = new FrontendLocalTypeStabilizationAnalyzer(); - var snapshot = analyzer.probe( - prepared.classRegistry(), - prepared.analysisData(), - prepared.diagnosticManager() - ); - analyzer.analyze( - prepared.classRegistry(), - prepared.analysisData(), - prepared.diagnosticManager() - ); - - var makerEntry = snapshot.findVariable("maker"); - assertNotNull(makerEntry); - assertAll( - () -> assertTypeNameEndsWith(currentLocalType(prepared.analysisData(), pingFunction.body(), "before"), "Point"), - () -> assertEquals(GdVariantType.VARIANT, currentLocalType(prepared.analysisData(), pingFunction.body(), "maker")), - () -> assertEquals(FrontendExpressionTypeStatus.UNSUPPORTED, makerEntry.initializerType().status()), - () -> assertNull(snapshot.findVariable("inside_lambda")), - () -> assertTrue(prepared.analysisData().resolvedMembers().isEmpty()), - () -> assertTrue(prepared.analysisData().resolvedCalls().isEmpty()), - () -> assertTrue(prepared.analysisData().expressionTypes().isEmpty()), - () -> assertTrue(prepared.analysisData().slotTypes().isEmpty()), - () -> assertEquals(diagnosticsBeforeAnalyze, prepared.diagnosticManager().snapshot().asList().size()) - ); - } - - @Test - void probeKeepsTrueDynamicInitializerAsDynamicWithoutWritingSharedFacts() throws Exception { - var prepared = prepareProbeInput( - "local_type_stabilization_dynamic_initializer.gd", - """ - class_name LocalTypeStabilizationDynamicInitializer - extends RefCounted - - func ping(dynamic_host): - var point := dynamic_host.next - point - """ - ); - - var snapshot = new FrontendLocalTypeStabilizationAnalyzer().probe( - prepared.classRegistry(), - prepared.analysisData(), - prepared.diagnosticManager() - ); - var entry = snapshot.findVariable("point"); - assertNotNull(entry); - - assertAll( - () -> assertEquals(FrontendExpressionTypeStatus.DYNAMIC, entry.initializerType().status()), - () -> assertEquals(GdVariantType.VARIANT, entry.initializerType().publishedType()), - () -> assertTrue(prepared.analysisData().resolvedMembers().isEmpty()), - () -> assertTrue(prepared.analysisData().resolvedCalls().isEmpty()), - () -> assertTrue(prepared.analysisData().expressionTypes().isEmpty()), - () -> assertEquals(0, prepared.diagnosticManager().snapshot().asList().size()) - ); - } - - @Test - void analyzeLeavesUnsupportedControlFlowSubtreeLocalsUnchanged() throws Exception { - var prepared = prepareProbeInput( - "local_type_stabilization_unsupported_subtrees_writeback.gd", - """ - class_name LocalTypeStabilizationUnsupportedSubtreesWriteback - extends RefCounted - - class Point: - var marker: int = -1 - - func make_point() -> Point: - return Point.new() - - func ping(toggle, choice, items): - var before_loop := make_point() - for item in items: - var inside_loop := make_point() - match choice: - 0: - var inside_match := make_point() - _: - pass - if toggle: - var inside_if := make_point() - before_loop - """ - ); - - var pingFunction = findFunction(prepared.unit().ast().statements(), "ping"); - var diagnosticsBeforeAnalyze = prepared.diagnosticManager().snapshot().asList().size(); - - var analyzer = new FrontendLocalTypeStabilizationAnalyzer(); - var snapshot = analyzer.probe( - prepared.classRegistry(), - prepared.analysisData(), - prepared.diagnosticManager() - ); - analyzer.analyze( - prepared.classRegistry(), - prepared.analysisData(), - prepared.diagnosticManager() - ); - - assertAll( - () -> assertTypeNameEndsWith( - currentLocalType(prepared.analysisData(), pingFunction.body(), "before_loop"), - "Point" - ), - () -> assertTypeNameEndsWith( - currentLocalType(prepared.analysisData(), findNode(pingFunction, Block.class, block -> - block.statements().stream().anyMatch(statement -> - statement instanceof VariableDeclaration declaration - && declaration.name().equals("inside_if") - ) - ), "inside_if"), - "Point" - ), - () -> assertNull(snapshot.findVariable("inside_loop")), - () -> assertNull(snapshot.findVariable("inside_match")), - () -> assertTrue(prepared.analysisData().resolvedMembers().isEmpty()), - () -> assertTrue(prepared.analysisData().resolvedCalls().isEmpty()), - () -> assertTrue(prepared.analysisData().expressionTypes().isEmpty()), - () -> assertEquals(diagnosticsBeforeAnalyze, prepared.diagnosticManager().snapshot().asList().size()) - ); - } - - @Test - void probeSkipsUnsupportedControlFlowSubtreesAndDoesNotLeakDiagnostics() throws Exception { - var prepared = prepareProbeInput( - "local_type_stabilization_unsupported_subtrees.gd", - """ - class_name LocalTypeStabilizationUnsupportedSubtrees - extends RefCounted - - class Point: - var marker: int = -1 - - func make_point() -> Point: - return Point.new() - - func ping(toggle, choice, items): - var before_loop := make_point() - for item in items: - var inside_loop := make_point() - match choice: - 0: - var inside_match := make_point() - _: - pass - if toggle: - var inside_if := make_point() - before_loop - """ - ); - - var pingFunction = findFunction(prepared.unit().ast().statements(), "ping"); - var forStatement = findNode(pingFunction, ForStatement.class, _ -> true); - var matchStatement = findNode(pingFunction, MatchStatement.class, _ -> true); - var forDeclaration = findNode(forStatement, VariableDeclaration.class, declaration -> declaration.name().equals("inside_loop")); - var matchDeclaration = findNode( - matchStatement, - VariableDeclaration.class, - declaration -> declaration.name().equals("inside_match") - ); - var diagnosticsBeforeProbe = prepared.diagnosticManager().snapshot().asList().size(); - - var snapshot = new FrontendLocalTypeStabilizationAnalyzer().probe( - prepared.classRegistry(), - prepared.analysisData(), - prepared.diagnosticManager() - ); - - assertAll( - () -> assertNotNull(snapshot.findVariable("before_loop")), - () -> assertNotNull(snapshot.findVariable("inside_if")), - () -> assertNull(snapshot.findVariable("inside_loop")), - () -> assertNull(snapshot.findVariable("inside_match")), - () -> assertNull(prepared.analysisData().expressionTypes().get(forDeclaration.value())), - () -> assertNull(prepared.analysisData().expressionTypes().get(matchDeclaration.value())), - () -> assertTrue(prepared.analysisData().resolvedMembers().isEmpty()), - () -> assertTrue(prepared.analysisData().resolvedCalls().isEmpty()), - () -> assertEquals(diagnosticsBeforeProbe, prepared.diagnosticManager().snapshot().asList().size()) - ); - } - - private static @NotNull PreparedProbeInput prepareProbeInput( - @NotNull String fileName, - @NotNull String source - ) throws Exception { - var parserService = new GdScriptParserService(); - var diagnosticManager = new DiagnosticManager(); - var unit = parserService.parseUnit(Path.of("tmp", fileName), source, diagnosticManager); - assertTrue(diagnosticManager.isEmpty(), () -> "Unexpected parse diagnostics: " + diagnosticManager.snapshot()); - - var classRegistry = new ClassRegistry(ExtensionApiLoader.loadDefault()); - var analysisData = FrontendAnalysisData.bootstrap(); - var moduleSkeleton = new FrontendClassSkeletonBuilder().build( - new FrontendModule("test_module", List.of(unit)), - classRegistry, - diagnosticManager, - analysisData - ); - analysisData.updateModuleSkeleton(moduleSkeleton); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - new FrontendScopeAnalyzer().analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - new FrontendVariableAnalyzer().analyze(analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - new FrontendTopBindingAnalyzer().analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - return new PreparedProbeInput(unit, analysisData, diagnosticManager, classRegistry); - } - - private static @NotNull FunctionDeclaration findFunction( - @NotNull List statements, - @NotNull String functionName - ) { - for (var statement : statements) { - if (statement instanceof FunctionDeclaration functionDeclaration - && functionDeclaration.name().equals(functionName)) { - return functionDeclaration; - } - } - throw new AssertionError("Function not found: " + functionName); - } - - private static @NotNull VariableDeclaration findVariable( - @NotNull List statements, - @NotNull String variableName - ) { - for (var statement : statements) { - if (statement instanceof VariableDeclaration variableDeclaration - && variableDeclaration.name().equals(variableName)) { - return variableDeclaration; - } - } - throw new AssertionError("Variable not found: " + variableName); - } - - private static @NotNull T findNode( - @NotNull Node root, - @NotNull Class nodeType, - @NotNull Predicate predicate - ) { - Objects.requireNonNull(root, "root must not be null"); - Objects.requireNonNull(nodeType, "nodeType must not be null"); - Objects.requireNonNull(predicate, "predicate must not be null"); - if (nodeType.isInstance(root)) { - var candidate = nodeType.cast(root); - if (predicate.test(candidate)) { - return candidate; - } - } - for (var child : root.getChildren()) { - try { - return findNode(child, nodeType, predicate); - } catch (AssertionError ignored) { - // Keep scanning remaining subtrees until one matches. - } - } - throw new AssertionError("Node not found: " + nodeType.getSimpleName()); - } - - private static @NotNull GdType currentLocalType( - @NotNull FrontendAnalysisData analysisData, - @NotNull Block body, - @NotNull String variableName - ) { - var bodyScope = assertInstanceOf(BlockScope.class, analysisData.scopesByAst().get(body)); - var value = bodyScope.resolveValue(variableName); - assertNotNull(value); - return value.type(); - } - - private static @NotNull BlockScope newBodyScope() throws Exception { - var registry = new ClassRegistry(ExtensionApiLoader.loadDefault()); - var ownerClass = new LirClassDef("SyntheticOwner", "RefCounted"); - var classScope = new ClassScope(registry, registry, ownerClass); - var callableScope = new CallableScope(classScope, CallableScopeKind.FUNCTION_DECLARATION); - return new BlockScope(callableScope, BlockScopeKind.FUNCTION_BODY); - } - - private static @NotNull VariableDeclaration variable( - @NotNull String name, - @NotNull AssignmentExpression initializer - ) { - return new VariableDeclaration( - DeclarationKind.VAR, - name, - new TypeRef(":=", SYNTHETIC_RANGE), - initializer, - false, - "variable_declaration", - SYNTHETIC_RANGE - ); - } - - private static @NotNull AssignmentExpression assignment( - @NotNull IdentifierExpression left, - @NotNull IdentifierExpression right - ) { - return new AssignmentExpression("=", left, right, SYNTHETIC_RANGE); - } - - private static @NotNull IdentifierExpression identifier(@NotNull String name) { - return new IdentifierExpression(name, SYNTHETIC_RANGE); - } - - private static void assertTypeNameEndsWith(@NotNull GdType type, @NotNull String suffix) { - assertTrue( - type.getTypeName().endsWith(suffix), - () -> "Expected type ending with '" + suffix + "', but was '" + type.getTypeName() + "'" - ); - } - - private record PreparedProbeInput( - @NotNull FrontendSourceUnit unit, - @NotNull FrontendAnalysisData analysisData, - @NotNull DiagnosticManager diagnosticManager, - @NotNull ClassRegistry classRegistry - ) { - } -} diff --git a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendLoopControlFlowAnalyzerTest.java b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendLoopControlFlowAnalyzerTest.java index c9b54828..84dad420 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendLoopControlFlowAnalyzerTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendLoopControlFlowAnalyzerTest.java @@ -108,9 +108,7 @@ func ping(values): """); assertTrue(diagnosticsByCategory(result.diagnostics(), "sema.loop_control_flow").isEmpty()); - assertTrue(result.diagnostics().asList().stream().anyMatch(diagnostic -> - !diagnostic.category().equals("sema.loop_control_flow") - )); + assertFalse(result.diagnostics().hasErrors()); } @Test diff --git a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolverStageTestSupport.java b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolverStageTestSupport.java new file mode 100644 index 00000000..881e48d2 --- /dev/null +++ b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolverStageTestSupport.java @@ -0,0 +1,95 @@ +package gd.script.gdcc.frontend.sema.analyzer; + +import dev.superice.gdparser.frontend.ast.Node; +import gd.script.gdcc.frontend.diagnostic.DiagnosticManager; +import gd.script.gdcc.frontend.sema.FrontendAnalysisData; +import gd.script.gdcc.frontend.sema.FrontendInterfaceSurface; +import gd.script.gdcc.frontend.sema.FrontendSemanticStage; +import gd.script.gdcc.scope.ClassRegistry; +import org.jetbrains.annotations.NotNull; + +import java.util.EnumSet; +import java.util.Set; + +/// Test harness for running real root-bounded owner procedures through the body SuiteResolver path. +final class FrontendSuiteResolverStageTestSupport { + private FrontendSuiteResolverStageTestSupport() { + } + + static void resolveAllOwners( + @NotNull ClassRegistry classRegistry, + @NotNull FrontendAnalysisData analysisData, + @NotNull DiagnosticManager diagnosticManager + ) { + resolveOwners( + classRegistry, + analysisData, + diagnosticManager, + EnumSet.allOf(FrontendSemanticStage.class) + ); + } + + static void resolveOwners( + @NotNull ClassRegistry classRegistry, + @NotNull FrontendAnalysisData analysisData, + @NotNull DiagnosticManager diagnosticManager, + @NotNull Set enabledStages + ) { + var interfaceSurface = new FrontendInterfacePhase().analyze(classRegistry, analysisData); + resolveOwners(interfaceSurface, classRegistry, analysisData, diagnosticManager, enabledStages); + } + + static void resolveOwners( + @NotNull FrontendInterfaceSurface interfaceSurface, + @NotNull ClassRegistry classRegistry, + @NotNull FrontendAnalysisData analysisData, + @NotNull DiagnosticManager diagnosticManager, + @NotNull Set enabledStages + ) { + var checkedStages = Set.copyOf(enabledStages); + var delegate = new FrontendBodyOwnerProcedures(); + var ownerProcedures = new FrontendStatementResolver.OwnerProcedures() { + @Override + public void runTopBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { + if (checkedStages.contains(FrontendSemanticStage.TOP_BINDING)) { + delegate.runTopBinding(context, root); + } + } + + @Override + public void runLocalTypeStabilization(@NotNull FrontendSuiteContext context, @NotNull Node root) { + if (checkedStages.contains(FrontendSemanticStage.LOCAL_TYPE_STABILIZATION)) { + delegate.runLocalTypeStabilization(context, root); + } + } + + @Override + public void runChainBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { + if (checkedStages.contains(FrontendSemanticStage.CHAIN_BINDING)) { + delegate.runChainBinding(context, root); + } + } + + @Override + public void runExprType(@NotNull FrontendSuiteContext context, @NotNull Node root) { + if (checkedStages.contains(FrontendSemanticStage.EXPR_TYPE)) { + delegate.runExprType(context, root); + } + } + + @Override + public void runVarTypePost(@NotNull FrontendSuiteContext context, @NotNull Node root) { + if (checkedStages.contains(FrontendSemanticStage.VAR_TYPE_POST)) { + delegate.runVarTypePost(context, root); + } + } + }; + new FrontendSuiteResolver(new FrontendStatementResolver(ownerProcedures)).resolve( + interfaceSurface, + classRegistry, + analysisData, + diagnosticManager + ); + analysisData.updateDiagnostics(diagnosticManager.snapshot()); + } +} diff --git a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTopBindingAnalyzerTest.java b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTopBindingAnalyzerTest.java deleted file mode 100644 index d0c6fd65..00000000 --- a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTopBindingAnalyzerTest.java +++ /dev/null @@ -1,2394 +0,0 @@ -package gd.script.gdcc.frontend.sema.analyzer; - -import gd.script.gdcc.frontend.diagnostic.DiagnosticManager; -import gd.script.gdcc.frontend.diagnostic.FrontendDiagnostic; -import gd.script.gdcc.frontend.diagnostic.FrontendDiagnosticSeverity; -import gd.script.gdcc.frontend.parse.FrontendModule; -import gd.script.gdcc.frontend.parse.FrontendSourceUnit; -import gd.script.gdcc.frontend.parse.GdScriptParserService; -import gd.script.gdcc.frontend.scope.ClassScope; -import gd.script.gdcc.frontend.scope.AbstractFrontendScope; -import gd.script.gdcc.frontend.sema.FrontendAnalysisData; -import gd.script.gdcc.frontend.sema.FrontendBinding; -import gd.script.gdcc.frontend.sema.FrontendBindingKind; -import gd.script.gdcc.frontend.sema.FrontendClassSkeletonBuilder; -import gd.script.gdcc.gdextension.ExtensionAPI; -import gd.script.gdcc.gdextension.ExtensionApiLoader; -import gd.script.gdcc.gdextension.ExtensionEnumValue; -import gd.script.gdcc.gdextension.ExtensionGdClass; -import gd.script.gdcc.gdextension.ExtensionGlobalConstant; -import gd.script.gdcc.gdextension.ExtensionSingleton; -import gd.script.gdcc.lir.LirFunctionDef; -import gd.script.gdcc.scope.ClassRegistry; -import gd.script.gdcc.scope.ScopeLookupStatus; -import gd.script.gdcc.scope.ScopeTypeMeta; -import gd.script.gdcc.scope.ScopeTypeMetaKind; -import gd.script.gdcc.scope.ScopeValueKind; -import gd.script.gdcc.type.GdObjectType; -import gd.script.gdcc.type.GdVariantType; -import dev.superice.gdparser.frontend.ast.ForStatement; -import dev.superice.gdparser.frontend.ast.FunctionDeclaration; -import dev.superice.gdparser.frontend.ast.IdentifierExpression; -import dev.superice.gdparser.frontend.ast.LambdaExpression; -import dev.superice.gdparser.frontend.ast.LiteralExpression; -import dev.superice.gdparser.frontend.ast.MatchStatement; -import dev.superice.gdparser.frontend.ast.Node; -import dev.superice.gdparser.frontend.ast.ReturnStatement; -import dev.superice.gdparser.frontend.ast.SelfExpression; -import dev.superice.gdparser.frontend.ast.SourceFile; -import dev.superice.gdparser.frontend.ast.VariableDeclaration; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.Predicate; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class FrontendTopBindingAnalyzerTest { - @Test - void analyzeRejectsMissingModuleSkeletonBoundary() { - var analyzer = new FrontendTopBindingAnalyzer(); - var analysisData = FrontendAnalysisData.bootstrap(); - - var thrown = assertThrows( - IllegalStateException.class, - () -> analyzer.analyze(new ClassRegistry(ExtensionApiLoader.loadDefault()), analysisData, new DiagnosticManager()) - ); - - assertTrue(thrown.getMessage().contains("moduleSkeleton")); - } - - @Test - void analyzeRejectsMissingDiagnosticsBoundary() throws Exception { - var preparedInput = prepareBindingInput("missing_binding_diagnostics.gd", """ - class_name MissingBindingDiagnostics - extends Node - - func ping(): - pass - """); - var analyzer = new FrontendTopBindingAnalyzer(); - var analysisData = FrontendAnalysisData.bootstrap(); - analysisData.updateModuleSkeleton(preparedInput.analysisData().moduleSkeleton()); - - var thrown = assertThrows( - IllegalStateException.class, - () -> analyzer.analyze(preparedInput.classRegistry(), analysisData, preparedInput.diagnosticManager()) - ); - - assertTrue(thrown.getMessage().contains("diagnostics")); - } - - @Test - void analyzeRejectsMissingPublishedSourceScope() throws Exception { - var preparedInput = prepareBindingInput("missing_source_scope.gd", """ - class_name MissingSourceScope - extends Node - - func ping(): - pass - """); - var analyzer = new FrontendTopBindingAnalyzer(); - preparedInput.analysisData().scopesByAst().remove(preparedInput.unit().ast()); - - var thrown = assertThrows( - IllegalStateException.class, - () -> analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()) - ); - - assertTrue(thrown.getMessage().contains(preparedInput.unit().path().toString())); - } - - @Test - void analyzeClearsStaleEntriesAndPublishesNoBindingsWhenBodyHasNoUseSites() throws Exception { - var preparedInput = prepareBindingInput("publish_empty_symbol_bindings.gd", """ - class_name PublishEmptySymbolBindings - extends Node - - func ping(): - pass - """); - var analyzer = new FrontendTopBindingAnalyzer(); - var publishedSymbolBindings = preparedInput.analysisData().symbolBindings(); - var staleNode = preparedInput.unit().ast(); - publishedSymbolBindings.put(staleNode, new FrontendBinding("__stale__", FrontendBindingKind.UNKNOWN, null)); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - assertSame(publishedSymbolBindings, preparedInput.analysisData().symbolBindings()); - assertTrue(preparedInput.analysisData().symbolBindings().isEmpty()); - assertNull(preparedInput.analysisData().symbolBindings().get(staleNode)); - assertTrue(preparedInput.analysisData().resolvedMembers().isEmpty()); - assertTrue(preparedInput.analysisData().resolvedCalls().isEmpty()); - assertTrue(preparedInput.analysisData().expressionTypes().isEmpty()); - } - - @Test - void analyzeBindsSupportedValueCategoriesAndLiteralsInExecutableBodies() throws Exception { - var api = ExtensionApiLoader.loadDefault(); - assertFalse(api.singletons().isEmpty()); - assertFalse(api.globalEnums().isEmpty()); - var singletonName = api.singletons().getFirst().name(); - var globalEnumName = api.globalEnums().getFirst().name(); - var preparedInput = prepareBindingInput( - "value_categories.gd", - """ - class_name ValueCategories - extends Node - - signal changed(value: int) - var hp = 1 - - func ping(value, arr, i): - var local = value - print(local) - print(hp) - print(changed) - print(%s) - print(%s) - print(arr[i + 1]) - print(self) - """.formatted(singletonName, globalEnumName), - new ClassRegistry(api) - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - var localDeclaration = findVariable(pingFunction.body().statements(), "local"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(localDeclaration.value(), "value"), - FrontendBindingKind.PARAMETER - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "local"), - FrontendBindingKind.LOCAL_VAR - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "hp"), - FrontendBindingKind.PROPERTY - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "changed"), - FrontendBindingKind.SIGNAL - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), singletonName), - FrontendBindingKind.SINGLETON - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), globalEnumName), - FrontendBindingKind.GLOBAL_ENUM - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "arr"), - FrontendBindingKind.PARAMETER - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "i"), - FrontendBindingKind.PARAMETER - ); - assertBinding( - preparedInput.analysisData(), - findLiteralExpression(pingFunction.body(), "1"), - FrontendBindingKind.LITERAL - ); - assertBinding( - preparedInput.analysisData(), - findSelfExpression(pingFunction.body()), - FrontendBindingKind.SELF - ); - assertTrue(preparedInput.diagnosticManager().snapshot().isEmpty()); - } - - @Test - void analyzeBindsGlobalConstantsAsOrdinaryConstantValues() throws Exception { - var preparedInput = prepareBindingInput( - "global_constant_value.gd", - """ - class_name GlobalConstantValue - extends Node - - func ping(): - print(GDCC_TEST_BIG_FLAG) - """, - new ClassRegistry(createGlobalConstantFixtureApi()) - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - var useSite = findIdentifierExpression(pingFunction.body(), "GDCC_TEST_BIG_FLAG"); - var binding = assertBinding(preparedInput.analysisData(), useSite, FrontendBindingKind.CONSTANT); - - assertAll( - () -> assertInstanceOf(ExtensionGlobalConstant.class, binding.declarationSite()), - () -> assertNull(preparedInput.classRegistry().resolveTypeMeta("GDCC_TEST_BIG_FLAG")), - () -> assertTrue(preparedInput.diagnosticManager().snapshot().isEmpty()) - ); - } - - @Test - void analyzePublishesStableSingletonResolvedValueForLaterLocalShadowing() throws Exception { - var preparedInput = prepareBindingInput( - "singleton_later_local_resolved_value.gd", - """ - class_name SingletonLaterLocalResolvedValue - extends RefCounted - - func ping(): - Engine.get_frames_drawn() - var Engine: String = "" - """ - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - var engineHead = findIdentifierExpression(pingFunction.body(), "Engine"); - var binding = assertBinding(preparedInput.analysisData(), engineHead, FrontendBindingKind.SINGLETON); - - assertAll( - () -> assertEquals(ScopeLookupStatus.FOUND_ALLOWED, binding.valueAccessStatus()), - () -> assertNotNull(binding.resolvedValue()), - () -> assertEquals(ScopeValueKind.SINGLETON, binding.resolvedValue().kind()), - () -> assertEquals("Engine", binding.resolvedValue().name()) - ); - } - - @Test - void analyzeKeepsInitializerSelfReferenceBoundToSingletonResolvedValue() throws Exception { - var preparedInput = prepareBindingInput( - "singleton_initializer_self_reference_resolved_value.gd", - """ - class_name SingletonInitializerSelfReferenceResolvedValue - extends RefCounted - - func ping(): - var Engine: String = Engine - """ - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - var engineDeclaration = findVariable(pingFunction.body().statements(), "Engine"); - var initializerEngine = findIdentifierExpression(engineDeclaration.value(), "Engine"); - var binding = assertBinding(preparedInput.analysisData(), initializerEngine, FrontendBindingKind.SINGLETON); - - assertAll( - () -> assertEquals(ScopeLookupStatus.FOUND_ALLOWED, binding.valueAccessStatus()), - () -> assertNotNull(binding.resolvedValue()), - () -> assertEquals(ScopeValueKind.SINGLETON, binding.resolvedValue().kind()), - () -> assertEquals("Engine", binding.resolvedValue().name()) - ); - } - - @Test - void analyzeSealsSameClassNonStaticPropertyInitializerHeadsButKeepsAllowedStaticRoutes() throws Exception { - var preparedInput = prepareBindingInput( - "property_initializer_bindings.gd", - """ - class_name PropertyInitializerBindings - extends RefCounted - - signal changed - var payload: int = 1 - static var shared: int = 1 - - class Worker: - static func build(): - return 1 - - static func helper() -> int: - return 1 - - func read() -> int: - return 1 - - var mirror := payload - var watched := changed - var built := Worker.build() - static var allowed_shared := shared - static var allowed_helper := helper() - static var blocked_value := payload - static var blocked_call := read() - var self_copy := self.payload - const Alias = payload - """ - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var sourceFile = preparedInput.unit().ast(); - var mirrorDeclaration = findVariable(sourceFile.statements(), "mirror"); - var watchedDeclaration = findVariable(sourceFile.statements(), "watched"); - var builtDeclaration = findVariable(sourceFile.statements(), "built"); - var allowedSharedDeclaration = findVariable(sourceFile.statements(), "allowed_shared"); - var allowedHelperDeclaration = findVariable(sourceFile.statements(), "allowed_helper"); - var blockedValueDeclaration = findVariable(sourceFile.statements(), "blocked_value"); - var blockedCallDeclaration = findVariable(sourceFile.statements(), "blocked_call"); - var selfCopyDeclaration = findVariable(sourceFile.statements(), "self_copy"); - var aliasDeclaration = findVariable(sourceFile.statements(), "Alias"); - - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(mirrorDeclaration.value(), "payload"), - FrontendBindingKind.PROPERTY - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(watchedDeclaration.value(), "changed"), - FrontendBindingKind.SIGNAL - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(builtDeclaration.value(), "Worker"), - FrontendBindingKind.TYPE_META - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(allowedSharedDeclaration.value(), "shared"), - FrontendBindingKind.PROPERTY - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(allowedHelperDeclaration.value(), "helper"), - FrontendBindingKind.STATIC_METHOD - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(blockedValueDeclaration.value(), "payload"), - FrontendBindingKind.PROPERTY - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(blockedCallDeclaration.value(), "read"), - FrontendBindingKind.METHOD - ); - assertBinding( - preparedInput.analysisData(), - findSelfExpression(selfCopyDeclaration.value()), - FrontendBindingKind.SELF - ); - assertNull(preparedInput.analysisData().symbolBindings().get(findIdentifierExpression(aliasDeclaration.value(), "payload"))); - - assertTrue(bindingDiagnostics(preparedInput.diagnosticManager()).isEmpty()); - - var unsupportedDiagnostics = unsupportedBindingDiagnostics(preparedInput.diagnosticManager()); - assertEquals(5, unsupportedDiagnostics.size()); - assertTrue(unsupportedDiagnostics.stream().allMatch(diagnostic -> - diagnostic.severity() == FrontendDiagnosticSeverity.ERROR - )); - assertTrue(unsupportedDiagnostics.stream().anyMatch(diagnostic -> diagnostic.message().contains("payload"))); - assertTrue(unsupportedDiagnostics.stream().anyMatch(diagnostic -> diagnostic.message().contains("changed"))); - assertTrue(unsupportedDiagnostics.stream().anyMatch(diagnostic -> diagnostic.message().contains("read"))); - assertTrue(unsupportedDiagnostics.stream().anyMatch(diagnostic -> diagnostic.message().contains("self"))); - assertFalse(unsupportedDiagnostics.stream().anyMatch(diagnostic -> diagnostic.message().contains("shared"))); - assertFalse(unsupportedDiagnostics.stream().anyMatch(diagnostic -> diagnostic.message().contains("helper"))); - } - - @Test - void analyzeSealsInheritedInstancePropertyInitializerHeadsButKeepsAllowedStaticRoutes() throws Exception { - var preparedInput = prepareBindingInput( - "property_initializer_inherited_bindings.gd", - """ - class_name PropertyInitializerInheritedBindings - extends PropertyInitializerBase - - static func helper() -> int: - return 1 - - var blocked_value := payload - var blocked_signal := changed - var blocked_call := read() - var allowed_helper := helper() - var allowed_global := print() - """ - , - FrontendAnalyzerTestRegistrySupport.registryWithInheritedPropertyInitializerBase() - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var sourceFile = preparedInput.unit().ast(); - var blockedValueDeclaration = findVariable(sourceFile.statements(), "blocked_value"); - var blockedSignalDeclaration = findVariable(sourceFile.statements(), "blocked_signal"); - var blockedCallDeclaration = findVariable(sourceFile.statements(), "blocked_call"); - var allowedHelperDeclaration = findVariable(sourceFile.statements(), "allowed_helper"); - var allowedGlobalDeclaration = findVariable(sourceFile.statements(), "allowed_global"); - - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(blockedValueDeclaration.value(), "payload"), - FrontendBindingKind.PROPERTY - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(blockedSignalDeclaration.value(), "changed"), - FrontendBindingKind.SIGNAL - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(blockedCallDeclaration.value(), "read"), - FrontendBindingKind.METHOD - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(allowedHelperDeclaration.value(), "helper"), - FrontendBindingKind.STATIC_METHOD - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(allowedGlobalDeclaration.value(), "print"), - FrontendBindingKind.UTILITY_FUNCTION - ); - - assertTrue(bindingDiagnostics(preparedInput.diagnosticManager()).isEmpty()); - - var unsupportedDiagnostics = unsupportedBindingDiagnostics(preparedInput.diagnosticManager()); - assertEquals(3, unsupportedDiagnostics.size()); - assertTrue(unsupportedDiagnostics.stream().allMatch(diagnostic -> - diagnostic.severity() == FrontendDiagnosticSeverity.ERROR - )); - assertTrue(unsupportedDiagnostics.stream().anyMatch(diagnostic -> diagnostic.message().contains("payload"))); - assertTrue(unsupportedDiagnostics.stream().anyMatch(diagnostic -> diagnostic.message().contains("changed"))); - assertTrue(unsupportedDiagnostics.stream().anyMatch(diagnostic -> diagnostic.message().contains("read"))); - assertFalse(unsupportedDiagnostics.stream().anyMatch(diagnostic -> diagnostic.message().contains("helper"))); - assertFalse(unsupportedDiagnostics.stream().anyMatch(diagnostic -> diagnostic.message().contains("print"))); - } - - @Test - void analyzeBindsTopLevelTypeMetaChainHeadsForEngineBuiltinGlobalEnumAndLexicalInnerClasses() throws Exception { - var api = ExtensionApiLoader.loadDefault(); - assertFalse(api.globalEnums().isEmpty()); - assertFalse(api.globalEnums().getFirst().values().isEmpty()); - var globalEnumName = api.globalEnums().getFirst().name(); - var globalEnumValueName = api.globalEnums().getFirst().values().getFirst().name(); - var preparedInput = prepareBindingInput( - "type_meta_chain_head.gd", - """ - class_name TypeMetaChainHead - extends Node - - class Inner: - static func build(): - return null - - func ping(): - Node.new() - String.num_int64(1) - %s.%s - Inner.build() - """.formatted(globalEnumName, globalEnumValueName), - new ClassRegistry(api) - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "Node"), - FrontendBindingKind.TYPE_META - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "String"), - FrontendBindingKind.TYPE_META - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), globalEnumName), - FrontendBindingKind.TYPE_META - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "Inner"), - FrontendBindingKind.TYPE_META - ); - assertTrue(preparedInput.diagnosticManager().snapshot().isEmpty()); - } - - @Test - void analyzeBindsMappedTopLevelTypeMetaChainHeadViaCallerSideRemap() throws Exception { - var preparedInput = prepareBindingInput( - "mapped_type_meta_chain_head.gd", - """ - class_name MappedWorker - extends RefCounted - - static func build(): - return null - - func ping(): - MappedWorker.build() - """, - new ClassRegistry(ExtensionApiLoader.loadDefault()), - Map.of("MappedWorker", "RuntimeWorker") - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "MappedWorker"), - FrontendBindingKind.TYPE_META - ); - assertTrue(bindingDiagnostics(preparedInput.diagnosticManager()).isEmpty()); - } - - @Test - void analyzePrefersLocalValueOverVisibleTypeMetaChainHeadAndReportsShadowingDiagnostic() throws Exception { - var preparedInput = prepareBindingInput("shadowed_type_meta_chain_head.gd", """ - class_name ShadowedTypeMetaChainHead - extends Node - - class Inner: - static func build(): - return null - - func ping(seed): - var Inner = seed - Inner.build() - """); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - var initializer = findVariable(pingFunction.body().statements(), "Inner"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(initializer.value(), "seed"), - FrontendBindingKind.PARAMETER - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "Inner"), - FrontendBindingKind.LOCAL_VAR - ); - - var publishedSymbolNames = preparedInput.analysisData().symbolBindings().values().stream() - .map(FrontendBinding::symbolName) - .toList(); - assertFalse(publishedSymbolNames.contains("build")); - - var bindingDiagnostics = bindingDiagnostics(preparedInput.diagnosticManager()); - assertEquals(1, bindingDiagnostics.size()); - assertTrue(bindingDiagnostics.getFirst().message().contains("Inner")); - assertTrue(bindingDiagnostics.getFirst().message().contains("shadows a visible type-meta")); - } - - @Test - void analyzeBindsBareMethodsStaticMethodsAndUtilityFunctionsWithoutPublishingResolvedCalls() throws Exception { - var api = ExtensionApiLoader.loadDefault(); - assertFalse(api.utilityFunctions().isEmpty()); - var utilityName = api.utilityFunctions().getFirst().name(); - var preparedInput = prepareBindingInput( - "bare_callee_bindings.gd", - """ - class_name BareCalleeBindings - extends Node - - func move(): - pass - - static func build(): - return null - - func ping(): - move() - build() - %s() - """.formatted(utilityName), - new ClassRegistry(api) - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "move"), - FrontendBindingKind.METHOD - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "build"), - FrontendBindingKind.STATIC_METHOD - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), utilityName), - FrontendBindingKind.UTILITY_FUNCTION - ); - assertTrue(preparedInput.analysisData().resolvedCalls().isEmpty()); - assertTrue(preparedInput.diagnosticManager().snapshot().isEmpty()); - } - - @Test - void analyzeBindsBareFunctionLikeSymbolsInValuePositionAfterValueNamespaceMiss() throws Exception { - var preparedInput = prepareBindingInput("bare_callable_values.gd", """ - class_name BareCallableValues - extends Node - - static func build(): - return null - - func helper(): - pass - - func ping(): - helper - build - print - var cb = helper - """); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBindingsForName( - preparedInput.analysisData(), - pingFunction.body(), - "helper", - FrontendBindingKind.METHOD, - 2 - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "build"), - FrontendBindingKind.STATIC_METHOD - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "print"), - FrontendBindingKind.UTILITY_FUNCTION - ); - assertTrue(bindingDiagnostics(preparedInput.diagnosticManager()).isEmpty()); - } - - @Test - void analyzePublishesSupportedTypeMetaValueBindingsAndReportsOrdinaryValueMisuse() throws Exception { - var preparedInput = prepareBindingInput("bare_type_meta_values.gd", """ - class_name BareTypeMetaValues - extends RefCounted - - class Worker: - static func build(): - return null - - func consume(value): - pass - - func ping(): - Worker - consume(Worker) - var bad = Worker - """); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBindingsForName( - preparedInput.analysisData(), - pingFunction.body(), - "Worker", - FrontendBindingKind.TYPE_META, - 3 - ); - - var bindingDiagnostics = bindingDiagnostics(preparedInput.diagnosticManager()); - assertEquals(3, bindingDiagnostics.size()); - assertTrue(bindingDiagnostics.stream().allMatch(diagnostic -> - diagnostic.message().contains("static-route head") - )); - assertTrue(bindingDiagnostics.stream().allMatch(diagnostic -> - diagnostic.message().contains("Worker.build") - )); - } - - @Test - void analyzePublishesBlockedBareInstanceMethodInStaticContextWithoutFallingBackToUtilityFunction() - throws Exception { - var api = ExtensionApiLoader.loadDefault(); - assertFalse(api.utilityFunctions().isEmpty()); - var utilityName = api.utilityFunctions().getFirst().name(); - var preparedInput = prepareBindingInput( - "static_context_bare_callee_shadowing.gd", - """ - class_name StaticContextBareCalleeShadowing - extends Node - - func %s(): - pass - - static func ping(): - %s() - """.formatted(utilityName, utilityName), - new ClassRegistry(api) - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), utilityName), - FrontendBindingKind.METHOD - ); - var bindingDiagnostics = bindingDiagnostics(preparedInput.diagnosticManager()); - assertEquals(1, bindingDiagnostics.size()); - assertTrue(bindingDiagnostics.getFirst().message().contains(utilityName)); - } - - @Test - void analyzePublishesUnknownForMissingBareCalleeAndReportsBindingError() throws Exception { - var preparedInput = prepareBindingInput("unknown_bare_callee.gd", """ - class_name UnknownBareCallee - extends Node - - func ping(): - missing_call() - """); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "missing_call"), - FrontendBindingKind.UNKNOWN - ); - var bindingDiagnostics = bindingDiagnostics(preparedInput.diagnosticManager()); - assertEquals(1, bindingDiagnostics.size()); - assertTrue(bindingDiagnostics.getFirst().message().contains("missing_call")); - } - - @Test - void analyzeFailsClosedWhenBareCalleeOverloadSetMixesStaticAndInstanceMethods() throws Exception { - var preparedInput = prepareBindingInput("mixed_bare_callee_overloads.gd", """ - class_name MixedBareCalleeOverloads - extends Node - - func ping(): - mix() - """); - var analyzer = new FrontendTopBindingAnalyzer(); - var classScope = assertInstanceOf( - ClassScope.class, - preparedInput.analysisData().scopesByAst().get(preparedInput.unit().ast()) - ); - classScope.defineFunction(createFunctionDef("mix", false)); - classScope.defineFunction(createFunctionDef("mix", true)); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertNull(preparedInput.analysisData().symbolBindings().get(findIdentifierExpression(pingFunction.body(), "mix"))); - var bindingDiagnostics = bindingDiagnostics(preparedInput.diagnosticManager()); - assertEquals(1, bindingDiagnostics.size()); - assertTrue(bindingDiagnostics.getFirst().message().contains("mixed static/non-static")); - assertTrue(bindingDiagnostics.getFirst().message().contains("mix")); - } - - @Test - void analyzeAllowsBuiltinTypeMetaHeadsButRejectsScopeLocalPseudoTypeMetaSources() throws Exception { - var preparedInput = prepareBindingInput("unsupported_type_meta_sources.gd", """ - class_name UnsupportedTypeMetaSources - extends Node - - func ping(): - String.num_int64(1) - Alias.build() - """); - var analyzer = new FrontendTopBindingAnalyzer(); - var sourceScope = assertInstanceOf( - AbstractFrontendScope.class, - preparedInput.analysisData().scopesByAst().get(preparedInput.unit().ast()) - ); - sourceScope.defineTypeMeta(new ScopeTypeMeta( - "EnemyAlias", - "Alias", - new GdObjectType("EnemyAlias"), - ScopeTypeMetaKind.GDCC_CLASS, - "preload alias", - true - )); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "String"), - FrontendBindingKind.TYPE_META - ); - assertNull(preparedInput.analysisData().symbolBindings().get(findIdentifierExpression(pingFunction.body(), "Alias"))); - - var bindingDiagnostics = bindingDiagnostics(preparedInput.diagnosticManager()); - assertEquals(1, bindingDiagnostics.size()); - assertTrue(bindingDiagnostics.stream().anyMatch(diagnostic -> diagnostic.message().contains("Alias"))); - } - - @Test - void analyzePreservesInitializerVisibilityFallbackAndUnknownContracts() throws Exception { - var preparedInput = prepareBindingInput("initializer_visibility.gd", """ - class_name InitializerVisibility - extends Node - - var node = 7 - - func bind_property(): - var node = node - - func bind_unknown(): - var answer = answer - """); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var propertyFunction = findFunction(preparedInput.unit().ast(), "bind_property"); - var propertyInitializer = findVariable(propertyFunction.body().statements(), "node"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(propertyInitializer.value(), "node"), - FrontendBindingKind.PROPERTY - ); - - var unknownFunction = findFunction(preparedInput.unit().ast(), "bind_unknown"); - var unknownInitializer = findVariable(unknownFunction.body().statements(), "answer"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(unknownInitializer.value(), "answer"), - FrontendBindingKind.UNKNOWN - ); - - var bindingDiagnostics = bindingDiagnostics(preparedInput.diagnosticManager()); - assertEquals(1, bindingDiagnostics.size()); - assertTrue(bindingDiagnostics.getFirst().message().contains("answer")); - } - - @Test - void analyzeBindsOnlyChainHeadsAndStepArgumentsForExplicitReceiverChains() throws Exception { - var preparedInput = prepareBindingInput("explicit_receiver_chain_heads.gd", """ - class_name ExplicitReceiverChainHeads - extends Node - - func get_player(): - pass - - func ping(player, i): - player.hp - self.hp - get_player().hp - player.move(i + 1) - player.list[i + 2] - """); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - var playerUseSites = findNodes( - pingFunction.body(), - IdentifierExpression.class, - identifierExpression -> identifierExpression.name().equals("player") - ); - assertEquals(3, playerUseSites.size()); - for (var playerUseSite : playerUseSites) { - assertBinding(preparedInput.analysisData(), playerUseSite, FrontendBindingKind.PARAMETER); - } - - var iteratorUseSites = findNodes( - pingFunction.body(), - IdentifierExpression.class, - identifierExpression -> identifierExpression.name().equals("i") - ); - assertEquals(2, iteratorUseSites.size()); - for (var iteratorUseSite : iteratorUseSites) { - assertBinding(preparedInput.analysisData(), iteratorUseSite, FrontendBindingKind.PARAMETER); - } - - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "get_player"), - FrontendBindingKind.METHOD - ); - assertBinding( - preparedInput.analysisData(), - findSelfExpression(pingFunction.body()), - FrontendBindingKind.SELF - ); - assertBinding( - preparedInput.analysisData(), - findLiteralExpression(pingFunction.body(), "1"), - FrontendBindingKind.LITERAL - ); - assertBinding( - preparedInput.analysisData(), - findLiteralExpression(pingFunction.body(), "2"), - FrontendBindingKind.LITERAL - ); - assertEquals(9, preparedInput.analysisData().symbolBindings().size()); - assertTrue(preparedInput.analysisData().resolvedMembers().isEmpty()); - assertTrue(preparedInput.analysisData().resolvedCalls().isEmpty()); - assertTrue(preparedInput.diagnosticManager().snapshot().isEmpty()); - } - - @Test - void analyzeBindsSupportedPartsAcrossComplexExpressionsExceptChainTails() throws Exception { - var preparedInput = prepareBindingInput("complex_expression_bindings.gd", """ - class_name ComplexExpressionBindings - extends Node - - func helper(value): - return value - - func get_player(): - return null - - func ping(player, arr, key, cond, value, obj, matrix, row, col): - print([value + 1, -(arr[key + 2]), helper(value + 3), player.move(value + 4)]) - print({value: arr[key + 5], "fallback": helper(value + 6)}) - print("yes" if cond and not false else get_player().hp) - var awaited = await helper(value + 7) - print(obj is Node) - print(obj as Node) - value = matrix[row][col + 8] - """); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - var helperFunction = findFunction(preparedInput.unit().ast(), "helper"); - var getPlayerFunction = findFunction(preparedInput.unit().ast(), "get_player"); - assertBindingsForName( - preparedInput.analysisData(), - pingFunction.body(), - "value", - FrontendBindingKind.PARAMETER, - 7 - ); - assertBindingsForName( - preparedInput.analysisData(), - pingFunction.body(), - "helper", - FrontendBindingKind.METHOD, - 3 - ); - assertBindingsForName( - preparedInput.analysisData(), - pingFunction.body(), - "arr", - FrontendBindingKind.PARAMETER, - 2 - ); - assertBindingsForName( - preparedInput.analysisData(), - pingFunction.body(), - "key", - FrontendBindingKind.PARAMETER, - 2 - ); - assertBindingsForName( - preparedInput.analysisData(), - pingFunction.body(), - "player", - FrontendBindingKind.PARAMETER, - 1 - ); - assertBindingsForName( - preparedInput.analysisData(), - pingFunction.body(), - "cond", - FrontendBindingKind.PARAMETER, - 1 - ); - assertBindingsForName( - preparedInput.analysisData(), - pingFunction.body(), - "get_player", - FrontendBindingKind.METHOD, - 1 - ); - assertBindingsForName( - preparedInput.analysisData(), - pingFunction.body(), - "obj", - FrontendBindingKind.PARAMETER, - 2 - ); - assertBindingsForName( - preparedInput.analysisData(), - pingFunction.body(), - "matrix", - FrontendBindingKind.PARAMETER, - 1 - ); - assertBindingsForName( - preparedInput.analysisData(), - pingFunction.body(), - "row", - FrontendBindingKind.PARAMETER, - 1 - ); - assertBindingsForName( - preparedInput.analysisData(), - pingFunction.body(), - "col", - FrontendBindingKind.PARAMETER, - 1 - ); - assertBindingsForName( - preparedInput.analysisData(), - pingFunction.body(), - "print", - FrontendBindingKind.UTILITY_FUNCTION, - 5 - ); - assertBindingsForName( - preparedInput.analysisData(), - helperFunction.body(), - "value", - FrontendBindingKind.PARAMETER, - 1 - ); - assertBinding( - preparedInput.analysisData(), - findLiteralExpression(pingFunction.body(), "1"), - FrontendBindingKind.LITERAL - ); - assertBinding( - preparedInput.analysisData(), - findLiteralExpression(pingFunction.body(), "2"), - FrontendBindingKind.LITERAL - ); - assertBinding( - preparedInput.analysisData(), - findLiteralExpression(pingFunction.body(), "3"), - FrontendBindingKind.LITERAL - ); - assertBinding( - preparedInput.analysisData(), - findLiteralExpression(pingFunction.body(), "4"), - FrontendBindingKind.LITERAL - ); - assertBinding( - preparedInput.analysisData(), - findLiteralExpression(pingFunction.body(), "5"), - FrontendBindingKind.LITERAL - ); - assertBinding( - preparedInput.analysisData(), - findLiteralExpression(pingFunction.body(), "6"), - FrontendBindingKind.LITERAL - ); - assertBinding( - preparedInput.analysisData(), - findLiteralExpression(pingFunction.body(), "7"), - FrontendBindingKind.LITERAL - ); - assertBinding( - preparedInput.analysisData(), - findLiteralExpression(pingFunction.body(), "8"), - FrontendBindingKind.LITERAL - ); - assertBinding( - preparedInput.analysisData(), - findLiteralExpression(pingFunction.body(), "\"fallback\""), - FrontendBindingKind.LITERAL - ); - assertBinding( - preparedInput.analysisData(), - findLiteralExpression(pingFunction.body(), "\"yes\""), - FrontendBindingKind.LITERAL - ); - assertBinding( - preparedInput.analysisData(), - findLiteralExpression(pingFunction.body(), "false"), - FrontendBindingKind.LITERAL - ); - assertBinding( - preparedInput.analysisData(), - findLiteralExpression(getPlayerFunction.body(), "null"), - FrontendBindingKind.LITERAL - ); - - var publishedSymbolNames = preparedInput.analysisData().symbolBindings().values().stream() - .map(FrontendBinding::symbolName) - .toList(); - assertFalse(publishedSymbolNames.contains("move")); - assertFalse(publishedSymbolNames.contains("hp")); - assertFalse(publishedSymbolNames.contains("Node")); - assertEquals(40, preparedInput.analysisData().symbolBindings().size()); - assertTrue(preparedInput.analysisData().resolvedMembers().isEmpty()); - assertTrue(preparedInput.analysisData().resolvedCalls().isEmpty()); - assertTrue(bindingDiagnostics(preparedInput.diagnosticManager()).isEmpty()); - assertTrue(unsupportedBindingDiagnostics(preparedInput.diagnosticManager()).isEmpty()); - } - - @Test - void analyzeTreatsAssignmentSitesAsUsageAgnosticBindingsForNow() throws Exception { - var preparedInput = prepareBindingInput("assignment_usage_agnostic.gd", """ - class_name AssignmentUsageAgnostic - extends Node - - func ping(value, matrix, row, col): - value = matrix[row][col + 1] - """); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBindingsForName( - preparedInput.analysisData(), - pingFunction.body(), - "value", - FrontendBindingKind.PARAMETER, - 1 - ); - assertBindingsForName( - preparedInput.analysisData(), - pingFunction.body(), - "matrix", - FrontendBindingKind.PARAMETER, - 1 - ); - assertBindingsForName( - preparedInput.analysisData(), - pingFunction.body(), - "row", - FrontendBindingKind.PARAMETER, - 1 - ); - assertBindingsForName( - preparedInput.analysisData(), - pingFunction.body(), - "col", - FrontendBindingKind.PARAMETER, - 1 - ); - assertBinding( - preparedInput.analysisData(), - findLiteralExpression(pingFunction.body(), "1"), - FrontendBindingKind.LITERAL - ); - assertTrue(preparedInput.diagnosticManager().snapshot().isEmpty()); - } - - @Test - void analyzeReportsDeferredBindingSubtreesAtRootsWithoutPublishingInnerBindings() throws Exception { - var preparedInput = prepareBindingInput("deferred_binding_subtrees.gd", """ - class_name DeferredBindingSubtrees - extends Node - - func helper(): - pass - - func ping(seed = helper()): - var body_local = 0 - var f = func(): - return body_local - const answer = body_local - for item in [body_local]: - assert(item) - match body_local: - var bound when bound > 0: - assert(bound) - return body_local - """); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - var defaultHelperUseSite = findIdentifierExpression( - pingFunction.parameters().getFirst().defaultValue(), - "helper" - ); - assertNull(preparedInput.analysisData().symbolBindings().get(defaultHelperUseSite)); - - var lambdaExpression = findNode(pingFunction.body(), LambdaExpression.class, _ -> true); - var lambdaUseSite = findIdentifierExpression(lambdaExpression, "body_local"); - assertNull(preparedInput.analysisData().symbolBindings().get(lambdaUseSite)); - - var constDeclaration = findVariable(pingFunction.body().statements(), "answer"); - var constInitializerUseSite = findIdentifierExpression(constDeclaration.value(), "body_local"); - assertNull(preparedInput.analysisData().symbolBindings().get(constInitializerUseSite)); - - var forStatement = findNode(pingFunction.body(), ForStatement.class, _ -> true); - var forIterableUseSite = findIdentifierExpression(forStatement.iterable(), "body_local"); - var forBodyUseSite = findIdentifierExpression(forStatement.body(), "item"); - assertNull(preparedInput.analysisData().symbolBindings().get(forIterableUseSite)); - assertNull(preparedInput.analysisData().symbolBindings().get(forBodyUseSite)); - - var matchStatement = findNode(pingFunction.body(), MatchStatement.class, _ -> true); - var matchValueUseSite = findIdentifierExpression(matchStatement.value(), "body_local"); - assertBinding( - preparedInput.analysisData(), - matchValueUseSite, - FrontendBindingKind.LOCAL_VAR - ); - var boundUseSites = findNodes( - matchStatement, - IdentifierExpression.class, - identifierExpression -> identifierExpression.name().equals("bound") - ); - for (var boundUseSite : boundUseSites) { - assertNull(preparedInput.analysisData().symbolBindings().get(boundUseSite)); - } - - var outerReturn = assertInstanceOf(ReturnStatement.class, pingFunction.body().statements().getLast()); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(outerReturn, "body_local"), - FrontendBindingKind.LOCAL_VAR - ); - - var unsupportedDiagnostics = unsupportedBindingDiagnostics(preparedInput.diagnosticManager()); - assertEquals(5, unsupportedDiagnostics.size()); - assertTrue(unsupportedDiagnostics.stream().allMatch(diagnostic -> - diagnostic.severity() == FrontendDiagnosticSeverity.ERROR - )); - assertTrue(unsupportedDiagnostics.stream().anyMatch(diagnostic -> diagnostic.message().contains("parameter default"))); - assertTrue(unsupportedDiagnostics.stream().anyMatch(diagnostic -> diagnostic.message().contains("lambda subtree"))); - assertTrue(unsupportedDiagnostics.stream().anyMatch(diagnostic -> diagnostic.message().contains("block-local const initializer"))); - assertTrue(unsupportedDiagnostics.stream().anyMatch(diagnostic -> diagnostic.message().contains("for subtree"))); - assertTrue(unsupportedDiagnostics.stream().anyMatch(diagnostic -> diagnostic.message().contains("match subtree"))); - assertTrue(bindingDiagnostics(preparedInput.diagnosticManager()).isEmpty()); - } - - @Test - void analyzeReportsSingleSkippedSubtreeWarningWhenCallableBodyScopeIsMissing() throws Exception { - var preparedInput = prepareBindingInput("missing_callable_body_scope.gd", """ - class_name MissingCallableBodyScope - extends Node - - func skipped(value): - print(value) - - func ok(value): - print(value) - """); - var analyzer = new FrontendTopBindingAnalyzer(); - var skippedFunction = findFunction(preparedInput.unit().ast(), "skipped"); - var okFunction = findFunction(preparedInput.unit().ast(), "ok"); - preparedInput.analysisData().scopesByAst().remove(skippedFunction.body()); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - assertNull(preparedInput.analysisData().symbolBindings().get(findIdentifierExpression(skippedFunction.body(), "value"))); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(okFunction.body(), "value"), - FrontendBindingKind.PARAMETER - ); - var unsupportedDiagnostics = unsupportedBindingDiagnostics(preparedInput.diagnosticManager()); - assertEquals(1, unsupportedDiagnostics.size()); - assertEquals(FrontendDiagnosticSeverity.WARNING, unsupportedDiagnostics.getFirst().severity()); - assertTrue(unsupportedDiagnostics.getFirst().message().contains("skipped subtree")); - assertTrue(bindingDiagnostics(preparedInput.diagnosticManager()).isEmpty()); - } - - @Test - void analyzeReportsSingleSkippedSubtreeWarningWhenNestedBlockScopeIsMissing() throws Exception { - var preparedInput = prepareBindingInput("missing_nested_block_scope.gd", """ - class_name MissingNestedBlockScope - extends Node - - func ping(value): - if value > 0: - print(value) - print(value) - """); - var analyzer = new FrontendTopBindingAnalyzer(); - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - var ifStatement = findNode(pingFunction.body(), dev.superice.gdparser.frontend.ast.IfStatement.class, _ -> true); - preparedInput.analysisData().scopesByAst().remove(ifStatement.body()); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var printedValues = findNodes( - pingFunction.body(), - IdentifierExpression.class, - identifierExpression -> identifierExpression.name().equals("value") - ); - assertEquals(3, printedValues.size()); - assertBinding(preparedInput.analysisData(), printedValues.getFirst(), FrontendBindingKind.PARAMETER); - assertNull(preparedInput.analysisData().symbolBindings().get(printedValues.get(1))); - assertBinding(preparedInput.analysisData(), printedValues.get(2), FrontendBindingKind.PARAMETER); - var unsupportedDiagnostics = unsupportedBindingDiagnostics(preparedInput.diagnosticManager()); - assertEquals(1, unsupportedDiagnostics.size()); - assertEquals(FrontendDiagnosticSeverity.WARNING, unsupportedDiagnostics.getFirst().severity()); - assertTrue(unsupportedDiagnostics.getFirst().message().contains("skipped subtree")); - assertTrue(bindingDiagnostics(preparedInput.diagnosticManager()).isEmpty()); - } - - @Test - void analyzePublishesBlockedMembersAndSelfInStaticContextWithoutFallingBack() throws Exception { - var preparedInput = prepareBindingInput("static_context_bindings.gd", """ - class_name StaticContextBindings - extends Node - - signal changed(value: int) - var hp = 1 - - static func ping(): - print(hp) - print(changed) - print(self) - """); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "hp"), - FrontendBindingKind.PROPERTY - ); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "changed"), - FrontendBindingKind.SIGNAL - ); - assertBinding( - preparedInput.analysisData(), - findSelfExpression(pingFunction.body()), - FrontendBindingKind.SELF - ); - - var bindingDiagnostics = bindingDiagnostics(preparedInput.diagnosticManager()); - assertEquals(3, bindingDiagnostics.size()); - assertTrue(bindingDiagnostics.stream().anyMatch(diagnostic -> diagnostic.message().contains("hp"))); - assertTrue(bindingDiagnostics.stream().anyMatch(diagnostic -> diagnostic.message().contains("changed"))); - assertTrue(bindingDiagnostics.stream().anyMatch(diagnostic -> diagnostic.message().contains("self"))); - } - - @Test - void analyzeReportsUnsupportedBindingSubtreeWhenVisitedUseSiteScopeIsMissing() throws Exception { - var preparedInput = prepareBindingInput("missing_use_site_scope.gd", """ - class_name MissingUseSiteScope - extends Node - - func ping(value): - print(value) - """); - var analyzer = new FrontendTopBindingAnalyzer(); - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - var useSite = findIdentifierExpression(pingFunction.body(), "value"); - preparedInput.analysisData().scopesByAst().remove(useSite); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - assertNull(preparedInput.analysisData().symbolBindings().get(useSite)); - var unsupportedDiagnostics = preparedInput.diagnosticManager().snapshot().asList().stream() - .filter(diagnostic -> diagnostic.category().equals("sema.unsupported_binding_subtree")) - .toList(); - assertEquals(1, unsupportedDiagnostics.size()); - assertEquals(FrontendDiagnosticSeverity.WARNING, unsupportedDiagnostics.getFirst().severity()); - assertTrue(unsupportedDiagnostics.getFirst().message().contains("value")); - } - - // ------------------------------------------------------------------ - // Dual-role chain-head route bias tests - // ------------------------------------------------------------------ - - /// `Engine.get_frames_drawn()` is a singleton instance call: the head `Engine` must stay - /// bound as `SINGLETON` so the chain enters the instance-method route, not `TYPE_META`. - @Test - void analyzeKeepsSingletonInstanceCallHeadAsSingletonBinding() throws Exception { - var preparedInput = prepareBindingInput( - "dual_role_singleton_instance_call.gd", - """ - class_name DualRoleSingletonInstanceCall - extends RefCounted - - func ping(): - Engine.get_frames_drawn() - """ - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "Engine"), - FrontendBindingKind.SINGLETON - ); - } - - /// `Input.is_action_pressed(...)` is a singleton instance call with arguments: the head - /// `Input` must stay `SINGLETON`. - @Test - void analyzeKeepsSingletonInstanceCallWithArgumentsAsSingletonBinding() throws Exception { - var preparedInput = prepareBindingInput( - "dual_role_singleton_instance_call_args.gd", - """ - class_name DualRoleSingletonInstanceCallArgs - extends RefCounted - - func ping(): - Input.is_action_pressed(&"ui_accept") - """ - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "Input"), - FrontendBindingKind.SINGLETON - ); - } - - /// `Engine.new()` is a constructor-like route on a dual-role name: the head `Engine` must - /// be published as `TYPE_META` so the chain enters the constructor primary route. Whether - /// the constructor is legal for a singleton is a downstream route concern, not a head-bias - /// concern. - @Test - void analyzePublishesTypeMetaForDualRoleConstructorNewRoute() throws Exception { - var preparedInput = prepareBindingInput( - "dual_role_constructor_new.gd", - """ - class_name DualRoleConstructorNew - extends RefCounted - - func ping(): - Engine.new() - """ - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "Engine"), - FrontendBindingKind.TYPE_META - ); - } - - /// `.new()` fail-closed: when the singleton's declared type has an instance method named - /// `new`, the suffix resolves in the singleton instance namespace. The head must stay - /// `SINGLETON`, not be blindly switched to `TYPE_META`. This uses a custom fixture where - /// the dual-role name's engine class defines `new` as an instance method. - @Test - void analyzeDualRoleConstructorNewFailClosedWhenSingletonHasInstanceNew() throws Exception { - var api = createDualRoleWithInstanceNewFixtureApi(); - var preparedInput = prepareBindingInput( - "dual_role_constructor_new_fail_closed.gd", - """ - class_name DualRoleConstructorNewFailClosed - extends RefCounted - - func ping(): - DualWithInstanceNew.new() - """, - new ClassRegistry(api) - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "DualWithInstanceNew"), - FrontendBindingKind.SINGLETON - ); - } - - /// Fail-closed for signal: when the singleton's declared type has a signal with the same - /// name as a type-meta static member (e.g. an engine class constant), the suffix resolves - /// in the singleton instance namespace via signal. The head must stay `SINGLETON`, not be - /// switched to `TYPE_META`. This uses a custom fixture where the dual-role name's engine - /// class defines both a signal `shared_name` and a constant `shared_name`. - @Test - void analyzeDualRoleFailClosedWhenSingletonHasSignalAndTypeMetaHasStaticMember() throws Exception { - var api = createDualRoleWithSignalAndConstantFixtureApi(); - var preparedInput = prepareBindingInput( - "dual_role_signal_fail_closed.gd", - """ - class_name DualRoleSignalFailClosed - extends RefCounted - - func ping(): - DualWithSignal.shared_name - """, - new ClassRegistry(api) - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "DualWithSignal"), - FrontendBindingKind.SINGLETON - ); - } - - /// `IP.RESOLVER_MAX_QUERIES` is an engine class constant on a dual-role singleton: the - /// suffix only resolves in the type-meta static namespace, so the head `IP` must be - /// published as `TYPE_META`. - @Test - void analyzePublishesTypeMetaForDualRoleEngineClassConstantAccess() throws Exception { - var preparedInput = prepareBindingInput( - "dual_role_engine_constant.gd", - """ - class_name DualRoleEngineConstant - extends RefCounted - - func ping(): - IP.RESOLVER_MAX_QUERIES - """ - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "IP"), - FrontendBindingKind.TYPE_META - ); - } - - /// `Input.MOUSE_MODE_VISIBLE` is a class enum value on a dual-role singleton: the suffix - /// only resolves in the type-meta static namespace (class enum), so the head `Input` must - /// be published as `TYPE_META`. - @Test - void analyzePublishesTypeMetaForDualRoleClassEnumValueAccess() throws Exception { - var preparedInput = prepareBindingInput( - "dual_role_class_enum_value.gd", - """ - class_name DualRoleClassEnumValue - extends RefCounted - - func ping(): - Input.MOUSE_MODE_VISIBLE - """ - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "Input"), - FrontendBindingKind.TYPE_META - ); - } - - /// Inherited static members must participate in the dual-role bias just like direct static - /// members: the singleton's class is `ChildInput`, but both suffixes are declared on `BaseInput`. - @Test - void analyzePublishesTypeMetaForDualRoleInheritedEngineStaticMembers() throws Exception { - var api = createDualRoleInheritedStaticFixtureApi(false); - var preparedInput = prepareBindingInput( - "dual_role_inherited_static_members.gd", - """ - class_name DualRoleInheritedStaticMembers - extends RefCounted - - func ping(): - ChildInput.PARENT_LIMIT - ChildInput.PARENT_MOUSE_MODE - """, - new ClassRegistry(api) - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBindingsForName( - preparedInput.analysisData(), - pingFunction.body(), - "ChildInput", - FrontendBindingKind.TYPE_META, - 2 - ); - } - - /// Fail-closed still applies when both namespaces are satisfied only through inherited members. - @Test - void analyzeKeepsSingletonWhenInheritedInstanceMemberConflictsWithInheritedStaticMember() throws Exception { - var api = createDualRoleInheritedStaticFixtureApi(true); - var preparedInput = prepareBindingInput( - "dual_role_inherited_static_fail_closed.gd", - """ - class_name DualRoleInheritedStaticFailClosed - extends RefCounted - - func ping(): - ChildInput.PARENT_LIMIT - """, - new ClassRegistry(api) - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "ChildInput"), - FrontendBindingKind.SINGLETON - ); - } - - /// `ResourceUID.uid_to_path(...)` is a static method on a dual-role singleton: the suffix - /// only resolves in the type-meta static namespace (static method), so the head `ResourceUID` - /// must be published as `TYPE_META`. - @Test - void analyzePublishesTypeMetaForDualRoleStaticMethodCall() throws Exception { - var preparedInput = prepareBindingInput( - "dual_role_static_method_call.gd", - """ - class_name DualRoleStaticMethodCall - extends RefCounted - - func ping(): - ResourceUID.uid_to_path() - """ - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "ResourceUID"), - FrontendBindingKind.TYPE_META - ); - } - - /// Mixed usage: bare `Input`, `Input.is_action_pressed(...)`, and `Input.MOUSE_MODE_VISIBLE` - /// in the same function. Each use-site binding must be independent: bare `Input` stays - /// `SINGLETON`, instance call head stays `SINGLETON`, and static constant head switches to - /// `TYPE_META`. - @Test - void analyzeKeepsDualRoleUseSitesIndependentInSameFunction() throws Exception { - var preparedInput = prepareBindingInput( - "dual_role_mixed_use_sites.gd", - """ - class_name DualRoleMixedUseSites - extends RefCounted - - func ping(): - var bare = Input - Input.is_action_pressed(&"ui_accept") - Input.MOUSE_MODE_VISIBLE - """ - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - var inputUseSites = findNodes( - pingFunction.body(), - IdentifierExpression.class, - identifier -> identifier.name().equals("Input") - ); - // Three use-sites: bare value, instance call head, static constant head. - assertEquals(3, inputUseSites.size()); - // Bare `Input` (inside VariableDeclaration initializer) stays SINGLETON. - var bareDecl = findVariable(pingFunction.body().statements(), "bare"); - var bareInput = findIdentifierExpression(bareDecl.value(), "Input"); - assertBinding(preparedInput.analysisData(), bareInput, FrontendBindingKind.SINGLETON); - // The other two use-sites: one is SINGLETON (instance call), one is TYPE_META (constant). - var nonBareSites = inputUseSites.stream() - .filter(site -> site != bareInput) - .toList(); - var kinds = nonBareSites.stream() - .map(site -> preparedInput.analysisData().symbolBindings().get(site).kind()) - .toList(); - assertTrue(kinds.contains(FrontendBindingKind.SINGLETON), "instance call head must stay SINGLETON"); - assertTrue(kinds.contains(FrontendBindingKind.TYPE_META), "static constant head must be TYPE_META"); - } - - /// Fail-closed: when the first suffix resolves in BOTH the singleton instance namespace and - /// the type-meta static namespace, the head must keep `SINGLETON`. This uses a custom fixture - /// where the dual-role name `AmbiguousDual` is both a singleton (type `AmbiguousDual`) and an - /// engine class. The engine class defines `shared_member` as BOTH a static method and an - /// instance method, so the suffix resolves in both namespaces. - @Test - void analyzeFailClosedKeepsSingletonWhenSuffixResolvesInBothNamespaces() throws Exception { - var api = createDualRoleAmbiguousFixtureApi(); - var preparedInput = prepareBindingInput( - "dual_role_ambiguous_suffix.gd", - """ - class_name DualRoleAmbiguousSuffix - extends RefCounted - - func ping(): - AmbiguousDual.shared_member - """, - new ClassRegistry(api) - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "AmbiguousDual"), - FrontendBindingKind.SINGLETON - ); - } - - /// Non-dual-role engine class static access (e.g. `Node.NOTIFICATION_ENTER_TREE`) must not - /// be affected by the dual-role bias: `Node` is not a singleton, so it follows the existing - /// `TYPE_META` route as before. - @Test - void analyzeKeepsNonDualRoleEngineClassStaticAsTypeMeta() throws Exception { - var preparedInput = prepareBindingInput( - "non_dual_role_engine_static.gd", - """ - class_name NonDualRoleEngineStatic - extends Node - - func ping(): - Node.NOTIFICATION_ENTER_TREE - """ - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "Node"), - FrontendBindingKind.TYPE_META - ); - } - - /// Property initializer: dual-role static constant access in a property initializer must - /// also publish `TYPE_META` at the head, consistent with executable body behavior. - @Test - void analyzePublishesTypeMetaForDualRoleConstantInPropertyInitializer() throws Exception { - var preparedInput = prepareBindingInput( - "dual_role_property_init_constant.gd", - """ - class_name DualRolePropertyInitConstant - extends RefCounted - - var queries: int = IP.RESOLVER_MAX_QUERIES - - func ping() -> int: - return queries - """ - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var sourceFile = preparedInput.unit().ast(); - var ipHead = findIdentifierExpression(sourceFile, "IP"); - assertBinding(preparedInput.analysisData(), ipHead, FrontendBindingKind.TYPE_META); - } - - /// Prior-declared local shadows a dual-role singleton: the value winner must be `LOCAL_VAR`, - /// not `SINGLETON` or `TYPE_META`. This anchors the contract that the dual-role bias consumes - /// `resolveVisibleValue(...)` as the value-winner authority — a local that wins visible-value - /// resolution must not be overridden by the bias. - @Test - void analyzeDualRoleBiasRespectsPriorLocalShadowingSingletonValue() throws Exception { - var preparedInput = prepareBindingInput( - "dual_role_prior_local_shadows_singleton.gd", - """ - class_name DualRolePriorLocalShadowsSingleton - extends RefCounted - - func ping(): - var Engine: String = "local" - Engine.get_frames_drawn() - """ - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - // The chain-head `Engine` (in `Engine.get_frames_drawn()`) must be LOCAL_VAR, not - // SINGLETON or TYPE_META, because the prior local wins visible-value resolution. - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "Engine"), - FrontendBindingKind.LOCAL_VAR - ); - } - - /// Prior-declared local shadows a dual-role singleton with a static-constant-only suffix: - /// even though `IP.RESOLVER_MAX_QUERIES` would normally trigger the TYPE_META bias, the - /// prior local wins visible-value resolution, so the head must stay `LOCAL_VAR`. The bias - /// must not bypass the value-winner authority. - @Test - void analyzeDualRoleBiasDoesNotOverridePriorLocalForStaticConstantSuffix() throws Exception { - var preparedInput = prepareBindingInput( - "dual_role_prior_local_shadows_static_constant.gd", - """ - class_name DualRolePriorLocalShadowsStaticConstant - extends RefCounted - - func ping(): - var IP: String = "local" - IP.RESOLVER_MAX_QUERIES - """ - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "IP"), - FrontendBindingKind.LOCAL_VAR - ); - } - - /// Prior-declared local shadows a dual-role singleton with a constructor `.new()` suffix: - /// even though `.new()` would normally trigger the TYPE_META bias, the prior local wins - /// visible-value resolution, so the head must stay `LOCAL_VAR`. - @Test - void analyzeDualRoleBiasDoesNotOverridePriorLocalForConstructorNewSuffix() throws Exception { - var preparedInput = prepareBindingInput( - "dual_role_prior_local_shadows_constructor_new.gd", - """ - class_name DualRolePriorLocalShadowsConstructorNew - extends RefCounted - - func ping(): - var Engine: String = "local" - Engine.new() - """ - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "Engine"), - FrontendBindingKind.LOCAL_VAR - ); - } - - /// Parameter shadows a dual-role singleton: the value winner must be `PARAMETER`, not - /// `SINGLETON` or `TYPE_META`. This confirms the bias respects parameter-level shadowing - /// in addition to local-level shadowing. - @Test - void analyzeDualRoleBiasRespectsParameterShadowingSingletonValue() throws Exception { - var preparedInput = prepareBindingInput( - "dual_role_parameter_shadows_singleton.gd", - """ - class_name DualRoleParameterShadowsSingleton - extends RefCounted - - func ping(Engine): - Engine.get_frames_drawn() - """ - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "Engine"), - FrontendBindingKind.PARAMETER - ); - } - - /// Later-declared local does NOT shadow a dual-role singleton: `resolveVisibleValue` filters - /// later locals, so the value winner is still `SINGLETON`. The dual-role bias then applies - /// normally — `Engine.get_frames_drawn()` stays `SINGLETON` (instance call), and - /// `IP.RESOLVER_MAX_QUERIES` switches to `TYPE_META`. - @Test - void analyzeDualRoleBiasStillAppliesWhenLaterLocalDoesNotShadowSingleton() throws Exception { - var preparedInput = prepareBindingInput( - "dual_role_later_local_no_shadow.gd", - """ - class_name DualRoleLaterLocalNoShadow - extends RefCounted - - func ping(): - Engine.get_frames_drawn() - IP.RESOLVER_MAX_QUERIES - var Engine: String = "later" - var IP: String = "later" - """ - ); - var analyzer = new FrontendTopBindingAnalyzer(); - - analyzer.analyze(preparedInput.classRegistry(), preparedInput.analysisData(), preparedInput.diagnosticManager()); - - var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); - // Instance call head stays SINGLETON (later local filtered by visible-value resolver). - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "Engine"), - FrontendBindingKind.SINGLETON - ); - // Static constant head switches to TYPE_META (later local filtered). - assertBinding( - preparedInput.analysisData(), - findIdentifierExpression(pingFunction.body(), "IP"), - FrontendBindingKind.TYPE_META - ); - } - - /// Custom fixture: a dual-role name `DualWithInstanceNew` that is both a singleton (type - /// `DualWithInstanceNew`) and an engine class. The engine class defines `new` as an instance - /// method, so `.new()` resolves in the singleton instance namespace, exercising the - /// `.new()` fail-closed rule. - private static @NotNull ExtensionAPI createDualRoleWithInstanceNewFixtureApi() throws Exception { - var defaultApi = ExtensionApiLoader.loadDefault(); - var dualClass = new ExtensionGdClass( - "DualWithInstanceNew", - false, - true, - "Object", - "core", - List.of(), - List.of(new ExtensionGdClass.ClassMethod( - "new", - false, - false, - false, - false, - 0L, - List.of(), - null, - List.of() - )), - List.of(), - List.of(), - List.of() - ); - var customClasses = new java.util.ArrayList<>(defaultApi.classes()); - customClasses.add(dualClass); - var customSingletons = new java.util.ArrayList<>(defaultApi.singletons()); - customSingletons.add(new ExtensionSingleton("DualWithInstanceNew", "DualWithInstanceNew")); - return new ExtensionAPI( - defaultApi.header(), - defaultApi.builtinClassSizes(), - defaultApi.builtinClassMemberOffsets(), - defaultApi.globalConstants(), - defaultApi.globalEnums(), - defaultApi.utilityFunctions(), - defaultApi.builtinClasses(), - customClasses, - customSingletons, - defaultApi.nativeStructures() - ); - } - - /// Custom fixture: a dual-role name `DualWithSignal` that is both a singleton (type - /// `DualWithSignal`) and an engine class. The engine class defines `shared_name` as BOTH - /// a signal and an integer constant, so the suffix resolves in both the singleton instance - /// namespace (via signal) and the type-meta static namespace (via constant), exercising the - /// signal fail-closed rule. - private static @NotNull ExtensionAPI createDualRoleWithSignalAndConstantFixtureApi() throws Exception { - var defaultApi = ExtensionApiLoader.loadDefault(); - var dualClass = new ExtensionGdClass( - "DualWithSignal", - false, - true, - "Object", - "core", - List.of(), - List.of(), - List.of(new ExtensionGdClass.SignalInfo("shared_name", List.of())), - List.of(), - List.of(new ExtensionGdClass.ConstantInfo("shared_name", "42")) - ); - var customClasses = new java.util.ArrayList<>(defaultApi.classes()); - customClasses.add(dualClass); - var customSingletons = new java.util.ArrayList<>(defaultApi.singletons()); - customSingletons.add(new ExtensionSingleton("DualWithSignal", "DualWithSignal")); - return new ExtensionAPI( - defaultApi.header(), - defaultApi.builtinClassSizes(), - defaultApi.builtinClassMemberOffsets(), - defaultApi.globalConstants(), - defaultApi.globalEnums(), - defaultApi.utilityFunctions(), - defaultApi.builtinClasses(), - customClasses, - customSingletons, - defaultApi.nativeStructures() - ); - } - - /// Custom fixture: a dual-role name `AmbiguousDual` that is both a singleton (type - /// `AmbiguousDual`) and an engine class. The engine class defines `shared_member` as BOTH - /// a static method and an instance method, so the suffix resolves in both the singleton - /// instance namespace and the type-meta static namespace, exercising the fail-closed rule. - private static @NotNull ExtensionAPI createDualRoleAmbiguousFixtureApi() throws Exception { - var defaultApi = ExtensionApiLoader.loadDefault(); - var ambiguousClass = new ExtensionGdClass( - "AmbiguousDual", - false, - true, - "Object", - "core", - List.of(), - List.of( - // static method: resolves in type-meta static namespace - new ExtensionGdClass.ClassMethod( - "shared_member", - false, - false, - true, - false, - 0L, - List.of(), - null, - List.of() - ), - // instance method: resolves in singleton instance namespace - new ExtensionGdClass.ClassMethod( - "shared_member", - false, - false, - false, - false, - 0L, - List.of(), - null, - List.of() - ) - ), - List.of(), - List.of(), - List.of() - ); - var customClasses = new java.util.ArrayList<>(defaultApi.classes()); - customClasses.add(ambiguousClass); - var customSingletons = new java.util.ArrayList<>(defaultApi.singletons()); - customSingletons.add(new ExtensionSingleton("AmbiguousDual", "AmbiguousDual")); - return new ExtensionAPI( - defaultApi.header(), - defaultApi.builtinClassSizes(), - defaultApi.builtinClassMemberOffsets(), - defaultApi.globalConstants(), - defaultApi.globalEnums(), - defaultApi.utilityFunctions(), - defaultApi.builtinClasses(), - customClasses, - customSingletons, - defaultApi.nativeStructures() - ); - } - - private static @NotNull ExtensionAPI createDualRoleInheritedStaticFixtureApi(boolean inheritedInstanceConflict) - throws IOException { - var defaultApi = ExtensionApiLoader.loadDefault(); - var parentClass = new ExtensionGdClass( - "BaseInput", - false, - true, - "Object", - "core", - List.of(new ExtensionGdClass.ClassEnum( - "MouseMode", false, List.of(new ExtensionEnumValue("PARENT_MOUSE_MODE", 7)) - )), - List.of(), - inheritedInstanceConflict - ? List.of(new ExtensionGdClass.SignalInfo("PARENT_LIMIT", List.of())) - : List.of(), - List.of(), - List.of(new ExtensionGdClass.ConstantInfo("PARENT_LIMIT", "42")) - ); - var childClass = new ExtensionGdClass( - "ChildInput", - false, - true, - "BaseInput", - "core", - List.of(), - List.of(), - List.of(), - List.of(), - List.of() - ); - var customClasses = new java.util.ArrayList<>(defaultApi.classes()); - customClasses.add(parentClass); - customClasses.add(childClass); - var customSingletons = new java.util.ArrayList<>(defaultApi.singletons()); - customSingletons.add(new ExtensionSingleton("ChildInput", "ChildInput")); - return new ExtensionAPI( - defaultApi.header(), - defaultApi.builtinClassSizes(), - defaultApi.builtinClassMemberOffsets(), - defaultApi.globalConstants(), - defaultApi.globalEnums(), - defaultApi.utilityFunctions(), - defaultApi.builtinClasses(), - customClasses, - customSingletons, - defaultApi.nativeStructures() - ); - } - - private static @NotNull List bindingDiagnostics(@NotNull DiagnosticManager diagnosticManager) { - return diagnosticManager.snapshot().asList().stream() - .filter(diagnostic -> diagnostic.category().equals("sema.binding")) - .toList(); - } - - private static @NotNull List unsupportedBindingDiagnostics( - @NotNull DiagnosticManager diagnosticManager - ) { - return diagnosticManager.snapshot().asList().stream() - .filter(diagnostic -> diagnostic.category().equals("sema.unsupported_binding_subtree")) - .toList(); - } - - private static @NotNull LirFunctionDef createFunctionDef(@NotNull String name, boolean isStatic) { - var function = new LirFunctionDef(name); - function.setReturnType(GdVariantType.VARIANT); - function.setStatic(isStatic); - return function; - } - - private static @NotNull FrontendBinding assertBinding( - @NotNull FrontendAnalysisData analysisData, - @NotNull Node useSite, - @NotNull FrontendBindingKind expectedKind - ) { - var binding = analysisData.symbolBindings().get(useSite); - assertEquals(expectedKind, Objects.requireNonNull(binding, "binding must not be null").kind()); - return binding; - } - - private static void assertBindingsForName( - @NotNull FrontendAnalysisData analysisData, - @NotNull Node root, - @NotNull String symbolName, - @NotNull FrontendBindingKind expectedKind, - int expectedCount - ) { - var useSites = findNodes( - root, - IdentifierExpression.class, - identifierExpression -> identifierExpression.name().equals(symbolName) - ); - assertEquals(expectedCount, useSites.size(), "Unexpected use-site count for " + symbolName); - for (var useSite : useSites) { - assertBinding(analysisData, useSite, expectedKind); - } - } - - private static @NotNull PreparedBindingInput prepareBindingInput( - @NotNull String fileName, - @NotNull String source - ) throws Exception { - return prepareBindingInput(fileName, source, new ClassRegistry(ExtensionApiLoader.loadDefault()), Map.of()); - } - - private static @NotNull ExtensionAPI createGlobalConstantFixtureApi() throws IOException { - var defaultApi = ExtensionApiLoader.loadDefault(); - return new ExtensionAPI( - defaultApi.header(), - defaultApi.builtinClassSizes(), - defaultApi.builtinClassMemberOffsets(), - List.of(new ExtensionGlobalConstant("GDCC_TEST_BIG_FLAG", 4_294_967_296L, true)), - defaultApi.globalEnums(), - defaultApi.utilityFunctions(), - defaultApi.builtinClasses(), - defaultApi.classes(), - defaultApi.singletons(), - defaultApi.nativeStructures() - ); - } - - private static @NotNull PreparedBindingInput prepareBindingInput( - @NotNull String fileName, - @NotNull String source, - @NotNull ClassRegistry classRegistry - ) { - return prepareBindingInput(fileName, source, classRegistry, Map.of()); - } - - private static @NotNull PreparedBindingInput prepareBindingInput( - @NotNull String fileName, - @NotNull String source, - @NotNull ClassRegistry classRegistry, - @NotNull Map topLevelCanonicalNameMap - ) { - var parserService = new GdScriptParserService(); - var diagnosticManager = new DiagnosticManager(); - var unit = parserService.parseUnit(Path.of("tmp", fileName), source, diagnosticManager); - assertTrue(diagnosticManager.isEmpty(), () -> "Unexpected parse diagnostics: " + diagnosticManager.snapshot()); - - var analysisData = FrontendAnalysisData.bootstrap(); - var moduleSkeleton = new FrontendClassSkeletonBuilder().build( - new FrontendModule("test_module", List.of(unit), topLevelCanonicalNameMap), - Objects.requireNonNull(classRegistry, "classRegistry must not be null"), - diagnosticManager, - analysisData - ); - analysisData.updateModuleSkeleton(moduleSkeleton); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - new FrontendScopeAnalyzer().analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - new FrontendVariableAnalyzer().analyze(analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - return new PreparedBindingInput(unit, analysisData, diagnosticManager, classRegistry); - } - - private static @NotNull FunctionDeclaration findFunction( - @NotNull SourceFile sourceFile, - @NotNull String name - ) { - return findNode( - sourceFile, - FunctionDeclaration.class, - functionDeclaration -> functionDeclaration.name().equals(name) - ); - } - - private static @NotNull VariableDeclaration findVariable( - @NotNull List statements, - @NotNull String name - ) { - return statements.stream() - .filter(VariableDeclaration.class::isInstance) - .map(VariableDeclaration.class::cast) - .filter(variableDeclaration -> variableDeclaration.name().equals(name)) - .findFirst() - .orElseThrow(() -> new AssertionError("Variable not found: " + name)); - } - - private static @NotNull IdentifierExpression findIdentifierExpression( - @NotNull Node root, - @NotNull String name - ) { - return findNode( - root, - IdentifierExpression.class, - identifierExpression -> identifierExpression.name().equals(name) - ); - } - - private static @NotNull LiteralExpression findLiteralExpression( - @NotNull Node root, - @NotNull String sourceText - ) { - return findNode( - root, - LiteralExpression.class, - literalExpression -> literalExpression.sourceText().equals(sourceText) - ); - } - - private static @NotNull SelfExpression findSelfExpression(@NotNull Node root) { - return findNode(root, SelfExpression.class, _ -> true); - } - - private static @NotNull List findNodes( - @NotNull Node root, - @NotNull Class nodeType, - @NotNull Predicate predicate - ) { - var matches = new ArrayList(); - collectMatchingNodes(root, nodeType, predicate, matches); - return List.copyOf(matches); - } - - private static @NotNull T findNode( - @NotNull Node root, - @NotNull Class nodeType, - @NotNull Predicate predicate - ) { - return findNodes(root, nodeType, predicate).stream() - .findFirst() - .orElseThrow(() -> new AssertionError("Node not found: " + nodeType.getSimpleName())); - } - - private static void collectMatchingNodes( - @NotNull Node node, - @NotNull Class nodeType, - @NotNull Predicate predicate, - @NotNull List matches - ) { - if (nodeType.isInstance(node)) { - var candidate = nodeType.cast(node); - if (predicate.test(candidate)) { - matches.add(candidate); - } - } - for (var child : node.getChildren()) { - collectMatchingNodes(child, nodeType, predicate, matches); - } - } - - private record PreparedBindingInput( - @NotNull FrontendSourceUnit unit, - @NotNull FrontendAnalysisData analysisData, - @NotNull DiagnosticManager diagnosticManager, - @NotNull ClassRegistry classRegistry - ) { - } -} diff --git a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTypeCheckAnalyzerTest.java b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTypeCheckAnalyzerTest.java index f633092c..ad1253f3 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTypeCheckAnalyzerTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTypeCheckAnalyzerTest.java @@ -612,8 +612,10 @@ static func make_count(): assertEquals(4, typeHintDiagnostics.size()); assertTrue(typeHintDiagnostics.stream().allMatch(diagnostic -> diagnostic.severity() == FrontendDiagnosticSeverity.WARNING - && FrontendDiagnostic.sourcePathText(Path.of("tmp", "type_check_property_compatibility.gd")) - .equals(diagnostic.sourcePath()) + && Objects.equals( + FrontendDiagnostic.sourcePathText(Path.of("tmp", "type_check_property_compatibility.gd")), + diagnostic.sourcePath() + ) && diagnostic.range() != null )); assertTrue(typeHintDiagnostics.stream().anyMatch(diagnostic -> @@ -685,8 +687,10 @@ func ping() -> void: assertEquals(4, typeCheckDiagnostics.size()); assertTrue(typeCheckDiagnostics.stream().allMatch(diagnostic -> diagnostic.severity() == FrontendDiagnosticSeverity.ERROR - && FrontendDiagnostic.sourcePathText(Path.of("tmp", "type_check_vector_initializer_compatibility.gd")) - .equals(diagnostic.sourcePath()) + && Objects.equals( + FrontendDiagnostic.sourcePathText(Path.of("tmp", "type_check_vector_initializer_compatibility.gd")), + diagnostic.sourcePath() + ) && diagnostic.range() != null )); assertTrue(typeCheckDiagnostics.stream().anyMatch(diagnostic -> @@ -776,8 +780,10 @@ func reject_return(text: String) -> int: assertEquals(3, typeCheckDiagnostics.size()); assertTrue(typeCheckDiagnostics.stream().allMatch(diagnostic -> diagnostic.severity() == FrontendDiagnosticSeverity.ERROR - && FrontendDiagnostic.sourcePathText(Path.of("tmp", "type_check_string_family_boundaries.gd")) - .equals(diagnostic.sourcePath()) + && Objects.equals( + FrontendDiagnostic.sourcePathText(Path.of("tmp", "type_check_string_family_boundaries.gd")), + diagnostic.sourcePath() + ) && diagnostic.range() != null )); assertTrue(typeCheckDiagnostics.stream().anyMatch(diagnostic -> @@ -832,8 +838,10 @@ void analyzeSkipsInheritedPropertyInitializerBoundaryDiagnosticsOwnedByUpstreamP ); assertEquals( "int", - requireInitializerType(preparedInput.unit().ast(), "allowed_helper", preparedInput) - .publishedType() + Objects.requireNonNull( + requireInitializerType(preparedInput.unit().ast(), "allowed_helper", preparedInput) + .publishedType() + ) .getTypeName() ); assertTrue(diagnosticsByCategory( @@ -903,8 +911,10 @@ func _init(): assertEquals(5, typeCheckDiagnostics.size()); assertTrue(typeCheckDiagnostics.stream().allMatch(diagnostic -> diagnostic.severity() == FrontendDiagnosticSeverity.ERROR - && FrontendDiagnostic.sourcePathText(Path.of("tmp", "type_check_return_compatibility.gd")) - .equals(diagnostic.sourcePath()) + && Objects.equals( + FrontendDiagnostic.sourcePathText(Path.of("tmp", "type_check_return_compatibility.gd")), + diagnostic.sourcePath() + ) && diagnostic.range() != null )); assertTrue(typeCheckDiagnostics.stream().anyMatch(diagnostic -> @@ -1020,8 +1030,10 @@ func rejects_wrong_dimension(value: Vector2i) -> Vector3: assertEquals(2, typeCheckDiagnostics.size()); assertTrue(typeCheckDiagnostics.stream().allMatch(diagnostic -> diagnostic.severity() == FrontendDiagnosticSeverity.ERROR - && FrontendDiagnostic.sourcePathText(Path.of("tmp", "type_check_vector_return_compatibility.gd")) - .equals(diagnostic.sourcePath()) + && Objects.equals( + FrontendDiagnostic.sourcePathText(Path.of("tmp", "type_check_vector_return_compatibility.gd")), + diagnostic.sourcePath() + ) && diagnostic.range() != null )); assertTrue(typeCheckDiagnostics.stream().anyMatch(diagnostic -> @@ -1277,7 +1289,7 @@ private static void assertEvent( @NotNull String fileName, @NotNull String source, @NotNull ClassRegistry classRegistry - ) throws Exception { + ) { var parserService = new GdScriptParserService(); var diagnosticManager = new DiagnosticManager(); var unit = parserService.parseUnit(Path.of("tmp", fileName), source, diagnosticManager); @@ -1296,12 +1308,7 @@ private static void assertEvent( analysisData.updateDiagnostics(diagnosticManager.snapshot()); new FrontendVariableAnalyzer().analyze(analysisData, diagnosticManager); analysisData.updateDiagnostics(diagnosticManager.snapshot()); - new FrontendTopBindingAnalyzer().analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - new FrontendChainBindingAnalyzer().analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - new FrontendExprTypeAnalyzer().analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); + FrontendSuiteResolverStageTestSupport.resolveAllOwners(classRegistry, analysisData, diagnosticManager); return new PreparedTypeCheckInput(unit, analysisData, diagnosticManager, classRegistry); } diff --git a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendVirtualOverrideAnalyzerTest.java b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendVirtualOverrideAnalyzerTest.java index f6d40ec2..626b0029 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendVirtualOverrideAnalyzerTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendVirtualOverrideAnalyzerTest.java @@ -158,28 +158,43 @@ static func _ready() -> void: && diagnostic.range() != null )); assertTrue(overrideDiagnostics.stream().anyMatch(diagnostic -> - diagnostic.sourcePath().equals(FrontendDiagnostic.sourcePathText(Path.of("tmp", "ready_with_arg.gd"))) + Objects.equals( + diagnostic.sourcePath(), + FrontendDiagnostic.sourcePathText(Path.of("tmp", "ready_with_arg.gd")) + ) && diagnostic.message().contains("_ready") && diagnostic.message().contains("declares 1 parameter(s); expected 0") )); assertTrue(overrideDiagnostics.stream().anyMatch(diagnostic -> - diagnostic.sourcePath().equals(FrontendDiagnostic.sourcePathText(Path.of("tmp", "ready_returns_int.gd"))) + Objects.equals( + diagnostic.sourcePath(), + FrontendDiagnostic.sourcePathText(Path.of("tmp", "ready_returns_int.gd")) + ) && diagnostic.message().contains("returns 'int'; expected 'void'") )); assertTrue(overrideDiagnostics.stream().anyMatch(diagnostic -> - diagnostic.sourcePath().equals(FrontendDiagnostic.sourcePathText(Path.of("tmp", "process_delta_variant.gd"))) + Objects.equals( + diagnostic.sourcePath(), + FrontendDiagnostic.sourcePathText(Path.of("tmp", "process_delta_variant.gd")) + ) && diagnostic.message().contains("parameter #1 'delta'") && diagnostic.message().contains("Variant") && diagnostic.message().contains("float") )); assertTrue(overrideDiagnostics.stream().anyMatch(diagnostic -> - diagnostic.sourcePath().equals(FrontendDiagnostic.sourcePathText(Path.of("tmp", "physics_process_int.gd"))) + Objects.equals( + diagnostic.sourcePath(), + FrontendDiagnostic.sourcePathText(Path.of("tmp", "physics_process_int.gd")) + ) && diagnostic.message().contains("_physics_process") && diagnostic.message().contains("int") && diagnostic.message().contains("float") )); assertTrue(overrideDiagnostics.stream().anyMatch(diagnostic -> - diagnostic.sourcePath().equals(FrontendDiagnostic.sourcePathText(Path.of("tmp", "static_ready.gd"))) + Objects.equals( + diagnostic.sourcePath(), + FrontendDiagnostic.sourcePathText(Path.of("tmp", "static_ready.gd")) + ) && diagnostic.message().contains("declared static") )); @@ -231,6 +246,7 @@ func _process(delta) -> void: assertEquals(GdVariantType.VARIANT, analyzedModule.analysisData().slotTypes().get(aliasVariable)); } + @SuppressWarnings("SameParameterValue") private static @NotNull AnalyzedModule analyze( @NotNull String fileName, @NotNull String source @@ -282,14 +298,7 @@ func _process(delta) -> void: analysisData.updateDiagnostics(diagnosticManager.snapshot()); new FrontendVariableAnalyzer().analyze(analysisData, diagnosticManager); analysisData.updateDiagnostics(diagnosticManager.snapshot()); - new FrontendTopBindingAnalyzer().analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - new FrontendChainBindingAnalyzer().analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - new FrontendExprTypeAnalyzer().analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - new FrontendVarTypePostAnalyzer().analyze(analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); + FrontendSuiteResolverStageTestSupport.resolveAllOwners(classRegistry, analysisData, diagnosticManager); new FrontendAnnotationUsageAnalyzer().analyze(classRegistry, analysisData, diagnosticManager); analysisData.updateDiagnostics(diagnosticManager.snapshot()); return new PreparedVirtualOverrideInput(units, analysisData, diagnosticManager, classRegistry); @@ -333,10 +342,12 @@ func _process(delta) -> void: .orElseThrow(() -> new AssertionError("Function not found: " + functionName)); } + @SuppressWarnings("SameParameterValue") private static @NotNull FunctionDeclaration findFunction(@NotNull Node root, @NotNull String functionName) { return findNode(root, FunctionDeclaration.class, function -> function.name().equals(functionName)); } + @SuppressWarnings("SameParameterValue") private static @NotNull VariableDeclaration findVariable( @NotNull List statements, @NotNull String variableName @@ -384,9 +395,9 @@ private record AnalyzedModule( ) { private AnalyzedModule { units = List.copyOf(Objects.requireNonNull(units, "units must not be null")); - analysisData = Objects.requireNonNull(analysisData, "analysisData must not be null"); - diagnosticManager = Objects.requireNonNull(diagnosticManager, "diagnosticManager must not be null"); - classRegistry = Objects.requireNonNull(classRegistry, "classRegistry must not be null"); + Objects.requireNonNull(analysisData, "analysisData must not be null"); + Objects.requireNonNull(diagnosticManager, "diagnosticManager must not be null"); + Objects.requireNonNull(classRegistry, "classRegistry must not be null"); } } @@ -398,9 +409,9 @@ private record PreparedVirtualOverrideInput( ) { private PreparedVirtualOverrideInput { units = List.copyOf(Objects.requireNonNull(units, "units must not be null")); - analysisData = Objects.requireNonNull(analysisData, "analysisData must not be null"); - diagnosticManager = Objects.requireNonNull(diagnosticManager, "diagnosticManager must not be null"); - classRegistry = Objects.requireNonNull(classRegistry, "classRegistry must not be null"); + Objects.requireNonNull(analysisData, "analysisData must not be null"); + Objects.requireNonNull(diagnosticManager, "diagnosticManager must not be null"); + Objects.requireNonNull(classRegistry, "classRegistry must not be null"); } } } diff --git a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendChainReductionFacadeTest.java b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendChainReductionFacadeTest.java index 2836ff77..6b8c1745 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendChainReductionFacadeTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendChainReductionFacadeTest.java @@ -8,6 +8,9 @@ import gd.script.gdcc.frontend.sema.FrontendAnalysisData; import gd.script.gdcc.frontend.sema.FrontendBinding; import gd.script.gdcc.frontend.sema.FrontendBindingKind; +import gd.script.gdcc.frontend.sema.FrontendSemanticStage; +import gd.script.gdcc.frontend.sema.FrontendTypedLexicalEnvironment; +import gd.script.gdcc.frontend.sema.patch.FrontendLocalSlotTypeUpdate; import gd.script.gdcc.gdextension.ExtensionApiLoader; import gd.script.gdcc.lir.LirClassDef; import gd.script.gdcc.lir.LirPropertyDef; @@ -18,6 +21,7 @@ import gd.script.gdcc.scope.ScopeValueKind; import gd.script.gdcc.type.GdObjectType; import gd.script.gdcc.type.GdStringType; +import gd.script.gdcc.type.GdVariantType; import dev.superice.gdparser.frontend.ast.AttributeExpression; import dev.superice.gdparser.frontend.ast.AttributePropertyStep; import dev.superice.gdparser.frontend.ast.AttributeStep; @@ -94,6 +98,53 @@ void reduceShouldCacheHeadFailureAsStructuredFailedReduction() throws Exception assertSame(first.result(), second.result()); } + @Test + void reduceUsesInjectedOverlayBindingBeforeStableVariantPayload() throws Exception { + var context = newTestContext(); + var worker = identifier("worker"); + var workerClass = new LirClassDef("Worker", "Object"); + workerClass.addProperty(new LirPropertyDef("payload", GdStringType.STRING)); + context.registry().addGdccClass(workerClass); + context.bodyScope().defineLocal("worker", GdVariantType.VARIANT, worker); + var stableValue = context.bodyScope().resolveValueHere("worker"); + assertNotNull(stableValue); + context.analysisData().scopesByAst().put(worker, context.bodyScope()); + context.analysisData().symbolBindings().put( + worker, + new FrontendBinding( + "worker", + FrontendBindingKind.LOCAL_VAR, + worker, + stableValue, + ScopeLookupStatus.FOUND_ALLOWED + ) + ); + var environment = new FrontendTypedLexicalEnvironment(context.bodyScope(), context.analysisData()); + environment.addLocalSlotTypeUpdate( + FrontendSemanticStage.LOCAL_TYPE_STABILIZATION, + new FrontendLocalSlotTypeUpdate(context.bodyScope(), "worker", worker, new GdObjectType("Worker")) + ); + var facade = new FrontendChainReductionFacade( + context.analysisData(), + context.analysisData().scopesByAst(), + ResolveRestriction::unrestricted, + () -> false, + () -> null, + context.registry(), + (expression, finalizeWindow) -> FrontendChainReductionHelper.ExpressionTypeResult.failed( + "unexpected expression type lookup for " + expression.getClass().getSimpleName() + ), + identifier -> environment.symbolBinding(identifier) + ); + + var reduction = facade.reduce(chain(worker, property("payload"))).result(); + + assertNotNull(reduction); + assertEquals(GdVariantType.VARIANT, stableValue.type()); + assertEquals(FrontendChainReductionHelper.Status.RESOLVED, reduction.finalReceiver().status()); + assertEquals(GdStringType.STRING, reduction.finalReceiver().receiverType()); + } + private static @NotNull FrontendChainReductionFacade newFacade(@NotNull TestContext context) { return new FrontendChainReductionFacade( context.analysisData(), diff --git a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendChainReductionHelperTest.java b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendChainReductionHelperTest.java index f79c2470..926a1329 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendChainReductionHelperTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendChainReductionHelperTest.java @@ -4,6 +4,7 @@ import gd.script.gdcc.frontend.sema.FrontendBindingKind; import gd.script.gdcc.frontend.sema.FrontendCallResolutionKind; import gd.script.gdcc.frontend.sema.FrontendCallResolutionStatus; +import gd.script.gdcc.frontend.sema.FrontendExpressionType; import gd.script.gdcc.frontend.sema.FrontendReceiverKind; import gd.script.gdcc.gdextension.ExtensionAPI; import gd.script.gdcc.gdextension.ExtensionBuiltinClass; @@ -197,6 +198,41 @@ void reduceRetriesDeferredCallArgumentsInside_AndContinuesExactSuffix() { assertNull(result.recoveryRoot()); } + @Test + void reduceRetriesOnlyDeferredArgumentsDuringFinalizeWindow() { + var worker = newClass("Worker"); + worker.addFunction(newMethod("combine", GdStringType.STRING, false, GdIntType.INT, GdStringType.STRING)); + var registry = newRegistry(List.of(stringBuiltinWithLength()), List.of(worker)); + var immediate = identifier("immediate"); + var deferred = identifier("deferred"); + var chain = chain(identifier("worker"), call("combine", immediate, deferred), property("length")); + + var result = FrontendChainReductionHelper.reduce(request( + chain, + FrontendChainReductionHelper.ReceiverState.resolvedInstance(new GdObjectType("Worker")), + registry, + (expression, finalizeWindow) -> { + if (expression == immediate) { + return FrontendChainReductionHelper.ExpressionTypeResult.resolved(GdIntType.INT); + } + if (expression == deferred) { + return finalizeWindow + ? FrontendChainReductionHelper.ExpressionTypeResult.resolved(GdStringType.STRING) + : FrontendChainReductionHelper.ExpressionTypeResult.deferred("deferred type pending"); + } + return FrontendChainReductionHelper.ExpressionTypeResult.failed("unexpected expression"); + } + )); + + assertEquals(FrontendChainReductionHelper.Status.RESOLVED, result.stepTraces().getFirst().status()); + assertTrue(result.stepTraces().getFirst().finalizeRetryUsed()); + var combineCall = result.stepTraces().getFirst().suggestedCall(); + assertNotNull(combineCall); + assertEquals(FrontendCallResolutionKind.INSTANCE_METHOD, combineCall.callKind()); + assertEquals(List.of(GdIntType.INT, GdStringType.STRING), combineCall.argumentTypes()); + assertEquals(FrontendChainReductionHelper.Status.RESOLVED, result.stepTraces().get(1).status()); + } + @Test void reducePublishesDeferredStepAfter_MissAndStopsSuffix() { var worker = newClass("Worker"); @@ -275,6 +311,38 @@ void reduceUsesDynamicVariantArgumentToContinueExactInstanceCallResolution() { assertEquals(List.of(GdVariantType.VARIANT), consumeCall.argumentTypes()); } + @Test + void reduceUsesInjectedResolverBeforeStaleStableExpressionType() { + var worker = newClass("Worker"); + worker.addFunction(newMethod("consume", GdStringType.STRING, false, GdIntType.INT)); + var registry = newRegistry(List.of(stringBuiltinWithLength()), List.of(worker)); + var seed = identifier("seed"); + var chain = chain(identifier("worker"), call("consume", seed), property("length")); + var analysisData = FrontendAnalysisData.bootstrap(); + analysisData.expressionTypes().put(seed, FrontendExpressionType.resolved(GdVariantType.VARIANT)); + + var result = FrontendChainReductionHelper.reduce(new FrontendChainReductionHelper.ReductionRequest( + chain, + FrontendChainReductionHelper.ReceiverState.resolvedInstance(new GdObjectType("Worker")), + analysisData, + registry, + (expression, _) -> expression == seed + ? FrontendChainReductionHelper.ExpressionTypeResult.resolved(GdIntType.INT) + : FrontendChainReductionHelper.ExpressionTypeResult.failed("unexpected expression"), + _ -> { + } + )); + + var consumeCall = result.stepTraces().getFirst().suggestedCall(); + assertNotNull(consumeCall); + assertAll( + () -> assertEquals(FrontendChainReductionHelper.Status.RESOLVED, result.stepTraces().getFirst().status()), + () -> assertEquals(FrontendChainReductionHelper.Status.RESOLVED, result.stepTraces().get(1).status()), + () -> assertEquals(List.of(GdIntType.INT), consumeCall.argumentTypes()), + () -> assertEquals(FrontendExpressionType.resolved(GdVariantType.VARIANT), analysisData.expressionTypes().get(seed)) + ); + } + @Test void reduceUsesExactVariantArgumentToResolveStaticTypeMetaCall() { var worker = newClass("Worker"); diff --git a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendExpressionSemanticSupportTest.java b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendExpressionSemanticSupportTest.java index 1a192272..fc50dc34 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendExpressionSemanticSupportTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendExpressionSemanticSupportTest.java @@ -1,5 +1,9 @@ package gd.script.gdcc.frontend.sema.analyzer.support; +import gd.script.gdcc.frontend.scope.BlockScope; +import gd.script.gdcc.frontend.scope.BlockScopeKind; +import gd.script.gdcc.frontend.scope.CallableScope; +import gd.script.gdcc.frontend.scope.CallableScopeKind; import gd.script.gdcc.frontend.diagnostic.DiagnosticManager; import gd.script.gdcc.frontend.parse.FrontendModule; import gd.script.gdcc.frontend.parse.GdScriptParserService; @@ -11,7 +15,10 @@ import gd.script.gdcc.frontend.sema.FrontendExpressionType; import gd.script.gdcc.frontend.sema.FrontendExpressionTypeStatus; import gd.script.gdcc.frontend.sema.FrontendReceiverKind; +import gd.script.gdcc.frontend.sema.FrontendSemanticStage; +import gd.script.gdcc.frontend.sema.FrontendTypedLexicalEnvironment; import gd.script.gdcc.frontend.sema.analyzer.FrontendSemanticAnalyzer; +import gd.script.gdcc.frontend.sema.patch.FrontendLocalSlotTypeUpdate; import gd.script.gdcc.frontend.scope.ClassScope; import gd.script.gdcc.gdextension.ExtensionApiLoader; import gd.script.gdcc.lir.LirClassDef; @@ -19,6 +26,7 @@ import gd.script.gdcc.lir.LirParameterDef; import gd.script.gdcc.scope.ClassRegistry; import gd.script.gdcc.scope.ResolveRestriction; +import gd.script.gdcc.scope.ScopeLookupStatus; import gd.script.gdcc.type.GdCallableType; import gd.script.gdcc.type.GdFloatType; import gd.script.gdcc.type.GdFloatVectorType; @@ -158,6 +166,52 @@ func ping(seed): assertTrue(seedResult.expressionType().detailReason().contains("missing its top-binding resolved value payload")); } + @Test + void resolveIdentifierExpressionTypeUsesInjectedOverlayBindingBeforeStableVariantPayload() throws Exception { + var analysisData = FrontendAnalysisData.bootstrap(); + var registry = new ClassRegistry(ExtensionApiLoader.loadDefault()); + var classScope = new ClassScope(registry, registry, new LirClassDef("OverlayHost", "RefCounted")); + var callableScope = new CallableScope(classScope, CallableScopeKind.FUNCTION_DECLARATION); + var bodyScope = new BlockScope(callableScope, BlockScopeKind.FUNCTION_BODY); + var seed = identifier("seed"); + bodyScope.defineLocal("seed", GdVariantType.VARIANT, seed); + var stableValue = bodyScope.resolveValueHere("seed"); + assertNotNull(stableValue); + analysisData.scopesByAst().put(seed, bodyScope); + analysisData.symbolBindings().put( + seed, + new FrontendBinding("seed", FrontendBindingKind.LOCAL_VAR, seed, stableValue, ScopeLookupStatus.FOUND_ALLOWED) + ); + var environment = new FrontendTypedLexicalEnvironment(bodyScope, analysisData); + environment.addLocalSlotTypeUpdate( + FrontendSemanticStage.LOCAL_TYPE_STABILIZATION, + new FrontendLocalSlotTypeUpdate(bodyScope, "seed", seed, GdIntType.INT) + ); + var support = new FrontendExpressionSemanticSupport( + identifier -> environment.symbolBinding(identifier), + analysisData.scopesByAst(), + ResolveRestriction::instanceContext, + () -> null, + registry, + () -> new FrontendChainHeadReceiverSupport( + analysisData, + analysisData.scopesByAst(), + identifier -> environment.symbolBinding(identifier), + ResolveRestriction.instanceContext(), + false, + null, + _ -> null, + _ -> null + ) + ); + + var result = support.resolveIdentifierExpressionType(seed); + + assertEquals(GdVariantType.VARIANT, stableValue.type()); + assertEquals(FrontendExpressionTypeStatus.RESOLVED, result.expressionType().status()); + assertEquals(GdIntType.INT, result.expressionType().publishedType()); + } + @Test void resolveCallExpressionTypeDistinguishesResolvedBlockedAndUnsupportedCalls() throws Exception { var analyzed = analyze( diff --git a/src/test/java/gd/script/gdcc/frontend/sema/resolver/FrontendVisibleValueResolverTest.java b/src/test/java/gd/script/gdcc/frontend/sema/resolver/FrontendVisibleValueResolverTest.java index 88dc1823..d0230013 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/resolver/FrontendVisibleValueResolverTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/resolver/FrontendVisibleValueResolverTest.java @@ -6,23 +6,33 @@ import gd.script.gdcc.frontend.parse.FrontendSourceUnit; import gd.script.gdcc.frontend.parse.GdScriptParserService; import gd.script.gdcc.frontend.sema.FrontendAnalysisData; +import gd.script.gdcc.frontend.sema.FrontendBodyDeclarationIndex; +import gd.script.gdcc.frontend.sema.FrontendBodyLocalDeclaration; import gd.script.gdcc.frontend.sema.analyzer.FrontendSemanticAnalyzer; +import gd.script.gdcc.frontend.sema.FrontendSemanticStage; +import gd.script.gdcc.frontend.sema.FrontendTypedLexicalEnvironment; +import gd.script.gdcc.frontend.sema.patch.FrontendLocalSlotTypeUpdate; import gd.script.gdcc.frontend.scope.BlockScope; import gd.script.gdcc.frontend.scope.BlockScopeKind; import gd.script.gdcc.gdextension.ExtensionApiLoader; import gd.script.gdcc.scope.ClassRegistry; import gd.script.gdcc.scope.ResolveRestriction; import gd.script.gdcc.scope.ScopeValueKind; +import gd.script.gdcc.type.GdIntType; +import gd.script.gdcc.type.GdVariantType; import dev.superice.gdparser.frontend.ast.FunctionDeclaration; +import dev.superice.gdparser.frontend.ast.ForStatement; import dev.superice.gdparser.frontend.ast.IdentifierExpression; import dev.superice.gdparser.frontend.ast.Node; import dev.superice.gdparser.frontend.ast.SourceFile; +import dev.superice.gdparser.frontend.ast.VariableDeclaration; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.function.Predicate; @@ -40,7 +50,7 @@ void resolveFindsVisibleParameterInsideExecutableBody() throws Exception { var analyzedInput = analyzedInput("visible_parameter.gd", """ class_name VisibleParameter extends Node - + func ping(value: int): print(value) """); @@ -67,7 +77,7 @@ void resolveReturnsNotFoundWithFilteredFutureLocal() throws Exception { var analyzedInput = analyzedInput("future_local_not_found.gd", """ class_name FutureLocalNotFound extends Node - + func ping(): print(count) var count := 1 @@ -92,6 +102,148 @@ func ping(): assertEquals(ScopeValueKind.LOCAL, result.primaryFilteredHit().value().kind()); } + @Test + void resolveValidatesPublishedDeclarationIdentityWithoutReplacingRangeFilter() throws Exception { + var analyzedInput = analyzedInput("future_local_index.gd", """ + class_name FutureLocalIndex + extends Node + + func ping(): + print(count) + var count := 1 + """); + var pingFunction = findFunction(analyzedInput.unit().ast(), "ping"); + var useSite = findIdentifierExpression(pingFunction.body(), "count"); + var declaration = findNode( + pingFunction.body(), + VariableDeclaration.class, + variableDeclaration -> variableDeclaration.name().equals("count") + ); + var scope = assertInstanceOf(BlockScope.class, analyzedInput.analysisData().scopesByAst().get(declaration)); + var binding = Objects.requireNonNull(scope.resolveValueHere("count"), "count local must be published"); + var publishedIndex = new FrontendBodyDeclarationIndex(Map.of( + pingFunction.body(), + List.of(new FrontendBodyLocalDeclaration( + declaration, + binding, + FrontendBodyLocalDeclaration.Kind.ORDINARY_VAR, + 0 + )) + )); + var resolver = new FrontendVisibleValueResolver( + analyzedInput.analysisData(), + publishedIndex + ); + + var result = resolver.resolve(new FrontendVisibleValueResolveRequest( + "count", + useSite, + FrontendVisibleValueDomain.EXECUTABLE_BODY + )); + + assertEquals(FrontendVisibleValueStatus.NOT_FOUND, result.status()); + assertEquals( + FrontendFilteredValueHitReason.DECLARATION_AFTER_USE_SITE, + result.primaryFilteredHit().reason() + ); + + var incompleteIndex = new FrontendBodyDeclarationIndex(Map.of()); + var incompleteResolver = new FrontendVisibleValueResolver( + analyzedInput.analysisData(), + incompleteIndex + ); + assertThrows(IllegalStateException.class, () -> incompleteResolver.resolve( + new FrontendVisibleValueResolveRequest( + "count", + useSite, + FrontendVisibleValueDomain.EXECUTABLE_BODY + ) + )); + } + + @Test + void resolveWithTypedEnvironmentUsesOverlayTypeWithoutBypassingVisibilityFilter() throws Exception { + var visibleInput = analyzedInput("visible_overlay_local.gd", """ + class_name VisibleOverlayLocal + extends Node + + func ping(): + var count + print(count) + """); + var visibleFunction = findFunction(visibleInput.unit().ast(), "ping"); + var visibleUseSite = findIdentifierExpression(visibleFunction.body(), "count"); + var visibleResolver = new FrontendVisibleValueResolver(visibleInput.analysisData()); + var visibleBaseline = visibleResolver.resolve(new FrontendVisibleValueResolveRequest( + "count", + visibleUseSite, + FrontendVisibleValueDomain.EXECUTABLE_BODY + )); + var visibleScope = assertInstanceOf(BlockScope.class, visibleInput.analysisData().scopesByAst().get(visibleUseSite)); + var visibleEnvironment = new FrontendTypedLexicalEnvironment(visibleScope, visibleInput.analysisData()); + visibleEnvironment.addLocalSlotTypeUpdate( + FrontendSemanticStage.LOCAL_TYPE_STABILIZATION, + new FrontendLocalSlotTypeUpdate( + visibleScope, + "count", + visibleBaseline.visibleValue().declaration(), + GdIntType.INT + ) + ); + + var visibleWithOverlay = visibleResolver.resolve(new FrontendVisibleValueResolveRequest( + "count", + visibleUseSite, + FrontendVisibleValueDomain.EXECUTABLE_BODY + ), visibleEnvironment); + + assertEquals(FrontendVisibleValueStatus.FOUND_ALLOWED, visibleWithOverlay.status()); + assertSame(GdVariantType.VARIANT, visibleBaseline.visibleValue().type()); + assertSame(GdIntType.INT, visibleWithOverlay.visibleValue().type()); + assertSame(GdVariantType.VARIANT, visibleScope.resolveValueHere("count").type()); + + var futureInput = analyzedInput("future_overlay_local.gd", """ + class_name FutureOverlayLocal + extends Node + + func ping(): + print(later) + var later + """); + var futureUseSite = findIdentifierExpression(futureInput.unit().ast(), "later"); + var futureResolver = new FrontendVisibleValueResolver(futureInput.analysisData()); + var futureBaseline = futureResolver.resolve(new FrontendVisibleValueResolveRequest( + "later", + futureUseSite, + FrontendVisibleValueDomain.EXECUTABLE_BODY + )); + var filteredHit = futureBaseline.primaryFilteredHit(); + var futureEnvironment = new FrontendTypedLexicalEnvironment(filteredHit.owningScope(), futureInput.analysisData()); + var futureScope = assertInstanceOf(BlockScope.class, filteredHit.owningScope()); + futureEnvironment.addLocalSlotTypeUpdate( + FrontendSemanticStage.LOCAL_TYPE_STABILIZATION, + new FrontendLocalSlotTypeUpdate( + futureScope, + "later", + filteredHit.value().declaration(), + GdIntType.INT + ) + ); + + var futureWithOverlay = futureResolver.resolve(new FrontendVisibleValueResolveRequest( + "later", + futureUseSite, + FrontendVisibleValueDomain.EXECUTABLE_BODY + ), futureEnvironment); + + assertEquals(FrontendVisibleValueStatus.NOT_FOUND, futureWithOverlay.status()); + assertNull(futureWithOverlay.visibleValue()); + assertEquals( + FrontendFilteredValueHitReason.DECLARATION_AFTER_USE_SITE, + futureWithOverlay.primaryFilteredHit().reason() + ); + } + @Test void resolveFiltersInitializerSelfReferenceAndFallsBackToOuterClassProperty() throws Exception { var analyzedInput = analyzedInput("self_reference_initializer.gd", """ @@ -208,7 +360,7 @@ func ping(value, alias = value): assertTrue(result.filteredHits().isEmpty()); assertEquals(FrontendVisibleValueDomain.PARAMETER_DEFAULT, result.deferredBoundary().domain()); assertEquals( - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED, + FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN, result.deferredBoundary().reason() ); } @@ -236,7 +388,7 @@ func ping(seed: int): assertEquals(FrontendVisibleValueStatus.DEFERRED_UNSUPPORTED, result.status()); assertEquals(FrontendVisibleValueDomain.LAMBDA_SUBTREE, result.deferredBoundary().domain()); assertEquals( - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED, + FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN, result.deferredBoundary().reason() ); } @@ -265,7 +417,7 @@ func ping(seed: int): assertTrue(result.filteredHits().isEmpty()); assertEquals(FrontendVisibleValueDomain.BLOCK_LOCAL_CONST_SUBTREE, result.deferredBoundary().domain()); assertEquals( - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED, + FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN, result.deferredBoundary().reason() ); } @@ -297,7 +449,7 @@ func ping(): assertTrue(result.filteredHits().isEmpty()); assertEquals(FrontendVisibleValueDomain.BLOCK_LOCAL_CONST_SUBTREE, result.deferredBoundary().domain()); assertEquals( - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED, + FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN, result.deferredBoundary().reason() ); } @@ -329,9 +481,9 @@ func ping(seed: int): } @Test - void resolveSealsForBodyUseSiteAsDeferredUnsupported() throws Exception { - var analyzedInput = analyzedInput("for_body_deferred.gd", """ - class_name ForBodyDeferred + void resolveForBodyIteratorAsVisibleLocalWithoutFallingBackToClassProperty() throws Exception { + var analyzedInput = analyzedInput("for_body_iterator_visible.gd", """ + class_name ForBodyIteratorVisible extends Node var item = 100 @@ -350,20 +502,45 @@ func ping(values): FrontendVisibleValueDomain.EXECUTABLE_BODY )); - assertEquals(FrontendVisibleValueStatus.DEFERRED_UNSUPPORTED, result.status()); - assertNull(result.visibleValue()); + assertEquals(FrontendVisibleValueStatus.FOUND_ALLOWED, result.status()); + assertNotNull(result.visibleValue()); + assertEquals(ScopeValueKind.LOCAL, result.visibleValue().kind()); + assertInstanceOf(ForStatement.class, result.visibleValue().declaration()); assertTrue(result.filteredHits().isEmpty()); - assertEquals(FrontendVisibleValueDomain.FOR_SUBTREE, result.deferredBoundary().domain()); - assertEquals( - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED, - result.deferredBoundary().reason() - ); + assertNull(result.deferredBoundary()); + } + + @Test + void resolveForBodyCanReadOuterParameterThroughNormalLookup() throws Exception { + var analyzedInput = analyzedInput("for_body_outer_parameter.gd", """ + class_name ForBodyOuterParameter + extends Node + + func ping(values, seed): + for item in values: + print(seed) + """); + var forStatement = findNode(analyzedInput.unit().ast(), ForStatement.class, _ -> true); + var useSite = findIdentifierExpression(forStatement.body(), "seed"); + var resolver = new FrontendVisibleValueResolver(analyzedInput.analysisData()); + + var result = resolver.resolve(new FrontendVisibleValueResolveRequest( + "seed", + useSite, + FrontendVisibleValueDomain.EXECUTABLE_BODY + )); + + assertEquals(FrontendVisibleValueStatus.FOUND_ALLOWED, result.status()); + assertNotNull(result.visibleValue()); + assertEquals(ScopeValueKind.PARAMETER, result.visibleValue().kind()); + assertTrue(result.filteredHits().isEmpty()); + assertNull(result.deferredBoundary()); } @Test - void resolveSealsForIterableAsDeferredUnsupported() throws Exception { - var analyzedInput = analyzedInput("for_iterable_deferred.gd", """ - class_name ForIterableDeferred + void resolveForIterableInOuterScopeNormally() throws Exception { + var analyzedInput = analyzedInput("for_iterable_outer_scope.gd", """ + class_name ForIterableOuterScope extends Node var item = 100 @@ -381,16 +558,14 @@ func ping(): FrontendVisibleValueDomain.EXECUTABLE_BODY )); - assertEquals(FrontendVisibleValueStatus.DEFERRED_UNSUPPORTED, result.status()); - assertEquals(FrontendVisibleValueDomain.FOR_SUBTREE, result.deferredBoundary().domain()); - assertEquals( - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED, - result.deferredBoundary().reason() - ); + assertEquals(FrontendVisibleValueStatus.FOUND_ALLOWED, result.status()); + assertNotNull(result.visibleValue()); + assertEquals(ScopeValueKind.PROPERTY, result.visibleValue().kind()); + assertNull(result.deferredBoundary()); } @Test - void resolveRejectsSyntheticForBodyCurrentScopeEvenWithoutForAstBoundary() throws Exception { + void resolveAllowsSyntheticForBodyCurrentScopeWithoutForAstBoundary() throws Exception { var analyzedInput = analyzedInput("synthetic_for_body_scope.gd", """ class_name SyntheticForBodyScope extends Node @@ -409,12 +584,10 @@ func ping(value): FrontendVisibleValueDomain.EXECUTABLE_BODY )); - assertEquals(FrontendVisibleValueStatus.DEFERRED_UNSUPPORTED, result.status()); - assertEquals(FrontendVisibleValueDomain.FOR_SUBTREE, result.deferredBoundary().domain()); - assertEquals( - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED, - result.deferredBoundary().reason() - ); + assertEquals(FrontendVisibleValueStatus.FOUND_ALLOWED, result.status()); + assertNotNull(result.visibleValue()); + assertEquals(ScopeValueKind.PARAMETER, result.visibleValue().kind()); + assertNull(result.deferredBoundary()); } @Test @@ -443,7 +616,7 @@ func ping(value): assertEquals(FrontendVisibleValueStatus.DEFERRED_UNSUPPORTED, result.status()); assertEquals(FrontendVisibleValueDomain.MATCH_SUBTREE, result.deferredBoundary().domain()); assertEquals( - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED, + FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN, result.deferredBoundary().reason() ); } @@ -471,7 +644,7 @@ func ping(value): assertEquals(FrontendVisibleValueStatus.DEFERRED_UNSUPPORTED, result.status()); assertEquals(FrontendVisibleValueDomain.LAMBDA_SUBTREE, result.deferredBoundary().domain()); assertEquals( - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED, + FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN, result.deferredBoundary().reason() ); } @@ -500,7 +673,7 @@ func ping(value): assertEquals(FrontendVisibleValueStatus.DEFERRED_UNSUPPORTED, result.status()); assertEquals(FrontendVisibleValueDomain.MATCH_SUBTREE, result.deferredBoundary().domain()); assertEquals( - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED, + FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN, result.deferredBoundary().reason() ); }