From 0fec99d65df837aa825b419b6beb08c8e6002c41 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Sun, 5 Jul 2026 10:39:41 +0800 Subject: [PATCH 01/27] docs(frontend): add implementation plans for range loop and segmented type resolution pipeline - Document for-range loop support roadmap, covering range iterator intrinsic contract, shared semantic, compile gate, and CFG/body lowering integration - Document segmented type resolution pipeline to enable source-order local type fact publication for prefix-typed expressions in loops - Outline scope, motivation, non-goals, dependencies, and fact-source references for both plans - Both plans target frontend sema, lowering, type modules and related test paths --- ...tend_for_range_loop_implementation_plan.md | 465 +++++++++++++ ...segmented_type_resolution_pipeline_plan.md | 647 ++++++++++++++++++ 2 files changed, 1112 insertions(+) create mode 100644 doc/module_impl/frontend/frontend_for_range_loop_implementation_plan.md create mode 100644 doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md 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..a7c404b6 --- /dev/null +++ b/doc/module_impl/frontend/frontend_for_range_loop_implementation_plan.md @@ -0,0 +1,465 @@ +# Frontend for-range loop 实施计划 + +> 本文档记录 `for` loop 从当前 deferred 状态推进到“仅支持 range-like loop”的具体实施计划。本文中的 `for-range` 特指所有最终复用 `gdcc.for_range_iter.*` intrinsic contract 的 loop form:既包括 `for iterator[: Type] in range(...)`,也包括 Godot 兼容的 `for iterator[: Type] in ` 数值简写。目标是先让这两类 loop 进入 frontend shared semantic、compile gate、frontend CFG 与 body lowering 的正式支持面,同时要求第一版实现就预留未来任意 iterable / iterator `for` 的可扩展内部 iterator contract。 + +## 文档状态 + +- 状态:计划中 +- 创建日期:2026-07-03 +- 适用范围: + - `src/main/java/gd/script/gdcc/frontend/sema/**` + - `src/main/java/gd/script/gdcc/frontend/lowering/**` + - `src/main/java/gd/script/gdcc/type/**` + - `src/test/java/gd/script/gdcc/frontend/**` + - `src/test/java/gd/script/gdcc/type/**` + - `src/test/java/gd/script/gdcc/backend/**` +- 关联事实源: + - `doc/module_impl/common_rules.md` + - `doc/module_impl/frontend/frontend_rules.md` + - `doc/module_impl/frontend/frontend_lowering_plan.md` + - `doc/module_impl/frontend/frontend_loop_control_flow_analyzer_implementation.md` + - `doc/module_impl/frontend/frontend_gdcompiler_type_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/gdcc_type_system.md` + - `doc/gdcc_lir_intrinsic.md` +- 外部语义参考: + - Godot docs `tutorials/scripting/gdscript/gdscript_basics.rst` + - Godot source `modules/gdscript/gdscript_utility_functions.cpp` + - Godot source `core/variant/variant_setget.cpp` + +## 1. 范围与非目标 + +本轮只实现 range-like loop 支持面: + +- 支持 `for i in range(stop):` +- 支持 `for i in range(start, end):` +- 支持 `for i in range(start, end, step):` +- 支持 `for i: int in range(...):` +- 支持 `for i in 3:`、`for i in limit:` 这类 `iterable` 已稳定发布为 `int` 的 Godot range 简写;其语义按 `range(stop)` 处理,但 frontend 不得通过伪造 `CallExpression("range")` AST 节点来实现。 +- range 参数必须在 frontend 表达式分析中拥有 lowering-ready typed fact,并能作为 `int` slot 下沉到 `gdcc.for_range_iter.init`。 +- `int` 简写的 `iterable` 表达式也必须在 frontend 表达式分析中拥有 lowering-ready typed fact,并能通过现有 ordinary boundary helper 进入 `int` stop slot。 +- loop iterator binding 的 source-facing element type 是 `int`,不是 `GdccForRangeIter`。 + +本轮明确不实现: + +- `for i in values:` 这类任意 iterable / iterator loop。 +- `Array`、`Dictionary`、`String`、`Object`、typed array 等容器遍历。 +- Godot 的 `for i in 2.2` / `for i in some_float` 浮点数值简写。它们仍属于 range-like 后续扩面,届时必须明确 `ceil` 语义并复用本文定义的内部 iterator contract,而不是再开一条平行 lowering 管线。 +- 将 `range(...)` 当作普通 user-facing callable value。`range(...)` 在本计划中只作为 `for` iterable 位置的 loop-specific form 识别,不向普通 `resolvedCalls()` 发布 utility function route。 +- 让 `GdCompilerType` 进入 source-facing declared type、ordinary `expressionTypes()`、ordinary local/property/return `slotTypes()`、public ABI、`Variant` pack/unpack 或 Godot runtime 普通调用路径。 + +## 2. 当前基线 + +当前代码库已经具备的基础: + +- `gdparser` 的 `ForStatement` AST 已包含 `iterator`、`iteratorType`、`iterable`、`body`、`range` 字段。 +- `GdScriptParserService` 只是外部 parser adapter,本仓库没有本地 parser grammar 可改;若 parse smoke 发现 `range` loop AST 形态不足,应先升级或修复外部 parser,而不是在 frontend analyzer 中猜文本。 +- `FrontendScopeAnalyzer` 已为 `ForStatement` 建立 `FOR_BODY` scope,并保证 `iteratorType` 与 `iterable` 在外层 scope 下遍历,`body` 在独立 `FOR_BODY` scope 下遍历。 +- `FrontendLoopControlFlowAnalyzer` 已把 `for` 与 `while` 一样视为 loop boundary,`break` / `continue` 在 `for` body 内不再报 `sema.loop_control_flow`。 +- `FrontendVisibleValueResolver`、`FrontendVariableAnalyzer`、`FrontendTopBindingAnalyzer`、`FrontendChainBindingAnalyzer`、`FrontendExprTypeAnalyzer`、`FrontendLocalTypeStabilizationAnalyzer`、`FrontendVarTypePostAnalyzer` 与 `FrontendCompileCheckAnalyzer` 仍分别把 `for` 作为 deferred / unsupported boundary。 +- `FrontendCfgGraphBuilder.processStatement(...)` 当前没有 `ForStatement` 分支;`break` / `continue` lowering 依赖 active `LoopFrame`。 +- `FrontendCfgRegion` 当前只允许 `BlockRegion`、`FrontendIfRegion`、`FrontendElifRegion`、`FrontendWhileRegion`。 +- `GdccForRangeIterType.FOR_RANGE_ITER` 已作为唯一 `GdCompilerType` 子类型存在。 +- `doc/gdcc_lir_intrinsic.md` 已冻结四个 backend-owned intrinsic:`gdcc.for_range_iter.init`、`gdcc.for_range_iter.should_continue`、`gdcc.for_range_iter.next`、`gdcc.for_range_iter.get`。 + +当前实现张力: + +- 文档仍把 `for` 放在 post-MVP / deferred 范围。 +- loop-control semantic 已把 `for` 当成合法 loop boundary。 +- backend/LIR 已具备 range iterator state 与 intrinsic。 +- frontend shared semantic、compile gate、CFG 与 body lowering 尚未承认 `for-range` 为 compile-ready surface。 + +本计划的实施目标就是把这个张力收口为:“复用 range intrinsic contract 的 `for-range` 正式支持;非 range-like `for` 继续 fail-closed。” + +## 3. 核心设计 + +### 3.1 range-like loop classifier + +新增一个 frontend 内部 helper,用于识别且描述 supported `for` loop source form,并把它规范化为 lowering 可消费的内部 contract。建议命名为 `FrontendForLoopSupport`,放在 frontend sema/lowering 均可复用的位置;如果只被少量 analyzer 使用,保持为普通 final util class,不引入 public interface。第一版虽然只返回 range-like contract,但 helper 名称与 API 要保持面向未来对象迭代的可扩展形状,避免第二种 iterable 接入时再做大面积改名。 + +识别规则: + +- 若 `ForStatement.iterable()` 是 `CallExpression`,则 `CallExpression.callee()` 必须是 bare `IdentifierExpression("range")`,且 `arguments().size()` 必须是 1、2 或 3。 +- 若 `ForStatement.iterable()` 不是 `range(...)` call,但其 root 已稳定发布 lowering-ready typed fact,且该 typed fact 可通过现有 ordinary typed-boundary helper 进入 `int` slot,则将其识别为 Godot 的 `range(stop)` 简写。 +- `float` 简写本轮仍不识别;它必须留给 follow-up,在明确 `ceil` 语义后复用同一 contract 扩面。 +- 不支持 named argument、attribute call、subscript call 或用户变量 `range` shadowing 的普通 callable 路由;本轮将 `range(...)` 视为 loop-specific syntax form。 +- `ForStatement.iterator()` 必须使用现有 AST 提供的 iterator name。 +- `ForStatement.iteratorType()` 可为空;为空时 iterator element type 为 `int`。 + +helper 输出建议使用一个简单 record,例如: + +```java +enum ForRangeSourceKind { + RANGE_CALL, + INT_SHORTHAND +} + +record ForRangeLoopSpec( + @NotNull ForStatement statement, + @NotNull String iteratorName, + @Nullable TypeRef declaredIteratorType, + @NotNull ForRangeSourceKind sourceKind, + @NotNull List sourceOperands, + @NotNull GdType elementType, + @NotNull GdCompilerType iteratorStateType, + @NotNull String initIntrinsicName, + @NotNull String shouldContinueIntrinsicName, + @NotNull String nextIntrinsicName, + @NotNull String getIntrinsicName +) {} +``` + +这里 `iteratorStateType` 第一版固定为 `GdccForRangeIterType.FOR_RANGE_ITER`,四个 intrinsic 名称第一版固定为 `gdcc.for_range_iter.*`。但 consumer 必须通过 `GdCompilerType` 与 contract 字段读取 storage / operation 协议,不能把 `GdccForRangeIterType` 或 `gdcc.for_range_iter` 字面量散落到多个 analyzer、CFG builder 与 lowering processor 中。`elementType` 第一版固定为 `GdIntType.INT`,用于 source-facing loop variable binding 与 ordinary typed boundary。 + +`sourceOperands` 保留源码级 iterable 形态,不伪造 AST: + +- `RANGE_CALL`:按源码顺序保存 1/2/3 个 `range(...)` arguments。 +- `INT_SHORTHAND`:只保存一个 `stop` expression;后续 CFG/lowering 通过 contract 语义把它解释为 `(start=0, end=stop, step=1)`,不要构造假的 `0` / `1` AST 节点或假的 `range(stop)` call。 + +不要为当前唯一实现新增 public `GdIteratorType`、`ForIterator` interface 或 builder。未来新增第二种 compiler-owned iterator state 时,再根据实际第二个 subtype 扩展 sealed `GdCompilerType` 层次与对应 contract;但第一版的 helper、CFG item 与 lowering API 必须已经采用通用命名,以便未来对象迭代直接挂到同一 contract 管线。 + +### 3.2 iterator state 与 element type 分离 + +range lowering 需要两个完全不同的类型事实: + +- iterator state type:`GdccForRangeIterType.FOR_RANGE_ITER`,只用于 hidden LIR local/temp、range intrinsic operand/result、C storage lifecycle。 +- element type:`GdIntType.INT`,用于 `for` iterator binding、body 中 `i` 的 visible value、`slotTypes()`、assignment/type-check 与 return/boundary materialization。 + +禁止事项: + +- 不把 `GdccForRangeIterType` 发布到 `expressionTypes()`。 +- 不把 `GdccForRangeIterType` 发布为 loop variable 的 `slotTypes()`。 +- 不让 `GdccForRangeIterType` 进入 declared type parser、ordinary implicit conversion matrix、public ABI 或 `Variant` pack/unpack。 +- 不通过普通 `CallItem` 表达 `range(...)`,避免 body lowering 把它当成 user-facing call route。 + +### 3.3 未来 iterator 接口预留 + +未来任意 iterable 的接线应保持以下形状: + +- semantic 阶段把 source iterable type 解析成一个内部 iterator contract。 +- iterator contract 持有: + - compiler-only `GdCompilerType` 子类型作为 iterator state storage。 + - source-facing `GdType` 作为 yielded element type。 + - init / should_continue / next / get 这组 operation 的 lowering 名称或 intrinsic 名称。 +- lowering 只消费该 contract,不重新推导 iterable 语义。 +- `range(...)`、`int` 简写、以及未来 `Object._iter_*` / container iterable 都必须复用这条 contract 管线;差异只能体现在 classifier 产出的 contract 内容,不得表现为多套互不相干的 `for` CFG / lowering 分支。 +- 第一版即使还不实现 `Array` / `Dictionary` / `Object` iterable,也必须把 `FrontendForRegion`、CFG item 与 lowering processor 命名设计为通用 `for-loop` 语义,而不是把 AST 级结构永久绑定到 `for-range` 专名。 + +第一版不需要把这个 contract 抽成 public interface;可以先把 range-like 的 record 与 helper 留在 frontend 内部。文档与测试必须锁住“state type 与 yield type 分离”这个接口边界,保证未来新增 `GdCompilerType` 子类型时不会把 compiler-only state 泄漏到 ordinary type 系统。 + +## 4. 分阶段实施步骤 + +### 阶段 A:parse / AST 基线测试 + +目标: + +- 确认外部 parser 对目标语法形态已经稳定产出 `ForStatement`。 +- 锁住 `iterator`、`iteratorType`、`iterable`、`body` 的 AST shape。 + +实施内容: + +- 增加或扩展 parse smoke / scope 测试,覆盖 `for i in range(3):`。 +- 增加或扩展 parse smoke / scope 测试,覆盖 `for i in 3:` 与 `for i in limit:`,确认 `iterable()` 保持原始 expression shape,而不是被 frontend 伪装成 `range(...)` call。 +- 覆盖 `for i: int in range(1, 3, 1):`,确认 `iteratorType()` 挂在 `ForStatement` 上,`iterable()` 是 `CallExpression`。 +- 不修改 `GdScriptParserService`,除非 parser diagnostic mapping 本身有问题。 + +验收细则: + +- `ForStatement.iterator()` 返回源码 iterator name。 +- `ForStatement.iteratorType()` 对 typed iterator 非空,对 untyped iterator 为空。 +- `ForStatement.iterable()` 是 bare `range(...)` call。 +- `for i in 3:` / `for i in limit:` 的 `ForStatement.iterable()` 保持原始 literal / identifier / expression 形态,不被 rewrite。 +- `ForStatement.body()` 已由 `FrontendScopeAnalyzer` 发布 `BlockScopeKind.FOR_BODY`。 + +### 阶段 B:range loop classifier 与诊断边界 + +目标: + +- 建立单一 for-range 判定入口。 +- 非 range-like `for` 保持 fail-closed,不因 `FOR_BODY` scope 已存在而误进入普通 executable body。 + +实施内容: + +- 新增 `FrontendForLoopSupport` 或等价 helper。 +- 提供 `tryClassifyRangeLoop(ForStatement)` / `isSupportedRangeLoop(ForStatement)` 这类简洁 API;它们虽然先返回 range-like contract,但名字与返回值形状要允许未来对象迭代扩展。 +- 对 arity 非 1/2/3 的 `range(...)` 发 frontend diagnostic。 +- 对 `range(..., 0)` 的 literal zero step 发 frontend diagnostic;非 literal step 不能静态确定为零时允许进入 lowering,由 runtime helper 保留最终保护。 +- 对 `for i in ` 识别为 supported range-like loop,并通过 contract 记下 `INT_SHORTHAND` source form。 +- 对 `for i in ` 与其它非 bare `range(...)`、非 `int` 简写的 `for` 不发新的 range-specific error,继续沿用现有 deferred / unsupported boundary。 + +验收细则: + +- `for i in range():` 与 `for i in range(1, 2, 3, 4):` 有清晰 diagnostic。 +- `for i in 3:`、`for i in limit:` 在 typed fact 为 lowering-ready `int` 时被识别为 supported range-like loop。 +- `for i in values:` 仍被归类为非 range-like `for`,保留 `FOR_SUBTREE` deferred / unsupported 行为。 +- `for i in 2.2:`、`for i in some_float:` 当前仍保留旧 deferred / unsupported 行为,不误走 `int` 简写捷径。 +- `for i in obj.range(3):`、`for i in some_range(3):` 不被误识别为 supported for-range。 +- classifier 不读取源码文本,不依赖 `range` identifier 的 source slice。 + +### 阶段 C:variable inventory 与 visible value 解封 + +目标: + +- 为 supported for-range 发布 loop iterator binding。 +- 只对 supported for-range body 发布 callable-local inventory。 +- 非 range `for` body 继续是 `FOR_SUBTREE` deferred domain。 + +实施内容: + +- 不要直接把 `BlockScopeKind.FOR_BODY` 加进 `FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(BlockScopeKind)` 的无条件 true 列表。 +- 为 `FOR_BODY` 引入 AST-aware 支持判定:只有能从 use-site / body block 找到 owning `ForStatement`,且该 statement 是 supported for-range,才视为可发布 inventory。 +- `FrontendVariableAnalyzer.handleForStatement(...)` 对 supported for-range 执行: + - 在 `forStatement.body()` 的 `BlockScope` 中定义 iterator local。 + - iterator name 来自 `forStatement.iterator()`。 + - declared iterator type 为空时使用 `GdIntType.INT`。 + - declared iterator type 非空时通过既有 declared-type resolver 解析,再用 existing typed-boundary helper 检查 `int -> declaredType` 是否允许。 + - iterator binding 与 body local 按现有 duplicate / shadowing 规则处理。 + - 然后遍历 body,发布 body 内普通 local `var`。 +- `FrontendVisibleValueResolver` 对 supported for-range body 返回正常 executable lookup,对非 range `FOR_BODY` 继续返回 `DEFERRED_UNSUPPORTED + FOR_SUBTREE`。 + +验收细则: + +- `for i in range(3): print(i)` 中 `i` 在 body 内可解析为 `LOCAL`,type 为 `int`。 +- `i` 在 loop 后不可见。 +- body 内 `var local := i` 正常发布为 `FOR_BODY` local。 +- `for i in values:` 的 body 内 lookup 仍返回 `DEFERRED_UNSUPPORTED + FOR_SUBTREE`。 +- 同一 callable 内 iterator name 与已有 parameter/local 冲突时按现有 local conflict 规则报错。 +- 非 range `for` 的旧 unsupported 测试继续存在,只把 supported range case 从旧断言中拆出。 + +### 阶段 D:top-binding、chain-binding、expr-type、local stabilization、slot type 与 type-check + +目标: + +- 让 supported for-range 的 iterable arguments 与 body 进入正常 shared semantic facts。 +- 仍不把 `range(...)` root 当 ordinary call。 +- 让 loop iterator slot type 稳定为 element type。 + +实施内容: + +- `FrontendTopBindingAnalyzer.handleForStatement(...)`: + - supported for-range:分析 `range(...)` arguments 或 `int` 简写 iterable expression 与 body。 + - 非 range:继续 `reportDeferredSubtree(..., FOR_SUBTREE)`。 +- `FrontendChainBindingAnalyzer.handleForStatement(...)`: + - supported for-range:分析 range arguments 或 `int` 简写 iterable expression 与 body。 + - 不为 `range(...)` root 发布 ordinary `resolvedCalls()`。 +- `FrontendExprTypeAnalyzer.handleForStatement(...)`: + - supported for-range:发布 range arguments 或 `int` 简写 iterable expression 的 expression types,遍历 body。 + - 不为 iterator state 发布 ordinary expression type。 +- `FrontendLocalTypeStabilizationAnalyzer`: + - supported for-range:进入 body,使 body 内 `:=` local 能从 iterator / arguments / body expressions 稳定。 +- `FrontendVarTypePostAnalyzer`: + - supported for-range:为 iterator binding 与 body locals 发布 final `slotTypes()`。 + - iterator slot type 是 source-facing element type,不是 compiler-only iterator state type。 +- `FrontendTypeCheckAnalyzer`: +- 检查 range arguments 必须是 `int` 或通过现有 ordinary boundary 可安全进入 `int` slot。 +- 检查 `int` 简写 iterable expression 也必须是 `int` 或通过现有 ordinary boundary 可安全进入 `int` stop slot。 +- 检查 explicit iterator type 能接收 `int` element。 +- 不新增一套 parallel conversion matrix;必须复用现有 compatibility helper。 + +验收细则: + +- `range(start, end, step)` 的每个 argument 都有 lowering-ready expression type。 +- `for i in limit:` 的 `limit` expression 在进入 CFG 前也已有 lowering-ready expression type。 +- `range(...)` call root 不出现在 ordinary `resolvedCalls()` 成功路径中。 +- `for i: float in range(3):` 若现有 matrix 允许 `int -> float`,则 body 中 `i` 为 `float`;若不允许则发 `sema.type_check` / `sema.type_hint` 对应 owner 的诊断。 +- `for i in 2.2:` 当前仍不静默进入 supported path。 +- `for i: String in range(3):` 不能静默放行。 +- `GdccForRangeIterType` 不出现在 ordinary `expressionTypes()` 与 user-facing `slotTypes()` 中。 + +### 阶段 E:compile gate 分阶段解封 + +目标: + +- 只在 shared semantic、CFG、body lowering 和 backend 消费都准备好之后,才让 supported for-range 通过 `analyzeForCompile(...)`。 + +实施内容: + +- 在 CFG/body lowering 完成前,`FrontendCompileCheckAnalyzer.handleForStatement(...)` 继续跳过 `for`,沿用 upstream unsupported owner。 +- CFG/body lowering 完成后: + - supported for-range 加入 compile surface。 + - mark `ForStatement`、range arguments、body block、body statements 为 compile surface。 + - 非 range `for` 继续跳过,不把 deferred subtree 打平成 generic compile blocker。 +- 同步更新: + - `frontend_rules.md` + - `frontend_compile_check_analyzer_implementation.md` + - `frontend_lowering_plan.md` + - `frontend_lowering_cfg_pass_implementation.md` + - `frontend_gdcompiler_type_implementation.md` 如 iterator contract 有新增事实 + - `diagnostic_manager.md` 如新增 category + +验收细则: + +- supported for-range 在无 error 时可通过 `analyzeForCompile(...)`。 +- 非 range `for` 在 compile mode 仍被阻断,且 diagnostic owner 仍是 upstream semantic boundary,不被 compile gate 重新包装。 +- compile gate 没有绕过 upstream `sema.loop_control_flow`、type-check 或 variable-binding error。 + +### 阶段 F:frontend CFG graph + +目标: + +- 在 `FrontendCfgGraphBuilder` 中为 supported for-range 建立显式 CFG。 +- `break` 跳到 loop exit。 +- `continue` 跳到 iterator update,再回 condition。 + +实施内容: + +- 新增 `FrontendForRegion`,不要复用 `FrontendWhileRegion`。 +- `FrontendCfgRegion` sealed permits 增加 `FrontendForRegion`。 +- `FrontendCfgGraphBuilder.processStatement(...)` 增加 `case ForStatement forStatement -> processForStatement(...)`。 +- `processForStatement(...)` 只接受 supported for-range;非 range `for` 到达这里必须 fail-fast。 +- CFG shape 建议: + - `range(...)` arguments 或 `int` 简写 iterable expression 在进入 loop 前按 source order 计算。 + - init sequence 调用 contract 指定的 `init` operation,第一版仍是 `gdcc.for_range_iter.init`,并产出 hidden iterator state slot。 + - condition entry 调用 `gdcc.for_range_iter.should_continue`,产出 bool condition value。 + - body entry 先调用 `gdcc.for_range_iter.get` 写入 source-facing iterator slot,再执行 body statements。 + - update entry 调用 `gdcc.for_range_iter.next` 更新 iterator state,再跳回 condition entry。 + - exit sequence 是 loop 后续 continuation。 +- `INT_SHORTHAND` source form 不生成伪造 `range(stop)` AST;它只在 contract 层带一个 `stop` operand,init item/lowering processor 负责按 `(0, stop, 1)` 解释。 +- active loop frame 对 for-range 应使用: + - `breakTargetId = exitId` + - `continueTargetId = updateEntryId` +- hidden iterator state value id / slot id 必须稳定命名,建议沿用现有 `cfg_tmp_` 风格或用明确前缀,例如 `cfg_for_iter_`;一旦选择,测试锁定。 + +验收细则: + +- CFG regions 中 `ForStatement` 映射到 `FrontendForRegion`。 +- `FrontendForRegion` 至少暴露 `initEntryId`、`conditionEntryId`、`bodyEntryId`、`updateEntryId`、`exitId`。 +- `continue` 的 target 是 update entry,不是 condition entry。 +- `break` 的 target 是 exit。 +- `range(...)` arguments 或 `int` 简写 stop operand 的 value ids 在 init item 前已经发布。 +- condition branch 的 condition value type 是 `bool`,不会触发 compiler-only condition normalization。 +- unreachable body 后续 statement 处理仍遵守现有 reachability 规则。 + +### 阶段 G:CFG item 与 body LIR lowering + +目标: + +- 把 range iterator state 与四个 intrinsic materialize 为 LIR。 +- 不重新扫描 AST,不重新推导 sema facts。 + +实施内容: + +- 在 `frontend.lowering.cfg.item` 增加最小 dedicated item,建议保持 narrow 且使用面向未来的通用命名: + - `ForLoopInitItem` + - `ForLoopShouldContinueItem` + - `ForLoopGetItem` + - `ForLoopNextItem` +- 这些 item 都实现现有 `ValueOpItem`,并持有: + - AST anchor + - operand value ids + - result value id + - iterator state type / element type / lowering operation 必要信息 +- body lowering 在 `FrontendSequenceItemInsnLoweringProcessors` 中增加对应 processor。 +- processor 生成 `CallIntrinsicInsn`;第一版 range-like contract 对应: + - 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 合同。 +- 不通过 `PackVariantInsn` / `UnpackVariantInsn` materialize iterator state。 + +验收细则: + +- LIR function variables 包含 hidden `compiler::GdccForRangeIter` local。 +- 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 相关路径。 +- `step == 0` literal 在 frontend 阶段阻断;动态零 step 仍由 runtime helper 防止无限循环。 + +## 5. 验收测试清单 + +### Parser / scope + +- `for i in range(3): pass` 解析为 `ForStatement`。 +- `for i in 3: pass` 与 `for i in limit: pass` 解析为 `ForStatement`,且 iterable 保持原始 expression 形态。 +- `for i: int in range(1, 3, 1): pass` 解析并保留 `iteratorType`。 +- `FrontendScopeAnalyzerTest` 继续断言 `iteratorType` / `iterable` 在外层 scope,`body` 在 `FOR_BODY`。 + +### Shared semantic + +- `for i in range(3): var x := i` 成功发布 `i` 与 `x`。 +- `for i in 3: var x := i` 与 `for i in limit: var x := i` 在 `limit` 已稳定为 `int` 时也成功发布 `i` 与 `x`。 +- `i` 在 loop 后不可见。 +- `for i in values:` 继续是 `FOR_SUBTREE` deferred / unsupported。 +- `for i in 2.2:` 继续是 deferred / unsupported,直到 follow-up 明确 `ceil` 语义。 +- `break` / `continue` 在 for-range body 内合法。 +- `break` / `continue` 在 loop 外仍报 `sema.loop_control_flow`。 +- lambda / nested callable 内的 loop-control 仍按 callable boundary 重新判定。 +- `range()`、`range(1, 2, 3, 4)`、literal `range(1, 2, 0)` 均有明确 diagnostic。 +- `for i: String in range(3): pass` 不静默通过 type-check。 + +### Compile gate + +- supported for-range 在 `analyzeForCompile(...)` 无 error 时进入 lowering。 +- `for i in ` 这类 supported range-like loop 在无 error 时也进入 lowering。 +- 非 range `for` 仍不能进入 lowering。 +- compile gate 不为已有 upstream semantic error 追加同级 generic `sema.compile_check` 噪声。 + +### CFG + +- `FrontendCfgGraphBuilderTest` 覆盖 for-range region shape。 +- `FrontendCfgGraphBuilderTest` 覆盖 `INT_SHORTHAND` source form 与 `range(...)` source form 共用同一 `FrontendForRegion` shape。 +- `continue` target 为 update entry。 +- `break` target 为 exit。 +- nested `if` 中的 `continue` / `break` 能正确连边。 +- 空 range body、只有 `pass` 的 body、body 内 `return` 的 reachable/fallthrough 行为均有断言。 + +### Body lowering / LIR / backend + +- `FrontendLoweringBuildCfgPassTest` 覆盖 function context 中发布 `FrontendForRegion`。 +- `FrontendLoweringBodyInsnPassTest` 覆盖 range intrinsic instruction sequence,并锁住 `INT_SHORTHAND` 仍走相同 intrinsic 路线。 +- `DomLirParserTest` / `DomLirSerializerTest` 继续覆盖 `compiler::GdccForRangeIter` local 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)` 的 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,FrontendTopBindingAnalyzerTest,FrontendTypeCheckAnalyzerTest +``` + +```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` 支持不能只靠 `BlockScopeKind` 判定,必须关联 owning `ForStatement` 是否为 supported for-range;否则会把未来 generic iterable `for` 误放行。 +- `range(...)` root 是否发布 expression type 必须统一。当前计划是不发布 ordinary root type,只发布 arguments 与 loop iterator binding,避免把 range 当作 user-facing `Array[int]` 或 builtin call。 +- `int` 简写不允许通过 AST rewrite 伪装成 `range(stop)` call;否则 parser/scope/diagnostic anchor、future object-iterator classifier 与测试都会被污染。 +- explicit iterator type 的兼容规则必须复用 ordinary typed-boundary helper;不要为 `for` 私下硬编码 `int -> T` 特例。 +- `continue` 对 for-range 的目标不是 condition entry,而是 update entry。这一点和 `while` 不同,不能复用 `FrontendWhileRegion`。 +- literal `step == 0` 可以前端诊断;动态零 step 不能静态证明时仍需要 backend runtime helper 保护。 +- `for i in ` 与 `range(...)` 现在就应走同一 contract;未来若接入 `float` 简写或 `Object._iter_*` / container iterable,也必须在 classifier 层扩充 contract,而不是新增第二套 CFG / lowering 基础设施。 + +## 8. 完成定义 + +本计划完成后,必须同时满足: + +1. `for i in range(...)` 与 `for i in ` 在 shared semantic 中都不再触发 `FOR_SUBTREE` unsupported boundary。 +2. 非 range-like `for` 仍保持 deferred / unsupported,不进入 compile-ready surface。 +3. loop iterator 在 body 内是 source-facing local,类型为 `int` 或显式兼容类型。 +4. compiler-only `GdccForRangeIterType` 只出现在 hidden LIR local / intrinsic operand-result / backend C storage 路径。 +5. CFG 中存在独立 `FrontendForRegion`,且 `continue` / `break` 连边正确;同一 region/item/processor 基础设施能承载未来对象迭代扩展。 +6. Body lowering 生成 contract 指定的 intrinsic;第一版 range-like loop 仍生成 `gdcc.for_range_iter.*`,且不重扫 AST、不重做 semantic 推导。 +7. 文档、正反测试、targeted tests 与 compile check 全部同步。 diff --git a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md new file mode 100644 index 00000000..b7e4df5f --- /dev/null +++ b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md @@ -0,0 +1,647 @@ +# Frontend Segmented Type Resolution Pipeline Plan + +## 1. 文档状态 + +- 性质:实施计划 +- 目标模块:`src/main/java/gd/script/gdcc/frontend/sema/**` +- 直接动机:让 frontend 可以按源码顺序分段发布局部类型事实,使 `var limit := 3; for i in limit:` 这类依赖前缀 typed fact 的语义支持成为可实现目标。 +- 非目标:本文不直接实现 `for-range` lowering,不改变 Godot range runtime 语义,不把 `lambda` / `match` / block-local `const` 一并转正。 + +关联文档: + +- `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_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_semantic_analyzer_research_report.md` + +## 2. 背景与问题 + +当前 `FrontendSemanticAnalyzer.analyze(...)` 是 whole-module phase pipeline: + +1. skeleton +2. scope +3. variable inventory +4. top binding +5. local type stabilization +6. chain binding +7. expr typing +8. var type post +9. annotation usage +10. virtual override +11. type check +12. loop control +13. compile-only gate,仅 `analyzeForCompile(...)` + +这条顺序让每个 phase 都能消费前一个 phase 的完整 module 事实,但也造成一个硬时序问题: + +- variable inventory 需要在 phase 3 决定某个 subtree 是否可以发布 local inventory。 +- 依赖 typed fact 的语义,例如 `for i in limit:` 是否是 int shorthand,需要 phase 5-7 之后才可能知道。 +- 如果 phase 3 不发布 `FOR_BODY` inventory,后续 resolver / binding / expr typing 会按 `FOR_SUBTREE` fail-closed。 +- 如果 phase 3 盲目发布 `FOR_BODY` inventory,又会把 unsupported `for` body 误纳入普通 executable body。 + +因此需要一个 source-order segmented pipeline:先为当前 block 做完整 lexical inventory,再按 statement window 逐步发布前缀 typed facts,遇到依赖 typed fact 的 feature gate 时再决定是否解封其子树 inventory。 + +## 3. 不变量 + +### 3.1 仍然保留的 phase owner + +分段不是允许任意 analyzer 写任意表。owner 边界保持不变: + +- `FrontendVariableAnalyzer` 仍拥有 parameter / local inventory publication。 +- `FrontendTopBindingAnalyzer` 仍拥有 `symbolBindings()`。 +- `FrontendLocalTypeStabilizationAnalyzer` 仍只拥有 local `:=` slot rewrite,不拥有 diagnostics,不发布 `resolvedMembers()` / `resolvedCalls()` / `expressionTypes()` / `slotTypes()`。 +- `FrontendChainBindingAnalyzer` 仍拥有 `resolvedMembers()` 与 chain-owned `resolvedCalls()`。 +- `FrontendExprTypeAnalyzer` 仍拥有 `expressionTypes()` 与 bare-call `resolvedCalls()`。 +- `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 在 segmented runner 中不得继续作为第二个 slot mutation owner;它必须收紧为严格 no-op / guard-only 路径。 +- `FrontendVarTypePostAnalyzer` 仍拥有 `slotTypes()`。 +- `FrontendTypeCheckAnalyzer`、`FrontendLoopControlFlowAnalyzer` 与 `FrontendCompileCheckAnalyzer` 仍是 diagnostics-only consumer。 + +### 3.2 完整 lexical inventory 必须先于分段 resolver + +`FrontendVisibleValueResolver.filterInvisibleCurrentLayerHit(...)` 只有在 `Scope.resolveValueHere(...)` 能看到同层 binding 时,才能把后续声明记录为 `DECLARATION_AFTER_USE_SITE` filtered hit。 + +因此分段方案不得只把“当前 segment 之前的 local”放入 `BlockScope`。正确模型是: + +- 对一个已支持的 block,先扫描并发布该 block 的完整 local inventory。 +- local `:=` 初始类型仍是 `Variant`。 +- 分段只负责逐步稳定类型和发布 use-site facts。 +- 若某个 child feature gate 后续转正,例如 supported `for` body,必须先对该 child body 做完整 inventory,再分析 body 内 statement segments。 + +这保证 `var x := y; var y := 1` 仍能产生 declaration-after-use filtered hit,而不是把 `y` 误判成普通 miss 或外层 fallback。 + +### 3.3 `FrontendAnalysisData` 稳定引用合同保持不变 + +现有测试要求 `FrontendAnalysisData.updateXxx(...)` 保留 side table 对象引用,并通过 clear + putAll 清理 stale entry。分段重构不能破坏这个外部合同。 + +新增增量能力时必须做到: + +- 保留现有 `updateXxx(...)` whole-table publication API 和测试。 +- 新增 segment patch / merge API,不复用 `updateXxx(...)` 表达部分提交。 +- 同一个 side table 的 stable reference 仍不替换。 +- 增量 merge 必须能检测冲突,不能静默覆盖不兼容 fact。 + +### 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 隔离 + +任何分段 patch merge 都必须拒绝将 `GdCompilerType` 写入用户可见 facts: + +- `expressionTypes()` 的 `publishedType()` +- source-facing local / parameter / iterator 的 `slotTypes()` +- ordinary local `ScopeValue.type()`,除非该 scope value 明确是 compiler-owned hidden storage,且不会被 resolver 暴露给源码 + +`GdccForRangeIterType` 只能作为 hidden iterator state contract 被 lowering 消费,不能通过普通 expression typing 或 local slot publication 泄漏。 + +## 4. 核心设计 + +### 4.1 两层 pipeline + +重构后的 shared semantic pipeline 分成两层。 + +基础 whole-module 层: + +1. skeleton +2. scope +3. baseline variable inventory + +基础层的职责是建立全局 class / callable / block scope 图,并对当前无需 typed fact 即可支持的 block 发布完整 local inventory。 + +分段 semantic 层: + +1. top binding segment +2. local type stabilization segment +3. chain binding segment +4. expr typing segment +5. slot type post segment + +分段层以源码顺序处理 supported executable block 的 statement window。每个 window 内仍按上述阶段顺序运行,不能把 expr typing 提前到 chain binding 之前,也不能让 local stabilization 越过 top binding。 + +诊断-only whole-module 层: + +1. annotation usage +2. virtual override +3. type check +4. loop control +5. compile-only final gate + +这些阶段应在分段 semantic 层完全收敛后运行,继续消费最终 facts。 + +### 4.2 Segment 的粒度 + +第一版 segment 粒度定义为 `FrontendSemanticWindow`: + +```java +record FrontendSemanticWindow( + @NotNull Node owner, + @NotNull Scope currentScope, + @NotNull List roots, + @NotNull FrontendSemanticWindowKind kind +) {} +``` + +建议的 window kind: + +- `PROPERTY_INITIALIZER` +- `CALLABLE_STATEMENT` +- `BLOCK_STATEMENT` +- `CONTROL_HEADER` +- `FEATURE_GATE_HEADER` +- `FEATURE_GATE_BODY` + +第一版实现可以先做到“每个 statement 一个 window”,不要在开始时做复杂 batching。性能问题等行为稳定后再通过相邻 safe window 合并优化。 + +### 4.3 Source-order scheduler + +新增 `FrontendSegmentedSemanticScheduler` 或等价 coordinator。它不拥有具体语义,只负责按源码顺序调度 analyzer segment。 + +调度规则: + +1. 对 accepted source file 按 class member 顺序进入。 +2. 对 function / constructor body,要求 callable parameters 和 body block baseline inventory 已发布。 +3. 对 supported block,先保证该 block 的完整 local inventory 已发布。 +4. 按 `block.statements()` 顺序处理 statement window。 +5. 每个 statement window 完成 top binding -> local stabilization -> chain binding -> expr typing -> var type post,并把 patch merge 回 `FrontendAnalysisData`。 +6. 遇到 `if` / `elif` / `else` / `while` 等已支持 control body 时,header window 先完成,再递归处理 body block。 +7. 遇到 typed-dependent feature gate,例如 future supported `for`,先分析 gate header 所需前置 facts,再运行 classifier。 +8. classifier 若转正,只能把 gate 状态推进到 `SUPPORTED`;此时 body inventory 仍必须保持 `NOT_PUBLISHED`,resolver / binder 继续 fail-closed。 +9. scheduler 随后运行专门的 child body inventory publication window。只有该 window 成功提交 iterator binding 与 body 完整 local inventory 后,才能把 body readiness 推进到 `PUBLISHED`。 +10. 只有 `status == SUPPORTED && bodyInventoryReadiness == PUBLISHED` 的 body 可以递归处理 child body semantic windows。 +11. classifier 若未转正,保留现有 deferred / unsupported boundary,不进入 child body segment,body readiness 也必须保持 `NOT_PUBLISHED`。 + +### 4.4 Feature gate + +新增 `FrontendInventoryGate` 记录 typed-dependent subtree 的待决状态。第一版至少应能表达: + +```java +enum FrontendInventoryGateStatus { + PENDING, + SUPPORTED, + UNSUPPORTED +} + +enum FrontendBodyInventoryReadiness { + NOT_PUBLISHED, + PUBLISHING, + PUBLISHED +} + +record FrontendInventoryGate( + @NotNull Node owner, + @NotNull Node headerRoot, + @NotNull Node bodyRoot, + @NotNull FrontendVisibleValueDomain deferredDomain, + @NotNull FrontendInventoryGateStatus status, + @NotNull FrontendBodyInventoryReadiness bodyInventoryReadiness +) {} +``` + +`FrontendVariableAnalyzer` 的 baseline pass 不应在需要 typed fact 的 gate 上做最终决定。它应: + +- 对已支持且不依赖 typed fact 的 block 发布完整 local inventory。 +- 对 typed-dependent subtree 记录 pending gate。 +- 对当前明确不会被本计划解封的 subtree 继续发 unsupported/deferred diagnostic。 + +后续 `FrontendSegmentedSemanticScheduler` 在 header facts 就绪后调用对应 classifier,并决定是否发布 body inventory。 + +### 4.4.1 Body inventory readiness + +`status == SUPPORTED` 与 `bodyInventoryReadiness == PUBLISHED` 是两件事,不能互相替代: + +- `SUPPORTED` 只表示 gate classifier 已确认该 subtree 可以被本轮 pipeline 解封。 +- `PUBLISHED` 表示对应 body scope 的 iterator binding 与完整 local inventory 已经通过 publication owner 写入,并且提交点已经成功完成。 +- `BlockScopeKind.FOR_BODY` scope 存在不表示 inventory 已发布;`FrontendScopeAnalyzer` 会无条件为 `ForStatement.body()` 建 scope,这只是 lexical graph 事实。 +- `bodyInventoryReadiness` 是 gate body inventory readiness 的单一真源。任何 analyzer、resolver、compile gate 或测试 helper 都不得用“scope 存在”、“gate 为 `SUPPORTED`”或“`BlockScopeKind.FOR_BODY`”推导 body 已可解析。 +- `PUBLISHING` 只允许表达 scheduler 正在发布 body inventory 的内部过渡状态。对 resolver / binder / downstream semantic windows 来说,它与 `NOT_PUBLISHED` 一样必须 fail-closed。 + +生命周期必须固定为: + +1. baseline pass 记录 typed-dependent gate:`status = PENDING`,`bodyInventoryReadiness = NOT_PUBLISHED`。 +2. classifier 判定 unsupported:`status = UNSUPPORTED`,`bodyInventoryReadiness = NOT_PUBLISHED`,保留 deferred / unsupported boundary。 +3. classifier 判定 supported:`status = SUPPORTED`,`bodyInventoryReadiness = NOT_PUBLISHED`,此时 body 仍不可解析。 +4. body inventory window 开始时可临时进入 `PUBLISHING`,但不得让 resolver 正常 lookup。 +5. body inventory window 成功提交后,原子推进为 `PUBLISHED`。 +6. body inventory window 失败或被丢弃时,必须回到 `NOT_PUBLISHED` 或直接 fail-fast;不得留下 `SUPPORTED + PUBLISHING` 的稳定 public 状态。 + +需要提供一个共享查询作为唯一入口,例如 `FrontendExecutableInventorySupport.isCallableLocalValueInventoryReady(BlockScope scope, Node useSite, FrontendAnalysisData data)` 或等价命名。它必须: + +- 对无条件支持的 block kind 继续返回 true。 +- 对 `FOR_BODY` 这类 gate body,只在能找到 owning gate,且 `status == SUPPORTED && bodyInventoryReadiness == PUBLISHED` 时返回 true。 +- 对缺失 gate、`PENDING`、`SUPPORTED + NOT_PUBLISHED`、`SUPPORTED + PUBLISHING`、`UNSUPPORTED`、合成但无 owning `ForStatement` 的 `FOR_BODY` 返回 false。 +- 被 `FrontendVariableAnalyzer`、`FrontendVisibleValueResolver`、`FrontendLocalTypeStabilizationAnalyzer`、`FrontendVarTypePostAnalyzer`、`FrontendCompileCheckAnalyzer` 等所有 callable-local inventory 消费者共同使用。 + +现有 `FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(BlockScopeKind)` 只能继续表达无条件支持的 block kind,不得成为 `FOR_BODY` readiness 的事实源,也不得通过把 `FOR_BODY` 加进 switch 来解封 gate body。 + +### 4.5 Side-table patch + +新增 transient patch 类型,例如 `FrontendAnalysisPatch`: + +```java +record FrontendAnalysisPatch( + @NotNull FrontendSemanticStage stage, + @NotNull FrontendAstSideTable symbolBindings, + @NotNull FrontendAstSideTable resolvedMembers, + @NotNull FrontendAstSideTable resolvedCalls, + @NotNull FrontendAstSideTable expressionTypes, + @NotNull FrontendAstSideTable slotTypes, + @NotNull List localSlotTypeUpdates +) {} +``` + +`FrontendAnalysisData` 新增 `applyPatch(...)` 或分表 merge 方法,规则如下: + +- 新 key 直接写入 stable side table。 +- 旧 key + 相同 value 视为 idempotent,允许。 +- 旧 key + 不同 value 默认 fail-fast。 +- `symbolBindings()` 允许因 `FrontendLocalSlotTypeUpdate` 刷新同一 declaration 的 `resolvedValue` payload,但必须保持 binding kind、name、declaration identity 不变。 +- `resolvedCalls()` 中 chain-owned call 与 bare-call 由 stage 标识区分,不能相互覆盖。 +- `expressionTypes()` 不允许从 `RESOLVED(int)` 变成 `RESOLVED(float)`,也不允许从 terminal negative status 改成 success;需要 retry 的场景不得先发布 provisional public fact。 +- `slotTypes()` 不允许同一 source slot 被不同类型覆盖;同类型 idempotent 允许。 +- merge 前统一检查 compiler-only type 不泄漏。 + +现有 `updateXxx(...)` 继续表达 whole-table snapshot publication;`applyPatch(...)` 只用于 segmented semantic layer。 + +### 4.5.1 Window-local publication surface + +`FrontendAnalysisPatch` 只表达 window 结束时提交到 stable side table 的增量,不表达 window 内部的即时可见性。分段实现必须额外引入 window-local publication surface,例如 `FrontendWindowPublicationSurface` 或等价对象。 + +window-local surface 的核心合同为 scratch-over-stable: + +- 每个 `FrontendSemanticWindow` 开始时创建一组空的 scratch side table,与当前 `FrontendAnalysisData` stable side table 组成 effective view。 +- window 内所有 stage 读取 semantic fact 时必须通过 effective view:先查 window scratch,再查 stable side table。 +- window 内所有新增或刷新事实只写入 scratch,不直接写入 `FrontendAnalysisData` stable side table。 +- window 成功完成后,scratch 被封装为 `FrontendAnalysisPatch`,再通过 `applyPatch(...)` 原子合并到 stable side table。 +- window 失败或被 classifier 判为 unsupported 时,scratch 直接丢弃,不得污染 stable side table。 + +这个 surface 不是 public publication。只有 `applyPatch(...)` 成功后,事实才成为后续 window、diagnostics-only 阶段、lowering 可消费的 stable published fact。 + +`ExprType` 的特殊要求必须明确保留: + +- `FrontendExprTypeAnalyzer` 当前依赖读己写语义,nested chain reduction 会读取同一 window 内刚发布的 expression fact。 +- 迁移后该读己写只能发生在 window scratch 内,不能通过提前写 stable side table 实现。 +- `FrontendChainReductionHelper`、`FrontendChainReductionFacade`、`FrontendExpressionSemanticSupport` 等 shared support 不得直接读取 `analysisData.expressionTypes()` 来判断当前 window 内 fact 是否已存在,必须通过 effective view 或由 window-aware resolver 封装该查找顺序。 +- `finalizeWindow=true` 表示“当前 bounded retry window 的最后一次补全尝试”。若补全得到稳定结果,只能写入当前 window scratch;是否成为 public fact 仍由 window 结束时的 `applyPatch(...)` 决定。 +- 需要 retry 的路径不得发布 provisional public fact;如果只能得到 `DEFERRED` / `FAILED` / `UNSUPPORTED` 等 terminal 或 unstable 结果,也应先停留在 scratch,按 `expressionTypes()` merge 规则在 window commit 时统一判定。 + +同一规则也适用于 window 内 stage 间依赖: + +- top binding segment 写入的 `symbolBindings()` 必须在同一 window 的 local stabilization、chain binding、expr typing 中可见。 +- chain binding segment 写入的 `resolvedMembers()` 与 chain-owned `resolvedCalls()` 必须在同一 window 的 expr typing 中可见。 +- expr typing segment 补充的 bare-call `resolvedCalls()` 与 `expressionTypes()` 必须在同一 window 的 var type post 中可见。 +- `publishAttributeStepExpressionTypes(...)` 这类 duplicate guard 必须同时检查 scratch 与 stable,允许同值 idempotent,拒绝不同值覆盖。 + +因此,window-capable analyzer API 不应只接收 `FrontendAnalysisData`。它应接收一个只暴露 effective reads 与 scratch writes 的上下文,例如 `FrontendWindowAnalysisContext`: + +```java +record FrontendWindowAnalysisContext( + @NotNull FrontendAnalysisData stableData, + @NotNull FrontendWindowPublicationSurface publications +) {} +``` + +具体命名可调整,但必须避免 analyzer 或 shared helper 绕过 window surface 直接写 stable side table。 + +### 4.6 Scope slot mutation + +`BlockScope.resetLocalType(...)` 不是 side-table 写入,但它会影响后续 resolver 和 binding payload。分段方案必须显式建模这类 mutation。 + +`FrontendLocalSlotTypeUpdate` 至少记录: + +```java +record FrontendLocalSlotTypeUpdate( + @NotNull BlockScope scope, + @NotNull String name, + @NotNull Object declaration, + @NotNull GdType type +) {} +``` + +应用规则: + +- `FrontendLocalTypeStabilizationAnalyzer` 是唯一允许产生 `FrontendLocalSlotTypeUpdate` 的 analyzer。 +- 只允许 `Variant -> exact` 或 exact same-type no-op。 +- 不允许 exact A -> exact B。 +- 不允许写入 `GdVoidType`。 +- 不允许写入 `GdCompilerType` 到 source-facing local。 +- 应用后必须刷新已发布且指向同一 declaration 的 `symbolBindings()` payload。 +- 刷新应优先使用 declaration identity index,第一版若继续全表扫描也必须被测试锁住语义正确性。 + +现存 `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 是必须显式收口的历史路径。它当前能在 expr phase 中直接调用 `BlockScope.resetLocalType(...)`,并通过 `refreshPublishedLocalValues(...)` 刷新已发布的 `symbolBindings()` payload;这两者都会绕过 `FrontendLocalSlotTypeUpdate` 的 owner 边界。 + +在 segmented runner 中,该路径只能保留为 guard-only 检查: + +- 不得调用 `BlockScope.resetLocalType(...)`。 +- 不得调用或复制 `refreshPublishedLocalValues(...)` 的 side-channel binding payload refresh。 +- 不得向 window surface 或 patch 追加 `FrontendLocalSlotTypeUpdate`。 +- 若 initializer fact 是 terminal negative、`DYNAMIC`、`void`、缺失或当前 slot 仍是 inventory-seeded `Variant`,直接 no-op;expr phase 不能补做 local stabilization 没有完成的 narrowing。 +- 若当前 slot 已是非 `Variant` 且 initializer exact type 一致,no-op。 +- 若当前 slot 已是非 `Variant` 但 initializer exact type 不一致,fail-fast 暴露内部阶段协议错误。 +- 若 initializer fact 试图把 `GdCompilerType` 作为 source-facing local 类型观测到,fail-fast。 + +如果后续发现某类 initializer 需要更强的 `:=` narrowing,必须扩展 `FrontendLocalTypeStabilizationAnalyzer` 或它的 window-local resolver,而不是恢复 expr-phase backfill mutation。 + +### 4.7 Resolver 复用规则 + +`FrontendVisibleValueResolver` 可以继续一次性索引完整 source AST,但它必须读取已经发布的完整 inventory。 + +重构时不得让 resolver 自己扫描 AST 补找普通 `var`。原因是这样会复制 `FrontendVariableAnalyzer` 的职责,并容易与 duplicate、shadowing、unsupported boundary、scope kind gate 漂移。 + +需要新增的 resolver 能力是 context-aware inventory readiness 判断: + +- 现有 `FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(BlockScopeKind)` 保留给无条件支持的 block kind。 +- 新增 AST-aware readiness 查询,例如 `FrontendExecutableInventorySupport.isCallableLocalValueInventoryReady(BlockScope scope, Node useSite, FrontendAnalysisData data)`。 +- 对 `FOR_BODY` 这类 gate body,只有共享 readiness 查询返回 true 时才放行;它内部必须同时检查对应 gate `status == SUPPORTED` 与 `bodyInventoryReadiness == PUBLISHED`。 +- `FrontendVisibleValueResolver.detectDeferredBoundary(...)` 的 `ForStatement.body()` edge 与 `classifyUnsupportedCurrentBlockScopeBoundary(...)` 的 `FOR_BODY` current-scope check 必须调用同一 readiness 查询,不能一个按 `SUPPORTED` 放行、另一个仍按 `FOR_BODY` 封口。 +- `ForStatement.iteratorType()` / `iterable()` 仍属于 gate header 语义,不得因为 body readiness 为 `PUBLISHED` 自动获得 body-local visibility。 +- 非 supported 或 readiness 未发布的 gate body 继续返回 `DEFERRED_UNSUPPORTED + FOR_SUBTREE` 或对应 domain。 + +## 5. 分步骤实施 + +### 阶段 A:补齐 side-table patch 基础设施 + +实施内容: + +- 新增 `FrontendSemanticStage`,枚举 `TOP_BINDING`、`LOCAL_TYPE_STABILIZATION`、`CHAIN_BINDING`、`EXPR_TYPE`、`VAR_TYPE_POST`。 +- 新增 `FrontendAnalysisPatch` 与 `FrontendLocalSlotTypeUpdate`。 +- 在 `FrontendAnalysisData` 中新增 patch merge API,保留现有 `updateXxx(...)`。 +- 为 merge 加入冲突检测、idempotent 规则、compiler-only type 泄漏检查。 +- 为 `symbolBindings()` 的 local slot refresh 建立显式 helper,替代 analyzer 内到处分散的 `entry.setValue(...)`。 + +验收细则: + +- `FrontendAnalysisDataTest` 继续通过现有 stable-reference / stale-clear 测试。 +- 新增测试覆盖 patch 写入新 key、idempotent merge、冲突 fail-fast。 +- 新增测试覆盖 `symbolBindings()` 仅允许同 declaration 的 local slot payload refresh。 +- 新增测试覆盖 local slot payload refresh 只由 `FrontendLocalSlotTypeUpdate` 应用触发;no-op update 与 expr-phase backfill guard 都不得刷新 binding payload。 +- 新增测试覆盖 `expressionTypes()` / `slotTypes()` 拒绝 `GdCompilerType` 泄漏。 +- 不修改任何 analyzer 行为时,现有 frontend semantic tests 输出不变。 + +### 阶段 B:抽出 window-local publication surface + +实施内容: + +- 新增 `FrontendWindowPublicationSurface` 或等价类型,封装每个 window 的 scratch side table。 +- 新增 `FrontendWindowAnalysisContext` 或等价类型,统一携带 stable `FrontendAnalysisData` 与 window-local publication surface。 +- 为 `symbolBindings()`、`resolvedMembers()`、`resolvedCalls()`、`expressionTypes()`、`slotTypes()` 提供 scratch-over-stable effective read API。 +- 为上述 side table 提供 scratch-only write API;所有 write 都不得直接落到 stable side table。 +- 提供 `toPatch(...)` / `drainPatch(...)` 或等价方法,只把 scratch 中由当前 window 产生的 entries 转成 `FrontendAnalysisPatch`。 +- 提供 discard 语义:window 未成功完成、classifier 判定 unsupported、或测试主动丢弃 surface 时,scratch 内容不会影响 stable side table。 +- 将 `finalizeWindow=true` 的 window 内含义固定为“bounded retry 的最后一次补全尝试,稳定结果写入 scratch”,不得在 surface 层写穿 stable。 +- 在 surface 层或 patch merge 层复用同一套 conflict / idempotent / compiler-only type guard,避免 scratch shadow stable 后把冲突延迟成静默覆盖。 + +验收细则: + +- 新增测试覆盖 effective read 顺序:同一 identity key 同时存在 stable 与 scratch 时,读取返回 scratch 值;scratch 缺失时回落 stable。 +- 新增测试覆盖 scratch-only write:写入 `expressionTypes()` / `resolvedCalls()` / `symbolBindings()` 等 scratch 表后,`FrontendAnalysisData` stable side table 在 `applyPatch(...)` 前保持不变。 +- 新增测试覆盖 `toPatch(...)` 只包含 scratch entries,不把 stable fallback entries 误复制进 patch。 +- 新增测试覆盖 window surface 不把 expr-phase `backfillInferredLocalType(...)` 观察到的 initializer type 转换为 `FrontendLocalSlotTypeUpdate`;slot update collector 只接受 local stabilization owner。 +- 新增测试覆盖 discard:丢弃 surface 后 stable side table、diagnostics snapshot、scope slot 均不被修改。 +- 新增测试覆盖 same-key idempotent:scratch 写入与 stable 相同 value 可通过,最终 `applyPatch(...)` 为 no-op 或 idempotent merge。 +- 新增测试覆盖 same-key conflict:scratch 写入或 patch merge 不允许把 stable `RESOLVED(int)` shadow 成 `RESOLVED(float)`,也不允许 terminal negative status shadow 成 success。 +- 新增测试覆盖 `finalizeWindow=true`:retry 产出的稳定 `ExprType` 在同一 surface 内可立即读到,但 stable `analysisData.expressionTypes()` 在 commit 前仍读不到。 +- 新增测试覆盖 attribute step key:`AttributePropertyStep` / `AttributeCallStep` / `AttributeSubscriptStep` 作为 key 时仍保持 identity lookup、scratch 优先、duplicate guard 生效。 +- 新增测试覆盖 compiler-only guard:`GdCompilerType` 不能写入 source-facing scratch `expressionTypes()` / `slotTypes()`,即使还未进入 `applyPatch(...)`。 +- 新增测试覆盖 local slot update isolation:surface 收集的 `FrontendLocalSlotTypeUpdate` 在 commit 前不调用 `BlockScope.resetLocalType(...)`,commit 后才按 4.6 规则应用并刷新 binding payload。 + +### 阶段 C:抽出 window-capable analyzer API + +实施内容: + +- 为 top binding、chain binding、expr typing、var type post 提取 window-level runner。 +- window-level runner 接收 window analysis context,不能直接向 stable side table 发布 facts。 +- 现有 whole-module `analyze(...)` 先改成构造一个覆盖全 module 的 window 列表,再调用 window runner,最后用 `updateXxx(...)` 发布 whole-table snapshot。 +- local type stabilization 提取 window-level runner,返回 `FrontendLocalSlotTypeUpdate`,由 caller 统一应用。 +- 将 `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 改造成 4.6 定义的 guard-only 检查,移除它对 `BlockScope.resetLocalType(...)` 与 `refreshPublishedLocalValues(...)` 的生产路径依赖。 +- 改造 `FrontendChainReductionHelper` / `FrontendChainReductionFacade` 的 expression type 查找入口,使其通过 window effective view 或 window-aware resolver 读取 `expressionTypes()`。 +- 保持现有 analyzer class 名称和 public `analyze(...)` 方法,避免一次性改动所有调用点。 + +验收细则: + +- `FrontendSemanticAnalyzerFrameworkTest.analyzePublishesPhaseBoundariesThroughVirtualOverridePhaseAndRefreshesDiagnosticsAfterEachPhase` 继续通过,证明 public phase boundary 未漂移。 +- top binding / chain binding / expr typing / var type post 的 focused tests 输出不变;例外是历史 backfill mutation 测试必须改为 guard-only 语义,不能继续期待 expr phase 改写 slot。 +- local type stabilization 的 probe 测试继续证明 probe 不写 shared side tables、不发 final diagnostics。 +- expr typing 的 nested chain / argument retry 场景继续保留读己写能力,但读写发生在 window scratch 内。 +- expr typing 的 local `:=` backfill 场景只做 guard:inventory-seeded `Variant` 保持不变,已稳定同类型 no-op,已稳定异类型 fail-fast,且不会刷新 `symbolBindings()` payload。 +- window runner 在单 window 与 whole-module wrapper 下产出的 side-table 内容一致。 + +### 阶段 D:引入 segment scheduler,但先保持行为等价 + +实施内容: + +- 新增 `FrontendSegmentedSemanticScheduler`,第一版只生成现有支持面的 statement windows。 +- scheduler 运行时仍不解封任何 typed-dependent gate。 +- 对每个 window 依次运行 top binding、local type stabilization、chain binding、expr typing、var type post,并用 `applyPatch(...)` 合并。 +- 每个 window 创建独立 publication surface;只有 window 成功完成后才把 surface 转为 patch 并合并。 +- `FrontendSemanticAnalyzer` 增加内部开关或 package-private 构造路径,用于测试 segmented runner 与 legacy whole-phase runner 的等价性。 +- 默认生产路径可以在阶段 D 末切换到 segmented runner,但必须先完成等价测试。 + +验收细则: + +- 对同一输入,legacy whole-phase runner 与 segmented runner 的 `symbolBindings()`、`resolvedMembers()`、`resolvedCalls()`、`expressionTypes()`、`slotTypes()` 等价。 +- 等价基线以阶段 C 后的 guard-only backfill 合同为准,不以旧的 expr-phase slot mutation 作为兼容目标。 +- 验证同一 statement window 内 `ExprType` 可读到自己刚发布的 scratch fact,且下一个 window 只能读到已经 merge 的 stable fact。 +- diagnostics category、range、顺序保持等价,或文档明确接受的 phase-boundary probe 差异已有新测试锚定。 +- `FrontendVisibleValueResolver` 的 declaration-after-use 与 initializer self-reference 测试继续通过。 +- `for`、`match`、lambda、block-local `const` 的 existing deferred / unsupported 行为不变。 + +### 阶段 E:baseline inventory 与 pending gate 分离 + +实施内容: + +- 调整 `FrontendVariableAnalyzer`,把“发布普通 local inventory”和“报告/记录 feature boundary”拆开。 +- 对现有无条件支持的 block,仍发布完整 local inventory。 +- 对 typed-dependent subtree,记录 `FrontendInventoryGate(PENDING, NOT_PUBLISHED)`,但不发布 body inventory。 +- 对明确不在本计划转正范围内的 subtree,继续按现有 owner 发 diagnostic。 +- 在 `FrontendAnalysisData` 中加入 gate side table 或专用 registry;若 gate 不需要长期暴露给 lowering,可先保持 package-private data structure,但必须可被 resolver / scheduler 查询。 +- gate registry 必须按 gate owner / body root identity 提供 body readiness update 和 lookup API,作为 4.4.1 定义的单一真源。 +- `FrontendVariableAnalyzer`、`FrontendLocalTypeStabilizationAnalyzer`、`FrontendVarTypePostAnalyzer`、`FrontendCompileCheckAnalyzer` 的 callable-local inventory 判断必须迁移到共享 readiness 查询;纯 `BlockScopeKind` 查询只能处理无条件支持的 block kind。 + +验收细则: + +- 普通 block 中未来声明仍在 scope 中可被 resolver 看到,并按 `DECLARATION_AFTER_USE_SITE` 过滤。 +- pending gate body 内 lookup 仍返回 `DEFERRED_UNSUPPORTED`,不能 fallback 到外层同名 local。 +- `SUPPORTED + NOT_PUBLISHED` 与 `SUPPORTED + PUBLISHING` 的 gate body lookup 仍返回 `DEFERRED_UNSUPPORTED`,不能 fallback 到外层同名 local。 +- 合成 `FOR_BODY` scope 或缺失 owning gate 的 body readiness 查询返回 false,即使 `scopesByAst()` 中已有 scope 记录。 +- 旧的 unsupported `for` / `match` / lambda tests 继续通过,除非某个 gate 在后续阶段显式转正。 +- duplicate / shadowing diagnostics owner 不变。 + +### 阶段 F:source-order typed fact 解锁 + +实施内容: + +- scheduler 在每个 block 内按源码顺序提交 statement patches。 +- 当前 statement 的 expr facts 提交后,后续 statement classifier 可以读取这些 facts。 +- 对 `var limit := 3; ` 建立测试用 synthetic gate 或选用 `for` range classifier 作为第一个真实 consumer。 +- local `:=` stabilization 必须在同一 statement window 内早于后续 statement 的 binding / classifier 消费。 +- child block 的完整 inventory 必须在 child body 第一个 semantic window 前发布,且共享 readiness 查询必须已经返回 true。 + +验收细则: + +- `var limit := 3; var next := limit` 在 segmented runner 下仍稳定为 `int`。 +- `var x := y; var y := 1` 仍不把 `y` 解析成可见 local;filtered hit reason 为 `DECLARATION_AFTER_USE_SITE`。 +- `var x := x` 仍记录 `SELF_REFERENCE_IN_INITIALIZER`。 +- 一个 statement 的 published `expressionTypes()` 能被后续 gate classifier 读取。 +- 不允许同一 expression key 被后续 window 重新发布成不同状态或类型。 + +### 阶段 G:接入第一个真实 typed-dependent feature gate + +建议以 `frontend_for_range_loop_implementation_plan.md` 中的 int shorthand `for` 作为验收用例,但只接语义解封,不在本阶段强制完成 lowering。 + +实施内容: + +- 将 `ForStatement` 注册为 typed-dependent inventory gate。 +- `range(...)` call 仍可由 AST shape 早期识别;`INT_SHORTHAND` 必须等 iterable expression typed fact 就绪后再判定。 +- `var limit := 3; for i in limit:` 中,scheduler 先完成 `limit` statement window,再分类 `for` gate。 +- supported gate 先推进为 `SUPPORTED + NOT_PUBLISHED`,再由 body inventory publication window 发布 iterator binding 和 body 完整 local inventory,最后原子推进为 `PUBLISHED`。 +- unsupported gate 继续保留 `FOR_SUBTREE` deferred / unsupported 行为。 + +验收细则: + +- `for i in range(3):` 的 body lookup 可以看到 `i : int`。 +- classifier 已返回 supported 但 body readiness 仍为 `NOT_PUBLISHED` / `PUBLISHING` 时,body lookup 仍必须是 `DEFERRED_UNSUPPORTED + FOR_SUBTREE`。 +- `var limit := 3; for i in limit:` 在 `limit` 已稳定为 `int` 后可被 classifier 判定为 int shorthand。 +- `for i in limit: var local := i` 中 body local inventory 在 body 分段前完整发布。 +- `for i in limit: var local := i` 只有在 body readiness 为 `PUBLISHED` 后,resolver、local stabilization、var type post、compile check 才能把 `FOR_BODY` 当作 ready inventory domain。 +- `for i in values:` 仍不会让 body 内 bare identifier fallback 到外层并制造误导 binding。 +- `GdccForRangeIterType` 不出现在 `expressionTypes()` 或 source-facing `slotTypes()`。 + +### 阶段 H:收敛 diagnostics 与 compile gate + +实施内容: + +- 确定 segmented runner 的 diagnostics snapshot 发布点:每个 stage patch 应用后刷新 `analysisData.updateDiagnostics(...)`,保证后续 stage 看到稳定 upstream diagnostics。 +- compile gate 继续只在 shared segmented pipeline 完成后运行。 +- 检查 compile gate 的 duplicate suppression 是否仍能识别跨 segment upstream diagnostics。 +- 对缺失 `slotTypes()`、`DEFERRED`、`FAILED`、`UNSUPPORTED` 的 final facts 保持现有 compile blocking 规则。 + +验收细则: + +- `FrontendCompileCheckAnalyzerTest` 中已有 compile gate 去重测试继续通过。 +- upstream diagnostic 已存在时,下游 segment 不补同级重复错误。 +- `analyze(...)` 不运行 compile gate;`analyzeForCompile(...)` 在 segmented facts 完成后运行 compile gate。 +- segmented runner 不改变 parse / skeleton / scope diagnostics 的顺序与可见性。 + +### 阶段 I:移除 legacy whole-phase 旁路 + +实施内容: + +- 等阶段 D-H 的等价与新增支持测试稳定后,删除仅用于迁移的 legacy whole-phase runner 旁路。 +- 保留 window-capable analyzer API,whole-module analyzer wrapper 只作为测试或调试入口时存在。 +- 更新相关文档,尤其是 variable analyzer、visible resolver、local type stabilization、chain/expr typing、compile check 与 for-range plan。 + +验收细则: + +- 所有 frontend semantic focused tests 通过。 +- 针对新增 segmented pipeline 的测试覆盖 patch merge、window-local surface、backfill guard-only、source-order typed fact、pending gate、resolver filtered hit、diagnostic dedup、compile gate。 +- `./gradlew classes --no-daemon --info --console=plain` 通过。 +- 相关 targeted tests 使用 `script/run-gradle-targeted-tests.sh --tests ...` 通过。 + +## 6. 必须新增或调整的测试 + +基础设施测试: + +- `FrontendAnalysisDataTest`:patch merge 新 key / idempotent / conflict / stable reference。 +- `FrontendAnalysisDataTest`:compiler-only type 泄漏 guard。 +- `FrontendWindowPublicationSurfaceTest` 或等价测试:scratch-over-stable read、scratch-only write、discard、`toPatch(...)` 不复制 stable fallback。 +- `FrontendWindowPublicationSurfaceTest` 或等价测试:`finalizeWindow=true` 产物只进入 scratch,commit 前 stable 不可见。 +- `FrontendAstSideTableTest`:若新增 patch view 或 owner metadata,确认 identity key 语义不变。 + +Pipeline 等价测试: + +- `FrontendSemanticAnalyzerFrameworkTest`:legacy runner 与 segmented runner side-table 等价。 +- `FrontendSemanticAnalyzerFrameworkTest`:等价基线使用 guard-only backfill 合同,不允许旧 expr-phase slot mutation 作为兼容路径回归。 +- `FrontendSemanticAnalyzerFrameworkTest`:phase diagnostics snapshot 在 segmented stage 后仍稳定。 + +Resolver 测试: + +- 同 block future local:`var x := y; var y := 1`,必须得到 `DECLARATION_AFTER_USE_SITE` filtered hit。 +- initializer self-reference:`var x := x`,必须得到 `SELF_REFERENCE_IN_INITIALIZER` filtered hit。 +- pending gate body:lookup 必须是 `DEFERRED_UNSUPPORTED`,不能 fallback。 +- `FrontendVisibleValueResolverTest`:`FOR_BODY` scope 已存在但 gate 缺失、`PENDING`、`SUPPORTED + NOT_PUBLISHED`、`SUPPORTED + PUBLISHING`、`UNSUPPORTED` 时,body lookup 都必须是 `DEFERRED_UNSUPPORTED + FOR_SUBTREE`。 +- `FrontendVisibleValueResolverTest`:同一 `FOR_BODY` 在 `SUPPORTED + PUBLISHED` 后,body 内 iterator 和 body local lookup 返回 `FOUND_ALLOWED`,并继续保留 declaration-after-use filtered hit。 + +Local stabilization 测试: + +- source-order alias chain 在 segmented runner 下保持稳定。 +- child block 读取 parent 前缀稳定 local。 +- exact type 不允许被后续 segment 改写为另一个 exact type。 +- `FrontendExprTypeAnalyzerTest` 中旧 backfill mutation / refresh 期望必须调整为 guard-only:inventory-seeded `Variant` 不被 expr phase narrowing,`symbolBindings()` payload 不刷新。 +- `FrontendExprTypeAnalyzerTest` 继续覆盖 guard:已稳定同类型 no-op,已稳定异类型 fail-fast,compiler-only initializer fact fail-fast。 + +Typed-dependent gate 测试: + +- 前缀 `:=` local 稳定后,后续 gate classifier 能读取 typed fact。 +- gate 转正只产生 `SUPPORTED + NOT_PUBLISHED`,不得使 body resolver / binder 放行。 +- body inventory publication window 成功提交后,readiness 原子推进为 `PUBLISHED`,未来声明仍能被 filtered hit 捕获。 +- `FrontendExecutableInventorySupport` 或等价 readiness 测试:所有 callable-local inventory 消费者对 `FOR_BODY` 使用同一 readiness 查询,不允许各自直接判断 `BlockScopeKind.FOR_BODY`。 +- gate 未转正时旧 deferred / unsupported 行为保持。 +- `FrontendVariableAnalyzerTest` / `FrontendLocalTypeStabilizationAnalyzerTest` / `FrontendVarTypePostAnalyzerTest` / `FrontendCompileCheckAnalyzerTest`:`SUPPORTED + NOT_PUBLISHED` 的 `FOR_BODY` 不发布 local、slot type 或 lowering-ready fact;`SUPPORTED + PUBLISHED` 后才发布。 + +Compile gate 测试: + +- segmented facts 中残留 `DEFERRED` / `FAILED` / `UNSUPPORTED` 时仍被 compile gate 阻断。 +- upstream diagnostic 去重跨 segment 生效。 +- `analyze(...)` 与 `analyzeForCompile(...)` split 不变。 + +## 7. 风险与缓解 + +### R1:side-table 冲突被静默覆盖 + +缓解:window 内 partial publication 只能写入 window-local scratch,window 结束后必须通过 patch merge;默认不同 value fail-fast。需要覆盖 scratch shadow stable、idempotent 与 conflict tests。 + +### R2:resolver 看不到未来声明 + +缓解:分段前必须为 supported block 发布完整 local inventory。禁止 resolver 自己扫描普通 `var` 弥补缺口。 + +### R3:scope slot mutation 与 published binding payload 脱节 + +缓解:local slot rewrite 通过 `FrontendLocalSlotTypeUpdate` 统一应用,并同步刷新同 declaration 的 `symbolBindings()`。 + +### R4:历史 backfill 路径恢复第二个 slot mutation owner + +缓解:`FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 必须是 strict no-op / guard-only;测试同时锁住“不调用 `BlockScope.resetLocalType(...)`”、“不刷新 `symbolBindings()` payload”、“不生成 `FrontendLocalSlotTypeUpdate`”。需要新的 narrowing 能力时只能扩展 local stabilization。 + +### R5:gate supported 与 body inventory readiness 漂移 + +缓解:`bodyInventoryReadiness` 是唯一可查询事实;`SUPPORTED` 只是 classifier 结果。resolver、variable analyzer、local stabilization、var type post、compile check 都必须通过共享 readiness 查询,测试覆盖 `SUPPORTED + NOT_PUBLISHED` 与 `SUPPORTED + PUBLISHING` 继续 fail-closed。 + +### R6:diagnostics 重复或顺序漂移 + +缓解:stage patch 应用后刷新 diagnostics snapshot;新增跨 segment duplicate suppression tests。若顺序需要调整,必须更新 framework probe tests 与文档。 + +### R7:unsupported subtree 被过早打开 + +缓解:pending gate 默认 fail-closed;只有 classifier 明确返回 supported 且 `bodyInventoryReadiness == PUBLISHED`,resolver 才能把对应 body 当普通 executable body。 + +### R8:compiler-only type 泄漏 + +缓解:window-local surface、patch merge 与 local slot update 都做 `GdCompilerType` guard;for iterator state 只能通过专用 contract 给 CFG/lowering。 + +### R9:实现一次性改动过大 + +缓解:先做 patch infra,再独立落地 window-local surface,再做 window runner 等价改造,之后切 scheduler,最后接 typed-dependent gate。每阶段都应有 targeted tests。 + +## 8. 完成定义 + +本计划完成时应满足: + +- frontend shared semantic 默认使用 segmented runner,且现有 supported surface 行为等价。 +- `FrontendAnalysisData` 同时支持 whole-table publication 与安全 partial patch merge。 +- window 内 semantic fact 通过 scratch-over-stable effective view 即时可见,且 commit 前不会污染 stable side table。 +- `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 不再改写 `BlockScope`、不刷新 `symbolBindings()` payload、不产生 slot update,只保留 guard-only 协议检查。 +- supported block 的完整 local inventory 先于分段 resolver,declaration-after-use filtered hit 行为不退化。 +- local `:=` 的 source-order type stabilization 可被后续 statement / gate classifier 消费。 +- pending feature gate 能在 typed fact 就绪后安全转正,但 child body 只有在 `bodyInventoryReadiness == PUBLISHED` 后才可解析。 +- `FOR_BODY` body inventory readiness 由单一 registry/query 表达;scope 存在或 `SUPPORTED` 状态都不能被当作 readiness 替代品。 +- unsupported gate 仍 fail-closed,不能 fallback 或误发布 body facts。 +- compile gate、lowering-ready fact 边界和 compiler-only type 隔离不变。 From 66eefb60276d1fac7f8338fe2488c8a1122d98cc Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Sun, 5 Jul 2026 15:38:45 +0800 Subject: [PATCH 02/27] feat(frontend): add segmented semantic stage, patch, and local binding refresh helper - Add new stage enum, immutable patch record, update payload record, and patch exception to support incremental per-stage fact publication in the segmented type resolution pipeline - Extend the shared analysis data container with patch apply and local binding payload refresh entry points while preserving the existing whole-table publication API - Refactor the local type stabilization and expression type analyzers to delegate local slot binding refresh through the new helper, keeping external behavior unchanged - Harden patch handling: side table snapshot on construct, merge conflict and idempotency enforcement, stage-owner validation, and compiler-only type leak detection for typed fact updates - Broaden the analysis data test coverage and mark the corresponding plan items as completed in the segmented type resolution pipeline doc --- ...segmented_type_resolution_pipeline_plan.md | 8 + .../FrontendAnalysisPatchException.java | 10 + .../frontend/sema/FrontendAnalysisData.java | 358 +++++++++++++ .../frontend/sema/FrontendAnalysisPatch.java | 44 ++ .../sema/FrontendLocalSlotTypeUpdate.java | 28 + .../frontend/sema/FrontendSemanticStage.java | 10 + .../analyzer/FrontendExprTypeAnalyzer.java | 35 +- ...rontendLocalTypeStabilizationAnalyzer.java | 37 +- .../sema/FrontendAnalysisDataTest.java | 487 +++++++++++++++++- 9 files changed, 968 insertions(+), 49 deletions(-) create mode 100644 src/main/java/gd/script/gdcc/exception/FrontendAnalysisPatchException.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendAnalysisPatch.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendLocalSlotTypeUpdate.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendSemanticStage.java diff --git a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md index b7e4df5f..53d66dd0 100644 --- a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md +++ b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md @@ -377,6 +377,14 @@ record FrontendLocalSlotTypeUpdate( - 为 merge 加入冲突检测、idempotent 规则、compiler-only type 泄漏检查。 - 为 `symbolBindings()` 的 local slot refresh 建立显式 helper,替代 analyzer 内到处分散的 `entry.setValue(...)`。 +当前状态(2026-07-05): + +- [x] A1 新增 `FrontendSemanticStage`,并固定五个 segmented semantic stage 常量。 +- [x] A2 新增 `FrontendAnalysisPatch` 与 `FrontendLocalSlotTypeUpdate`,其中 patch 在创建时复制 side table,避免 drain 后被 scratch 二次污染。 +- [x] A3 在 `FrontendAnalysisData` 中新增 `applyPatch(...)`,并继续保留现有 `updateXxx(...)` whole-table publication API。 +- [x] A4 为 merge 加入冲突检测、idempotent 规则、`LOCAL_TYPE_STABILIZATION` owner 校验,以及 `expressionTypes()` / `slotTypes()` / local slot update 的 compiler-only type 泄漏检查。 +- [x] A5 为 `symbolBindings()` 的 local slot refresh 建立显式 helper,并让 `FrontendLocalTypeStabilizationAnalyzer` 与 `FrontendExprTypeAnalyzer` 复用该 helper,保持现有 analyzer 对外行为不变。 + 验收细则: - `FrontendAnalysisDataTest` 继续通过现有 stable-reference / stale-clear 测试。 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/sema/FrontendAnalysisData.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendAnalysisData.java index b2ba4bc1..3b526957 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,19 @@ package gd.script.gdcc.frontend.sema; +import dev.superice.gdparser.frontend.ast.Node; +import gd.script.gdcc.exception.FrontendAnalysisPatchException; 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.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.ArrayList; import java.util.List; import java.util.Objects; @@ -116,6 +124,46 @@ public void updateSlotTypes(@NotNull FrontendAstSideTable slotTypes) { ); } + /// Applies one segmented semantic patch without replacing any stable side-table reference. + /// + /// Whole-table `updateXxx(...)` remains the legacy publication path. This API is reserved for + /// segmented stages, so every merge validates conflicts and compiler-only leaks before mutating + /// the stable publication surface. + public void applyPatch(@NotNull FrontendAnalysisPatch patch) { + var checkedPatch = Objects.requireNonNull(patch, "patch must not be null"); + checkPatchDoesNotLeakCompilerOnlyTypes(checkedPatch); + var validatedLocalSlotUpdates = validateLocalSlotTypeUpdates(checkedPatch); + checkPatchConflicts(symbolBindings, checkedPatch.symbolBindings(), "symbolBindings", FrontendAnalysisData::sameBinding); + checkPatchConflicts( + resolvedMembers, + checkedPatch.resolvedMembers(), + "resolvedMembers", + FrontendAnalysisData::sameResolvedMember + ); + checkPatchConflicts( + resolvedCalls, + checkedPatch.resolvedCalls(), + "resolvedCalls", + FrontendAnalysisData::sameResolvedCall + ); + checkPatchConflicts( + expressionTypes, + checkedPatch.expressionTypes(), + "expressionTypes", + FrontendAnalysisData::sameExpressionType + ); + checkPatchConflicts(slotTypes, checkedPatch.slotTypes(), "slotTypes", FrontendAnalysisData::sameType); + + mergeSideTable(symbolBindings, checkedPatch.symbolBindings()); + mergeSideTable(resolvedMembers, checkedPatch.resolvedMembers()); + mergeSideTable(resolvedCalls, checkedPatch.resolvedCalls()); + mergeSideTable(expressionTypes, checkedPatch.expressionTypes()); + mergeSideTable(slotTypes, checkedPatch.slotTypes()); + for (var validatedUpdate : validatedLocalSlotUpdates) { + applyLocalSlotTypeUpdate(validatedUpdate); + } + } + public @NotNull FrontendModuleSkeleton moduleSkeleton() { return requirePublished(moduleSkeleton, "moduleSkeleton"); } @@ -159,6 +207,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 +256,260 @@ public void updateSlotTypes(@NotNull FrontendAstSideTable slotTypes) { return value; } + private @NotNull List validateLocalSlotTypeUpdates( + @NotNull FrontendAnalysisPatch patch + ) { + if (patch.localSlotTypeUpdates().isEmpty()) { + return List.of(); + } + if (patch.stage() != FrontendSemanticStage.LOCAL_TYPE_STABILIZATION) { + throw patchFailure( + "Only LOCAL_TYPE_STABILIZATION patches may publish local slot type updates, but got " + + patch.stage() + ); + } + var validatedUpdates = new ArrayList(patch.localSlotTypeUpdates().size()); + for (var update : patch.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()); + } + } + } + + private void checkPatchDoesNotLeakCompilerOnlyTypes(@NotNull FrontendAnalysisPatch patch) { + for (var expressionType : patch.expressionTypes().values()) { + checkNoCompilerOnlyLeak(expressionType.publishedType(), "expressionTypes() published type"); + } + for (var slotType : patch.slotTypes().values()) { + checkNoCompilerOnlyLeak(slotType, "slotTypes() value"); + } + } + + private void checkNoCompilerOnlyLeak(@Nullable GdType type, @NotNull String fieldName) { + if (type instanceof GdCompilerType compilerOnlyType) { + throw patchFailure( + fieldName + + " leaked compiler-only type " + + compilerOnlyType.getTypeName() + ); + } + } + + private void checkNoVoidLocalSlotType(@NotNull GdType type, @NotNull String localName) { + if (type instanceof GdVoidType) { + throw patchFailure("local slot update for '" + localName + "' must not publish void"); + } + } + + private 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()); + } + + private 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()); + } + + private 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()); + } + + private 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()); + } + + private 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()); + } + + private 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()); + } + + private 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; + } + + private 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())); + } + + private static @NotNull String describeNode(@NotNull Node node) { + return node.getClass().getSimpleName(); + } + + private static @NotNull FrontendAnalysisPatchException patchFailure(@NotNull String message) { + return new FrontendAnalysisPatchException(message); + } + private static void replaceSideTableContents( @NotNull FrontendAstSideTable target, @NotNull FrontendAstSideTable source, @@ -179,4 +523,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/FrontendAnalysisPatch.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendAnalysisPatch.java new file mode 100644 index 00000000..186a3928 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendAnalysisPatch.java @@ -0,0 +1,44 @@ +package gd.script.gdcc.frontend.sema; + +import gd.script.gdcc.type.GdType; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Objects; + +/// Incremental semantic facts published by one segmented frontend stage. +/// +/// Patch contents are copied on creation so later scratch mutations cannot silently change the +/// already-drained publication payload. +public record FrontendAnalysisPatch( + @NotNull FrontendSemanticStage stage, + @NotNull FrontendAstSideTable symbolBindings, + @NotNull FrontendAstSideTable resolvedMembers, + @NotNull FrontendAstSideTable resolvedCalls, + @NotNull FrontendAstSideTable expressionTypes, + @NotNull FrontendAstSideTable slotTypes, + @NotNull List localSlotTypeUpdates +) { + public FrontendAnalysisPatch { + Objects.requireNonNull(stage, "stage must not be null"); + symbolBindings = copySideTable(symbolBindings, "symbolBindings"); + resolvedMembers = copySideTable(resolvedMembers, "resolvedMembers"); + resolvedCalls = copySideTable(resolvedCalls, "resolvedCalls"); + expressionTypes = copySideTable(expressionTypes, "expressionTypes"); + slotTypes = copySideTable(slotTypes, "slotTypes"); + localSlotTypeUpdates = List.copyOf(Objects.requireNonNull( + localSlotTypeUpdates, + "localSlotTypeUpdates must not be null" + )); + } + + private static @NotNull FrontendAstSideTable copySideTable( + 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/FrontendLocalSlotTypeUpdate.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendLocalSlotTypeUpdate.java new file mode 100644 index 00000000..1468f120 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendLocalSlotTypeUpdate.java @@ -0,0 +1,28 @@ +package gd.script.gdcc.frontend.sema; + +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 stage-scoped metadata. `FrontendAnalysisData.applyPatch(...)` owns the +/// actual scope mutation rules, compiler-only guards, and published binding payload refresh. +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/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/analyzer/FrontendExprTypeAnalyzer.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzer.java index 7f4ce0d7..43fbe30a 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzer.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzer.java @@ -17,7 +17,6 @@ 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; @@ -569,30 +568,16 @@ private void backfillInferredLocalType(@NotNull VariableDeclaration variableDecl 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)); + if (updatedValue != null && updatedValue.declaration() == variableDeclaration) { + analysisData.refreshPublishedLocalBindingPayloads( + new FrontendLocalSlotTypeUpdate( + blockScope, + localName, + variableDeclaration, + updatedValue.type() + ), + updatedValue + ); } } 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 index 66d49fc2..114ba4da 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendLocalTypeStabilizationAnalyzer.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendLocalTypeStabilizationAnalyzer.java @@ -32,12 +32,12 @@ 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.FrontendLocalSlotTypeUpdate; 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; @@ -177,7 +177,8 @@ static void probeStabilizeLocalSlot( } var localName = variableDeclaration.name().trim(); blockScope.resetLocalType(localName, variableDeclaration, stableType); - return blockScope.resolveValueHere(localName); + var updatedValue = blockScope.resolveValueHere(localName); + return updatedValue != null && updatedValue.declaration() == variableDeclaration ? updatedValue : null; } private static @Nullable GdType stableLocalTypeOrNull( @@ -235,7 +236,7 @@ record ProbeEntry( private static final class AstWalkerLocalTypeStabilizer implements ASTNodeHandler { private final @NotNull FrontendAstSideTable scopesByAst; - private final @NotNull FrontendAstSideTable symbolBindings; + private final @NotNull FrontendAnalysisData analysisData; private final @NotNull ASTWalker astWalker; private final @NotNull SilentExpressionResolver silentExpressionResolver; private final @NotNull List probes; @@ -253,8 +254,8 @@ private AstWalkerLocalTypeStabilizer( boolean writeBackStableSlots ) { var checkedAnalysisData = Objects.requireNonNull(analysisData, "analysisData must not be null"); + this.analysisData = checkedAnalysisData; 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); @@ -362,28 +363,20 @@ private void walk(@NotNull SourceFile sourceFile) { if (writeBackStableSlots) { var updatedValue = stabilizeLocalSlot(blockScope, variableDeclaration, initializerType); if (updatedValue != null) { - refreshPublishedLocalValues(variableDeclaration, updatedValue); + analysisData.refreshPublishedLocalBindingPayloads( + new FrontendLocalSlotTypeUpdate( + blockScope, + variableDeclaration.name().trim(), + variableDeclaration, + updatedValue.type() + ), + 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)) { @@ -504,7 +497,7 @@ private void walkSupportedExecutableBlock(@Nullable Block block) { if (!(initializer instanceof IdentifierExpression identifierExpression)) { return null; } - var binding = symbolBindings.get(identifierExpression); + var binding = analysisData.symbolBindings().get(identifierExpression); if (binding == null || binding.kind() != FrontendBindingKind.TYPE_META) { return null; } 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..49aa246f 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,43 @@ 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.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.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 +178,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 +243,470 @@ void updateSlotTypesClearsStaleEntriesWithoutReplacingStableSideTableReference() assertEquals(GdIntType.INT, analysisData.slotTypes().get(freshNode)); } + @Test + void analysisPatchCopiesSourceTablesAtConstructionTime() { + var bindingNode = identifier("value"); + var symbolBindings = new FrontendAstSideTable(); + var binding = new FrontendBinding("self", FrontendBindingKind.SELF, null); + symbolBindings.put(bindingNode, binding); + + var patch = new FrontendAnalysisPatch( + FrontendSemanticStage.TOP_BINDING, + symbolBindings, + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + List.of() + ); + 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 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 applyPatchRejectsWrongStageLocalSlotUpdatesAndSourceFacingCompilerOnlyLeaks() 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.EXPR_TYPE, + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + List.of(new FrontendLocalSlotTypeUpdate(localScope, "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, + GdccForRangeIterType.FOR_RANGE_ITER + )) + )) + ); + } + + @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 + ); + } + + 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); + } + + private static @NotNull FrontendAnalysisPatch patch( + @NotNull FrontendSemanticStage stage, + @NotNull FrontendAstSideTable symbolBindings, + @NotNull FrontendAstSideTable resolvedMembers, + @NotNull FrontendAstSideTable resolvedCalls, + @NotNull FrontendAstSideTable expressionTypes, + @NotNull FrontendAstSideTable slotTypes, + @NotNull List localSlotTypeUpdates + ) { + return new FrontendAnalysisPatch( + stage, + symbolBindings, + resolvedMembers, + resolvedCalls, + expressionTypes, + slotTypes, + localSlotTypeUpdates + ); + } + + private static @NotNull FrontendAnalysisPatch patch( + @NotNull FrontendSemanticStage stage, + @NotNull FrontendAstSideTable symbolBindings + ) { + return patch( + stage, + symbolBindings, + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + new FrontendAstSideTable<>(), + List.of() + ); + } } From f6cfaab5e67bf40459ece943f424ba4f1baa781a Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Sun, 5 Jul 2026 22:05:52 +0800 Subject: [PATCH 03/27] feat(frontend): add window-scoped publication surface for segmented type resolution pipeline - Introduce a window-bound scratch publication surface carrying the same fact categories as the stable container with scratch-over-stable effective reads and scratch-only writes - Provide window context, patch extraction/drain, and discard semantics so per-stage facts never leak into the stable surface until committed - Reuse shared compiler-only and merge guard helpers from the existing analysis data container to enforce conflict, idempotency, and typed fact safety uniformly - Cover the new scratch/patch/discard surface with focused unit tests - Mark the corresponding plan items as completed in the segmented type resolution pipeline doc --- ...segmented_type_resolution_pipeline_plan.md | 11 + .../frontend/sema/FrontendAnalysisData.java | 24 +- .../sema/FrontendWindowAnalysisContext.java | 32 ++ .../FrontendWindowPublicationSurface.java | 243 +++++++++++ .../FrontendWindowPublicationSurfaceTest.java | 388 ++++++++++++++++++ 5 files changed, 686 insertions(+), 12 deletions(-) create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendWindowAnalysisContext.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendWindowPublicationSurface.java create mode 100644 src/test/java/gd/script/gdcc/frontend/sema/FrontendWindowPublicationSurfaceTest.java diff --git a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md index 53d66dd0..c478f871 100644 --- a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md +++ b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md @@ -407,6 +407,17 @@ record FrontendLocalSlotTypeUpdate( - 将 `finalizeWindow=true` 的 window 内含义固定为“bounded retry 的最后一次补全尝试,稳定结果写入 scratch”,不得在 surface 层写穿 stable。 - 在 surface 层或 patch merge 层复用同一套 conflict / idempotent / compiler-only type guard,避免 scratch shadow stable 后把冲突延迟成静默覆盖。 +当前状态(2026-07-05): + +- [x] B1 新增 `FrontendWindowPublicationSurface`,封装 `symbolBindings()`、`resolvedMembers()`、`resolvedCalls()`、`expressionTypes()`、`slotTypes()` 的 window-local scratch side table。 +- [x] B2 新增 `FrontendWindowAnalysisContext`,统一携带 stable `FrontendAnalysisData` 与 window-local publication surface。 +- [x] B3 为上述五张 side table 提供 scratch-over-stable effective read API,window 内读取统一先查 scratch、再查 stable。 +- [x] B4 为上述五张 side table 提供 scratch-only write API;所有 write 都固定只落到 scratch,不直接修改 stable side table。 +- [x] B5 提供 `toPatch(...)` / `drainPatch(...)`,并保证 patch 只包含当前 scratch facts,不复制 stable fallback entries。 +- [x] B6 提供 discard 语义;discard 后 scratch facts 与 pending local slot updates 都会被直接丢弃,不影响 stable publication surface。 +- [x] B7 固定 window 内 `finalizeWindow=true` 的基础设施语义为“最终稳定结果仍只写入 scratch,commit 前 stable 不可见”。 +- [x] B8 在 surface 写入路径与 patch merge 路径复用同一套冲突 / idempotent / compiler-only type guard,并为 local slot update 增加 pre-commit 隔离收集入口。 + 验收细则: - 新增测试覆盖 effective read 顺序:同一 identity key 同时存在 stable 与 scratch 时,读取返回 scratch 值;scratch 缺失时回落 stable。 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 3b526957..ca00c1ca 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendAnalysisData.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendAnalysisData.java @@ -396,7 +396,7 @@ private void checkPatchDoesNotLeakCompilerOnlyTypes(@NotNull FrontendAnalysisPat } } - private void checkNoCompilerOnlyLeak(@Nullable GdType type, @NotNull String fieldName) { + static void checkNoCompilerOnlyLeak(@Nullable GdType type, @NotNull String fieldName) { if (type instanceof GdCompilerType compilerOnlyType) { throw patchFailure( fieldName @@ -406,13 +406,13 @@ private void checkNoCompilerOnlyLeak(@Nullable GdType type, @NotNull String fiel } } - private void checkNoVoidLocalSlotType(@NotNull GdType type, @NotNull String localName) { + static void checkNoVoidLocalSlotType(@NotNull GdType type, @NotNull String localName) { if (type instanceof GdVoidType) { throw patchFailure("local slot update for '" + localName + "' must not publish void"); } } - private static boolean sameBinding(@NotNull FrontendBinding first, @NotNull FrontendBinding second) { + static boolean sameBinding(@NotNull FrontendBinding first, @NotNull FrontendBinding second) { return first.kind() == second.kind() && first.symbolName().equals(second.symbolName()) && first.declarationSite() == second.declarationSite() @@ -420,7 +420,7 @@ private static boolean sameBinding(@NotNull FrontendBinding first, @NotNull Fron && sameScopeValue(first.resolvedValue(), second.resolvedValue()); } - private static boolean sameResolvedMember( + static boolean sameResolvedMember( @NotNull FrontendResolvedMember first, @NotNull FrontendResolvedMember second ) { @@ -435,7 +435,7 @@ && sameType(first.resultType(), second.resultType()) && Objects.equals(first.detailReason(), second.detailReason()); } - private static boolean sameResolvedCall(@NotNull FrontendResolvedCall first, @NotNull FrontendResolvedCall second) { + static boolean sameResolvedCall(@NotNull FrontendResolvedCall first, @NotNull FrontendResolvedCall second) { return first.callKind() == second.callKind() && first.status() == second.status() && first.receiverKind() == second.receiverKind() @@ -449,7 +449,7 @@ && sameExactCallableBoundary(first.exactCallableBoundary(), second.exactCallable && Objects.equals(first.detailReason(), second.detailReason()); } - private static boolean sameExactCallableBoundary( + static boolean sameExactCallableBoundary( @Nullable FrontendResolvedCall.ExactCallableBoundary first, @Nullable FrontendResolvedCall.ExactCallableBoundary second ) { @@ -460,7 +460,7 @@ private static boolean sameExactCallableBoundary( && sameTypeList(first.fixedParameterTypes(), second.fixedParameterTypes()); } - private static boolean sameExpressionType( + static boolean sameExpressionType( @NotNull FrontendExpressionType first, @NotNull FrontendExpressionType second ) { @@ -469,7 +469,7 @@ && sameType(first.publishedType(), second.publishedType()) && Objects.equals(first.detailReason(), second.detailReason()); } - private static boolean sameScopeValue(@Nullable ScopeValue first, @Nullable ScopeValue second) { + static boolean sameScopeValue(@Nullable ScopeValue first, @Nullable ScopeValue second) { if (first == null || second == null) { return first == second; } @@ -482,7 +482,7 @@ private static boolean sameScopeValue(@Nullable ScopeValue first, @Nullable Scop && sameType(first.type(), second.type()); } - private static boolean sameTypeList(@NotNull List first, @NotNull List second) { + static boolean sameTypeList(@NotNull List first, @NotNull List second) { if (first.size() != second.size()) { return false; } @@ -494,7 +494,7 @@ private static boolean sameTypeList(@NotNull List first, @NotNull List symbolBindings; + private final @NotNull WindowSideTableView resolvedMembers; + private final @NotNull WindowSideTableView resolvedCalls; + private final @NotNull WindowSideTableView expressionTypes; + private final @NotNull WindowSideTableView slotTypes; + private final @NotNull List localSlotTypeUpdates = new ArrayList<>(); + private boolean open = true; + + public FrontendWindowPublicationSurface(@NotNull FrontendAnalysisData stableData) { + var checkedStableData = Objects.requireNonNull(stableData, "stableData must not be null"); + symbolBindings = new WindowSideTableView<>( + checkedStableData.symbolBindings(), + FrontendAnalysisData::sameBinding, + "symbolBindings", + null + ); + resolvedMembers = new WindowSideTableView<>( + checkedStableData.resolvedMembers(), + FrontendAnalysisData::sameResolvedMember, + "resolvedMembers", + null + ); + resolvedCalls = new WindowSideTableView<>( + checkedStableData.resolvedCalls(), + FrontendAnalysisData::sameResolvedCall, + "resolvedCalls", + null + ); + expressionTypes = new WindowSideTableView<>( + checkedStableData.expressionTypes(), + FrontendAnalysisData::sameExpressionType, + "expressionTypes", + value -> FrontendAnalysisData.checkNoCompilerOnlyLeak( + value.publishedType(), + "expressionTypes() published type" + ) + ); + slotTypes = new WindowSideTableView<>( + checkedStableData.slotTypes(), + FrontendAnalysisData::sameType, + "slotTypes", + value -> FrontendAnalysisData.checkNoCompilerOnlyLeak(value, "slotTypes() value") + ); + } + + public @NotNull WindowSideTableView symbolBindings() { + return symbolBindings; + } + + public @NotNull WindowSideTableView resolvedMembers() { + return resolvedMembers; + } + + public @NotNull WindowSideTableView resolvedCalls() { + return resolvedCalls; + } + + public @NotNull WindowSideTableView expressionTypes() { + return expressionTypes; + } + + public @NotNull WindowSideTableView slotTypes() { + return slotTypes; + } + + /// Collects one local-slot rewrite without mutating the underlying scope until commit time. + public void addLocalSlotTypeUpdate(@NotNull FrontendLocalSlotTypeUpdate update) { + requireOpen(); + var checkedUpdate = Objects.requireNonNull(update, "update must not be null"); + FrontendAnalysisData.checkNoVoidLocalSlotType(checkedUpdate.type(), checkedUpdate.name()); + FrontendAnalysisData.checkNoCompilerOnlyLeak( + checkedUpdate.type(), + "local slot update for '" + checkedUpdate.name() + "'" + ); + localSlotTypeUpdates.add(checkedUpdate); + } + + public @NotNull List localSlotTypeUpdates() { + return List.copyOf(localSlotTypeUpdates); + } + + /// Snapshots only scratch-owned facts into a patch. Stable fallback entries are intentionally + /// excluded so commit remains an explicit delta over the pre-window publication surface. + public @NotNull FrontendAnalysisPatch toPatch(@NotNull FrontendSemanticStage stage) { + requireOpen(); + var checkedStage = Objects.requireNonNull(stage, "stage must not be null"); + checkLocalSlotUpdateStage(checkedStage); + return new FrontendAnalysisPatch( + checkedStage, + symbolBindings.copyScratch(), + resolvedMembers.copyScratch(), + resolvedCalls.copyScratch(), + expressionTypes.copyScratch(), + slotTypes.copyScratch(), + List.copyOf(localSlotTypeUpdates) + ); + } + + /// Snapshots the scratch facts into a patch and then closes the surface. + public @NotNull FrontendAnalysisPatch drainPatch(@NotNull FrontendSemanticStage stage) { + var patch = toPatch(stage); + clearScratch(); + open = false; + return patch; + } + + /// Drops every scratch fact so an unsupported or failed window cannot affect stable data. + public void discard() { + if (!open) { + return; + } + clearScratch(); + open = false; + } + + private void requireOpen() { + if (!open) { + throw new IllegalStateException("window publication surface is closed"); + } + } + + private void checkLocalSlotUpdateStage(@NotNull FrontendSemanticStage stage) { + if (!localSlotTypeUpdates.isEmpty() && stage != FrontendSemanticStage.LOCAL_TYPE_STABILIZATION) { + throw FrontendAnalysisData.patchFailure( + "Only LOCAL_TYPE_STABILIZATION patches may publish local slot type updates, but got " + + stage + ); + } + } + + private void clearScratch() { + symbolBindings.clearScratch(); + resolvedMembers.clearScratch(); + resolvedCalls.clearScratch(); + expressionTypes.clearScratch(); + slotTypes.clearScratch(); + localSlotTypeUpdates.clear(); + } + + /// Per-table effective view used by one semantic window. + public final class WindowSideTableView { + private final @NotNull FrontendAstSideTable stable; + private final @NotNull FrontendAstSideTable scratch = new FrontendAstSideTable<>(); + private final @NotNull SameValueChecker sameValueChecker; + private final @NotNull String fieldName; + private final @Nullable ValueGuard valueGuard; + + private WindowSideTableView( + @NotNull FrontendAstSideTable stable, + @NotNull SameValueChecker sameValueChecker, + @NotNull String fieldName, + @Nullable ValueGuard valueGuard + ) { + this.stable = Objects.requireNonNull(stable, "stable must not be null"); + this.sameValueChecker = Objects.requireNonNull(sameValueChecker, "sameValueChecker must not be null"); + this.fieldName = Objects.requireNonNull(fieldName, "fieldName must not be null"); + this.valueGuard = valueGuard; + } + + public @Nullable V get(@NotNull Node astNode) { + var checkedNode = Objects.requireNonNull(astNode, "astNode must not be null"); + var scratchValue = scratch.get(checkedNode); + return scratchValue != null ? scratchValue : stable.get(checkedNode); + } + + public @Nullable V getScratch(@NotNull Node astNode) { + return scratch.get(Objects.requireNonNull(astNode, "astNode must not be null")); + } + + public @Nullable V getStable(@NotNull Node astNode) { + return stable.get(Objects.requireNonNull(astNode, "astNode must not be null")); + } + + public boolean containsKey(@NotNull Node astNode) { + var checkedNode = Objects.requireNonNull(astNode, "astNode must not be null"); + return scratch.containsKey(checkedNode) || stable.containsKey(checkedNode); + } + + /// Records one scratch fact. Same-key writes are idempotent only when the logical value is + /// unchanged; otherwise the write fails immediately instead of silently shadowing stable data. + public void put(@NotNull Node astNode, @NotNull V value) { + requireOpen(); + var checkedNode = Objects.requireNonNull(astNode, "astNode must not be null"); + var checkedValue = Objects.requireNonNull(value, "value must not be null"); + if (valueGuard != null) { + valueGuard.check(checkedValue); + } + checkConflictingWrite(stable.get(checkedNode), checkedValue, checkedNode); + checkConflictingWrite(scratch.get(checkedNode), checkedValue, checkedNode); + scratch.put(checkedNode, checkedValue); + } + + private void checkConflictingWrite( + @Nullable V existingValue, + @NotNull V newValue, + @NotNull Node astNode + ) { + if (existingValue == null || sameValueChecker.sameValue(existingValue, newValue)) { + return; + } + throw FrontendAnalysisData.patchFailure( + fieldName + " scratch write conflicted on " + FrontendAnalysisData.describeNode(astNode) + ); + } + + private @NotNull FrontendAstSideTable copyScratch() { + var copy = new FrontendAstSideTable(); + copy.putAll(scratch); + return copy; + } + + private void clearScratch() { + scratch.clear(); + } + } + + @FunctionalInterface + private interface SameValueChecker { + boolean sameValue(@NotNull V first, @NotNull V second); + } + + @FunctionalInterface + private interface ValueGuard { + void check(@NotNull V value); + } +} diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendWindowPublicationSurfaceTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendWindowPublicationSurfaceTest.java new file mode 100644 index 00000000..a2298d17 --- /dev/null +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendWindowPublicationSurfaceTest.java @@ -0,0 +1,388 @@ +package gd.script.gdcc.frontend.sema; + +import dev.superice.gdparser.frontend.ast.AttributeCallStep; +import dev.superice.gdparser.frontend.ast.AttributePropertyStep; +import dev.superice.gdparser.frontend.ast.AttributeSubscriptStep; +import dev.superice.gdparser.frontend.ast.DeclarationKind; +import dev.superice.gdparser.frontend.ast.Expression; +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.diagnostic.DiagnosticSnapshot; +import gd.script.gdcc.frontend.diagnostic.FrontendDiagnostic; +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.ScopeLookupStatus; +import gd.script.gdcc.scope.ScopeOwnerKind; +import gd.script.gdcc.scope.ScopeValue; +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.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; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class FrontendWindowPublicationSurfaceTest { + private static final Range RANGE = new Range(0, 1, new Point(0, 0), new Point(0, 1)); + + @Test + void windowContextKeepsStableDataAndEffectiveReadsPreferScratch() { + var analysisData = FrontendAnalysisData.bootstrap(); + var stableNode = identifier("stable_value"); + var fallbackNode = identifier("fallback_value"); + var stableType = FrontendExpressionType.resolved(GdIntType.INT); + var fallbackType = FrontendExpressionType.failed("stable only"); + analysisData.expressionTypes().put(stableNode, stableType); + analysisData.expressionTypes().put(fallbackNode, fallbackType); + + var window = new FrontendWindowAnalysisContext(analysisData); + var scratchType = FrontendExpressionType.resolved(new GdIntType()); + window.publications().expressionTypes().put(stableNode, scratchType); + + assertSame(analysisData, window.stableData()); + assertSame(scratchType, window.publications().expressionTypes().get(stableNode)); + assertSame(scratchType, window.publications().expressionTypes().getScratch(stableNode)); + assertSame(stableType, window.publications().expressionTypes().getStable(stableNode)); + assertSame(fallbackType, window.publications().expressionTypes().get(fallbackNode)); + } + + @Test + void scratchWritesDoNotMutateStableSideTablesBeforeCommit() { + var analysisData = FrontendAnalysisData.bootstrap(); + var window = new FrontendWindowAnalysisContext(analysisData); + var bindingNode = identifier("local_use"); + var callNode = call("move", identifier("distance")); + var expressionNode = identifier("value"); + + window.publications().symbolBindings().put( + bindingNode, + new FrontendBinding("self", FrontendBindingKind.SELF, null) + ); + window.publications().resolvedCalls().put( + callNode, + FrontendResolvedCall.resolved( + "move", + FrontendCallResolutionKind.INSTANCE_METHOD, + FrontendReceiverKind.INSTANCE, + ScopeOwnerKind.GDCC, + new GdObjectType("Player"), + GdIntType.INT, + List.of(GdIntType.INT), + "Player.move" + ) + ); + window.publications().expressionTypes().put(expressionNode, FrontendExpressionType.resolved(GdIntType.INT)); + + assertNull(analysisData.symbolBindings().get(bindingNode)); + assertNull(analysisData.resolvedCalls().get(callNode)); + assertNull(analysisData.expressionTypes().get(expressionNode)); + assertTrue(window.publications().symbolBindings().containsKey(bindingNode)); + assertTrue(window.publications().resolvedCalls().containsKey(callNode)); + assertTrue(window.publications().expressionTypes().containsKey(expressionNode)); + } + + @Test + void toPatchCopiesOnlyScratchFactsWithoutStableFallbackEntries() { + var analysisData = FrontendAnalysisData.bootstrap(); + var stableNode = identifier("published"); + var scratchNode = identifier("pending"); + analysisData.expressionTypes().put(stableNode, FrontendExpressionType.resolved(GdIntType.INT)); + + var window = new FrontendWindowAnalysisContext(analysisData); + var scratchType = FrontendExpressionType.dynamic("retry result"); + window.publications().expressionTypes().put(scratchNode, scratchType); + + var patch = window.toPatch(FrontendSemanticStage.EXPR_TYPE); + + assertNull(patch.expressionTypes().get(stableNode)); + assertSame(scratchType, patch.expressionTypes().get(scratchNode)); + assertSame(analysisData.expressionTypes().get(stableNode), window.publications().expressionTypes().get(stableNode)); + } + + @Test + void toPatchRejectsNonLocalStabilizationSlotUpdateOwner() throws Exception { + var analysisData = FrontendAnalysisData.bootstrap(); + var window = new FrontendWindowAnalysisContext(analysisData); + var localScope = newBodyScope(); + var declaration = variable("local"); + localScope.defineLocal("local", GdVariantType.VARIANT, declaration); + window.publications().addLocalSlotTypeUpdate( + new FrontendLocalSlotTypeUpdate(localScope, "local", declaration, GdIntType.INT) + ); + + assertThrows( + FrontendAnalysisPatchException.class, + () -> window.toPatch(FrontendSemanticStage.EXPR_TYPE) + ); + assertSame(GdVariantType.VARIANT, requireLocal(localScope, "local").type()); + } + + @Test + void discardDropsScratchFactsWithoutTouchingStableDiagnosticsOrScope() throws Exception { + var analysisData = FrontendAnalysisData.bootstrap(); + var diagnostics = new DiagnosticSnapshot(List.of( + FrontendDiagnostic.warning("sema.window", "window warning", null, null) + )); + analysisData.updateDiagnostics(diagnostics); + var localScope = newBodyScope(); + var declaration = variable("local"); + localScope.defineLocal("local", GdVariantType.VARIANT, declaration); + var expressionNode = identifier("pending_value"); + + var window = new FrontendWindowAnalysisContext(analysisData); + window.publications().expressionTypes().put(expressionNode, FrontendExpressionType.resolved(GdIntType.INT)); + window.publications().addLocalSlotTypeUpdate( + new FrontendLocalSlotTypeUpdate(localScope, "local", declaration, GdIntType.INT) + ); + window.discard(); + + assertSame(diagnostics, analysisData.diagnostics()); + assertNull(analysisData.expressionTypes().get(expressionNode)); + assertTrue(analysisData.symbolBindings().isEmpty()); + assertSame(GdVariantType.VARIANT, requireLocal(localScope, "local").type()); + } + + @Test + void sameKeyIdempotentWriteCanShadowStableFactWithoutReplacingItOnCommit() { + var analysisData = FrontendAnalysisData.bootstrap(); + var expressionNode = identifier("value"); + var stableType = FrontendExpressionType.resolved(GdIntType.INT); + analysisData.expressionTypes().put(expressionNode, stableType); + + var window = new FrontendWindowAnalysisContext(analysisData); + var scratchType = FrontendExpressionType.resolved(new GdIntType()); + window.publications().expressionTypes().put(expressionNode, scratchType); + analysisData.applyPatch(window.drainPatch(FrontendSemanticStage.EXPR_TYPE)); + + assertSame(stableType, window.publications().expressionTypes().getStable(expressionNode)); + assertSame(stableType, analysisData.expressionTypes().get(expressionNode)); + } + + @Test + void sameKeyConflictsRejectStableShadowingAndNegativeToSuccessOverride() { + var analysisData = FrontendAnalysisData.bootstrap(); + var stableNode = identifier("value"); + var failedNode = identifier("failed"); + var slotNode = variable("slot"); + analysisData.expressionTypes().put(stableNode, FrontendExpressionType.resolved(GdIntType.INT)); + analysisData.expressionTypes().put(failedNode, FrontendExpressionType.failed("original failure")); + analysisData.slotTypes().put(slotNode, GdIntType.INT); + + var window = new FrontendWindowAnalysisContext(analysisData); + + assertThrows( + FrontendAnalysisPatchException.class, + () -> window.publications().expressionTypes().put(stableNode, FrontendExpressionType.resolved(GdFloatType.FLOAT)) + ); + assertThrows( + FrontendAnalysisPatchException.class, + () -> window.publications().expressionTypes().put(failedNode, FrontendExpressionType.resolved(GdIntType.INT)) + ); + assertThrows( + FrontendAnalysisPatchException.class, + () -> window.publications().slotTypes().put(slotNode, GdFloatType.FLOAT) + ); + } + + @Test + void finalRetryFactsStayScratchLocalUntilPatchCommit() { + var analysisData = FrontendAnalysisData.bootstrap(); + var expressionNode = identifier("retry_result"); + var window = new FrontendWindowAnalysisContext(analysisData); + var finalizedType = FrontendExpressionType.resolved(GdIntType.INT); + + window.publications().expressionTypes().put(expressionNode, finalizedType); + + assertSame(finalizedType, window.publications().expressionTypes().get(expressionNode)); + assertNull(analysisData.expressionTypes().get(expressionNode)); + + var patch = window.drainPatch(FrontendSemanticStage.EXPR_TYPE); + assertNull(analysisData.expressionTypes().get(expressionNode)); + analysisData.applyPatch(patch); + + assertSame(finalizedType, analysisData.expressionTypes().get(expressionNode)); + } + + @Test + void attributeStepKeysKeepIdentityLookupAndDuplicateGuards() { + var analysisData = FrontendAnalysisData.bootstrap(); + var window = new FrontendWindowAnalysisContext(analysisData); + var propertyStep = property("marker"); + var callStep = call("fetch", identifier("seed")); + var subscriptStep = subscript("items", identifier("index")); + var propertyMember = FrontendResolvedMember.resolved( + "marker", + FrontendBindingKind.PROPERTY, + FrontendReceiverKind.INSTANCE, + ScopeOwnerKind.GDCC, + new GdObjectType("Player"), + GdIntType.INT, + "Player.marker" + ); + var resolvedCall = FrontendResolvedCall.resolved( + "fetch", + FrontendCallResolutionKind.INSTANCE_METHOD, + FrontendReceiverKind.INSTANCE, + ScopeOwnerKind.GDCC, + new GdObjectType("Player"), + GdIntType.INT, + List.of(GdVariantType.VARIANT), + "Player.fetch" + ); + + window.publications().resolvedMembers().put(propertyStep, propertyMember); + window.publications().resolvedCalls().put(callStep, resolvedCall); + window.publications().expressionTypes().put(subscriptStep, FrontendExpressionType.resolved(GdIntType.INT)); + window.publications().expressionTypes().put(subscriptStep, FrontendExpressionType.resolved(new GdIntType())); + + assertSame(propertyMember, window.publications().resolvedMembers().get(propertyStep)); + assertSame(resolvedCall, window.publications().resolvedCalls().get(callStep)); + assertNull(window.publications().resolvedMembers().get(property("marker"))); + assertNull(window.publications().resolvedCalls().get(call("fetch", identifier("seed")))); + assertThrows( + FrontendAnalysisPatchException.class, + () -> window.publications().expressionTypes().put(subscriptStep, FrontendExpressionType.resolved(GdFloatType.FLOAT)) + ); + } + + @Test + void compilerOnlyTypesAreRejectedBeforeCommit() throws Exception { + var analysisData = FrontendAnalysisData.bootstrap(); + var window = new FrontendWindowAnalysisContext(analysisData); + + assertThrows( + FrontendAnalysisPatchException.class, + () -> window.publications().expressionTypes().put( + identifier("iter"), + FrontendExpressionType.resolved(GdccForRangeIterType.FOR_RANGE_ITER) + ) + ); + assertThrows( + FrontendAnalysisPatchException.class, + () -> window.publications().slotTypes().put(variable("iter_slot"), GdccForRangeIterType.FOR_RANGE_ITER) + ); + + var localScope = newBodyScope(); + var declaration = variable("local"); + localScope.defineLocal("local", GdVariantType.VARIANT, declaration); + assertThrows( + FrontendAnalysisPatchException.class, + () -> window.publications().addLocalSlotTypeUpdate( + new FrontendLocalSlotTypeUpdate( + localScope, + "local", + declaration, + GdccForRangeIterType.FOR_RANGE_ITER + ) + ) + ); + } + + @Test + void localSlotUpdatesRemainIsolatedUntilPatchApply() throws Exception { + var analysisData = FrontendAnalysisData.bootstrap(); + var localScope = newBodyScope(); + var declaration = variable("local"); + localScope.defineLocal("local", GdVariantType.VARIANT, declaration); + var bindingNode = identifier("local_use"); + var originalBinding = localBinding("local", declaration, requireLocal(localScope, "local")); + analysisData.symbolBindings().put(bindingNode, originalBinding); + + var window = new FrontendWindowAnalysisContext(analysisData); + window.publications().addLocalSlotTypeUpdate( + new FrontendLocalSlotTypeUpdate(localScope, "local", declaration, GdIntType.INT) + ); + + assertSame(GdVariantType.VARIANT, requireLocal(localScope, "local").type()); + assertSame(originalBinding, analysisData.symbolBindings().get(bindingNode)); + + var patch = window.toPatch(FrontendSemanticStage.LOCAL_TYPE_STABILIZATION); + + assertSame(GdVariantType.VARIANT, requireLocal(localScope, "local").type()); + assertSame(originalBinding, analysisData.symbolBindings().get(bindingNode)); + analysisData.applyPatch(patch); + + var refreshedBinding = analysisData.symbolBindings().get(bindingNode); + var refreshedValue = Objects.requireNonNull(refreshedBinding).resolvedValue(); + assertNotSame(originalBinding, refreshedBinding); + assertSame(GdIntType.INT, Objects.requireNonNull(refreshedValue).type()); + assertSame(GdIntType.INT, requireLocal(localScope, "local").type()); + } + + 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 AttributePropertyStep property(@NotNull String name) { + return new AttributePropertyStep(name, RANGE); + } + + private static @NotNull AttributeCallStep call(@NotNull String name, @NotNull Expression... arguments) { + return new AttributeCallStep(name, List.of(arguments), RANGE); + } + + private static @NotNull AttributeSubscriptStep subscript(@NotNull String name, @NotNull Expression... arguments) { + return new AttributeSubscriptStep(name, List.of(arguments), RANGE); + } + + 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); + } +} From 28d7eb70b22d30f2ede24821ce1d37690bc08d2d Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Mon, 6 Jul 2026 10:41:52 +0800 Subject: [PATCH 04/27] feat(frontend): route semantic analyzers through window-scoped publication - Add window-level runners so per-stage facts publish to a scratch surface instead of the stable side table - Preserve legacy whole-module entry by draining the patch and replaying stale-clear semantics for owners - Tighten inferred local type backfill to guard-only and defer local slot refresh to the patch commit - Cover scratch publication, commit-time isolation, and guard-only backfill with focused tests - Mark the corresponding plan items as completed in the segmented type resolution pipeline doc --- ...segmented_type_resolution_pipeline_plan.md | 9 +++ .../FrontendChainBindingAnalyzer.java | 31 +++++++- .../analyzer/FrontendExprTypeAnalyzer.java | 75 ++++++++++--------- ...rontendLocalTypeStabilizationAnalyzer.java | 44 +++++++++-- .../analyzer/FrontendTopBindingAnalyzer.java | 26 ++++++- .../analyzer/FrontendVarTypePostAnalyzer.java | 24 +++++- .../FrontendExprTypeAnalyzerTest.java | 68 ++++++++++++++--- ...endLocalTypeStabilizationAnalyzerTest.java | 63 ++++++++++++++-- 8 files changed, 274 insertions(+), 66 deletions(-) diff --git a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md index c478f871..de4d9874 100644 --- a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md +++ b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md @@ -444,6 +444,15 @@ record FrontendLocalSlotTypeUpdate( - 改造 `FrontendChainReductionHelper` / `FrontendChainReductionFacade` 的 expression type 查找入口,使其通过 window effective view 或 window-aware resolver 读取 `expressionTypes()`。 - 保持现有 analyzer class 名称和 public `analyze(...)` 方法,避免一次性改动所有调用点。 +当前状态(2026-07-05): + +- [x] C1 为 `FrontendTopBindingAnalyzer`、`FrontendChainBindingAnalyzer`、`FrontendExprTypeAnalyzer`、`FrontendVarTypePostAnalyzer` 提取 window-level runner,runner 将 stage 产物写入 `FrontendWindowAnalysisContext` scratch surface,不直接发布到 stable side table。 +- [x] C2 保持现有 public `analyze(...)` 方法签名;whole-module wrapper 先清空本阶段拥有的 snapshot 表,再通过 window runner 生成 patch,以保留 legacy stale-clear 语义。 +- [x] C3 为 `FrontendLocalTypeStabilizationAnalyzer` 提取 window-level runner,并让 window 路径只收集 `FrontendLocalSlotTypeUpdate`;实际 `BlockScope` mutation 与 binding payload refresh 仍由 patch commit 统一执行。 +- [x] C4 将 `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 收紧为 guard-only:不再调用 `BlockScope.resetLocalType(...)`,不再刷新 `symbolBindings()` payload,也不产生 local slot update。 +- [x] C5 将 expr typing 的 `expressionTypes()` 与 bare-call `resolvedCalls()` 发布改为先写 window scratch;nested expression dependency 继续通过当前 window 的 scratch table 保留读己写能力。 +- [x] C6 增加 focused tests 锚定 guard-only backfill、expr window scratch-only publication、local stabilization window commit 前隔离与 commit 后 slot/binding refresh。 + 验收细则: - `FrontendSemanticAnalyzerFrameworkTest.analyzePublishesPhaseBoundariesThroughVirtualOverridePhaseAndRefreshesDiagnosticsAfterEachPhase` 继续通过,证明 public phase boundary 未漂移。 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 index 5e9208b0..37df1ca7 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendChainBindingAnalyzer.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendChainBindingAnalyzer.java @@ -5,6 +5,8 @@ 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.FrontendSemanticStage; +import gd.script.gdcc.frontend.sema.FrontendWindowAnalysisContext; 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; @@ -73,11 +75,30 @@ public void analyze( @NotNull ClassRegistry classRegistry, @NotNull FrontendAnalysisData analysisData, @NotNull DiagnosticManager diagnosticManager + ) { + analysisData.updateResolvedMembers(new FrontendAstSideTable<>()); + analysisData.updateResolvedCalls(new FrontendAstSideTable<>()); + var window = new FrontendWindowAnalysisContext(analysisData); + analyzeInWindow(classRegistry, window, diagnosticManager); + + // Preserve whole-module replacement semantics for legacy callers; segmented callers commit + // the patch directly and therefore get conflict-checked incremental publication. + var patch = window.drainPatch(FrontendSemanticStage.CHAIN_BINDING); + analysisData.updateResolvedMembers(patch.resolvedMembers()); + analysisData.updateResolvedCalls(patch.resolvedCalls()); + } + + void analyzeInWindow( + @NotNull ClassRegistry classRegistry, + @NotNull FrontendWindowAnalysisContext window, + @NotNull DiagnosticManager diagnosticManager ) { Objects.requireNonNull(classRegistry, "classRegistry must not be null"); - Objects.requireNonNull(analysisData, "analysisData must not be null"); + Objects.requireNonNull(window, "window must not be null"); Objects.requireNonNull(diagnosticManager, "diagnosticManager must not be null"); + var analysisData = window.stableData(); + var moduleSkeleton = analysisData.moduleSkeleton(); analysisData.diagnostics(); @@ -104,8 +125,12 @@ public void analyze( diagnosticManager ).walk(sourceClassRelation.unit().ast()); } - analysisData.updateResolvedMembers(resolvedMembers); - analysisData.updateResolvedCalls(resolvedCalls); + for (var entry : resolvedMembers.entrySet()) { + window.publications().resolvedMembers().put(entry.getKey(), entry.getValue()); + } + for (var entry : resolvedCalls.entrySet()) { + window.publications().resolvedCalls().put(entry.getKey(), entry.getValue()); + } } private static final class AstWalkerChainBinder implements ASTNodeHandler { 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 index 43fbe30a..297a7a48 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzer.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzer.java @@ -66,12 +66,12 @@ /// 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. +/// The phase rebuilds `expressionTypes()` through a window scratch table so nested chain reduction can +/// immediately consume freshly published inner expression facts without writing stable data early. /// `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. +/// stabilization phase; the backfill path below is now a guard-only protocol check. public class FrontendExprTypeAnalyzer { private static final @NotNull String EXPRESSION_RESOLUTION_CATEGORY = "sema.expression_resolution"; private static final @NotNull String DEFERRED_EXPRESSION_RESOLUTION_CATEGORY = @@ -85,11 +85,29 @@ public void analyze( @NotNull ClassRegistry classRegistry, @NotNull FrontendAnalysisData analysisData, @NotNull DiagnosticManager diagnosticManager + ) { + analysisData.updateExpressionTypes(new FrontendAstSideTable<>()); + var window = new FrontendWindowAnalysisContext(analysisData); + analyzeInWindow(classRegistry, window, diagnosticManager); + + // Keep legacy whole-module expression typing as a complete snapshot replacement while bare + // call facts are merged incrementally into the resolvedCalls() table owned by this phase. + var patch = window.drainPatch(FrontendSemanticStage.EXPR_TYPE); + analysisData.updateExpressionTypes(patch.expressionTypes()); + analysisData.applyPatch(patch); + } + + void analyzeInWindow( + @NotNull ClassRegistry classRegistry, + @NotNull FrontendWindowAnalysisContext window, + @NotNull DiagnosticManager diagnosticManager ) { Objects.requireNonNull(classRegistry, "classRegistry must not be null"); - Objects.requireNonNull(analysisData, "analysisData must not be null"); + Objects.requireNonNull(window, "window must not be null"); Objects.requireNonNull(diagnosticManager, "diagnosticManager must not be null"); + var analysisData = window.stableData(); + var moduleSkeleton = analysisData.moduleSkeleton(); analysisData.diagnostics(); @@ -103,8 +121,8 @@ public void analyze( } } - var expressionTypes = analysisData.expressionTypes(); - expressionTypes.clear(); + var expressionTypes = new FrontendAstSideTable(); + var bareResolvedCalls = new FrontendAstSideTable(); for (var sourceClassRelation : moduleSkeleton.sourceClassRelations()) { new AstWalkerExprTypePublisher( sourceClassRelation.unit().path(), @@ -112,10 +130,16 @@ public void analyze( analysisData, scopesByAst, expressionTypes, + bareResolvedCalls, diagnosticManager ).walk(sourceClassRelation.unit().ast()); } - analysisData.updateExpressionTypes(expressionTypes); + for (var entry : expressionTypes.entrySet()) { + window.publications().expressionTypes().put(entry.getKey(), entry.getValue()); + } + for (var entry : bareResolvedCalls.entrySet()) { + window.publications().resolvedCalls().put(entry.getKey(), entry.getValue()); + } } private static final class AstWalkerExprTypePublisher implements ASTNodeHandler { @@ -124,6 +148,7 @@ private static final class AstWalkerExprTypePublisher implements ASTNodeHandler private final @NotNull FrontendAnalysisData analysisData; private final @NotNull FrontendAstSideTable scopesByAst; private final @NotNull FrontendAstSideTable expressionTypes; + private final @NotNull FrontendAstSideTable bareResolvedCalls; private final @NotNull DiagnosticManager diagnosticManager; private final @NotNull ASTWalker astWalker; private final @NotNull FrontendChainReductionFacade chainReduction; @@ -145,6 +170,7 @@ private AstWalkerExprTypePublisher( @NotNull FrontendAnalysisData analysisData, @NotNull FrontendAstSideTable scopesByAst, @NotNull FrontendAstSideTable expressionTypes, + @NotNull FrontendAstSideTable bareResolvedCalls, @NotNull DiagnosticManager diagnosticManager ) { this.sourcePath = Objects.requireNonNull(sourcePath, "sourcePath must not be null"); @@ -152,6 +178,7 @@ private AstWalkerExprTypePublisher( 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.bareResolvedCalls = Objects.requireNonNull(bareResolvedCalls, "bareResolvedCalls must not be null"); this.diagnosticManager = Objects.requireNonNull(diagnosticManager, "diagnosticManager must not be null"); astWalker = new ASTWalker(this); chainReduction = new FrontendChainReductionFacade( @@ -514,14 +541,9 @@ private boolean isRouteHeadOnlyTypeMeta(@NotNull Expression expression) { && 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. + /// Supported local `:=` declarations must be stabilized before expression typing. This guard + /// only verifies that an already-stabilized exact slot still agrees with its initializer fact; + /// it never narrows an inventory-seeded `Variant` slot or refreshes binding payloads. private void backfillInferredLocalType(@NotNull VariableDeclaration variableDeclaration) { if (!FrontendDeclaredTypeSupport.isInferredTypeRef(variableDeclaration.type()) || variableDeclaration.value() == null) { @@ -540,7 +562,8 @@ private void backfillInferredLocalType(@NotNull VariableDeclaration variableDecl return; } var backfilledType = switch (publishedInitializerType.status()) { - case RESOLVED, DYNAMIC -> publishedInitializerType.publishedType(); + case RESOLVED -> publishedInitializerType.publishedType(); + case DYNAMIC -> null; case BLOCKED, DEFERRED, FAILED, UNSUPPORTED -> null; }; if (backfilledType == null || backfilledType instanceof GdVoidType) { @@ -563,21 +586,6 @@ private void backfillInferredLocalType(@NotNull VariableDeclaration variableDecl + backfilledType.getTypeName() ); } - return; - } - var localName = variableDeclaration.name().trim(); - blockScope.resetLocalType(localName, variableDeclaration, backfilledType); - var updatedValue = blockScope.resolveValueHere(localName); - if (updatedValue != null && updatedValue.declaration() == variableDeclaration) { - analysisData.refreshPublishedLocalBindingPayloads( - new FrontendLocalSlotTypeUpdate( - blockScope, - localName, - variableDeclaration, - updatedValue.type() - ), - updatedValue - ); } } @@ -824,14 +832,13 @@ private void publishBareResolvedCall( if (publishedCall == null) { return; } - var resolvedCalls = analysisData.resolvedCalls(); - if (resolvedCalls.containsKey(callExpression)) { + if (analysisData.resolvedCalls().containsKey(callExpression) || bareResolvedCalls.containsKey(callExpression)) { throw new IllegalStateException( "resolvedCalls() already contains a published fact for bare CallExpression at " + callExpression.range() ); } - resolvedCalls.put(callExpression, publishedCall); + bareResolvedCalls.put(callExpression, publishedCall); reportUnsafeCallArgumentWarning(callExpression, publishedCall); } 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 index 114ba4da..f51d6d98 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendLocalTypeStabilizationAnalyzer.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendLocalTypeStabilizationAnalyzer.java @@ -38,6 +38,7 @@ import gd.script.gdcc.frontend.sema.FrontendExpressionType; import gd.script.gdcc.frontend.sema.FrontendExpressionTypeStatus; import gd.script.gdcc.frontend.sema.FrontendLocalSlotTypeUpdate; +import gd.script.gdcc.frontend.sema.FrontendWindowAnalysisContext; 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; @@ -86,7 +87,16 @@ public void analyze( @NotNull FrontendAnalysisData analysisData, @NotNull DiagnosticManager diagnosticManager ) { - run(classRegistry, analysisData, diagnosticManager, true); + run(classRegistry, analysisData, diagnosticManager, true, null); + } + + void analyzeInWindow( + @NotNull ClassRegistry classRegistry, + @NotNull FrontendWindowAnalysisContext window, + @NotNull DiagnosticManager diagnosticManager + ) { + Objects.requireNonNull(window, "window must not be null"); + run(classRegistry, window.stableData(), diagnosticManager, true, window); } /// Package-private test helper for observing transient initializer typing. @@ -100,7 +110,7 @@ public void analyze( @NotNull FrontendAnalysisData analysisData, @NotNull DiagnosticManager diagnosticManager ) { - return run(classRegistry, analysisData, diagnosticManager, false); + return run(classRegistry, analysisData, diagnosticManager, false, null); } static @Nullable FrontendExpressionType probeAssignmentOrdinaryValueInitializerFailure( @@ -121,7 +131,8 @@ static void probeStabilizeLocalSlot( @NotNull ClassRegistry classRegistry, @NotNull FrontendAnalysisData analysisData, @NotNull DiagnosticManager diagnosticManager, - boolean writeBackStableSlots + boolean writeBackStableSlots, + @Nullable FrontendWindowAnalysisContext window ) { Objects.requireNonNull(classRegistry, "classRegistry must not be null"); Objects.requireNonNull(analysisData, "analysisData must not be null"); @@ -146,7 +157,8 @@ static void probeStabilizeLocalSlot( analysisData, scopesByAst, probes, - writeBackStableSlots + writeBackStableSlots, + window ).walk(sourceClassRelation.unit().ast()); } return new ProbeSnapshot(probes); @@ -241,6 +253,7 @@ private static final class AstWalkerLocalTypeStabilizer implements ASTNodeHandle private final @NotNull SilentExpressionResolver silentExpressionResolver; private final @NotNull List probes; private final boolean writeBackStableSlots; + private final @Nullable FrontendWindowAnalysisContext window; private int supportedExecutableBlockDepth; private @NotNull ResolveRestriction currentRestriction = ResolveRestriction.unrestricted(); private boolean currentStaticContext; @@ -251,13 +264,15 @@ private AstWalkerLocalTypeStabilizer( @NotNull FrontendAnalysisData analysisData, @NotNull FrontendAstSideTable scopesByAst, @NotNull List probes, - boolean writeBackStableSlots + boolean writeBackStableSlots, + @Nullable FrontendWindowAnalysisContext window ) { var checkedAnalysisData = Objects.requireNonNull(analysisData, "analysisData must not be null"); this.analysisData = checkedAnalysisData; this.scopesByAst = Objects.requireNonNull(scopesByAst, "scopesByAst must not be null"); this.probes = Objects.requireNonNull(probes, "probes must not be null"); this.writeBackStableSlots = writeBackStableSlots; + this.window = window; astWalker = new ASTWalker(this); silentExpressionResolver = new SilentExpressionResolver( Objects.requireNonNull(sourcePath, "sourcePath must not be null"), @@ -361,8 +376,23 @@ private void walk(@NotNull SourceFile sourceFile) { var initializerType = silentExpressionResolver.resolveExpressionType(initializer); probes.add(new ProbeEntry(variableDeclaration, initializer, initializerType)); if (writeBackStableSlots) { - var updatedValue = stabilizeLocalSlot(blockScope, variableDeclaration, initializerType); - if (updatedValue != null) { + if (window != null) { + var stableType = stableLocalTypeOrNull(initializerType); + if (stableType != null) { + // Window execution records the owner-approved rewrite and leaves the actual + // BlockScope mutation to patch commit, so discarded windows stay isolated. + window.publications().addLocalSlotTypeUpdate(new FrontendLocalSlotTypeUpdate( + blockScope, + variableDeclaration.name().trim(), + variableDeclaration, + stableType + )); + } + } else { + var updatedValue = stabilizeLocalSlot(blockScope, variableDeclaration, initializerType); + if (updatedValue == null) { + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } analysisData.refreshPublishedLocalBindingPayloads( new FrontendLocalSlotTypeUpdate( blockScope, 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 index 53a0d52a..a2e09d78 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTopBindingAnalyzer.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTopBindingAnalyzer.java @@ -7,6 +7,8 @@ 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.FrontendSemanticStage; +import gd.script.gdcc.frontend.sema.FrontendWindowAnalysisContext; 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; @@ -82,11 +84,29 @@ public void analyze( @NotNull ClassRegistry classRegistry, @NotNull FrontendAnalysisData analysisData, @NotNull DiagnosticManager diagnosticManager + ) { + analysisData.updateSymbolBindings(new FrontendAstSideTable<>()); + var window = new FrontendWindowAnalysisContext(analysisData); + analyzeInWindow(classRegistry, window, diagnosticManager); + + // The legacy whole-module API still means "replace the complete top-binding snapshot". + // Segmented callers commit the drained patch directly instead. + var patch = window.drainPatch(FrontendSemanticStage.TOP_BINDING); + analysisData.updateSymbolBindings(patch.symbolBindings()); + } + + /// Runs top-binding analysis into one window-local scratch publication surface. + void analyzeInWindow( + @NotNull ClassRegistry classRegistry, + @NotNull FrontendWindowAnalysisContext window, + @NotNull DiagnosticManager diagnosticManager ) { Objects.requireNonNull(classRegistry, "classRegistry must not be null"); - Objects.requireNonNull(analysisData, "analysisData must not be null"); + Objects.requireNonNull(window, "window must not be null"); Objects.requireNonNull(diagnosticManager, "diagnosticManager must not be null"); + var analysisData = window.stableData(); + var moduleSkeleton = analysisData.moduleSkeleton(); analysisData.diagnostics(); @@ -113,7 +133,9 @@ public void analyze( classRegistry ).walk(sourceClassRelation.unit().ast()); } - analysisData.updateSymbolBindings(symbolBindings); + for (var entry : symbolBindings.entrySet()) { + window.publications().symbolBindings().put(entry.getKey(), entry.getValue()); + } } private enum ExpressionPosition { 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 index 1177ffc4..2b2da114 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendVarTypePostAnalyzer.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendVarTypePostAnalyzer.java @@ -7,6 +7,8 @@ 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.frontend.sema.FrontendSemanticStage; +import gd.script.gdcc.frontend.sema.FrontendWindowAnalysisContext; import gd.script.gdcc.scope.Scope; import gd.script.gdcc.scope.ScopeValue; import gd.script.gdcc.scope.ScopeValueKind; @@ -52,9 +54,25 @@ public void analyze( @NotNull FrontendAnalysisData analysisData, @NotNull DiagnosticManager diagnosticManager ) { - Objects.requireNonNull(analysisData, "analysisData must not be null"); + analysisData.updateSlotTypes(new FrontendAstSideTable<>()); + var window = new FrontendWindowAnalysisContext(analysisData); + analyzeInWindow(window, diagnosticManager); + + // Slot types are a final whole-module snapshot for existing callers; incremental segmented + // execution should apply the drained patch instead of replacing the table. + var patch = window.drainPatch(FrontendSemanticStage.VAR_TYPE_POST); + analysisData.updateSlotTypes(patch.slotTypes()); + } + + void analyzeInWindow( + @NotNull FrontendWindowAnalysisContext window, + @NotNull DiagnosticManager diagnosticManager + ) { + Objects.requireNonNull(window, "window must not be null"); Objects.requireNonNull(diagnosticManager, "diagnosticManager must not be null"); + var analysisData = window.stableData(); + var moduleSkeleton = analysisData.moduleSkeleton(); analysisData.diagnostics(); @@ -78,7 +96,9 @@ public void analyze( slotTypes ).walk(sourceClassRelation.unit().ast()); } - analysisData.updateSlotTypes(slotTypes); + for (var entry : slotTypes.entrySet()) { + window.publications().slotTypes().put(entry.getKey(), entry.getValue()); + } } /// Traverses only the current supported executable surface and republishes the already-settled diff --git a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzerTest.java b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzerTest.java index 3155756d..17e2302c 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzerTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzerTest.java @@ -12,6 +12,8 @@ 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.FrontendWindowAnalysisContext; +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; @@ -39,6 +41,7 @@ import gd.script.gdcc.type.GdVariantType; import gd.script.gdcc.frontend.diagnostic.FrontendDiagnostic; import gd.script.gdcc.frontend.sema.FrontendAnalysisData; +import gd.script.gdcc.frontend.sema.FrontendAstSideTable; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; @@ -2319,11 +2322,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(): @@ -2349,14 +2352,59 @@ func ping(): input.diagnosticManager() ); - var refreshedBinding = input.analysisData().symbolBindings().get(valueUse); + var guardedBinding = input.analysisData().symbolBindings().get(valueUse); var valueUseType = input.analysisData().expressionTypes().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(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()), + () -> assertNotNull(valueUseType), + () -> assertEquals(FrontendExpressionTypeStatus.RESOLVED, valueUseType.status()), + () -> assertEquals(GdVariantType.VARIANT, valueUseType.publishedType()) + ); + } + + @Test + void analyzeInWindowKeepsExpressionFactsScratchOnlyUntilPatchCommit() throws Exception { + var input = prepareInputBeforeExpressionTyping( + "expr_type_window_scratch_publication.gd", + """ + class_name ExprTypeWindowScratchPublication + extends RefCounted + + func make_value() -> int: + return 1 + + func ping(): + var value := make_value() + value + """ + ); + var pingFunction = findFunction(input.unit().ast(), "ping"); + var valueDeclaration = findVariable(pingFunction.body().statements(), "value"); + var valueUse = assertInstanceOf( + IdentifierExpression.class, + assertInstanceOf(ExpressionStatement.class, pingFunction.body().statements().get(1)).expression() + ); + input.analysisData().updateExpressionTypes(new FrontendAstSideTable<>()); + + var window = new FrontendWindowAnalysisContext(input.analysisData()); + new FrontendExprTypeAnalyzer().analyzeInWindow(input.classRegistry(), window, input.diagnosticManager()); + + assertNull(input.analysisData().expressionTypes().get(valueDeclaration.value())); + assertNull(input.analysisData().expressionTypes().get(valueUse)); + + input.analysisData().applyPatch(window.drainPatch(FrontendSemanticStage.EXPR_TYPE)); + + var initializerType = input.analysisData().expressionTypes().get(valueDeclaration.value()); + var valueUseType = input.analysisData().expressionTypes().get(valueUse); + assertAll( + () -> assertNotNull(initializerType), + () -> assertEquals(FrontendExpressionTypeStatus.RESOLVED, initializerType.status()), + () -> assertEquals("int", initializerType.publishedType().getTypeName()), () -> assertNotNull(valueUseType), () -> assertEquals(FrontendExpressionTypeStatus.RESOLVED, valueUseType.status()), () -> assertEquals("int", valueUseType.publishedType().getTypeName()) @@ -2441,7 +2489,7 @@ void analyzeFailsFastWhenResolvedCompilerOnlyExpressionFactWouldBePublished() th """ class_name ExprTypeCompilerOnlyExpressionFact extends RefCounted - + func ping(): var value: int = 1 value 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 index 2957725f..ce1555f0 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendLocalTypeStabilizationAnalyzerTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendLocalTypeStabilizationAnalyzerTest.java @@ -28,6 +28,8 @@ 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.frontend.sema.FrontendSemanticStage; +import gd.script.gdcc.frontend.sema.FrontendWindowAnalysisContext; import gd.script.gdcc.gdextension.ExtensionApiLoader; import gd.script.gdcc.lir.LirClassDef; import gd.script.gdcc.scope.ClassRegistry; @@ -47,6 +49,7 @@ 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; @@ -107,6 +110,50 @@ func ping(factory: Factory, seed: int): ); } + @Test + void analyzeInWindowDefersLocalSlotRewriteUntilPatchCommit() throws Exception { + var prepared = prepareProbeInput( + "local_type_stabilization_window_commit.gd", + """ + class_name LocalTypeStabilizationWindowCommit + extends RefCounted + + func ping(): + var value := 1 + value + """ + ); + var pingFunction = findFunction(prepared.unit().ast().statements(), "ping"); + var bodyScope = assertInstanceOf(BlockScope.class, prepared.analysisData().scopesByAst().get(pingFunction.body())); + var valueUse = assertInstanceOf( + IdentifierExpression.class, + assertInstanceOf(ExpressionStatement.class, pingFunction.body().statements().get(1)).expression() + ); + var originalBinding = prepared.analysisData().symbolBindings().get(valueUse); + assertNotNull(originalBinding); + var originalResolvedValue = originalBinding.resolvedValue(); + assertNotNull(originalResolvedValue); + assertSame(GdVariantType.VARIANT, originalResolvedValue.type()); + + var window = new FrontendWindowAnalysisContext(prepared.analysisData()); + new FrontendLocalTypeStabilizationAnalyzer().analyzeInWindow( + prepared.classRegistry(), + window, + prepared.diagnosticManager() + ); + + assertSame(GdVariantType.VARIANT, bodyScope.resolveValue("value").type()); + assertSame(originalBinding, prepared.analysisData().symbolBindings().get(valueUse)); + + prepared.analysisData().applyPatch(window.drainPatch(FrontendSemanticStage.LOCAL_TYPE_STABILIZATION)); + + var refreshedBinding = prepared.analysisData().symbolBindings().get(valueUse); + assertNotNull(refreshedBinding); + assertNotNull(refreshedBinding.resolvedValue()); + assertEquals("int", bodyScope.resolveValue("value").type().getTypeName()); + assertEquals("int", refreshedBinding.resolvedValue().type().getTypeName()); + } + @Test void analyzeKeepsBareTypeMetaInitializerOutOfLocalStabilization() throws Exception { var prepared = prepareProbeInput( @@ -114,11 +161,11 @@ void analyzeKeepsBareTypeMetaInitializerOutOfLocalStabilization() throws Excepti """ class_name LocalTypeStabilizationTypeMetaGuard extends RefCounted - + class Worker: static func build() -> int: return 1 - + func ping(): var bad := Worker bad @@ -296,13 +343,13 @@ void analyzeLetsChildBlockReadParentStabilizedLocal() throws Exception { """ 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: @@ -339,13 +386,13 @@ void analyzeKeepsShadowedChildBlockLocalFromPollutingParentSlot() throws Excepti """ 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: From eb712785afa608cd0ceebccf1c18e8a94e201681 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Mon, 6 Jul 2026 17:06:29 +0800 Subject: [PATCH 05/27] feat(frontend): add segmented scheduler with test entry and equivalence coverage - Add a segmented scheduler driving binding/typing stages via window-scoped patches, keeping legacy whole-phase as the default production path. - Expose a test-only analyzer entry that routes publication through a shared helper so both paths share diagnostics refresh. - Keep local type stabilization on the legacy direct phase, deferring per-window slot visibility to a follow-up root-bounded window stage. - Cover legacy vs segmented side-table and diagnostics parity, including unsupported loop/match/block-local-const behaviors. - Mark corresponding plan items completed and gate the production switch on the remaining root-bounded window execution work. --- ...segmented_type_resolution_pipeline_plan.md | 15 ++ .../FrontendSegmentedSemanticScheduler.java | 119 ++++++++++ .../analyzer/FrontendSemanticAnalyzer.java | 116 +++++++-- ...FrontendSemanticAnalyzerFrameworkTest.java | 224 ++++++++++++++++++ 4 files changed, 453 insertions(+), 21 deletions(-) create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSegmentedSemanticScheduler.java diff --git a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md index de4d9874..8d0f0edd 100644 --- a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md +++ b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md @@ -473,6 +473,15 @@ record FrontendLocalSlotTypeUpdate( - `FrontendSemanticAnalyzer` 增加内部开关或 package-private 构造路径,用于测试 segmented runner 与 legacy whole-phase runner 的等价性。 - 默认生产路径可以在阶段 D 末切换到 segmented runner,但必须先完成等价测试。 +当前状态(2026-07-06): + +- [x] D1 新增 `FrontendSegmentedSemanticScheduler`,接入阶段 A-C 已有 window publication / patch merge 基础设施。 +- [x] D2 为 `FrontendSemanticAnalyzer` 增加测试用 segmented runner 内部入口,默认生产路径仍保持 legacy whole-phase runner。 +- [x] D3 scheduler 对 top binding、chain binding、expr typing、var type post 使用独立 window surface,并在每个 owner stage 完成后通过 `applyPatch(...)` 增量提交和刷新 diagnostics snapshot。 +- [x] D4 scheduler 的 local type stabilization 暂时沿用 legacy direct phase,以保持现有源码顺序 `:=` alias chain 行为等价;原因是当前 window runner 会把 slot update 延迟到 patch commit,若整模块一次性运行会让后续 local initializer 读不到前序 local 的稳定 slot 类型。 +- [x] D5 新增 legacy runner 与 segmented runner 的 side-table / diagnostics 等价测试,并覆盖 unsupported `for` / `match` / block-local `const` 行为不变。 +- [ ] D6 真正 root-bounded 的 statement window 执行仍需后续细化:需要让五个 window-capable analyzer 按 `FrontendSemanticWindow.roots()` 限定遍历,同时保持 callable/property initializer 上下文和 local slot update 对同 window 后续 stage 的可见性。该项完成前不切换默认生产路径。 + 验收细则: - 对同一输入,legacy whole-phase runner 与 segmented runner 的 `symbolBindings()`、`resolvedMembers()`、`resolvedCalls()`、`expressionTypes()`、`slotTypes()` 等价。 @@ -493,6 +502,7 @@ record FrontendLocalSlotTypeUpdate( - 在 `FrontendAnalysisData` 中加入 gate side table 或专用 registry;若 gate 不需要长期暴露给 lowering,可先保持 package-private data structure,但必须可被 resolver / scheduler 查询。 - gate registry 必须按 gate owner / body root identity 提供 body readiness update 和 lookup API,作为 4.4.1 定义的单一真源。 - `FrontendVariableAnalyzer`、`FrontendLocalTypeStabilizationAnalyzer`、`FrontendVarTypePostAnalyzer`、`FrontendCompileCheckAnalyzer` 的 callable-local inventory 判断必须迁移到共享 readiness 查询;纯 `BlockScopeKind` 查询只能处理无条件支持的 block kind。 +- 本阶段可以先建立 gate registry / readiness 查询,但不得假设阶段 D6 已完成。任何依赖 statement window 顺序提交或 child body window 递归的行为,只能在 D6 完成后接入 scheduler。 验收细则: @@ -508,10 +518,12 @@ record FrontendLocalSlotTypeUpdate( 实施内容: - scheduler 在每个 block 内按源码顺序提交 statement patches。 +- 本阶段必须与阶段 D6 联动完成:`FrontendSemanticWindow.roots()` 必须真正限制 analyzer 遍历范围,否则“当前 statement 提交后供后续 statement 消费”的合同没有实现基础。 - 当前 statement 的 expr facts 提交后,后续 statement classifier 可以读取这些 facts。 - 对 `var limit := 3; ` 建立测试用 synthetic gate 或选用 `for` range classifier 作为第一个真实 consumer。 - local `:=` stabilization 必须在同一 statement window 内早于后续 statement 的 binding / classifier 消费。 - child block 的完整 inventory 必须在 child body 第一个 semantic window 前发布,且共享 readiness 查询必须已经返回 true。 +- local stabilization 不得继续依赖整模块 legacy direct phase 来表达 source-order 行为;D6 必须提供 per-window slot update 可见性,使同一 statement window 内后续 stage 能消费已稳定的 local slot,下一 statement 只能消费已 commit 的 stable facts。 验收细则: @@ -528,6 +540,7 @@ record FrontendLocalSlotTypeUpdate( 实施内容: - 将 `ForStatement` 注册为 typed-dependent inventory gate。 +- 本阶段以前必须完成 D6;真实 `ForStatement` gate classifier 依赖前序 statement 已提交的 `expressionTypes()`,不能建立在 whole-module window 或 legacy local stabilization 旁路之上。 - `range(...)` call 仍可由 AST shape 早期识别;`INT_SHORTHAND` 必须等 iterable expression typed fact 就绪后再判定。 - `var limit := 3; for i in limit:` 中,scheduler 先完成 `limit` statement window,再分类 `for` gate。 - supported gate 先推进为 `SUPPORTED + NOT_PUBLISHED`,再由 body inventory publication window 发布 iterator binding 和 body 完整 local inventory,最后原子推进为 `PUBLISHED`。 @@ -548,6 +561,7 @@ record FrontendLocalSlotTypeUpdate( 实施内容: - 确定 segmented runner 的 diagnostics snapshot 发布点:每个 stage patch 应用后刷新 `analysisData.updateDiagnostics(...)`,保证后续 stage 看到稳定 upstream diagnostics。 +- 若 D6 将 analyzer 从 whole-module window 切到 root-bounded statement window,本阶段必须重新锚定 diagnostics snapshot 的刷新点与顺序,特别是同一源码位置跨 window 的 duplicate suppression。 - compile gate 继续只在 shared segmented pipeline 完成后运行。 - 检查 compile gate 的 duplicate suppression 是否仍能识别跨 segment upstream diagnostics。 - 对缺失 `slotTypes()`、`DEFERRED`、`FAILED`、`UNSUPPORTED` 的 final facts 保持现有 compile blocking 规则。 @@ -564,6 +578,7 @@ record FrontendLocalSlotTypeUpdate( 实施内容: - 等阶段 D-H 的等价与新增支持测试稳定后,删除仅用于迁移的 legacy whole-phase runner 旁路。 +- 删除 legacy 旁路前必须完成 D6,并证明默认 segmented runner 不再依赖整模块 legacy local stabilization direct phase。 - 保留 window-capable analyzer API,whole-module analyzer wrapper 只作为测试或调试入口时存在。 - 更新相关文档,尤其是 variable analyzer、visible resolver、local type stabilization、chain/expr typing、compile check 与 for-range plan。 diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSegmentedSemanticScheduler.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSegmentedSemanticScheduler.java new file mode 100644 index 00000000..0a2c7c3d --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSegmentedSemanticScheduler.java @@ -0,0 +1,119 @@ +package gd.script.gdcc.frontend.sema.analyzer; + +import gd.script.gdcc.frontend.diagnostic.DiagnosticManager; +import gd.script.gdcc.frontend.sema.FrontendAnalysisData; +import gd.script.gdcc.frontend.sema.FrontendSemanticStage; +import gd.script.gdcc.frontend.sema.FrontendWindowAnalysisContext; +import gd.script.gdcc.scope.ClassRegistry; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +/// Coordinates the segmented semantic publication layer introduced after the window-capable +/// analyzer APIs. +/// +/// This first scheduler keeps the current supported frontend behavior equivalent by committing one +/// owner stage at a time through `FrontendAnalysisData.applyPatch(...)`. That still exercises the +/// segmented publication path, preserves stable side-table references, and makes local slot updates +/// visible before chain binding consumes receiver slots. +final class FrontendSegmentedSemanticScheduler { + 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; + + FrontendSegmentedSemanticScheduler( + @NotNull FrontendTopBindingAnalyzer topBindingAnalyzer, + @NotNull FrontendLocalTypeStabilizationAnalyzer localTypeStabilizationAnalyzer, + @NotNull FrontendChainBindingAnalyzer chainBindingAnalyzer, + @NotNull FrontendExprTypeAnalyzer exprTypeAnalyzer, + @NotNull FrontendVarTypePostAnalyzer varTypePostAnalyzer + ) { + 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"); + } + + void run( + @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"); + + runTopBinding(classRegistry, analysisData, diagnosticManager); + runLocalTypeStabilization(classRegistry, analysisData, diagnosticManager); + runChainBinding(classRegistry, analysisData, diagnosticManager); + runExprType(classRegistry, analysisData, diagnosticManager); + runVarTypePost(analysisData, diagnosticManager); + } + + private void runTopBinding( + @NotNull ClassRegistry classRegistry, + @NotNull FrontendAnalysisData analysisData, + @NotNull DiagnosticManager diagnosticManager + ) { + var window = new FrontendWindowAnalysisContext(analysisData); + topBindingAnalyzer.analyzeInWindow(classRegistry, window, diagnosticManager); + commit(window, FrontendSemanticStage.TOP_BINDING, analysisData, diagnosticManager); + } + + private void runLocalTypeStabilization( + @NotNull ClassRegistry classRegistry, + @NotNull FrontendAnalysisData analysisData, + @NotNull DiagnosticManager diagnosticManager + ) { + // This phase must keep its existing source-order scope writes until the scheduler grows + // true root-bounded statement windows. A whole-module local window would delay every slot + // update until commit and break alias chains such as `var b := a` after `var a := typed`. + localTypeStabilizationAnalyzer.analyze(classRegistry, analysisData, diagnosticManager); + analysisData.updateDiagnostics(diagnosticManager.snapshot()); + } + + private void runChainBinding( + @NotNull ClassRegistry classRegistry, + @NotNull FrontendAnalysisData analysisData, + @NotNull DiagnosticManager diagnosticManager + ) { + var window = new FrontendWindowAnalysisContext(analysisData); + chainBindingAnalyzer.analyzeInWindow(classRegistry, window, diagnosticManager); + commit(window, FrontendSemanticStage.CHAIN_BINDING, analysisData, diagnosticManager); + } + + private void runExprType( + @NotNull ClassRegistry classRegistry, + @NotNull FrontendAnalysisData analysisData, + @NotNull DiagnosticManager diagnosticManager + ) { + var window = new FrontendWindowAnalysisContext(analysisData); + exprTypeAnalyzer.analyzeInWindow(classRegistry, window, diagnosticManager); + commit(window, FrontendSemanticStage.EXPR_TYPE, analysisData, diagnosticManager); + } + + private void runVarTypePost( + @NotNull FrontendAnalysisData analysisData, + @NotNull DiagnosticManager diagnosticManager + ) { + var window = new FrontendWindowAnalysisContext(analysisData); + varTypePostAnalyzer.analyzeInWindow(window, diagnosticManager); + commit(window, FrontendSemanticStage.VAR_TYPE_POST, analysisData, diagnosticManager); + } + + private void commit( + @NotNull FrontendWindowAnalysisContext window, + @NotNull FrontendSemanticStage stage, + @NotNull FrontendAnalysisData analysisData, + @NotNull DiagnosticManager diagnosticManager + ) { + analysisData.applyPatch(window.drainPatch(stage)); + analysisData.updateDiagnostics(diagnosticManager.snapshot()); + } +} 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..2927fb47 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 @@ -41,6 +41,7 @@ public final class FrontendSemanticAnalyzer { private final @NotNull FrontendTypeCheckAnalyzer typeCheckAnalyzer; private final @NotNull FrontendLoopControlFlowAnalyzer loopControlFlowAnalyzer; private final @NotNull FrontendCompileCheckAnalyzer compileCheckAnalyzer; + private final boolean segmentedSemanticRunner; public FrontendSemanticAnalyzer() { this( @@ -406,6 +407,40 @@ public FrontendSemanticAnalyzer( @NotNull FrontendTypeCheckAnalyzer typeCheckAnalyzer, @NotNull FrontendLoopControlFlowAnalyzer loopControlFlowAnalyzer, @NotNull FrontendCompileCheckAnalyzer compileCheckAnalyzer + ) { + this( + classSkeletonBuilder, + scopeAnalyzer, + variableAnalyzer, + topBindingAnalyzer, + localTypeStabilizationAnalyzer, + chainBindingAnalyzer, + exprTypeAnalyzer, + varTypePostAnalyzer, + annotationUsageAnalyzer, + virtualOverrideAnalyzer, + typeCheckAnalyzer, + loopControlFlowAnalyzer, + compileCheckAnalyzer, + false + ); + } + + 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, + boolean segmentedSemanticRunner ) { this.classSkeletonBuilder = Objects.requireNonNull(classSkeletonBuilder, "classSkeletonBuilder must not be null"); this.scopeAnalyzer = Objects.requireNonNull(scopeAnalyzer, "scopeAnalyzer must not be null"); @@ -435,6 +470,26 @@ public FrontendSemanticAnalyzer( "loopControlFlowAnalyzer must not be null" ); this.compileCheckAnalyzer = Objects.requireNonNull(compileCheckAnalyzer, "compileCheckAnalyzer must not be null"); + this.segmentedSemanticRunner = segmentedSemanticRunner; + } + + public static @NotNull FrontendSemanticAnalyzer withSegmentedSemanticRunnerForTesting() { + return new FrontendSemanticAnalyzer( + 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(), + true + ); } /// Runs the current frontend analyzer framework against one module using a shared @@ -481,6 +536,46 @@ public FrontendSemanticAnalyzer( variableAnalyzer.analyze(analysisData, diagnosticManager); analysisData.updateDiagnostics(diagnosticManager.snapshot()); + runSharedSemanticPublication(classRegistry, diagnosticManager, analysisData); + + // Annotation-usage validation consumes retained annotations plus the published class/scope + // facts, but still stays diagnostics-only and does not mutate semantic side tables. + annotationUsageAnalyzer.analyze(classRegistry, analysisData, diagnosticManager); + analysisData.updateDiagnostics(diagnosticManager.snapshot()); + + // Engine virtual override validation consumes the published class/function metadata and + // reports signature mismatches without skipping the owning function subtree. + virtualOverrideAnalyzer.analyze(classRegistry, analysisData, diagnosticManager); + analysisData.updateDiagnostics(diagnosticManager.snapshot()); + + // Type checking is diagnostics-only for now: it consumes the published frontend facts but + // must not introduce new side tables or rewrite earlier publication boundaries. + typeCheckAnalyzer.analyze(classRegistry, analysisData, diagnosticManager); + analysisData.updateDiagnostics(diagnosticManager.snapshot()); + + // Loop-control legality is also diagnostics-only, but it must run on the shared semantic + // path so invalid `break` / `continue` never rely on lowering fail-fast to become visible. + loopControlFlowAnalyzer.analyze(analysisData, diagnosticManager); + analysisData.updateDiagnostics(diagnosticManager.snapshot()); + return analysisData; + } + + private void runSharedSemanticPublication( + @NotNull ClassRegistry classRegistry, + @NotNull DiagnosticManager diagnosticManager, + @NotNull FrontendAnalysisData analysisData + ) { + if (segmentedSemanticRunner) { + new FrontendSegmentedSemanticScheduler( + topBindingAnalyzer, + localTypeStabilizationAnalyzer, + chainBindingAnalyzer, + exprTypeAnalyzer, + varTypePostAnalyzer + ).run(classRegistry, analysisData, diagnosticManager); + return; + } + // 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. @@ -508,27 +603,6 @@ public FrontendSemanticAnalyzer( // the lexical inventory state. varTypePostAnalyzer.analyze(analysisData, diagnosticManager); analysisData.updateDiagnostics(diagnosticManager.snapshot()); - - // Annotation-usage validation consumes retained annotations plus the published class/scope - // facts, but still stays diagnostics-only and does not mutate semantic side tables. - annotationUsageAnalyzer.analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - - // Engine virtual override validation consumes the published class/function metadata and - // reports signature mismatches without skipping the owning function subtree. - virtualOverrideAnalyzer.analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - - // Type checking is diagnostics-only for now: it consumes the published frontend facts but - // must not introduce new side tables or rewrite earlier publication boundaries. - typeCheckAnalyzer.analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - - // Loop-control legality is also diagnostics-only, but it must run on the shared semantic - // path so invalid `break` / `continue` never rely on lowering fail-fast to become visible. - loopControlFlowAnalyzer.analyze(analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - return analysisData; } /// Runs the shared semantic pipeline plus the compile-only final gate. 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..503037fc 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSemanticAnalyzerFrameworkTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSemanticAnalyzerFrameworkTest.java @@ -48,7 +48,9 @@ import gd.script.gdcc.gdextension.ExtensionApiLoader; import gd.script.gdcc.scope.ClassRegistry; import gd.script.gdcc.scope.ClassDef; +import gd.script.gdcc.scope.FunctionDef; import gd.script.gdcc.scope.PropertyDef; +import gd.script.gdcc.scope.ScopeValue; import gd.script.gdcc.scope.ScopeValueKind; import gd.script.gdcc.scope.resolver.ScopeResolvedMethod; import gd.script.gdcc.type.GdArrayType; @@ -65,6 +67,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.function.BiPredicate; import java.util.function.Predicate; import static org.junit.jupiter.api.Assertions.assertAll; @@ -972,6 +975,90 @@ func ping(value: Point) -> int: ); } + @Test + void segmentedRunnerProducesEquivalentSharedSemanticSideTablesAndDiagnostics() 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 + var ready_value := 1 + func ping(value: Point) -> int: + var alias := value + var number := ready_value + if number > 0: + var nested := alias + number = nested.marker + while number < 3: + number += 1 + return alias.marker + """, parseDiagnostics); + var legacyDiagnostics = new DiagnosticManager(); + var segmentedDiagnostics = new DiagnosticManager(); + + var legacy = analyzeModule( + new FrontendSemanticAnalyzer(), + "test_module", + List.of(unit), + new ClassRegistry(ExtensionApiLoader.loadDefault()), + legacyDiagnostics + ); + var segmented = analyzeModule( + FrontendSemanticAnalyzer.withSegmentedSemanticRunnerForTesting(), + "test_module", + List.of(unit), + new ClassRegistry(ExtensionApiLoader.loadDefault()), + segmentedDiagnostics + ); + + assertEquivalentSharedSemanticFacts(legacy, segmented); + assertEquals(legacy.diagnostics(), segmented.diagnostics()); + assertEquals(legacyDiagnostics.snapshot(), segmentedDiagnostics.snapshot()); + } + + @Test + void segmentedRunnerKeepsExistingUnsupportedSubtreeBehaviorEquivalent() 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 legacyDiagnostics = new DiagnosticManager(); + var segmentedDiagnostics = new DiagnosticManager(); + + var legacy = analyzeModule( + new FrontendSemanticAnalyzer(), + "test_module", + List.of(unit), + new ClassRegistry(ExtensionApiLoader.loadDefault()), + legacyDiagnostics + ); + var segmented = analyzeModule( + FrontendSemanticAnalyzer.withSegmentedSemanticRunnerForTesting(), + "test_module", + List.of(unit), + new ClassRegistry(ExtensionApiLoader.loadDefault()), + segmentedDiagnostics + ); + + assertEquivalentSharedSemanticFacts(legacy, segmented); + assertEquals( + diagnosticsByCategory(legacy.diagnostics(), "sema.unsupported_binding_subtree"), + diagnosticsByCategory(segmented.diagnostics(), "sema.unsupported_binding_subtree") + ); + assertEquals(legacy.diagnostics(), segmented.diagnostics()); + } + @Test void analyzeForCompileRunsCompileGateAfterLoopControlWhileAnalyzeStaysCompileCheckFree() throws Exception { var parserService = new GdScriptParserService(); @@ -1400,6 +1487,143 @@ private FrontendAnalysisData analyzeModuleForCompile( return analyzer.analyzeForCompile(new FrontendModule(moduleName, units), registry, diagnostics); } + private void assertEquivalentSharedSemanticFacts( + @NotNull FrontendAnalysisData expected, + @NotNull FrontendAnalysisData actual + ) { + assertEquivalentSideTable( + expected.symbolBindings(), + actual.symbolBindings(), + this::sameBindingAcrossIndependentRuns, + "symbolBindings" + ); + assertEquivalentSideTable( + expected.resolvedMembers(), + actual.resolvedMembers(), + this::sameResolvedMemberAcrossIndependentRuns, + "resolvedMembers" + ); + assertEquivalentSideTable( + expected.resolvedCalls(), + actual.resolvedCalls(), + this::sameResolvedCallAcrossIndependentRuns, + "resolvedCalls" + ); + assertEquivalentSideTable( + expected.expressionTypes(), + actual.expressionTypes(), + FrontendAnalysisData::sameExpressionType, + "expressionTypes" + ); + assertEquivalentSideTable( + expected.slotTypes(), + actual.slotTypes(), + FrontendAnalysisData::sameType, + "slotTypes" + ); + } + + private boolean sameBindingAcrossIndependentRuns( + @NotNull FrontendBinding first, + @NotNull FrontendBinding second + ) { + return first.kind() == second.kind() + && first.symbolName().equals(second.symbolName()) + && sameDeclarationAcrossIndependentRuns(first.declarationSite(), second.declarationSite()) + && first.valueAccessStatus() == second.valueAccessStatus() + && sameScopeValueAcrossIndependentRuns(first.resolvedValue(), second.resolvedValue()); + } + + private boolean sameResolvedMemberAcrossIndependentRuns( + @NotNull FrontendResolvedMember first, + @NotNull FrontendResolvedMember second + ) { + return first.bindingKind() == second.bindingKind() + && first.status() == second.status() + && first.receiverKind() == second.receiverKind() + && first.ownerKind() == second.ownerKind() + && sameDeclarationAcrossIndependentRuns(first.declarationSite(), second.declarationSite()) + && first.memberName().equals(second.memberName()) + && FrontendAnalysisData.sameType(first.receiverType(), second.receiverType()) + && FrontendAnalysisData.sameType(first.resultType(), second.resultType()) + && Objects.equals(first.detailReason(), second.detailReason()); + } + + private boolean sameResolvedCallAcrossIndependentRuns( + @NotNull FrontendResolvedCall first, + @NotNull FrontendResolvedCall second + ) { + return first.callKind() == second.callKind() + && first.status() == second.status() + && first.receiverKind() == second.receiverKind() + && first.ownerKind() == second.ownerKind() + && sameDeclarationAcrossIndependentRuns(first.declarationSite(), second.declarationSite()) + && first.callableName().equals(second.callableName()) + && FrontendAnalysisData.sameType(first.receiverType(), second.receiverType()) + && FrontendAnalysisData.sameType(first.returnType(), second.returnType()) + && FrontendAnalysisData.sameTypeList(first.argumentTypes(), second.argumentTypes()) + && FrontendAnalysisData.sameExactCallableBoundary( + first.exactCallableBoundary(), + second.exactCallableBoundary() + ) + && Objects.equals(first.detailReason(), second.detailReason()); + } + + private boolean sameScopeValueAcrossIndependentRuns(ScopeValue first, 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() + && sameDeclarationAcrossIndependentRuns(first.declaration(), second.declaration()) + && first.name().equals(second.name()) + && FrontendAnalysisData.sameType(first.type(), second.type()); + } + + private boolean sameDeclarationAcrossIndependentRuns(Object first, Object second) { + if (first == second) { + return true; + } + if (first == null || second == null || first instanceof Node || second instanceof Node) { + return false; + } + return declarationFingerprint(first).equals(declarationFingerprint(second)); + } + + private String declarationFingerprint(@NotNull Object declaration) { + if (declaration instanceof ClassDef classDef) { + return "class:" + classDef.getName(); + } + if (declaration instanceof PropertyDef propertyDef) { + return "property:" + propertyDef.getName(); + } + if (declaration instanceof FunctionDef functionDef) { + return "function:" + functionDef.getName(); + } + return declaration.getClass().getName() + ':' + declaration; + } + + private void assertEquivalentSideTable( + @NotNull FrontendAstSideTable expected, + @NotNull FrontendAstSideTable actual, + @NotNull BiPredicate sameValue, + @NotNull String tableName + ) { + assertEquals(expected.size(), actual.size(), tableName + " size changed"); + for (var entry : expected.entrySet()) { + assertTrue(actual.containsKey(entry.getKey()), tableName + " lost key " + entry.getKey()); + var actualValue = actual.get(entry.getKey()); + assertTrue( + sameValue.test(entry.getValue(), actualValue), + tableName + " changed value for " + entry.getKey() + + ": expected " + entry.getValue() + + ", actual " + actualValue + ); + } + } + private FunctionDeclaration findFunction(List statements, String name) { return statements.stream() .filter(FunctionDeclaration.class::isInstance) From c5ea2adf7f33a24021438c32a1ba5de65883e94c Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Tue, 7 Jul 2026 18:00:09 +0800 Subject: [PATCH 06/27] docs(frontend): rewrite segmented type resolution pipeline plan and add execution summary - Reframe plan around an interface/body two-layer architecture with source-order body suite resolution, replacing the previous statement-window runner direction. - Demote existing window publication surface and segmented scheduler to legacy comparison assets due to scratch-over-stable contract violations. - Tighten owner boundaries, compiler-only type isolation, and per-owner patch transaction rules for the new overlay/export flow. - Move all patch carriers and local slot update records into a dedicated sema patch package, preserving stable data ownership and merge entrypoints. - Archive the completed exploratory work in a new execution summary to anchor future implementation on verified evidence. --- ...e_resolution_pipeline_execution_summary.md | 302 +++++ ...segmented_type_resolution_pipeline_plan.md | 1004 +++++++++++------ 2 files changed, 930 insertions(+), 376 deletions(-) create mode 100644 doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md 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..793e393a --- /dev/null +++ b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md @@ -0,0 +1,302 @@ +# Frontend Segmented Type Resolution Pipeline Execution Summary + +本文总结 `doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md` 执行完成后的前端分析流水线形态。内容只描述目标架构、执行顺序与不变量,不展开旧 whole-module 流水线或过渡实现资产。 + +## 1. 总体形态 + +计划完成后,frontend shared semantic pipeline 分为四个层次: + +1. 基础结构层:建立 module skeleton、scope graph 与 baseline inventory。 +2. Interface 层:建立 body 解析所需的 declaration index、gate registry、typed lexical baseline 与 suite plan。 +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 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 的完整 declaration 列表与 source order。 +- `FrontendInventoryGateRegistry`:记录 typed-dependent subtree 的 gate owner、header root、body root、deferred domain 与 readiness。 +- `FrontendTypedLexicalBaseline`:记录参数、显式 typed local 与 interface 层可静态确定的 source-facing slot baseline。 +- `FrontendSuitePlan`:列出 body layer 可进入的 callable、property initializer 与 supported block。 + +Interface 层不得发布 body typed facts。特别是不得发布 `expressionTypes()`、`resolvedMembers()` 或 `resolvedCalls()`,也不得把 `GdCompilerType` 写入 source-facing lexical baseline。 + +## 4. Body `SuiteResolver` + +Body 层由 `FrontendSuiteResolver` 或等价 coordinator 驱动。它按源码顺序进入 supported suite,形态如下: + +```text +resolveSuite(context, block): + for statement in block.statements(): + resolveStatement(context, statement) + flushStatementFacts(context, statement) + resolvePendingBodiesIfAllowed(context) +``` + +`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 建立隐式上下文。 + +第一版 body statement 支持面包括: + +- ordinary local `VariableDeclaration`。 +- supported property initializer。 +- `ExpressionStatement`。 +- `ReturnStatement`。 +- `AssertStatement`。 +- `IfStatement` / `ElifClause` / `else`。 +- `WhileStatement`。 + +`ForStatement`、`MatchStatement`、`LambdaExpression` 与 block-local `const` 默认保持 deferred / unsupported,除非后续 feature gate 明确转正。 + +## 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. Statement flush。 + +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。 + +Statement flush 把当前 statement pending overlay 转入 current-suite committed overlay,供后续 statement 与 gate classifier 读取。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. `BlockScope` / `CallableScope` 中的 stable lexical inventory 与 stable side tables。 +4. parent typed lexical environment。 +5. class / global / singleton / type-meta lookup。 + +Pending overlay 只对当前 statement 后续 owner 子过程可见。Statement flush 后,pending facts 才进入 current-suite committed overlay,并对后续 statement 与 gate classifier 可见。Committed overlay 仍不是 stable publication。 + +`TypedLexicalEnvironment` 的目标是模拟 source-order body resolution 中“前缀 statement 已解析出的类型可被后续 statement 使用”的行为,同时避免提前污染 stable semantic facts。 + +## 7. Fact 生命周期 + +目标架构固定四层事实可见性模型: + +1. `FrontendOwnerRetryMemo`:owner 子过程私有,只给当前 chain / expr reduction 的 retry 回调读取,owner 子过程结束即丢弃。 +2. Current statement pending overlay:当前 statement 后续 owner 子过程可读,只接受每个 AST key 的最终 publication fact。 +3. Current-suite committed overlay:由 statement flush 合并而来,后续 statement 与 gate classifier 可读,但仍不是 stable publication。 +4. `FrontendAnalysisData` stable side tables / `BlockScope` stable slot:只在 suite export 的 per-owner patch apply / stable export helper 后更新。 + +Nested chain / argument retry 的中间事实只能存在于 owner-local memo 中。Retry 中出现的临时 `DEFERRED`、暂定 `Variant`、中间 status 或 detailReason 不得写入 pending overlay、committed overlay 或 stable side table。 + +`expressionTypes()` 对同一 key 只能发布最终 fact 一次。如果一个 expression 在 reduction 过程中需要先得到临时 fact 再得到 exact result,中间状态必须留在 owner-local memo 或专用非导出状态中。 + +## 8. Overlay 写入、Flush 与 Export + +Owner runner 只能写当前 statement pending overlay。Overlay write 必须携带 owner metadata,并在写入时执行 owner、conflict、idempotent、exact-type 与 compiler-only guard。 + +`flushStatementFacts(...)` 只把 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。Stable side table 与 `BlockScope.resetLocalType(...)` 只能在 per-owner patch apply 或 stable export helper 中更新。 + +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`。 +- `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。 + +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 允许。 + +## 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 与 Gate Readiness + +`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。 + +Typed-dependent gate 使用统一 readiness policy。三道封口必须同步条件化: + +1. Request-domain gate。 +2. AST boundary gate。 +3. Current-scope gate。 + +只有 owning gate 达到 `SUPPORTED + PUBLISHED` 后,body lookup 才能作为普通 executable body lookup 进入 resolver。`PENDING`、`SUPPORTED + NOT_PUBLISHED`、`PUBLISHING`、`UNSUPPORTED`、缺失 owning gate 或找不到 owner 的情况都必须 fail-closed。 + +## 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,不得补第二条同级错误。 + +`FrontendCompileCheckAnalyzer` 只运行在 compile-only 入口。默认 shared semantic `analyze(...)`、inspection 与未来 LSP 入口不得隐式运行 compile-only gate。Lowering 只能以 `analyzeForCompile(...)` 且 diagnostics 无 error 的结果作为最低前置条件。 + +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。 +- 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。 +- `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 绕过。 +- typed-dependent gate 的 request-domain、AST boundary 与 current-scope 三道封口由同一 readiness policy 控制。 +- diagnostics-only phase、compile gate 与 lowering 只能读取 suite export 后的 stable facts。 diff --git a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md index 8d0f0edd..8f6fdfa4 100644 --- a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md +++ b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md @@ -2,10 +2,13 @@ ## 1. 文档状态 -- 性质:实施计划 -- 目标模块:`src/main/java/gd/script/gdcc/frontend/sema/**` -- 直接动机:让 frontend 可以按源码顺序分段发布局部类型事实,使 `var limit := 3; for i in limit:` 这类依赖前缀 typed fact 的语义支持成为可实现目标。 -- 非目标:本文不直接实现 `for-range` lowering,不改变 Godot range runtime 语义,不把 `lambda` / `match` / block-local `const` 一并转正。 +- 性质:重启后的实施计划。 +- 目标模块:`src/main/java/gd/script/gdcc/frontend/sema/**`,并影响 `frontend/scope/**` 与 frontend lowering 的 analysis contract。 +- 直接动机:用 Godot 风格的 interface/body 分层与 source-order suite 解析替代原先 statement-window segmented runner 路线,使 `var limit := 3; for i in limit:` 这类依赖前缀 typed fact 的语义支持拥有稳定架构基础。 +- 主体方案:以 interface phase + body `SuiteResolver` 为主线,即先建立 class/callable/block 的 lexical 与 signature/interface 事实,再按 body suite 源码顺序解析 statement。 +- 辅助方案:引入 `TypedLexicalEnvironment` overlay,使当前 statement / 当前 suite 内的 local slot typed fact 能被后续 semantic step 读取,而不提前污染最终 stable side table。 +- 实施策略:允许把阶段 A-D 的已有实现视为“可保留资产 / 可重写参考 / 可回退资产”。本文不假设当前 segmented scheduler 已经是新路线的可继续扩展基础,也不把现有 `analyzeInWindow(...)` 当作可抽取的 statement runner。 +- 非目标:本文不直接完成 `for-range` lowering,不改变 Godot range runtime 语义,不一次性转正 `lambda` / `match` / block-local `const`。 关联文档: @@ -14,6 +17,7 @@ - `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` @@ -38,53 +42,88 @@ 12. loop control 13. compile-only gate,仅 `analyzeForCompile(...)` -这条顺序让每个 phase 都能消费前一个 phase 的完整 module 事实,但也造成一个硬时序问题: +这条顺序让每个 phase 都能消费前一个 phase 的完整 module 事实,但它无法自然表达 Godot 的 body 解析模型: -- variable inventory 需要在 phase 3 决定某个 subtree 是否可以发布 local inventory。 -- 依赖 typed fact 的语义,例如 `for i in limit:` 是否是 int shorthand,需要 phase 5-7 之后才可能知道。 -- 如果 phase 3 不发布 `FOR_BODY` inventory,后续 resolver / binding / expr typing 会按 `FOR_SUBTREE` fail-closed。 -- 如果 phase 3 盲目发布 `FOR_BODY` inventory,又会把 unsupported `for` body 误纳入普通 executable body。 +- Godot parser 只建立 AST / suite / local name 结构,不做类型解析。 +- 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。 -因此需要一个 source-order segmented pipeline:先为当前 block 做完整 lexical inventory,再按 statement window 逐步发布前缀 typed facts,遇到依赖 typed fact 的 feature gate 时再决定是否解封其子树 inventory。 +这里的 Godot 对照只用于说明 interface/body 边界与 `resolve_suite()` 的 source-order 解析形状,不用于把“完整 local inventory”与“增量/source-order analysis”对立起来。Godot parser/suite local registry 与 analyzer source-order 解析可以同时成立;GDCC 的完整 inventory 要求来自自己的 resolver filtered-hit 模型,见第 3.2 节。 + +原 statement-window segmented runner 方案试图在现有 phase pipeline 上模拟 source-order,但实际暴露出两个结构性阻塞: + +- 已提取的 `analyzeInWindow(...)` 只改变发布表面,不限制 analyzer 遍历范围,仍偏 whole-module。 +- `FrontendLocalTypeStabilizationAnalyzer` 的 window 路径把 slot update 延迟到 patch commit,整模块运行时会让后续 `var b := a` 读不到前序 `a` 的稳定类型。 +- `FrontendVarTypePostAnalyzer.analyzeInWindow(...)` 不是纯 scratch-over-stable:它取得 stable `analysisData.slotTypes()` 引用、直接 `clear()` 并把该引用传给 `SlotTypePublisher` 写入,然后才复制到 `window.publications().slotTypes()`。因此现有 window publication 路径已经能在 patch commit / discard 前污染 stable side table。 + +现有 analyzer 不是“window runner”。它们的 `analyze(...)` / `analyzeInWindow(...)` 入口仍以 `moduleSkeleton.sourceClassRelations()` 为根,创建内部 `AstWalker...` / visitor 并遍历完整 `SourceFile` AST。`FrontendWindowAnalysisContext` 只把发布目标换成 scratch surface;它没有把遍历 root 限制到当前 statement、当前 suite 或当前 body。因此,新的 body `SuiteResolver` 不能通过简单迁移这些入口获得。它需要在现有语义规则与 owner 合同之上,重写每个 owner 的 statement-local 调度、显式上下文状态和发布逻辑。 + +这也是一次执行框架重写,而不是 statement-window 方案的续作:`FrontendSegmentedSemanticScheduler` 只能作为证明 patch merge / stable reference 行为的过渡资产。它仍按 whole-module owner stage 调度,并且 local type stabilization 仍走 legacy direct phase 以避免延迟 slot update 破坏 source-order alias chain。 + +因此,`FrontendWindowPublicationSurface` / `FrontendWindowAnalysisContext` 不能作为新 overlay/export 的正确性参考。类型本身的 API 试图表达 scratch view,但现有 analyzer 使用方式已经破坏该承诺;计划只能把它们作为 legacy comparison / targeted regression 的输入,不能把它们当作可复用设计。 + +因此新计划改为 interface/body 双层结构: + +- interface 层基于基础结构层已发布的 lexical inventory 建立 declaration index、signature/interface facts、typed-dependent gate registry。 +- body 层用 `SuiteResolver` 按源码顺序解析 supported body statements。 +- `TypedLexicalEnvironment` overlay 在 body 层提供 Godot 风格的“当前语句已知 typed fact 对后续语义立即可见”能力,同时保留 GDCC 的 side-table owner、patch conflict 与 compiler-only type 隔离。 +- Suite 收敛后的 stable export 采用按 owner 有序的 patch transaction:每个 owner 子过程的最终 facts 按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 顺序分别提交为独立 owner patch,不能合并成一个多 owner `FrontendAnalysisPatch`。 +- Patch 相关类型统一迁入 `gd.script.gdcc.frontend.sema.patch` 包,包括旧 `FrontendAnalysisPatch`、`FrontendLocalSlotTypeUpdate` 与新建 per-owner patch / transaction 类型;window publication 类型若保留,只能作为 legacy shim。 ## 3. 不变量 -### 3.1 仍然保留的 phase owner +### 3.1 Phase owner 边界仍然保留 -分段不是允许任意 analyzer 写任意表。owner 边界保持不变: +重启计划不是允许任意 resolver 写任意表。owner 边界保持不变: -- `FrontendVariableAnalyzer` 仍拥有 parameter / local inventory publication。 +- `FrontendVariableAnalyzer` 仍拥有 parameter / ordinary local inventory publication。 - `FrontendTopBindingAnalyzer` 仍拥有 `symbolBindings()`。 -- `FrontendLocalTypeStabilizationAnalyzer` 仍只拥有 local `:=` slot rewrite,不拥有 diagnostics,不发布 `resolvedMembers()` / `resolvedCalls()` / `expressionTypes()` / `slotTypes()`。 +- `FrontendLocalTypeStabilizationAnalyzer` 仍只拥有 source-facing local `:=` slot rewrite,不拥有 diagnostics,不发布 `resolvedMembers()` / `resolvedCalls()` / `expressionTypes()` / `slotTypes()`。 - `FrontendChainBindingAnalyzer` 仍拥有 `resolvedMembers()` 与 chain-owned `resolvedCalls()`。 - `FrontendExprTypeAnalyzer` 仍拥有 `expressionTypes()` 与 bare-call `resolvedCalls()`。 -- `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 在 segmented runner 中不得继续作为第二个 slot mutation owner;它必须收紧为严格 no-op / guard-only 路径。 +- `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 不得恢复为第二个 slot mutation owner;它必须保持 strict no-op / guard-only。 - `FrontendVarTypePostAnalyzer` 仍拥有 `slotTypes()`。 -- `FrontendTypeCheckAnalyzer`、`FrontendLoopControlFlowAnalyzer` 与 `FrontendCompileCheckAnalyzer` 仍是 diagnostics-only consumer。 +- `FrontendTypeCheckAnalyzer`、`FrontendLoopControlFlowAnalyzer`、`FrontendCompileCheckAnalyzer` 仍是 diagnostics-only consumer。 + +上述 owner 边界是语义合同,不是当前 analyzer 遍历代码可直接复用的证明。当前 top binding、local stabilization、chain binding、expr typing、var type post analyzer 都把遍历控制、隐式状态和 side-table 发布耦合在 whole-module AST walker 内。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 必须先于分段 resolver +### 3.2 完整 lexical inventory 先于 body typed resolution -`FrontendVisibleValueResolver.filterInvisibleCurrentLayerHit(...)` 只有在 `Scope.resolveValueHere(...)` 能看到同层 binding 时,才能把后续声明记录为 `DECLARATION_AFTER_USE_SITE` filtered hit。 +GDCC 的完整 lexical inventory 要求不是因为 Godot 缺少前向 local 检测,也不是把 complete inventory 与 source-order analysis 当成二选一。Godot parser / suite local registry 先让 analyzer 阶段能查询完整 local set,`resolve_suite()` 再按 source order 解析;前向 local usage 以 `CONFUSABLE_LOCAL_USAGE` warning 表达。GDCC 与 Godot 的真实差异在诊断级别和实现模型:GDCC 把 future declaration 编码为 resolver filtered hit,并把它作为 binding / diagnostics provenance 的一部分。 -因此分段方案不得只把“当前 segment 之前的 local”放入 `BlockScope`。正确模型是: +在 GDCC 当前模型中,已支持 executable block 的 ordinary local inventory 由 `FrontendVariableAnalyzer` + scope graph 发布。`BlockScope.resolveValueHere(...)` 对当前层只做无 source-order 过滤的 map lookup;`FrontendVisibleValueResolver.resolve(...)` 在拿到当前层 hit 后,再由 `filterInvisibleCurrentLayerHit(...)` 按 source byte order 过滤。 -- 对一个已支持的 block,先扫描并发布该 block 的完整 local inventory。 +因此 `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。 + +正确模型是: + +- 对已支持的 block,基础结构层沿用现有 `FrontendVariableAnalyzer` + scope graph 发布完整 ordinary local inventory;interface 层建立 body declaration index / typed baseline view,而不是另起一套重复发布通道。 - local `:=` 初始类型仍是 `Variant`。 -- 分段只负责逐步稳定类型和发布 use-site facts。 -- 若某个 child feature gate 后续转正,例如 supported `for` body,必须先对该 child body 做完整 inventory,再分析 body 内 statement segments。 +- body `SuiteResolver` 只负责按源码顺序稳定类型、发布 use-site facts、推进 gate readiness。 +- 若某个 child feature gate 后续转正,必须先发布该 child body 的 gate-owned bindings 与完整 local inventory,再解析 body suite。 这保证 `var x := y; var y := 1` 仍能产生 declaration-after-use filtered hit,而不是把 `y` 误判成普通 miss 或外层 fallback。 ### 3.3 `FrontendAnalysisData` 稳定引用合同保持不变 -现有测试要求 `FrontendAnalysisData.updateXxx(...)` 保留 side table 对象引用,并通过 clear + putAll 清理 stale entry。分段重构不能破坏这个外部合同。 +现有测试要求 `FrontendAnalysisData.updateXxx(...)` 保留 side table 对象引用,并通过 clear + putAll 清理 stale entry。新计划不能破坏这个外部合同。 -新增增量能力时必须做到: +必须做到: - 保留现有 `updateXxx(...)` whole-table publication API 和测试。 -- 新增 segment patch / merge API,不复用 `updateXxx(...)` 表达部分提交。 +- 保留 `applyPatch(...)` 或等价 merge API 表达部分提交,但其输入必须是单一 owner patch;suite export 通过有序 patch transaction 依次调用 merge API,不能把跨 owner facts 塞进同一个 patch。 - 同一个 side table 的 stable reference 仍不替换。 -- 增量 merge 必须能检测冲突,不能静默覆盖不兼容 fact。 +- 增量 merge 必须检测冲突,不能静默覆盖不兼容 fact。 +- `TypedLexicalEnvironment` 的 overlay fact 只有在 owner 合法、冲突校验和 compiler-only guard 通过后,才能封装为对应 owner patch 并导出到 stable side table。 +- 保留 `updateXxx(...)` 不等于允许它继续作为 source-facing typed publication 的自由入口。只要某个 `updateXxx(...)` 仍会接收 `symbolBindings()`、`resolvedMembers()`、`resolvedCalls()`、`expressionTypes()`、`slotTypes()` 这类用户可见 typed facts,它就必须先复用与 overlay write / patch merge 相同的 compiler-only guard;否则它只能继续作为 legacy whole-table publication API 存在,不能出现在 production SuiteResolver path。 +- 所有 production patch carrier、patch transaction 与 local slot update carrier 都位于 `gd.script.gdcc.frontend.sema.patch` 包;`FrontendAnalysisData` 保留 stable data ownership,只暴露 merge entrypoint。Window publication 类型若迁入该包,也必须标记为 legacy shim。 ### 3.4 skipped / deferred subtree 合同保持不变 @@ -95,39 +134,73 @@ ### 3.5 compiler-only type 隔离 -任何分段 patch merge 都必须拒绝将 `GdCompilerType` 写入用户可见 facts: +任何 stable publication 或 overlay export 都必须拒绝将 `GdCompilerType` 写入用户可见 facts: - `expressionTypes()` 的 `publishedType()` - source-facing local / parameter / iterator 的 `slotTypes()` -- ordinary local `ScopeValue.type()`,除非该 scope value 明确是 compiler-owned hidden storage,且不会被 resolver 暴露给源码 +- 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 消费,不能通过普通 expression typing、ordinary local slot publication 或 user-visible binding payload 泄漏。 + +当前实现的 `FrontendAnalysisData.checkPatchDoesNotLeakCompilerOnlyTypes(...)` 只扫描 `expressionTypes()` 与 `slotTypes()`;`refreshPublishedLocalBindingPayloads(...)` 只覆盖 local slot update 后刷新 binding payload 的路径。因此本节是不变量目标,不是现有 `applyPatch` guard 已经完整覆盖的事实。阶段 C 必须把 patch-commit guard 与新 typed overlay guard 扩展到上面的所有 type-bearing publication surfaces,至少先关闭 `symbolBindings().resolvedValue.type()` 的直接 patch / overlay bypass;不得用 legacy window publication 行为证明该 guard 完整。 + +这里的“统一 guard”不是一句抽象约束,而是同一个 type-bearing field walker / validator 合同: -`GdccForRangeIterType` 只能作为 hidden iterator state contract 被 lowering 消费,不能通过普通 expression typing 或 local slot publication 泄漏。 +- patch commit、overlay pending write、overlay flush、任何仍保留的 source-facing `updateXxx(...)` whole-table publish,都必须复用同一套 field walker,而不是各自挑几张表做局部检查。 +- walker 至少要能递归访问 `FrontendBinding.resolvedValue().type()`、`FrontendResolvedMember.receiverType()` / `resultType()`、`FrontendResolvedCall.receiverType()` / `returnType()` / `argumentTypes()` / exact callable boundary parameter types、`FrontendExpressionType.publishedType()`、`slotTypes()` value、`FrontendLocalSlotTypeUpdate.type()`。 +- 只检查顶层 side table name 不足以证明安全;必须检查 fact payload 中所有 user-visible `GdType` 字段。 +- 任何依赖 `FrontendAnalysisData.symbolBindings()` / `resolvedMembers()` / `resolvedCalls()` / `expressionTypes()` / `slotTypes()` 返回的可变 stable 引用来直接 `put()` / `clear()` / `putAll()` 的路径,都不能被视为满足本节不变量的 publication path。 +- Godot 在这里没有等价的 compiler-only publication guard 或 overlay export 机制;本节约束完全由 GDCC 自己的 side-table / lowering 边界不变量驱动,不能靠 Godot 对照“类比证明”。 + +### 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 已实施资产可回退但不能静默改变语义 + +阶段 A-D 已有代码可以按新计划保留、移动、作为重写参考、废弃或回退,但任何回退必须保持: + +- `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` guard-only 合同。 +- `FrontendAnalysisData` stable reference 合同。 +- patch merge 的冲突检测与 compiler-only guard。 +- unsupported / deferred subtree fail-closed 合同。 ## 4. 核心设计 -### 4.1 两层 pipeline +### 4.1 新的三层 pipeline -重构后的 shared semantic pipeline 分成两层。 +重启后的 shared semantic pipeline 分成三层。 -基础 whole-module 层: +基础结构层: 1. skeleton -2. scope -3. baseline variable inventory +2. scope graph +3. baseline inventory + +基础结构层职责是建立 `FrontendModuleSkeleton`、`scopesByAst()`、callable parameter inventory、supported ordinary local inventory,以及 skipped/deferred subtree 的硬边界。它不做 body expression typing。 + +Interface 层: -基础层的职责是建立全局 class / callable / block scope 图,并对当前无需 typed fact 即可支持的 block 发布完整 local inventory。 +1. class / callable / property signature interface +2. per-callable body declaration index +3. typed-dependent gate registry +4. source-order local typed baseline -分段 semantic 层: +Interface 层借鉴 Godot `resolve_interface()` 与 `resolve_body()` 之间的边界:它不直接 lowering body,也不发布 compile-ready body facts,但必须准备 body `SuiteResolver` 所需的 typed lexical baseline。 -1. top binding segment -2. local type stabilization segment -3. chain binding segment -4. expr typing segment -5. slot type post segment +Body 层: -分段层以源码顺序处理 supported executable block 的 statement window。每个 window 内仍按上述阶段顺序运行,不能把 expr typing 提前到 chain binding 之前,也不能让 local stabilization 越过 top binding。 +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 whole-module 层: +诊断-only 层仍在 body facts 完全收敛后运行: 1. annotation usage 2. virtual override @@ -135,53 +208,136 @@ 4. loop control 5. compile-only final gate -这些阶段应在分段 semantic 层完全收敛后运行,继续消费最终 facts。 +### 4.2 Interface phase -### 4.2 Segment 的粒度 +新增 `FrontendInterfacePhase` 或等价 coordinator。它不取代 skeleton/scope/variable analyzer,而是在它们之后建立 body 解析所需的 interface surface。 -第一版 segment 粒度定义为 `FrontendSemanticWindow`: +Interface phase 输入: -```java -record FrontendSemanticWindow( - @NotNull Node owner, - @NotNull Scope currentScope, - @NotNull List roots, - @NotNull FrontendSemanticWindowKind kind -) {} +- `FrontendModuleSkeleton` +- `scopesByAst()` +- baseline parameter / ordinary local inventory +- current diagnostics snapshot +- `ClassRegistry` + +Interface phase 输出: + +- `FrontendBodyDeclarationIndex`:每个 supported block 的完整 declaration 列表与 source order。 +- `FrontendInventoryGateRegistry`:typed-dependent subtree 的 gate owner、header root、body root、deferred domain、readiness。 +- `FrontendTypedLexicalBaseline`:参数、显式 typed local、已可静态确定的 interface-level source-facing slot baseline。 +- `FrontendSuitePlan`:body layer 可进入的 callable/property initializer/supported block 列表。 + +Interface phase 不得: + +- 发布 `expressionTypes()`。 +- 发布 `resolvedMembers()` / `resolvedCalls()`。 +- 打开 `FOR_BODY` / `MATCH_SECTION_BODY` / `LAMBDA_BODY`,除非对应 gate readiness 已明确 published。 +- 将 `GdCompilerType` 写入 source-facing lexical baseline。 + +### 4.3 Body `SuiteResolver` + +新增 `FrontendSuiteResolver` 或等价 body coordinator。它按 Godot `resolve_suite()` 的形状处理 body: + +```text +resolveSuite(context, block): + for statement in block.statements(): + resolveStatement(context, statement) + flushStatementFacts(context, statement) + resolvePendingBodiesIfAllowed(context) ``` -建议的 window kind: +`resolveStatement(...)` 不是新的 owner。它只负责按 statement 结构调用 body-aware owner 子过程: + +- identifier / route head 绑定仍由 top binding owner 产出。 +- local `:=` slot rewrite 仍由 local stabilization owner 产出。 +- chain member / chain call 仍由 chain binding owner 产出。 +- expression type / bare call 仍由 expr typing owner 产出。 +- source slot final publication 仍由 var type post owner 产出。 + +这些 owner 子过程是新实现,不是现有 `analyzeInWindow(...)` 的改名。实现时可以抽取纯 helper(例如 binding 分类、chain reduction、expression semantic support),但不能复用 whole-module `AstWalker...walk(sourceFile)` 入口作为 production SuiteResolver procedure。当前 analyzer 内部的大型 visitor 状态机必须被拆成显式 context + statement-local dispatch;否则 source-order prefix facts、pending overlay 与 per-owner patch transaction 都无法保证。 + +`resolveStatement(...)` 内部的 owner 子过程顺序是新的硬不变量,必须保持 legacy whole-phase 的可见性顺序: + +1. Top binding runner:为当前 statement 内的 bare identifier / chain head use-site 写入 binding overlay。 +2. Local stabilization runner:在 top binding overlay、当前 suite 已提交 typed fact、stable lexical inventory 之上解析 eligible `:=` initializer,并写入当前 statement 的 local slot pending overlay。 +3. Chain binding runner:消费 top binding overlay 与 local stabilization pending / committed slot fact,发布 `resolvedMembers()` 与 chain-owned `resolvedCalls()` overlay。 +4. Expr typing runner:消费 binding、member、call 与 local slot overlay,发布 `expressionTypes()` 与 bare-call `resolvedCalls()` overlay;`backfillInferredLocalType(...)` 仍只做 guard-only 检查。 +5. Var type post procedure:消费 expression type 与 source-facing local slot overlay,发布 final `slotTypes()` overlay。 +6. Statement flush:把当前 statement pending overlay 转入 current-suite committed overlay,供后续 statement / gate classifier 读取;不得在这一步写 stable side table。 + +Suite 收敛后,不能把 current-suite committed overlay 整体打包为单个多 owner `FrontendAnalysisPatch`。Stable export 必须构造按 owner 有序的 patch transaction,并按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 顺序依次 apply 每个 owner patch。这样既保留 source-order suite 的前缀可见性,又不破坏 `FrontendAnalysisPatch` 现有单 stage / 单 owner 约束。 + +不得重排 1-5。尤其是 chain binding 读取 receiver local slot 时,必须先看到 local stabilization 对前序 statement 或当前 statement 前序子过程写入的 exact slot fact;否则会重新打开 receiver 被误读成 `Variant` 的历史回归。 + +Nested chain / argument retry 只能发生在当前 owner 子过程内部的 `FrontendOwnerRetryMemo`(实现命名可调整)或等价非导出 transient memo 中。Retry 可读取本次 reduction 已经推导出的 receiver / argument / step 临时事实,也可读取当前 statement 之前 owner 子过程已发布到 pending / committed overlay 的事实;但 retry 产生的中间 facts 不写入 `expressionTypes()` overlay、statement pending overlay、current-suite committed overlay 或 stable side table,也不能被 `TypedLexicalEnvironment` 的普通 lookup 读取。它们在当前 owner 子过程结束时丢弃。 + +`FrontendExprTypeAnalyzer` 对同一 expression / step key 只能在完成 retry、选定最终 `FrontendExpressionType` 后写入一次 expression type overlay。若同一 key 需要先得到 `DEFERRED`、暂定 `Variant` 或其他非最终状态,再得到 exact result,必须把中间值保存在 owner-local memo 或专用非导出状态里,而不是发布为 `expressionTypes()` 后再 narrowing / status upgrade。 + +Gate classifier 属于 statement 结构处理的一部分,只能在其 header 所需的 top binding、local stabilization、chain binding 与 expr typing 子过程完成后运行。Classifier 可读取当前 statement pending overlay 与 current-suite committed overlay,但不能读取后续 statement facts。 + +第一版 body statement 支持面: + +- `VariableDeclaration`,仅 ordinary local `var` 与 supported property initializer。 +- `ExpressionStatement`。 +- `ReturnStatement`。 +- `AssertStatement`,保持现有 compile gate blocker。 +- `IfStatement` / `ElifClause` / `else`,header 先解析,body 后递归。 +- `WhileStatement`,condition 先解析,body 后递归。 +- `ForStatement` 第一版继续 fail-closed;后续 for-range plan 可作为 generic typed-dependent gate infra 的真实消费者。 +- `MatchStatement`、`LambdaExpression`、block-local `const` 继续 deferred / unsupported,除非后续阶段显式转正。 + +### 4.4 `TypedLexicalEnvironment` overlay -- `PROPERTY_INITIALIZER` -- `CALLABLE_STATEMENT` -- `BLOCK_STATEMENT` -- `CONTROL_HEADER` -- `FEATURE_GATE_HEADER` -- `FEATURE_GATE_BODY` +新增 `FrontendTypedLexicalEnvironment`,作为 body 层所有 value/type lookup 的 effective view。它不替换 `Scope`,而是包装 `Scope` 与当前 suite 的 typed overlay。 -第一版实现可以先做到“每个 statement 一个 window”,不要在开始时做复杂 batching。性能问题等行为稳定后再通过相邻 safe window 合并优化。 +这不是给现有 resolver 增加一个可选参数那么简单。当前 `FrontendVisibleValueResolver`、`FrontendChainReductionFacade`、`FrontendExpressionSemanticSupport` 与各 analyzer 内部回调都默认读取 stable `FrontendAnalysisData` / `Scope` / analyzer-local state。SuiteResolver 路线必须为这些 lookup 建立新的 effective view 入口,使 identifier binding、receiver type、argument type、call/member result 与 local slot type 读取都能先看 pending / committed overlay,再回退到完整 lexical inventory 与 stable side table。 -### 4.3 Source-order scheduler +读取顺序: -新增 `FrontendSegmentedSemanticScheduler` 或等价 coordinator。它不拥有具体语义,只负责按源码顺序调度 analyzer segment。 +1. 当前 statement pending overlay。 +2. 当前 suite committed overlay。 +3. `BlockScope` / `CallableScope` 中的 stable lexical inventory 与 stable side tables。 +4. parent typed lexical environment。 +5. class/global/singleton/type-meta lookup。 -调度规则: +Pending overlay 只对当前 statement 内后续 owner 子过程可见。Statement flush 后,它才变成 current-suite committed overlay,并对后续 statement / gate classifier 可见。Committed overlay 仍不是 stable publication。 -1. 对 accepted source file 按 class member 顺序进入。 -2. 对 function / constructor body,要求 callable parameters 和 body block baseline inventory 已发布。 -3. 对 supported block,先保证该 block 的完整 local inventory 已发布。 -4. 按 `block.statements()` 顺序处理 statement window。 -5. 每个 statement window 完成 top binding -> local stabilization -> chain binding -> expr typing -> var type post,并把 patch merge 回 `FrontendAnalysisData`。 -6. 遇到 `if` / `elif` / `else` / `while` 等已支持 control body 时,header window 先完成,再递归处理 body block。 -7. 遇到 typed-dependent feature gate,例如 future supported `for`,先分析 gate header 所需前置 facts,再运行 classifier。 -8. classifier 若转正,只能把 gate 状态推进到 `SUPPORTED`;此时 body inventory 仍必须保持 `NOT_PUBLISHED`,resolver / binder 继续 fail-closed。 -9. scheduler 随后运行专门的 child body inventory publication window。只有该 window 成功提交 iterator binding 与 body 完整 local inventory 后,才能把 body readiness 推进到 `PUBLISHED`。 -10. 只有 `status == SUPPORTED && bodyInventoryReadiness == PUBLISHED` 的 body 可以递归处理 child body semantic windows。 -11. classifier 若未转正,保留现有 deferred / unsupported boundary,不进入 child body segment,body readiness 也必须保持 `NOT_PUBLISHED`。 +写入规则: -### 4.4 Feature gate +- 只有 `FrontendLocalTypeStabilizationAnalyzer` 可写 source-facing local slot overlay。 +- 只有 `FrontendTopBindingAnalyzer` 可写 binding overlay。 +- 只有 `FrontendChainBindingAnalyzer` 可写 member / chain-call overlay。 +- 只有 `FrontendExprTypeAnalyzer` 可写 expression type / bare-call overlay。 +- 只有 `FrontendVarTypePostAnalyzer` 可写 source-facing slot type overlay。 +- overlay fact 必须带 owner metadata,并在导出到 stable side table 前执行冲突检测、idempotent 检查和 compiler-only guard。 +- compiler-only guard 必须检查该 fact 可达的每个 user-visible `GdType` payload;不能只检查 `expressionTypes()` / `slotTypes()` 两个表。 +- compiler-only guard 必须在 pending overlay write 时就 fail-fast,而不是等 suite export 时才补救。任何 scratch 写入如果命中 `GdCompilerType`,必须在 write API 返回前拒绝该 fact,不能先写入 pending / committed overlay 再在 export 时回滚。 +- `expressionTypes()` overlay 只接受当前 statement 内每个 AST key 的最终 publication fact。retry 中间计算(包括首 pass 的 `DEFERRED`、暂定 `Variant`、临时 status / detailReason)必须留在 `FrontendOwnerRetryMemo`,不得作为 overlay fact 写入。 +- `expressionTypes()` overlay 不提供 `Variant -> exact`、parent -> child 或 terminal status -> success 的 narrowing 例外。这不是 overlay 的局部规则,而是对 `FrontendAnalysisData.sameExpressionType` 严格判据(status + publishedType + detailReason 全等,见第 4.6 节)的直接引用。需要 narrowing 的 local slot 变化必须走 `FrontendLocalSlotTypeUpdate`,不得绕道 `expressionTypes()` republish。 -新增 `FrontendInventoryGate` 记录 typed-dependent subtree 的待决状态。第一版至少应能表达: +四层事实可见性模型固定为: + +- `FrontendOwnerRetryMemo`:owner 子过程私有,只给当前 chain / expr reduction 的 retry 回调读取;不属于 `TypedLexicalEnvironment`,不参与 flush / export,owner 子过程结束即丢弃。 +- 当前 statement pending overlay:只给当前 statement 后续 owner 子过程读取;只接受每个 AST key 的最终 publication fact。 +- current-suite committed overlay:由 statement flush 合并而来,给后续 statement 与 gate classifier 读取;仍不是 stable publication。 +- `FrontendAnalysisData` stable side tables / `BlockScope` stable slot:只在 suite export 的 per-owner patch apply / stable export helper 后更新,供 diagnostics-only phase、compile gate 与 lowering 使用。 + +Overlay 的目标是模拟 Godot “当前 statement 已解析出的类型可被后续 statement 使用”的效果,同时避免提前污染 `FrontendAnalysisData` stable tables。 + +写入与导出时机: + +- Owner runner 只能写当前 statement pending overlay。 +- `flushStatementFacts(...)` 只把 pending overlay 合并到 current-suite committed overlay,并执行 owner、conflict、idempotent、exact-type 与 compiler-only guard。该 guard 必须与 suite export 使用同一 type-bearing field walker,避免 scratch 层接受 stable merge 层会拒绝的 compiler-only payload。 +- `flushStatementFacts(...)` 不得通过 stable side table 的临时 whole-table publish 再“读回 overlay”。如果当前实现为了复用旧 analyzer helper 需要 `updateXxx(...)` / stable side table 作为中转,则该 helper 必须先被改写或隔离为 legacy path,不能把中转污染当作阶段性可接受状态。 +- Suite 收敛时,current-suite committed overlay 只能导出为按 owner 有序的 patch transaction,不能导出为一个跨 owner `FrontendAnalysisPatch`。 +- Patch transaction 固定按 top binding -> local stabilization -> chain binding -> expr typing -> var type post apply;每个 step 只包含该 owner 的 facts。 +- Stable side table 与 `BlockScope.resetLocalType(...)` 只能在 per-owner patch apply / stable export helper 中更新。 +- Diagnostics-only phase、compile gate 与 lowering 只能读取 suite export 后的 stable facts。 +- Nested supported suite 收敛后,可把其 per-owner patch transaction 追加到外层 export transaction;lexical visibility 仍由 scope graph 与 resolver filter 决定,不能因为 transaction 合并放宽 local 可见性。 + +### 4.5 Feature gate 与 body readiness + +新增 `FrontendInventoryGate` 记录 typed-dependent subtree 的待决状态。第一版至少表达: ```java enum FrontendInventoryGateStatus { @@ -206,114 +362,71 @@ record FrontendInventoryGate( ) {} ``` -`FrontendVariableAnalyzer` 的 baseline pass 不应在需要 typed fact 的 gate 上做最终决定。它应: - -- 对已支持且不依赖 typed fact 的 block 发布完整 local inventory。 -- 对 typed-dependent subtree 记录 pending gate。 -- 对当前明确不会被本计划解封的 subtree 继续发 unsupported/deferred diagnostic。 - -后续 `FrontendSegmentedSemanticScheduler` 在 header facts 就绪后调用对应 classifier,并决定是否发布 body inventory。 +生命周期固定为: -### 4.4.1 Body inventory readiness +1. Interface phase 发现 typed-dependent gate:`PENDING + NOT_PUBLISHED`。 +2. Body suite 解析 gate header 所需 expression facts。 +3. classifier 判定 unsupported:`UNSUPPORTED + NOT_PUBLISHED`,保留 deferred / unsupported boundary。 +4. classifier 判定 supported:`SUPPORTED + NOT_PUBLISHED`,此时 body 仍不可解析。 +5. body inventory publication 开始:临时 `PUBLISHING`,resolver / binder 仍 fail-closed。 +6. gate-owned body binding 与 body 完整 local inventory 成功发布后:`SUPPORTED + PUBLISHED`。 +7. 只有 `SUPPORTED + PUBLISHED` 的 body 可以进入 `SuiteResolver`。 -`status == SUPPORTED` 与 `bodyInventoryReadiness == PUBLISHED` 是两件事,不能互相替代: - -- `SUPPORTED` 只表示 gate classifier 已确认该 subtree 可以被本轮 pipeline 解封。 -- `PUBLISHED` 表示对应 body scope 的 iterator binding 与完整 local inventory 已经通过 publication owner 写入,并且提交点已经成功完成。 -- `BlockScopeKind.FOR_BODY` scope 存在不表示 inventory 已发布;`FrontendScopeAnalyzer` 会无条件为 `ForStatement.body()` 建 scope,这只是 lexical graph 事实。 -- `bodyInventoryReadiness` 是 gate body inventory readiness 的单一真源。任何 analyzer、resolver、compile gate 或测试 helper 都不得用“scope 存在”、“gate 为 `SUPPORTED`”或“`BlockScopeKind.FOR_BODY`”推导 body 已可解析。 -- `PUBLISHING` 只允许表达 scheduler 正在发布 body inventory 的内部过渡状态。对 resolver / binder / downstream semantic windows 来说,它与 `NOT_PUBLISHED` 一样必须 fail-closed。 - -生命周期必须固定为: - -1. baseline pass 记录 typed-dependent gate:`status = PENDING`,`bodyInventoryReadiness = NOT_PUBLISHED`。 -2. classifier 判定 unsupported:`status = UNSUPPORTED`,`bodyInventoryReadiness = NOT_PUBLISHED`,保留 deferred / unsupported boundary。 -3. classifier 判定 supported:`status = SUPPORTED`,`bodyInventoryReadiness = NOT_PUBLISHED`,此时 body 仍不可解析。 -4. body inventory window 开始时可临时进入 `PUBLISHING`,但不得让 resolver 正常 lookup。 -5. body inventory window 成功提交后,原子推进为 `PUBLISHED`。 -6. body inventory window 失败或被丢弃时,必须回到 `NOT_PUBLISHED` 或直接 fail-fast;不得留下 `SUPPORTED + PUBLISHING` 的稳定 public 状态。 - -需要提供一个共享查询作为唯一入口,例如 `FrontendExecutableInventorySupport.isCallableLocalValueInventoryReady(BlockScope scope, Node useSite, FrontendAnalysisData data)` 或等价命名。它必须: +需要提供共享 readiness policy 作为唯一入口,例如 `FrontendExecutableInventorySupport.isCallableLocalValueInventoryReady(BlockScope scope, Node useSite, FrontendAnalysisData data)`、`FrontendInventoryGateRegistry.isResolverGateReady(...)` 或等价命名。它不能只回答 `BlockScopeKind`,还必须能回答 owner/body/domain 级别的问题。它必须: - 对无条件支持的 block kind 继续返回 true。 -- 对 `FOR_BODY` 这类 gate body,只在能找到 owning gate,且 `status == SUPPORTED && bodyInventoryReadiness == PUBLISHED` 时返回 true。 -- 对缺失 gate、`PENDING`、`SUPPORTED + NOT_PUBLISHED`、`SUPPORTED + PUBLISHING`、`UNSUPPORTED`、合成但无 owning `ForStatement` 的 `FOR_BODY` 返回 false。 +- 对 gate body,只在能找到 owning gate,且 `status == SUPPORTED && bodyInventoryReadiness == PUBLISHED` 时返回 true。 +- 对缺失 gate、`PENDING`、`SUPPORTED + NOT_PUBLISHED`、`SUPPORTED + PUBLISHING`、`UNSUPPORTED`、合成但无 owning gate 的 body 返回 false。 +- 为 resolver request-domain、AST boundary edge、current-scope fail-closed 三处使用同一 readiness 事实,避免三处各自判断 `BlockScopeKind.FOR_BODY` 或 deferred domain。 - 被 `FrontendVariableAnalyzer`、`FrontendVisibleValueResolver`、`FrontendLocalTypeStabilizationAnalyzer`、`FrontendVarTypePostAnalyzer`、`FrontendCompileCheckAnalyzer` 等所有 callable-local inventory 消费者共同使用。 -现有 `FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(BlockScopeKind)` 只能继续表达无条件支持的 block kind,不得成为 `FOR_BODY` readiness 的事实源,也不得通过把 `FOR_BODY` 加进 switch 来解封 gate body。 +现有 `FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(BlockScopeKind)` 只能继续表达无条件支持的 block kind,不得成为 typed-dependent body readiness 的事实源。 -### 4.5 Side-table patch +### 4.6 Stable export 与 per-owner patch transaction -新增 transient patch 类型,例如 `FrontendAnalysisPatch`: +`TypedLexicalEnvironment` 不是 public publication。只有导出并通过 stable merge 后,事实才成为 diagnostics-only phase、compile gate 和 lowering 可消费的事实。 -```java -record FrontendAnalysisPatch( - @NotNull FrontendSemanticStage stage, - @NotNull FrontendAstSideTable symbolBindings, - @NotNull FrontendAstSideTable resolvedMembers, - @NotNull FrontendAstSideTable resolvedCalls, - @NotNull FrontendAstSideTable expressionTypes, - @NotNull FrontendAstSideTable slotTypes, - @NotNull List localSlotTypeUpdates -) {} -``` +新增 `gd.script.gdcc.frontend.sema.patch` 包承载 patch 相关类型: -`FrontendAnalysisData` 新增 `applyPatch(...)` 或分表 merge 方法,规则如下: +- `FrontendOwnerPatch` 或等价 sealed interface,表达“单一 semantic owner 的 publication delta”。这里的 owner 是 analyzer / publication owner,不是 `FrontendResolvedMember.ownerKind()` / `FrontendResolvedCall.ownerKind()` 中的 `ScopeOwnerKind`。 +- `FrontendTopBindingPatch`:只包含 `symbolBindings()` delta。 +- `FrontendLocalTypeStabilizationPatch`:只包含 `FrontendLocalSlotTypeUpdate` delta,不直接携带刷新后的 `symbolBindings()` entry。 +- `FrontendChainBindingPatch`:只包含 `resolvedMembers()` 与 chain-owned `resolvedCalls()` delta。 +- `FrontendExprTypePatch`:只包含 `expressionTypes()` 与 bare-call `resolvedCalls()` delta。 +- `FrontendVarTypePostPatch`:只包含 `slotTypes()` delta。 +- `FrontendPatchTransaction` 或等价 batch container,保存上述 patch 的有序列表并负责按固定 owner 顺序 apply。 +- 旧 `FrontendAnalysisPatch`、`FrontendLocalSlotTypeUpdate` 迁入同一 package。旧 `FrontendAnalysisPatch` 只能作为 legacy single-stage patch / 测试兼容层或被拆解;suite export 生产路径不得再用它承载多 owner facts。`FrontendWindowPublicationSurface`、`FrontendWindowAnalysisContext` 若迁入该包,必须单独标记为 legacy shim,不属于 production overlay/export 参考资产。 + +保留 `FrontendLocalSlotTypeUpdate` / `FrontendAnalysisData.applyPatch(...)` 的核心规则,但 `applyPatch` 的输入必须是单一 owner patch。以下规则适用于每个 per-owner patch 的 merge: - 新 key 直接写入 stable side table。 - 旧 key + 相同 value 视为 idempotent,允许。 - 旧 key + 不同 value 默认 fail-fast。 -- `symbolBindings()` 允许因 `FrontendLocalSlotTypeUpdate` 刷新同一 declaration 的 `resolvedValue` payload,但必须保持 binding kind、name、declaration identity 不变。 -- `resolvedCalls()` 中 chain-owned call 与 bare-call 由 stage 标识区分,不能相互覆盖。 -- `expressionTypes()` 不允许从 `RESOLVED(int)` 变成 `RESOLVED(float)`,也不允许从 terminal negative status 改成 success;需要 retry 的场景不得先发布 provisional public fact。 +- `symbolBindings()` 允许因 `FrontendLocalSlotTypeUpdate` 的 commit helper 刷新同一 declaration 的 `resolvedValue` payload,但 local stabilization patch 本身不得携带独立的 `symbolBindings()` delta。刷新必须由 `BlockScope.resetLocalType(...)` 与 binding payload refresh 的同一个 helper 派生完成,并保持 binding kind、name、declaration identity 不变。 +- `resolvedCalls()` 中 chain-owned call 与 bare-call 由 semantic owner patch 与 key-space contract 区分,不能相互覆盖;不得把 `ScopeOwnerKind` 当成 semantic publication owner。 +- `expressionTypes()` 的同 key republish 只有 `FrontendAnalysisData.sameExpressionType(...)` 判定为同值时允许;stable 判据保持 status + publishedType + detailReason 严格相等。 +- `expressionTypes()` 不允许同一 key 出现 status change、same-status + different publishedType、same-status + different detailReason,也不允许从发布 `Variant` 的 fact 变成 exact `int` fact。 +- `FrontendExprTypePatch.expressionTypes()` 必须是 expr typing owner overlay 收敛后的 per-key final view。Patch 内同一 key 不得出现不同 logical value,suite export 也不得把 retry 中间事实导出为 stable fact 后再 narrowing / status upgrade。 - `slotTypes()` 不允许同一 source slot 被不同类型覆盖;同类型 idempotent 允许。 -- merge 前统一检查 compiler-only type 不泄漏。 - -现有 `updateXxx(...)` 继续表达 whole-table snapshot publication;`applyPatch(...)` 只用于 segmented semantic layer。 - -### 4.5.1 Window-local publication surface - -`FrontendAnalysisPatch` 只表达 window 结束时提交到 stable side table 的增量,不表达 window 内部的即时可见性。分段实现必须额外引入 window-local publication surface,例如 `FrontendWindowPublicationSurface` 或等价对象。 +- merge 前统一检查 compiler-only type 不泄漏。当前 `FrontendAnalysisData.checkPatchDoesNotLeakCompilerOnlyTypes(...)` 只覆盖 `expressionTypes()` 与 `slotTypes()`,阶段 C 必须把该检查扩展到 `symbolBindings().resolvedValue.type()`、`resolvedMembers()` 的 `receiverType` / `resultType`、`resolvedCalls()` 的 `receiverType` / `returnType` / `argumentTypes` / callable boundary parameter types。否则不能宣称 overlay export 已复用完整 compiler-only guard。 -window-local surface 的核心合同为 scratch-over-stable: +新增或扩展统一 helper,例如 `checkNoCompilerOnlyLeakInPublishedFact(...)` / `FrontendPublishedFactTypeGuard`,用于 patch commit 与 typed overlay 写入两处。它必须遍历 `FrontendBinding`、`FrontendResolvedMember`、`FrontendResolvedCall`、`FrontendExpressionType`、`slotTypes()` value 与 `FrontendLocalSlotTypeUpdate` 中所有 user-visible `GdType`,并复用同一错误策略。Legacy window publication surface 不能作为该 helper 的正确性基线。 -- 每个 `FrontendSemanticWindow` 开始时创建一组空的 scratch side table,与当前 `FrontendAnalysisData` stable side table 组成 effective view。 -- window 内所有 stage 读取 semantic fact 时必须通过 effective view:先查 window scratch,再查 stable side table。 -- window 内所有新增或刷新事实只写入 scratch,不直接写入 `FrontendAnalysisData` stable side table。 -- window 成功完成后,scratch 被封装为 `FrontendAnalysisPatch`,再通过 `applyPatch(...)` 原子合并到 stable side table。 -- window 失败或被 classifier 判为 unsupported 时,scratch 直接丢弃,不得污染 stable side table。 +阶段 C 实施前,至少要明确并锁定下面的 guard 扩展 checklist: -这个 surface 不是 public publication。只有 `applyPatch(...)` 成功后,事实才成为后续 window、diagnostics-only 阶段、lowering 可消费的 stable published fact。 +- `FrontendAnalysisData.checkPatchDoesNotLeakCompilerOnlyTypes(...)` 从只看 expression/slot 扩展为调用 shared walker,覆盖 binding/member/call payload。 +- `FrontendWindowPublicationSurface.WindowSideTableView` 中 `symbolBindings()`、`resolvedMembers()`、`resolvedCalls()` 若仍保留,必须接入同一 walker;不能继续用 `null` guard 只保护 expression/slot。 +- `FrontendAnalysisData.updateSymbolBindings(...)`、`updateResolvedMembers(...)`、`updateResolvedCalls(...)`、`updateExpressionTypes(...)`、`updateSlotTypes(...)` 若还保留 source-facing publication 语义,必须先经过 shared walker;否则文档上必须把它们限定为 legacy whole-table path,并从 production SuiteResolver path 排除。 +- `refreshPublishedLocalBindingPayloads(...)` 只是一条 local slot commit helper 派生路径,不能被误写成“symbolBindings 已完整受保护”的证明。 +- 任何仍暴露可变 stable 引用的 API,都只能依赖调用方 contract 保证“不直接写 stable table”;计划的完成标准必须通过 rewrite/inspection/tests 确保 production body path 不再走这些 bypass。 -`ExprType` 的特殊要求必须明确保留: +可调整的是通信路径:body layer 可以先写 `TypedLexicalEnvironment` overlay,再在 suite / callable / module 边界导出 per-owner patch transaction。不得用 `updateXxx(...)` 表达部分提交,也不得用一个 single-stage patch 表达多 owner suite export。 -- `FrontendExprTypeAnalyzer` 当前依赖读己写语义,nested chain reduction 会读取同一 window 内刚发布的 expression fact。 -- 迁移后该读己写只能发生在 window scratch 内,不能通过提前写 stable side table 实现。 -- `FrontendChainReductionHelper`、`FrontendChainReductionFacade`、`FrontendExpressionSemanticSupport` 等 shared support 不得直接读取 `analysisData.expressionTypes()` 来判断当前 window 内 fact 是否已存在,必须通过 effective view 或由 window-aware resolver 封装该查找顺序。 -- `finalizeWindow=true` 表示“当前 bounded retry window 的最后一次补全尝试”。若补全得到稳定结果,只能写入当前 window scratch;是否成为 public fact 仍由 window 结束时的 `applyPatch(...)` 决定。 -- 需要 retry 的路径不得发布 provisional public fact;如果只能得到 `DEFERRED` / `FAILED` / `UNSUPPORTED` 等 terminal 或 unstable 结果,也应先停留在 scratch,按 `expressionTypes()` merge 规则在 window commit 时统一判定。 +这意味着 Stage E 的 “nested chain / argument retry 读己写” 不是 stable side-table rewrite 能力。Retry 的读己写发生在 chain/expr owner 的 `FrontendOwnerRetryMemo` 与当前 statement effective view 中;stable `expressionTypes()` 仍只看到每个 key 的最终一次 publication。 -同一规则也适用于 window 内 stage 间依赖: +### 4.7 Scope slot mutation -- top binding segment 写入的 `symbolBindings()` 必须在同一 window 的 local stabilization、chain binding、expr typing 中可见。 -- chain binding segment 写入的 `resolvedMembers()` 与 chain-owned `resolvedCalls()` 必须在同一 window 的 expr typing 中可见。 -- expr typing segment 补充的 bare-call `resolvedCalls()` 与 `expressionTypes()` 必须在同一 window 的 var type post 中可见。 -- `publishAttributeStepExpressionTypes(...)` 这类 duplicate guard 必须同时检查 scratch 与 stable,允许同值 idempotent,拒绝不同值覆盖。 - -因此,window-capable analyzer API 不应只接收 `FrontendAnalysisData`。它应接收一个只暴露 effective reads 与 scratch writes 的上下文,例如 `FrontendWindowAnalysisContext`: - -```java -record FrontendWindowAnalysisContext( - @NotNull FrontendAnalysisData stableData, - @NotNull FrontendWindowPublicationSurface publications -) {} -``` - -具体命名可调整,但必须避免 analyzer 或 shared helper 绕过 window surface 直接写 stable side table。 - -### 4.6 Scope slot mutation - -`BlockScope.resetLocalType(...)` 不是 side-table 写入,但它会影响后续 resolver 和 binding payload。分段方案必须显式建模这类 mutation。 +`BlockScope.resetLocalType(...)` 不是 side-table 写入,但它会影响后续 resolver 和 binding payload。新计划必须把这类 mutation 建模为 owner-controlled slot update。 `FrontendLocalSlotTypeUpdate` 至少记录: @@ -326,266 +439,314 @@ record FrontendLocalSlotTypeUpdate( ) {} ``` +`FrontendLocalSlotTypeUpdate` 迁入 `gd.script.gdcc.frontend.sema.patch` 包,并由 `FrontendLocalTypeStabilizationPatch` 携带。它仍然是 local stabilization owner 的专用 carrier,不得被 expr typing 或 var type post patch 复用。 + 应用规则: -- `FrontendLocalTypeStabilizationAnalyzer` 是唯一允许产生 `FrontendLocalSlotTypeUpdate` 的 analyzer。 +- `FrontendLocalTypeStabilizationAnalyzer` 是唯一允许产生 source-facing `FrontendLocalSlotTypeUpdate` 的 analyzer。 - 只允许 `Variant -> exact` 或 exact same-type no-op。 - 不允许 exact A -> exact B。 - 不允许写入 `GdVoidType`。 - 不允许写入 `GdCompilerType` 到 source-facing local。 - 应用后必须刷新已发布且指向同一 declaration 的 `symbolBindings()` payload。 -- 刷新应优先使用 declaration identity index,第一版若继续全表扫描也必须被测试锁住语义正确性。 -现存 `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 是必须显式收口的历史路径。它当前能在 expr phase 中直接调用 `BlockScope.resetLocalType(...)`,并通过 `refreshPublishedLocalValues(...)` 刷新已发布的 `symbolBindings()` payload;这两者都会绕过 `FrontendLocalSlotTypeUpdate` 的 owner 边界。 +Overlay 可在导出前提供 effective type,但最终 stable `BlockScope.resetLocalType(...)` 和 binding payload refresh 仍必须走同一个 commit helper,避免出现第二条 slot mutation side channel。 -在 segmented runner 中,该路径只能保留为 guard-only 检查: +`FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 必须继续保持 guard-only: - 不得调用 `BlockScope.resetLocalType(...)`。 -- 不得调用或复制 `refreshPublishedLocalValues(...)` 的 side-channel binding payload refresh。 -- 不得向 window surface 或 patch 追加 `FrontendLocalSlotTypeUpdate`。 -- 若 initializer fact 是 terminal negative、`DYNAMIC`、`void`、缺失或当前 slot 仍是 inventory-seeded `Variant`,直接 no-op;expr phase 不能补做 local stabilization 没有完成的 narrowing。 -- 若当前 slot 已是非 `Variant` 且 initializer exact type 一致,no-op。 -- 若当前 slot 已是非 `Variant` 但 initializer exact type 不一致,fail-fast 暴露内部阶段协议错误。 -- 若 initializer fact 试图把 `GdCompilerType` 作为 source-facing local 类型观测到,fail-fast。 +- 不得刷新 `symbolBindings()` payload。 +- 不得产生 `FrontendLocalSlotTypeUpdate`。 +- 已稳定同类型 no-op,已稳定异类型 fail-fast。 +- compiler-only initializer fact fail-fast。 -如果后续发现某类 initializer 需要更强的 `:=` narrowing,必须扩展 `FrontendLocalTypeStabilizationAnalyzer` 或它的 window-local resolver,而不是恢复 expr-phase backfill mutation。 +### 4.8 Resolver 复用规则 -### 4.7 Resolver 复用规则 +`FrontendVisibleValueResolver` 继续一次性索引完整 source AST,并继续读取已经发布的完整 lexical inventory。完整 inventory 是 `DECLARATION_AFTER_USE_SITE` filtered hit 的前提条件;重构时不得让 resolver 自己扫描 AST 补找普通 `var`,也不得把 supported block inventory 缩成只包含当前 source-order 前缀的 incremental view。 -`FrontendVisibleValueResolver` 可以继续一次性索引完整 source AST,但它必须读取已经发布的完整 inventory。 +新增 resolver 能力: -重构时不得让 resolver 自己扫描 AST 补找普通 `var`。原因是这样会复制 `FrontendVariableAnalyzer` 的职责,并容易与 duplicate、shadowing、unsupported boundary、scope kind gate 漂移。 +- 接受 `TypedLexicalEnvironment` 或等价 effective lexical view,用于读取当前 statement / current suite 的 typed overlay。 +- 继续通过 declaration-order filter 处理 future local 与 initializer self-reference。 +- 对 gate header / gate body 使用共享 readiness policy,不允许只靠 `BlockScopeKind.FOR_BODY`、`FrontendVisibleValueDomain.FOR_SUBTREE` 或 scope 存在放行。 +- domain gate、AST boundary 检测与 current-scope fail-closed 三道封口都必须保留,并作为同一个 gate readiness contract 同步条件化。 -需要新增的 resolver 能力是 context-aware inventory readiness 判断: +三道封口的 gate 化规则: -- 现有 `FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(BlockScopeKind)` 保留给无条件支持的 block kind。 -- 新增 AST-aware readiness 查询,例如 `FrontendExecutableInventorySupport.isCallableLocalValueInventoryReady(BlockScope scope, Node useSite, FrontendAnalysisData data)`。 -- 对 `FOR_BODY` 这类 gate body,只有共享 readiness 查询返回 true 时才放行;它内部必须同时检查对应 gate `status == SUPPORTED` 与 `bodyInventoryReadiness == PUBLISHED`。 -- `FrontendVisibleValueResolver.detectDeferredBoundary(...)` 的 `ForStatement.body()` edge 与 `classifyUnsupportedCurrentBlockScopeBoundary(...)` 的 `FOR_BODY` current-scope check 必须调用同一 readiness 查询,不能一个按 `SUPPORTED` 放行、另一个仍按 `FOR_BODY` 封口。 -- `ForStatement.iteratorType()` / `iterable()` 仍属于 gate header 语义,不得因为 body readiness 为 `PUBLISHED` 自动获得 body-local visibility。 -- 非 supported 或 readiness 未发布的 gate body 继续返回 `DEFERRED_UNSUPPORTED + FOR_SUBTREE` 或对应 domain。 +1. Request-domain gate:gate body use-site 的 `FrontendVisibleValueResolveRequest` 不能由各 analyzer 手写 domain。`SuiteResolver` / `FrontendSuiteContext` / resolver request factory 必须根据 owning gate readiness 统一决定 domain。`SUPPORTED + PUBLISHED` 的 body lookup 才能作为 `EXECUTABLE_BODY` 进入 resolver;未发布、unsupported、缺失 owning gate 的 body lookup 继续返回 deferred domain 或不进入 resolver。若过渡实现仍允许传入 `FOR_SUBTREE` 等 deferred domain,domain gate 必须先通过同一个 readiness policy 做 owner/body 归属校验与 normalization,不能在裸 `domain != EXECUTABLE_BODY` 判断处提前拒止一个已经 `PUBLISHED` 的 owning body。 +2. AST boundary gate:`classifyBoundaryEdge(...)` 不能只按 `ForStatement.body() == childNode` 恒定返回 `FOR_SUBTREE`。对 gate owner 的 body edge,只有 owning gate `SUPPORTED + PUBLISHED` 时才返回 `null` 并允许继续 normal lookup;所有非 published 状态继续返回原 deferred boundary。对 gate header edge,只有当前正在解析该 owner 的 header/classifier 时才允许读取前缀 executable facts;header edge 放行不代表 body edge 已发布。非 gate 或 unsupported edge 保持 fail-closed。 +3. Current-scope gate:`BlockScopeKind.FOR_BODY`、`MATCH_SECTION_BODY`、`LAMBDA_BODY` 等 current-scope 兜底仍保留。对 typed-dependent gate body,resolver 必须从 use-site scope 找到 owning body/gate,并且只有 `SUPPORTED + PUBLISHED` 返回 `null`;找不到 owner、gate 缺失、`PENDING`、`SUPPORTED + NOT_PUBLISHED`、`PUBLISHING`、`UNSUPPORTED` 都继续返回对应 deferred boundary。 -## 5. 分步骤实施 +Header/body edge 需要显式区分。`FrontendInventoryGate.headerRoot` 表示 gate-owned header region;如果某个 feature 有多个 header child root,实施时可以把它扩展为 immutable roots 列表或使用等价的 edge classifier,但必须能判断“当前 use-site 是 header 解析”还是“当前 use-site 是 body 解析”。Body readiness 为 `PUBLISHED` 只打开 body lookup,不自动赋予 header feature-specific state 或 body-local visibility。 -### 阶段 A:补齐 side-table patch 基础设施 +Overlay 不得绕过 resolver filter: -实施内容: +- Resolver 先按 request-domain gate、AST boundary、current-scope gate、declaration order 与 initializer self-reference 过滤候选 declaration,再从 `TypedLexicalEnvironment` 读取该候选的 effective type / binding payload。 +- 当前 statement pending slot fact 可被同一 statement 后续 owner 子过程消费,但不能让 `var x := x` 的右侧 `x` 通过 self-reference 过滤。 +- 跨 statement committed fact 可被后续 statement 消费,但 future declaration 仍必须报告 `DECLARATION_AFTER_USE_SITE` filtered hit。 +- Gate classifier 使用 resolver 时也只能读取 header use-site 当前可见的 overlay fact,不能扫描后续 suite statement 弥补 typed fact。 -- 新增 `FrontendSemanticStage`,枚举 `TOP_BINDING`、`LOCAL_TYPE_STABILIZATION`、`CHAIN_BINDING`、`EXPR_TYPE`、`VAR_TYPE_POST`。 -- 新增 `FrontendAnalysisPatch` 与 `FrontendLocalSlotTypeUpdate`。 -- 在 `FrontendAnalysisData` 中新增 patch merge API,保留现有 `updateXxx(...)`。 -- 为 merge 加入冲突检测、idempotent 规则、compiler-only type 泄漏检查。 -- 为 `symbolBindings()` 的 local slot refresh 建立显式 helper,替代 analyzer 内到处分散的 `entry.setValue(...)`。 +Feature-specific header state 仍属于 gate header 语义,不得因为 body readiness 为 `PUBLISHED` 自动获得 body-local visibility。非 supported 或 readiness 未发布的 gate body 继续返回 `DEFERRED_UNSUPPORTED` 或对应 deferred domain。 -当前状态(2026-07-05): +### 4.9 已实施资产分类 -- [x] A1 新增 `FrontendSemanticStage`,并固定五个 segmented semantic stage 常量。 -- [x] A2 新增 `FrontendAnalysisPatch` 与 `FrontendLocalSlotTypeUpdate`,其中 patch 在创建时复制 side table,避免 drain 后被 scratch 二次污染。 -- [x] A3 在 `FrontendAnalysisData` 中新增 `applyPatch(...)`,并继续保留现有 `updateXxx(...)` whole-table publication API。 -- [x] A4 为 merge 加入冲突检测、idempotent 规则、`LOCAL_TYPE_STABILIZATION` owner 校验,以及 `expressionTypes()` / `slotTypes()` / local slot update 的 compiler-only type 泄漏检查。 -- [x] A5 为 `symbolBindings()` 的 local slot refresh 建立显式 helper,并让 `FrontendLocalTypeStabilizationAnalyzer` 与 `FrontendExprTypeAnalyzer` 复用该 helper,保持现有 analyzer 对外行为不变。 +允许代码回退或重新开始实施,但已有资产必须按类别处理。分类标准必须区分“可保留的语义规则 / helper / 测试”与“不可复用的 whole-module traversal 入口”。 -验收细则: +保留资产: -- `FrontendAnalysisDataTest` 继续通过现有 stable-reference / stale-clear 测试。 -- 新增测试覆盖 patch 写入新 key、idempotent merge、冲突 fail-fast。 -- 新增测试覆盖 `symbolBindings()` 仅允许同 declaration 的 local slot payload refresh。 -- 新增测试覆盖 local slot payload refresh 只由 `FrontendLocalSlotTypeUpdate` 应用触发;no-op update 与 expr-phase backfill guard 都不得刷新 binding payload。 -- 新增测试覆盖 `expressionTypes()` / `slotTypes()` 拒绝 `GdCompilerType` 泄漏。 -- 不修改任何 analyzer 行为时,现有 frontend semantic tests 输出不变。 +- `FrontendSemanticStage` +- `FrontendAnalysisData.applyPatch(...)`,但要泛化为消费 `FrontendOwnerPatch` 或等价 per-owner patch carrier。 +- `FrontendAnalysisData.refreshPublishedLocalBindingPayloads(...)` +- `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 的 guard-only 收口 +- `FrontendAnalysisDataTest` 中关于 stable reference、conflict、idempotent、compiler-only guard 的测试 +- analyzer 内部可抽取的纯语义 helper,如 binding 分类、type compatibility、chain reduction 与 expression support 的无遍历部分;只能在去除 whole-module AST walker 依赖后复用 -### 阶段 B:抽出 window-local publication surface +新增资产: -实施内容: +- `gd.script.gdcc.frontend.sema.patch` 包。 +- `FrontendOwnerPatch` 或等价 sealed interface。 +- `FrontendPatchTransaction` 或等价有序 transaction 类型。 +- `FrontendTopBindingPatch`、`FrontendLocalTypeStabilizationPatch`、`FrontendChainBindingPatch`、`FrontendExprTypePatch`、`FrontendVarTypePostPatch` 等 per-owner patch carrier。 +- patch package 内的 shared merge helper / type-bearing compiler-only guard / owner-field validator。 +- `FrontendSuiteResolver` / `FrontendStatementResolver` 的 root-bounded statement dispatch 子框架。 +- statement-local owner procedure interface 或等价显式调用约定,接收 `FrontendSuiteContext`、当前 statement root 与 `FrontendTypedLexicalEnvironment`。 +- 每个 owner 的显式上下文状态 record / stack,例如 restriction、static context、property initializer context、block scope stack、diagnostic suppression state、owner-local retry memo。 -- 新增 `FrontendWindowPublicationSurface` 或等价类型,封装每个 window 的 scratch side table。 -- 新增 `FrontendWindowAnalysisContext` 或等价类型,统一携带 stable `FrontendAnalysisData` 与 window-local publication surface。 -- 为 `symbolBindings()`、`resolvedMembers()`、`resolvedCalls()`、`expressionTypes()`、`slotTypes()` 提供 scratch-over-stable effective read API。 -- 为上述 side table 提供 scratch-only write API;所有 write 都不得直接落到 stable side table。 -- 提供 `toPatch(...)` / `drainPatch(...)` 或等价方法,只把 scratch 中由当前 window 产生的 entries 转成 `FrontendAnalysisPatch`。 -- 提供 discard 语义:window 未成功完成、classifier 判定 unsupported、或测试主动丢弃 surface 时,scratch 内容不会影响 stable side table。 -- 将 `finalizeWindow=true` 的 window 内含义固定为“bounded retry 的最后一次补全尝试,稳定结果写入 scratch”,不得在 surface 层写穿 stable。 -- 在 surface 层或 patch merge 层复用同一套 conflict / idempotent / compiler-only type guard,避免 scratch shadow stable 后把冲突延迟成静默覆盖。 - -当前状态(2026-07-05): - -- [x] B1 新增 `FrontendWindowPublicationSurface`,封装 `symbolBindings()`、`resolvedMembers()`、`resolvedCalls()`、`expressionTypes()`、`slotTypes()` 的 window-local scratch side table。 -- [x] B2 新增 `FrontendWindowAnalysisContext`,统一携带 stable `FrontendAnalysisData` 与 window-local publication surface。 -- [x] B3 为上述五张 side table 提供 scratch-over-stable effective read API,window 内读取统一先查 scratch、再查 stable。 -- [x] B4 为上述五张 side table 提供 scratch-only write API;所有 write 都固定只落到 scratch,不直接修改 stable side table。 -- [x] B5 提供 `toPatch(...)` / `drainPatch(...)`,并保证 patch 只包含当前 scratch facts,不复制 stable fallback entries。 -- [x] B6 提供 discard 语义;discard 后 scratch facts 与 pending local slot updates 都会被直接丢弃,不影响 stable publication surface。 -- [x] B7 固定 window 内 `finalizeWindow=true` 的基础设施语义为“最终稳定结果仍只写入 scratch,commit 前 stable 不可见”。 -- [x] B8 在 surface 写入路径与 patch merge 路径复用同一套冲突 / idempotent / compiler-only type guard,并为 local slot update 增加 pre-commit 隔离收集入口。 +移动 / 兼容资产: -验收细则: +- `FrontendAnalysisPatch` 迁入 patch package;若保留,它只能是 legacy single-stage patch / 测试兼容层,不能作为 suite export 生产路径的 multi-owner carrier。 +- `FrontendLocalSlotTypeUpdate` 迁入 patch package,并只由 `FrontendLocalTypeStabilizationPatch` 携带。 +- `FrontendWindowPublicationSurface` 与 `FrontendWindowAnalysisContext` 若迁入 patch package,也只能作为 legacy comparison shim。它们不再是 overlay/export 参考实现;若最终删除这两个类型,必须把仍然有效的 surface API tests 拆到 overlay 与 per-owner patch transaction 测试,并新增 VarTypePost window contamination regression test 记录旧路径问题。 +- `FrontendSemanticAnalyzer.withSegmentedSemanticRunnerForTesting()`,可改造为新 pipeline 等价测试入口。 + +重写参考资产: -- 新增测试覆盖 effective read 顺序:同一 identity key 同时存在 stable 与 scratch 时,读取返回 scratch 值;scratch 缺失时回落 stable。 -- 新增测试覆盖 scratch-only write:写入 `expressionTypes()` / `resolvedCalls()` / `symbolBindings()` 等 scratch 表后,`FrontendAnalysisData` stable side table 在 `applyPatch(...)` 前保持不变。 -- 新增测试覆盖 `toPatch(...)` 只包含 scratch entries,不把 stable fallback entries 误复制进 patch。 -- 新增测试覆盖 window surface 不把 expr-phase `backfillInferredLocalType(...)` 观察到的 initializer type 转换为 `FrontendLocalSlotTypeUpdate`;slot update collector 只接受 local stabilization owner。 -- 新增测试覆盖 discard:丢弃 surface 后 stable side table、diagnostics snapshot、scope slot 均不被修改。 -- 新增测试覆盖 same-key idempotent:scratch 写入与 stable 相同 value 可通过,最终 `applyPatch(...)` 为 no-op 或 idempotent merge。 -- 新增测试覆盖 same-key conflict:scratch 写入或 patch merge 不允许把 stable `RESOLVED(int)` shadow 成 `RESOLVED(float)`,也不允许 terminal negative status shadow 成 success。 -- 新增测试覆盖 `finalizeWindow=true`:retry 产出的稳定 `ExprType` 在同一 surface 内可立即读到,但 stable `analysisData.expressionTypes()` 在 commit 前仍读不到。 -- 新增测试覆盖 attribute step key:`AttributePropertyStep` / `AttributeCallStep` / `AttributeSubscriptStep` 作为 key 时仍保持 identity lookup、scratch 优先、duplicate guard 生效。 -- 新增测试覆盖 compiler-only guard:`GdCompilerType` 不能写入 source-facing scratch `expressionTypes()` / `slotTypes()`,即使还未进入 `applyPatch(...)`。 -- 新增测试覆盖 local slot update isolation:surface 收集的 `FrontendLocalSlotTypeUpdate` 在 commit 前不调用 `BlockScope.resetLocalType(...)`,commit 后才按 4.6 规则应用并刷新 binding payload。 +- `FrontendTopBindingAnalyzer.AstWalkerTopBindingBinder`、`FrontendLocalTypeStabilizationAnalyzer.AstWalkerLocalTypeStabilizer`、`FrontendChainBindingAnalyzer.AstWalkerChainBinder`、`FrontendExprTypeAnalyzer.AstWalkerExprTypePublisher`、`FrontendVarTypePostAnalyzer.SlotTypePublisher` 等内部 walker 只能作为语义规则参考。它们的 whole-module `walk(sourceFile)` 入口、隐式字段状态与整表发布策略不得作为 production SuiteResolver procedure 复用。 +- 各 analyzer 的 `analyzeInWindow(...)` 方法名称具有误导性:它只改变 publication surface,不改变 traversal root。它可以保留为 legacy / comparison test path,但不能被归类为可迁移 runner。 +- `FrontendVarTypePostAnalyzer.analyzeInWindow(...)` 更不能作为参考:它直接清空并写入 stable `slotTypes()`,再事后复制到 window scratch。新 var type post procedure 必须从这个实现重新写起,而不是修饰调用路径。 +- `FrontendChainReductionFacade` 与 `FrontendExpressionSemanticSupport` 可保留算法价值,但它们当前通过 analyzer-local 回调读取 dependency type;SuiteResolver 下必须改为从 `FrontendTypedLexicalEnvironment` 与 owner-local memo 读取。 + +可回退或废弃资产: + +- `FrontendSegmentedSemanticScheduler` 当前实现。它是阶段 D 的行为等价过渡调度器,仍包含 legacy local stabilization direct phase,不是新计划的主体。 +- `FrontendSemanticAnalyzer.segmentedSemanticRunner` 生产路径开关。新计划最终应只有默认新 pipeline,legacy whole-phase 只作为迁移测试旁路存在。 +- 把 `analyzeInWindow(...)` 作为 production body procedure 的任何调用路径。 + +## 5. 分步骤实施 -### 阶段 C:抽出 window-capable analyzer API +### 阶段 A:资产盘点与回退边界冻结 实施内容: -- 为 top binding、chain binding、expr typing、var type post 提取 window-level runner。 -- window-level runner 接收 window analysis context,不能直接向 stable side table 发布 facts。 -- 现有 whole-module `analyze(...)` 先改成构造一个覆盖全 module 的 window 列表,再调用 window runner,最后用 `updateXxx(...)` 发布 whole-table snapshot。 -- local type stabilization 提取 window-level runner,返回 `FrontendLocalSlotTypeUpdate`,由 caller 统一应用。 -- 将 `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 改造成 4.6 定义的 guard-only 检查,移除它对 `BlockScope.resetLocalType(...)` 与 `refreshPublishedLocalValues(...)` 的生产路径依赖。 -- 改造 `FrontendChainReductionHelper` / `FrontendChainReductionFacade` 的 expression type 查找入口,使其通过 window effective view 或 window-aware resolver 读取 `expressionTypes()`。 -- 保持现有 analyzer class 名称和 public `analyze(...)` 方法,避免一次性改动所有调用点。 +- 明确阶段 A-D 已实施代码的处理方式:保留、移动、重写参考、废弃或回退。 +- 将 `FrontendSegmentedSemanticScheduler` 当前实现标记为过渡实现,不再作为新路线的主体。 +- 保留 `FrontendAnalysisPatch`、`FrontendLocalSlotTypeUpdate`、`FrontendAnalysisData.applyPatch(...)` 的测试价值。`FrontendWindowPublicationSurface` 只保留 API-level / legacy comparison 测试价值,不能作为 overlay 隔离参考;patch 相关类型是否迁入 `gd.script.gdcc.frontend.sema.patch` 包需与其 legacy shim 定位一致。 +- 明确 per-owner patch 类型与 `FrontendPatchTransaction` 命名方案,旧 `FrontendAnalysisPatch` 不再作为 suite export 生产路径的 multi-owner carrier。 +- 明确 `FrontendSemanticAnalyzer.segmentedSemanticRunner` 生产路径开关最终要移除。 +- 明确 `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` guard-only 合同不可回退。 +- 为五个 owner analyzer 建立 walker-state inventory:列出当前内部 visitor 的 traversal root、隐式字段状态、读取的 stable side table、写入的 side table、diagnostic emission 与可抽取 helper。该 inventory 是重写输入,不是迁移完成标志。 +- 为 compiler-only guard 建立 payload matrix:逐项记录 `expressionTypes()`、`slotTypes()`、`localSlotTypeUpdates()`、`symbolBindings()`、`resolvedMembers()`、`resolvedCalls()` 当前由谁检查、谁未检查、哪些 API 仍可直接绕过 guard。该 matrix 必须与阶段 C shared walker 设计一起冻结。 -当前状态(2026-07-05): +当前状态(2026-07-06): -- [x] C1 为 `FrontendTopBindingAnalyzer`、`FrontendChainBindingAnalyzer`、`FrontendExprTypeAnalyzer`、`FrontendVarTypePostAnalyzer` 提取 window-level runner,runner 将 stage 产物写入 `FrontendWindowAnalysisContext` scratch surface,不直接发布到 stable side table。 -- [x] C2 保持现有 public `analyze(...)` 方法签名;whole-module wrapper 先清空本阶段拥有的 snapshot 表,再通过 window runner 生成 patch,以保留 legacy stale-clear 语义。 -- [x] C3 为 `FrontendLocalTypeStabilizationAnalyzer` 提取 window-level runner,并让 window 路径只收集 `FrontendLocalSlotTypeUpdate`;实际 `BlockScope` mutation 与 binding payload refresh 仍由 patch commit 统一执行。 -- [x] C4 将 `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 收紧为 guard-only:不再调用 `BlockScope.resetLocalType(...)`,不再刷新 `symbolBindings()` payload,也不产生 local slot update。 -- [x] C5 将 expr typing 的 `expressionTypes()` 与 bare-call `resolvedCalls()` 发布改为先写 window scratch;nested expression dependency 继续通过当前 window 的 scratch table 保留读己写能力。 -- [x] C6 增加 focused tests 锚定 guard-only backfill、expr window scratch-only publication、local stabilization window commit 前隔离与 commit 后 slot/binding refresh。 +- [ ] A1 在文档中完成资产分类。 +- [ ] A2 在代码中将 `FrontendSegmentedSemanticScheduler` 标记为 deprecated 或 legacy comparison entry。 +- [ ] A3 保留 `FrontendAnalysisDataTest`,并将 `FrontendWindowPublicationSurfaceTest` 降级为 API-level / legacy contamination regression 测试,不再作为新 overlay 正确性的参考测试。 +- [ ] A4 移除或隔离 `segmentedSemanticRunner` 对生产路径的影响。 +- [ ] A5 确定 patch package 迁移计划、per-owner patch 命名与 transaction apply 顺序。 +- [ ] A6 完成 whole-module walker state inventory,并标记 `analyzeInWindow(...)` 只允许作为 legacy comparison path。 +- [ ] A7 记录 `FrontendVarTypePostAnalyzer.analyzeInWindow(...)` 的 stable `slotTypes()` 污染路径,并决定修复、隔离或删除该 legacy path。 +- [ ] A8 完成 compiler-only guard payload matrix,并明确 `updateXxx(...)` / direct stable side-table 引用哪些是 legacy-only,哪些必须在后续阶段接入 shared walker。 验收细则: -- `FrontendSemanticAnalyzerFrameworkTest.analyzePublishesPhaseBoundariesThroughVirtualOverridePhaseAndRefreshesDiagnosticsAfterEachPhase` 继续通过,证明 public phase boundary 未漂移。 -- top binding / chain binding / expr typing / var type post 的 focused tests 输出不变;例外是历史 backfill mutation 测试必须改为 guard-only 语义,不能继续期待 expr phase 改写 slot。 -- local type stabilization 的 probe 测试继续证明 probe 不写 shared side tables、不发 final diagnostics。 -- expr typing 的 nested chain / argument retry 场景继续保留读己写能力,但读写发生在 window scratch 内。 -- expr typing 的 local `:=` backfill 场景只做 guard:inventory-seeded `Variant` 保持不变,已稳定同类型 no-op,已稳定异类型 fail-fast,且不会刷新 `symbolBindings()` payload。 -- window runner 在单 window 与 whole-module wrapper 下产出的 side-table 内容一致。 +- 已实施 A-D 资产在文档中均有归类。 +- 所有 patch 相关类型都被归类为保留、移动、新增或可删除资产。 +- 不执行 destructive git 回退也能清晰说明哪些代码可删除,哪些代码需保留。 +- 现有 patch/backfill guard focused tests 继续通过;surface tests 不再被解释为“所有 analyzer window path 都 scratch-safe”。 +- 没有任何阶段把 `analyzeInWindow(...)` 声明为 production SuiteResolver procedure。 +- 文档明确 `FrontendWindowPublicationSurface` 的 API scratch contract 与 VarTypePost legacy 调用行为不一致。 +- 文档明确 shared walker 是 overlay write、patch merge 与保留 whole-table publish 的共同 guard 基线,不存在“先写 overlay / stable,export 时再补查”的宽松解释。 -### 阶段 D:引入 segment scheduler,但先保持行为等价 +### 阶段 B:建立 Interface phase 数据结构 实施内容: -- 新增 `FrontendSegmentedSemanticScheduler`,第一版只生成现有支持面的 statement windows。 -- scheduler 运行时仍不解封任何 typed-dependent gate。 -- 对每个 window 依次运行 top binding、local type stabilization、chain binding、expr typing、var type post,并用 `applyPatch(...)` 合并。 -- 每个 window 创建独立 publication surface;只有 window 成功完成后才把 surface 转为 patch 并合并。 -- `FrontendSemanticAnalyzer` 增加内部开关或 package-private 构造路径,用于测试 segmented runner 与 legacy whole-phase runner 的等价性。 -- 默认生产路径可以在阶段 D 末切换到 segmented runner,但必须先完成等价测试。 +- 新增 `FrontendInterfacePhase` 或等价 coordinator。 +- 新增 `FrontendBodyDeclarationIndex`,按 callable/block 记录完整 ordinary local declaration 与 source order。 +- 新增 `FrontendInventoryGateRegistry`,记录 typed-dependent subtree 与 body readiness。 +- 新增 `FrontendTypedLexicalBaseline`,记录 interface 层可确定的 source-facing typed baseline。 +- 新增 `FrontendSuitePlan`,列出 body layer 可进入的 callable/property initializer/supported block。 +- 保持 skeleton/scope/variable analyzer 的 public contract 不变。 -当前状态(2026-07-06): +验收细则: + +- `FrontendSemanticAnalyzerFrameworkTest` 继续证明 skeleton/scope/variable phase boundary 未漂移。 +- `FrontendVariableAnalyzerTest` 继续证明 supported block 的完整 local inventory 先发布。 +- `var x := y; var y := 1` 仍能通过 resolver 看到 future declaration 并过滤为 `DECLARATION_AFTER_USE_SITE`。 +- `for` / `match` / lambda / block-local `const` 仍默认 deferred / unsupported。 + +### 阶段 C:引入 `TypedLexicalEnvironment` overlay + +实施内容: -- [x] D1 新增 `FrontendSegmentedSemanticScheduler`,接入阶段 A-C 已有 window publication / patch merge 基础设施。 -- [x] D2 为 `FrontendSemanticAnalyzer` 增加测试用 segmented runner 内部入口,默认生产路径仍保持 legacy whole-phase runner。 -- [x] D3 scheduler 对 top binding、chain binding、expr typing、var type post 使用独立 window surface,并在每个 owner stage 完成后通过 `applyPatch(...)` 增量提交和刷新 diagnostics snapshot。 -- [x] D4 scheduler 的 local type stabilization 暂时沿用 legacy direct phase,以保持现有源码顺序 `:=` alias chain 行为等价;原因是当前 window runner 会把 slot update 延迟到 patch commit,若整模块一次性运行会让后续 local initializer 读不到前序 local 的稳定 slot 类型。 -- [x] D5 新增 legacy runner 与 segmented runner 的 side-table / diagnostics 等价测试,并覆盖 unsupported `for` / `match` / block-local `const` 行为不变。 -- [ ] D6 真正 root-bounded 的 statement window 执行仍需后续细化:需要让五个 window-capable analyzer 按 `FrontendSemanticWindow.roots()` 限定遍历,同时保持 callable/property initializer 上下文和 local slot update 对同 window 后续 stage 的可见性。该项完成前不切换默认生产路径。 +- 新增 `FrontendTypedLexicalEnvironment`,包装 `Scope`、suite-local typed facts、pending local slot updates。 +- 为 visible value resolver、expression semantic support、chain reduction facade 提供 effective local type 读取入口;现有 stable-data / analyzer-local callback 读取路径必须逐步替换为 explicit environment lookup。 +- Overlay 写入必须带 owner metadata。 +- Overlay export 必须先按 owner 拆成 per-owner patch,再复用 `FrontendAnalysisData.applyPatch(...)` 的冲突检测;compiler-only guard 必须先扩展为第 4.6 节的全 type-bearing fact guard 后才能复用。 +- 为 overlay scratch 写入补同等 compiler-only guard,覆盖 `symbolBindings()`、`resolvedMembers()`、`resolvedCalls()`、`expressionTypes()`、`slotTypes()` 与 `localSlotTypeUpdates()`。 +- Overlay write / flush / export 必须复用同一个 shared walker;测试要能证明 pending write fail-fast 与 export-time fail-fast 的覆盖面完全一致,而不是两套各自演化的 guard 名单。 +- 不以 `FrontendWindowPublicationSurface` 作为 overlay 实现参考。它的 API 形状可作为反例/legacy comparison,但 Stage C overlay 必须独立实现并独立证明:写入 pending / committed overlay 时 stable side table 与 `BlockScope` 保持不变。 验收细则: -- 对同一输入,legacy whole-phase runner 与 segmented runner 的 `symbolBindings()`、`resolvedMembers()`、`resolvedCalls()`、`expressionTypes()`、`slotTypes()` 等价。 -- 等价基线以阶段 C 后的 guard-only backfill 合同为准,不以旧的 expr-phase slot mutation 作为兼容目标。 -- 验证同一 statement window 内 `ExprType` 可读到自己刚发布的 scratch fact,且下一个 window 只能读到已经 merge 的 stable fact。 -- diagnostics category、range、顺序保持等价,或文档明确接受的 phase-boundary probe 差异已有新测试锚定。 -- `FrontendVisibleValueResolver` 的 declaration-after-use 与 initializer self-reference 测试继续通过。 -- `for`、`match`、lambda、block-local `const` 的 existing deferred / unsupported 行为不变。 +- 当前 statement pending local slot update 对同一 statement 后续 semantic step 可见。 +- 前一 statement committed typed fact 对后一 statement 可见。 +- Overlay 不修改 stable side table,直到 export / apply patch。 +- Overlay isolation tests 不能复用 VarTypePost window path 作为等价 oracle;必须直接断言 stable `symbolBindings()`、`resolvedMembers()`、`resolvedCalls()`、`expressionTypes()`、`slotTypes()` 与 local slot backing scope 在 overlay 生命周期内不变。 +- Overlay export 不允许跨 owner 混合在同一 patch 中;suite 收敛后必须按固定顺序 apply per-owner patches。 +- Overlay 不允许 exact A -> exact B。 +- Overlay 不允许 source-facing `GdCompilerType`,测试必须覆盖 binding/member/call/expression/slot/update 六类写入面。 +- 若保留 `updateXxx(...)` whole-table publish 参与 legacy flow,必须额外证明这些入口对六类 source-facing typed payload 使用与 overlay/export 相同的 walker;否则它们只能被标记为 non-production compatibility API。 -### 阶段 E:baseline inventory 与 pending gate 分离 +### 阶段 D:实现 body `SuiteResolver` 骨架 实施内容: -- 调整 `FrontendVariableAnalyzer`,把“发布普通 local inventory”和“报告/记录 feature boundary”拆开。 -- 对现有无条件支持的 block,仍发布完整 local inventory。 -- 对 typed-dependent subtree,记录 `FrontendInventoryGate(PENDING, NOT_PUBLISHED)`,但不发布 body inventory。 -- 对明确不在本计划转正范围内的 subtree,继续按现有 owner 发 diagnostic。 -- 在 `FrontendAnalysisData` 中加入 gate side table 或专用 registry;若 gate 不需要长期暴露给 lowering,可先保持 package-private data structure,但必须可被 resolver / scheduler 查询。 -- gate registry 必须按 gate owner / body root identity 提供 body readiness update 和 lookup API,作为 4.4.1 定义的单一真源。 -- `FrontendVariableAnalyzer`、`FrontendLocalTypeStabilizationAnalyzer`、`FrontendVarTypePostAnalyzer`、`FrontendCompileCheckAnalyzer` 的 callable-local inventory 判断必须迁移到共享 readiness 查询;纯 `BlockScopeKind` 查询只能处理无条件支持的 block kind。 -- 本阶段可以先建立 gate registry / readiness 查询,但不得假设阶段 D6 已完成。任何依赖 statement window 顺序提交或 child body window 递归的行为,只能在 D6 完成后接入 scheduler。 +- 新增 `FrontendSuiteResolver`。 +- 新增 `FrontendSuiteContext`,携带 source path、callable owner、current block scope、restriction、static context、property initializer context、gate registry、typed lexical environment。 +- 新增 `FrontendStatementResolver` 或等价 statement dispatcher。 +- 新增 owner procedure registry / dispatch contract,但第一版只接线 no-op 或 fail-closed hook,不复用 whole-module `analyzeInWindow(...)`。 +- 第一版只遍历当前已支持 body 结构:ordinary statements、`if` / `elif` / `else`、`while`、property initializer。 +- `for` / `match` / lambda / block-local `const` 继续 fail-closed。 +- 暂不删除 legacy whole-phase analyzer wrappers。 + +阶段 D 是新 analyzer 子框架的骨架阶段,不是把现有 analyzer 包一层调度器。它必须建立 root-bounded statement traversal:外层 SuiteResolver 决定进入哪个 statement / header / child suite,owner procedure 只能处理传入 root 及其允许的子表达式,不能重新从 `SourceFile` root walk。Godot 的 `resolve_suite()` / `resolve_node()` 只作为 dispatch 形状参考;GDCC 仍必须保留自己的完整 inventory、filtered-hit resolver 与 side-table publication contracts。 验收细则: -- 普通 block 中未来声明仍在 scope 中可被 resolver 看到,并按 `DECLARATION_AFTER_USE_SITE` 过滤。 -- pending gate body 内 lookup 仍返回 `DEFERRED_UNSUPPORTED`,不能 fallback 到外层同名 local。 -- `SUPPORTED + NOT_PUBLISHED` 与 `SUPPORTED + PUBLISHING` 的 gate body lookup 仍返回 `DEFERRED_UNSUPPORTED`,不能 fallback 到外层同名 local。 -- 合成 `FOR_BODY` scope 或缺失 owning gate 的 body readiness 查询返回 false,即使 `scopesByAst()` 中已有 scope 记录。 -- 旧的 unsupported `for` / `match` / lambda tests 继续通过,除非某个 gate 在后续阶段显式转正。 -- duplicate / shadowing diagnostics owner 不变。 +- `SuiteResolver` 只进入 `FrontendSuitePlan` 标记为可进入的 body。 +- source-order traversal 与 AST statement order 一致。 +- child block 递归顺序为 header 先解析,body 后解析。 +- `FrontendVisibleValueResolver` 的 declaration-after-use 与 self-reference 测试继续通过。 +- Body-aware resolver 调用必须传入当前 `FrontendSuiteContext` 的 `TypedLexicalEnvironment`。 +- Statement flush 前 stable side table 与 `BlockScope` 不变;flush 后仅 current-suite committed overlay 可见。 +- Suite 收敛后,stable side table 只能通过按序 apply per-owner patch transaction 更新。 +- Production SuiteResolver path 不调用任何 analyzer 的 `analyzeInWindow(...)`,也不调用内部 `AstWalker...walk(sourceFile)`。 -### 阶段 F:source-order typed fact 解锁 +### 阶段 E:重写 owner 子过程到 suite/body 上下文 实施内容: -- scheduler 在每个 block 内按源码顺序提交 statement patches。 -- 本阶段必须与阶段 D6 联动完成:`FrontendSemanticWindow.roots()` 必须真正限制 analyzer 遍历范围,否则“当前 statement 提交后供后续 statement 消费”的合同没有实现基础。 -- 当前 statement 的 expr facts 提交后,后续 statement classifier 可以读取这些 facts。 -- 对 `var limit := 3; ` 建立测试用 synthetic gate 或选用 `for` range classifier 作为第一个真实 consumer。 -- local `:=` stabilization 必须在同一 statement window 内早于后续 statement 的 binding / classifier 消费。 -- child block 的完整 inventory 必须在 child body 第一个 semantic window 前发布,且共享 readiness 查询必须已经返回 true。 -- local stabilization 不得继续依赖整模块 legacy direct phase 来表达 source-order 行为;D6 必须提供 per-window slot update 可见性,使同一 statement window 内后续 stage 能消费已稳定的 local slot,下一 statement 只能消费已 commit 的 stable facts。 +- 为 top binding、local stabilization、chain binding、expr typing、var type post 重写 statement-local owner procedure。 +- Owner procedure 接收 `FrontendSuiteContext`、当前 statement/header/expression root 与 `FrontendTypedLexicalEnvironment`,不再依赖 whole-module AST traversal 建立隐式上下文。 +- `FrontendStatementResolver` 必须按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 的顺序调用 owner procedure。 +- 保留现有 analyzer class 名称和 owner 边界。 +- `FrontendLocalTypeStabilizationAnalyzer` 不再通过整模块 legacy direct phase 表达 source-order 行为。 +- `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 继续 guard-only。 +- Chain / argument retry 的中间 expression facts 必须保存在 `FrontendOwnerRetryMemo` 或等价 owner-local memo,不得写入 `expressionTypes()` overlay 后再以 narrowing / status upgrade 方式覆盖。 + +每个 owner 的重写范围必须显式记录: + +- Top binding:把 `AstWalkerTopBindingBinder` 的 use-site binding 规则拆为 statement-local binding procedure,并把 restriction、static context、property initializer context 由 `FrontendSuiteContext` 显式传入。 +- Local stabilization:把 `AstWalkerLocalTypeStabilizer` 的 eligible `:=` initializer 解析改为立即写 pending overlay,使同 statement 后续 owner 与后续 statement 可按 flush 规则读取;禁止继续依赖整模块 direct phase 更新 `BlockScope`。 +- Chain binding:把 chain reduction 的 dependency type 回调改为读取 `FrontendTypedLexicalEnvironment` 与 owner-local memo,而不是 analyzer-local stable side table snapshot。 +- Expr typing:把 expression fact 发布改为 statement-local final fact publication;父索引、duplicate-report state、retry state 都必须显式化,不能藏在 whole-module walker 字段里。 +- Var type post:把 slot type publication 改为消费当前 statement / current-suite typed facts 的 statement-local publication,不再通过整表 `updateSlotTypes(...)` 表达 body 结果,也不得复用旧 `analyzeInWindow(...)` 的“stable `slotTypes()` clear/write 后再复制到 window”模式。 +- Resolver request:request-domain gate、AST boundary gate 与 current-scope gate 的创建必须由 `FrontendSuiteContext` 统一完成,不能继续由各 analyzer 手写 deferred domain。 验收细则: -- `var limit := 3; var next := limit` 在 segmented runner 下仍稳定为 `int`。 -- `var x := y; var y := 1` 仍不把 `y` 解析成可见 local;filtered hit reason 为 `DECLARATION_AFTER_USE_SITE`。 -- `var x := x` 仍记录 `SELF_REFERENCE_IN_INITIALIZER`。 -- 一个 statement 的 published `expressionTypes()` 能被后续 gate classifier 读取。 -- 不允许同一 expression key 被后续 window 重新发布成不同状态或类型。 +- `var a := typed_value; var b := a; var c := b` 在 body resolver 下稳定为同一 exact type。 +- child block 可读取 parent 前缀 stable local。 +- 对 `var b := a`,`b` 的 local stabilization 必须读取前一 statement 已 committed 的 `a` exact slot fact。 +- 对 `var x := receiver.member`,chain binding 必须在 local stabilization 子过程之后运行,并消费已稳定的 `receiver` slot fact;不能直接读取 interface baseline `Variant`。 +- rejected shadow declaration 不污染 parent slot。 +- nested chain / argument retry 保持读己写能力,但这个能力只存在于 owner-local memo;同一 expression / step key 在 statement flush 和 suite export 中只产生一条最终 `expressionTypes()` fact。 +- retry 过程中出现的任何非最终 expression fact(含 `DEFERRED` 代理类型、暂定 `Variant`、中间 status / detailReason)不得先发布到 pending overlay、committed overlay 或 stable side table 再被最终 fact 覆盖。 +- owner 以外的 procedure 不能写对应 side table 或 slot update。 +- 每个 owner 子过程的 suite export 以独立 per-owner patch 出现在 transaction 中;transaction coordinator 按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 顺序 apply。 +- Var type post procedure 在 statement flush 前不得改变 stable `slotTypes()`;targeted test 必须在 procedure 运行、flush、suite export 三个点分别断言 stable table 只在 export/apply patch 后变化。 -### 阶段 G:接入第一个真实 typed-dependent feature gate +### 阶段 F:接入 generic typed-dependent gate readiness -建议以 `frontend_for_range_loop_implementation_plan.md` 中的 int shorthand `for` 作为验收用例,但只接语义解封,不在本阶段强制完成 lowering。 +本阶段只验收 gate registry、readiness 查询与 fail-closed 生命周期,不使用 for-range 作为验收目标。`frontend_for_range_loop_implementation_plan.md` 是该基础设施的后续真实消费者,不是本阶段的前置或完成条件。 实施内容: -- 将 `ForStatement` 注册为 typed-dependent inventory gate。 -- 本阶段以前必须完成 D6;真实 `ForStatement` gate classifier 依赖前序 statement 已提交的 `expressionTypes()`,不能建立在 whole-module window 或 legacy local stabilization 旁路之上。 -- `range(...)` call 仍可由 AST shape 早期识别;`INT_SHORTHAND` 必须等 iterable expression typed fact 就绪后再判定。 -- `var limit := 3; for i in limit:` 中,scheduler 先完成 `limit` statement window,再分类 `for` gate。 -- supported gate 先推进为 `SUPPORTED + NOT_PUBLISHED`,再由 body inventory publication window 发布 iterator binding 和 body 完整 local inventory,最后原子推进为 `PUBLISHED`。 -- unsupported gate 继续保留 `FOR_SUBTREE` deferred / unsupported 行为。 +- 建立 typed-dependent inventory gate 的注册、lookup、status update 与 readiness update API。 +- 使用合成 fixture 或最小测试 gate 验证 `PENDING + NOT_PUBLISHED`、`SUPPORTED + NOT_PUBLISHED`、`PUBLISHING`、`SUPPORTED + PUBLISHED`、`UNSUPPORTED` 的转换。 +- 支持由前缀 statement typed fact 驱动的测试 classifier,但 classifier 只服务于 gate lifecycle,不实现任何 for-range 规则。 +- `SUPPORTED` 只表示 header/classifier 通过,不能使 body resolver / binder 放行。 +- body inventory publication 成功后才原子推进为 `PUBLISHED`。 +- 建立 resolver gate readiness policy,并接入 request-domain gate、AST boundary gate、current-scope gate 三处判断。 +- 统一 resolver request 创建路径:gate header / gate body lookup 都必须由 `SuiteResolver` / `FrontendSuiteContext` 构造,不能由 analyzer 直接决定 deferred domain。 +- unsupported gate 继续保留对应 deferred / unsupported boundary。 +- 明确非目标:不实现 `range(...)` AST shape 识别、不实现 `INT_SHORTHAND`、不发布 iterator binding、不解封 supported for-range body、不调整 for-range compile gate。 验收细则: -- `for i in range(3):` 的 body lookup 可以看到 `i : int`。 -- classifier 已返回 supported 但 body readiness 仍为 `NOT_PUBLISHED` / `PUBLISHING` 时,body lookup 仍必须是 `DEFERRED_UNSUPPORTED + FOR_SUBTREE`。 -- `var limit := 3; for i in limit:` 在 `limit` 已稳定为 `int` 后可被 classifier 判定为 int shorthand。 -- `for i in limit: var local := i` 中 body local inventory 在 body 分段前完整发布。 -- `for i in limit: var local := i` 只有在 body readiness 为 `PUBLISHED` 后,resolver、local stabilization、var type post、compile check 才能把 `FOR_BODY` 当作 ready inventory domain。 -- `for i in values:` 仍不会让 body 内 bare identifier fallback 到外层并制造误导 binding。 -- `GdccForRangeIterType` 不出现在 `expressionTypes()` 或 source-facing `slotTypes()`。 +- 合成 gate classifier 可读取前缀 `:=` local 的 typed fact,并只推进该合成 gate 的 lifecycle。 +- classifier 已返回 supported 但 body readiness 仍为 `NOT_PUBLISHED` / `PUBLISHING` 时,body lookup 仍必须是 `DEFERRED_UNSUPPORTED` 或对应 deferred domain。 +- Classifier 只能在 header statement 的 top binding、local stabilization、chain binding 与 expr typing 子过程完成后读取 overlay。 +- Classifier 可读取前序 statement committed typed fact,但不能读取后续 statement 或未运行子过程的 fact。 +- Classifier 只能读取 expr typing 已最终发布到 overlay 的 expression facts,不能读取 retry memo 中未导出的中间 expression type。 +- body inventory publication 成功后,readiness 原子推进为 `PUBLISHED`,resolver 才允许进入该合成 body。 +- 合成 gate 的 body use-site 在 `PUBLISHED` 后必须同时通过 request-domain gate、AST boundary gate 与 current-scope gate;任一 gate 仍按旧常量逻辑封口都应有测试失败。 +- 合成 gate 的 header use-site 可在 header/classifier 上下文读取前缀 overlay fact,但 header 放行不能让 body lookup 提前通过。 +- `UNSUPPORTED` gate body lookup 不能 fallback 到外层并制造误导 binding。 +- 缺失 owning gate 的合成 body 即使已有 scope,也必须返回 readiness false。 +- source-facing facts 仍拒绝所有 feature-specific `GdCompilerType`。 -### 阶段 H:收敛 diagnostics 与 compile gate +### 阶段 G:收敛 diagnostics 与 compile gate 实施内容: -- 确定 segmented runner 的 diagnostics snapshot 发布点:每个 stage patch 应用后刷新 `analysisData.updateDiagnostics(...)`,保证后续 stage 看到稳定 upstream diagnostics。 -- 若 D6 将 analyzer 从 whole-module window 切到 root-bounded statement window,本阶段必须重新锚定 diagnostics snapshot 的刷新点与顺序,特别是同一源码位置跨 window 的 duplicate suppression。 -- compile gate 继续只在 shared segmented pipeline 完成后运行。 -- 检查 compile gate 的 duplicate suppression 是否仍能识别跨 segment upstream diagnostics。 +- 确定 interface phase、body suite statement、suite export、diagnostics-only phase 的 diagnostics snapshot 刷新点。 +- 重新定义 diagnostics 可见性:body statement 解析期间的诊断写入仍进入 `DiagnosticManager`,但 duplicate suppression 必须能区分 statement-local upstream、current-suite upstream 与稳定 phase upstream;不能继续假设每个 whole-module analyzer 结束后才刷新一次 snapshot。 +- 保持 compile gate 只在 `analyzeForCompile(...)` 运行。 +- 检查 compile gate 的 duplicate suppression 是否仍能识别 interface/body upstream diagnostics。 - 对缺失 `slotTypes()`、`DEFERRED`、`FAILED`、`UNSUPPORTED` 的 final facts 保持现有 compile blocking 规则。 验收细则: - `FrontendCompileCheckAnalyzerTest` 中已有 compile gate 去重测试继续通过。 -- upstream diagnostic 已存在时,下游 segment 不补同级重复错误。 -- `analyze(...)` 不运行 compile gate;`analyzeForCompile(...)` 在 segmented facts 完成后运行 compile gate。 -- segmented runner 不改变 parse / skeleton / scope diagnostics 的顺序与可见性。 +- upstream diagnostic 已存在时,下游 phase 不补同级重复错误。 +- `analyze(...)` 不运行 compile gate。 +- `analyzeForCompile(...)` 在 interface + body facts 完成后运行 compile gate。 +- parse / skeleton / scope diagnostics 的顺序与可见性不变。 +- 同一 statement 内 owner procedure 产生 upstream diagnostic 后,后续 owner procedure 不再补同 anchor / 同类别重复错误;后一 statement 仍能读取 current-suite diagnostics snapshot 进行去重。 -### 阶段 I:移除 legacy whole-phase 旁路 +### 阶段 H:切换默认 shared semantic pipeline 实施内容: -- 等阶段 D-H 的等价与新增支持测试稳定后,删除仅用于迁移的 legacy whole-phase runner 旁路。 -- 删除 legacy 旁路前必须完成 D6,并证明默认 segmented runner 不再依赖整模块 legacy local stabilization direct phase。 -- 保留 window-capable analyzer API,whole-module analyzer wrapper 只作为测试或调试入口时存在。 -- 更新相关文档,尤其是 variable analyzer、visible resolver、local type stabilization、chain/expr typing、compile check 与 for-range plan。 +- `FrontendSemanticAnalyzer.analyze(...)` 默认运行新 interface/body pipeline。 +- legacy whole-phase runner 只保留为 package-private 测试旁路。 +- 移除 `segmentedSemanticRunner` 生产路径开关。 +- 新增 legacy vs interface/body pipeline 等价测试,等价基线以 guard-only backfill 合同为准。 + +验收细则: + +- 对同一输入,legacy 与新 pipeline 的 `symbolBindings()`、`resolvedMembers()`、`resolvedCalls()`、`expressionTypes()`、`slotTypes()` 等价,或文档明确接受的 diagnostics 顺序差异有测试锚定。 +- unsupported `for` / `match` / lambda / block-local `const` 行为不变,除非对应 gate 已在本阶段显式转正。 +- `FrontendVisibleValueResolver` declaration-after-use 与 initializer self-reference 测试继续通过。 + +### 阶段 I:移除 legacy whole-phase 旁路并更新相关文档 + +实施内容: + +- 删除 legacy whole-phase body semantic 旁路。 +- 删除或重构 `FrontendSegmentedSemanticScheduler` 过渡实现。 +- 保留 patch/overlay/export 基础设施,并移除 single-stage `FrontendAnalysisPatch` 作为 suite export 生产路径的用途。 +- 更新 variable analyzer、visible resolver、local stabilization、chain/expr typing、compile check、for-range plan。 +- 更新 `doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md`,使其反映最终实现的层级职责、owner 顺序、fact 生命周期、patch/export 合同、compiler-only guard 与 diagnostics / compile gate 边界,并继续保持为目标架构摘要而非旧流水线或过渡资产说明。 验收细则: - 所有 frontend semantic focused tests 通过。 -- 针对新增 segmented pipeline 的测试覆盖 patch merge、window-local surface、backfill guard-only、source-order typed fact、pending gate、resolver filtered hit、diagnostic dedup、compile gate。 +- 新 pipeline 测试覆盖 per-owner patch merge、patch transaction 顺序、typed overlay、backfill guard-only、source-order typed fact、pending gate、resolver filtered hit、diagnostic dedup、compile gate。 +- `doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md` 已与最终代码行为和本计划完成定义同步,且未把 legacy whole-phase 或 window / scheduler 过渡资产写成目标流水线的一部分。 - `./gradlew classes --no-daemon --info --console=plain` 通过。 - 相关 targeted tests 使用 `script/run-gradle-targeted-tests.sh --tests ...` 通过。 @@ -594,61 +755,101 @@ record FrontendLocalSlotTypeUpdate( 基础设施测试: - `FrontendAnalysisDataTest`:patch merge 新 key / idempotent / conflict / stable reference。 -- `FrontendAnalysisDataTest`:compiler-only type 泄漏 guard。 -- `FrontendWindowPublicationSurfaceTest` 或等价测试:scratch-over-stable read、scratch-only write、discard、`toPatch(...)` 不复制 stable fallback。 -- `FrontendWindowPublicationSurfaceTest` 或等价测试:`finalizeWindow=true` 产物只进入 scratch,commit 前 stable 不可见。 -- `FrontendAstSideTableTest`:若新增 patch view 或 owner metadata,确认 identity key 语义不变。 - -Pipeline 等价测试: - -- `FrontendSemanticAnalyzerFrameworkTest`:legacy runner 与 segmented runner side-table 等价。 -- `FrontendSemanticAnalyzerFrameworkTest`:等价基线使用 guard-only backfill 合同,不允许旧 expr-phase slot mutation 作为兼容路径回归。 -- `FrontendSemanticAnalyzerFrameworkTest`:phase diagnostics snapshot 在 segmented stage 后仍稳定。 +- `FrontendAnalysisDataTest`:`FrontendOwnerPatch` / per-owner patch merge 时只能携带该 owner 允许的 side table 或 local slot update;跨 owner payload fail-fast。 +- `FrontendAnalysisDataTest`:`FrontendLocalTypeStabilizationPatch` 是唯一允许携带 `FrontendLocalSlotTypeUpdate` 的 patch,且不能同时携带独立 `symbolBindings()` delta。 +- `FrontendAnalysisDataTest`:`FrontendPatchTransaction` 按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 顺序 apply;乱序或重复 owner patch fail-fast。 +- `FrontendAnalysisDataTest`:`expressionTypes()` patch 对同一 stable key 的 same-status different publishedType、status change、Variant-published fact -> exact fact 仍 fail-fast,证明没有 slot-style narrowing 例外。 +- `FrontendAnalysisDataTest`:compiler-only type 泄漏 guard 覆盖 `expressionTypes()`、`slotTypes()`、`localSlotTypeUpdates()`。 +- `FrontendAnalysisDataTest`:`applyPatch` 拒绝 `symbolBindings()` 中 `resolvedValue.type()` 为 `GdCompilerType` 的 patch entry。 +- `FrontendAnalysisDataTest`:`applyPatch` 拒绝 `resolvedMembers()` 中 `receiverType` / `resultType` 为 `GdCompilerType` 的 patch entry。 +- `FrontendAnalysisDataTest`:`applyPatch` 拒绝 `resolvedCalls()` 中 `receiverType` / `returnType` / `argumentTypes` / callable boundary parameter types 含 `GdCompilerType` 的 patch entry。 +- `FrontendAnalysisDataTest`:shared walker 对 `FrontendBinding` / `FrontendResolvedMember` / `FrontendResolvedCall` / `FrontendExpressionType` / `FrontendLocalSlotTypeUpdate` 的 type-bearing field coverage 有明确 regression tests,防止未来新增字段后 guard 漏扫。 +- `FrontendAnalysisDataTest`:若 `updateSymbolBindings()` / `updateResolvedMembers()` / `updateResolvedCalls()` / `updateExpressionTypes()` / `updateSlotTypes()` 仍保留 source-facing publication 语义,它们必须拒绝 compiler-only payload;若不做该保护,则测试与文档必须把这些入口显式限定为 legacy non-production path。 +- `FrontendAstSideTableTest`:identity key 语义不变。 +- `FrontendTypedLexicalEnvironmentTest`:overlay read order、owner metadata、source-facing compiler-only guard、exact type conflict、export 前 stable 不变。 +- `FrontendTypedLexicalEnvironmentTest`:pending overlay 只对当前 statement 后续子过程可见,flush 后才进入 current-suite committed overlay。 +- `FrontendTypedLexicalEnvironmentTest`:suite export 前 stable side table 与 `BlockScope` 不变,export 后只通过 patch apply 更新。 +- `FrontendTypedLexicalEnvironmentTest`:suite export 生成 per-owner patch transaction,而不是单个 multi-owner patch。 +- `FrontendTypedLexicalEnvironmentTest`:`expressionTypes()` overlay 同 key 同值幂等,不同值 fail-fast;retry 中间 fact 不能作为可导出的 expression type fact 留存。 +- `FrontendTypedLexicalEnvironmentTest`:`FrontendOwnerRetryMemo` 中的临时 facts 不会进入 pending overlay、committed overlay 或 stable side table。 +- `FrontendTypedLexicalEnvironmentTest`:overlay 写入拒绝 `symbolBindings()`、`resolvedMembers()`、`resolvedCalls()` 中所有 type-bearing payload 的 `GdCompilerType`;不得用 `FrontendWindowPublicationSurfaceTest` 代替该覆盖。 +- `FrontendTypedLexicalEnvironmentTest`:pending write、statement flush、suite export 三个时点对 compiler-only payload 的拒绝集合一致;不能出现 pending 接受、export 才拒绝的 coverage drift。 +- `FrontendWindowPublicationSurfaceTest`:保留为 API-level / legacy shim 测试,只验证 surface 自身 direct API 的 scratch / discard / conflict 语义;不得声明所有 `analyzeInWindow(...)` caller 都满足 scratch-over-stable。 +- `FrontendVarTypePostAnalyzerTest` 或 dedicated legacy regression:直接调用旧 `analyzeInWindow(...)` 后,即使调用 `window.discard()`,stable `slotTypes()` 已被 clear/write 污染;该测试用于记录旧路径不可作为 overlay 参考,而不是把污染行为转正为新 pipeline 行为。 +- `FrontendExprTypeAnalyzerTest` 或 dedicated legacy regression:旧 whole-module flow 中 `updateExpressionTypes(...)` 先于 `applyPatch(...)` 的 whole-table replace 行为只能作为 legacy snapshot path 记录,不得被新 overlay/export 方案复用为“先写 stable 再校验”先例。 +- Patch package 迁移测试:`FrontendAnalysisPatch`、`FrontendLocalSlotTypeUpdate` 的引用改为 `gd.script.gdcc.frontend.sema.patch`,旧 package 不保留同名生产类型;`FrontendWindowPublicationSurface` / `FrontendWindowAnalysisContext` 若保留或迁入,也必须标记为 legacy shim,不得被 production overlay 引用。 +- Analyzer rewrite inventory test / inspection:五个 owner analyzer 的 production SuiteResolver path 不调用 `analyzeInWindow(...)`,不从 `SourceFile` root 调用内部 `AstWalker...walk(...)`。 + +Interface phase 测试: + +- `FrontendInterfacePhaseTest`:构建 `FrontendBodyDeclarationIndex`,完整记录 supported block local declaration source order。 +- `FrontendInterfacePhaseTest`:记录 `FrontendInventoryGate(PENDING, NOT_PUBLISHED)`,但不发布 gate body inventory。 +- `FrontendInterfacePhaseTest`:`for` / `match` / lambda / block-local `const` 未转正时仍 fail-closed。 +- `FrontendSemanticAnalyzerFrameworkTest`:skeleton / scope / variable diagnostics snapshot boundary 不漂移。 + +Suite/body pipeline 测试: + +- `FrontendSuiteResolverTest`:source-order statement traversal 与 AST order 一致。 +- `FrontendSuiteResolverTest`:fake owner procedure 只收到当前 statement/header/expression root,不能拿到 module `SourceFile` root 重新 whole-module walk。 +- `FrontendSuiteResolverTest`:单个 statement 内 owner procedure 顺序固定为 top binding -> local stabilization -> chain binding -> expr typing -> var type post。 +- `FrontendSuiteResolverTest`:chain binding 消费 receiver local 时看到 local stabilization 已写入的 exact slot fact。 +- `FrontendSuiteResolverTest`:nested chain / argument retry 可读取 owner-local transient facts,但这些 facts 不对其他 owner procedure 或 `TypedLexicalEnvironment` 普通 lookup 可见。 +- `FrontendSuiteResolverTest`:retry 后 final result 只写入同一 key 的最终 fact,不会把 earlier intermediate fact 写入 stable 或 committed overlay。 +- `FrontendSuiteResolverTest`:suite export 后 stable side table 状态等同于按固定顺序 apply 对应 per-owner patches 的结果。 +- `FrontendSuiteResolverTest`:local stabilization patch apply 后由 commit helper 派生刷新同 declaration 的 `symbolBindings()` payload,而不是通过同一个 patch 携带 binding delta。 +- `FrontendSuiteResolverTest`:`if` / `elif` / `else` / `while` header 先解析,body 后递归。 +- `FrontendSuiteResolverTest`:unsupported body 不进入 resolver。 +- `FrontendSemanticAnalyzerFrameworkTest`:legacy pipeline 与 interface/body pipeline side-table 等价。 +- `FrontendSemanticAnalyzerFrameworkTest`:等价基线使用 guard-only backfill 合同,不允许恢复旧 expr-phase slot mutation。 Resolver 测试: - 同 block future local:`var x := y; var y := 1`,必须得到 `DECLARATION_AFTER_USE_SITE` filtered hit。 - initializer self-reference:`var x := x`,必须得到 `SELF_REFERENCE_IN_INITIALIZER` filtered hit。 -- pending gate body:lookup 必须是 `DEFERRED_UNSUPPORTED`,不能 fallback。 -- `FrontendVisibleValueResolverTest`:`FOR_BODY` scope 已存在但 gate 缺失、`PENDING`、`SUPPORTED + NOT_PUBLISHED`、`SUPPORTED + PUBLISHING`、`UNSUPPORTED` 时,body lookup 都必须是 `DEFERRED_UNSUPPORTED + FOR_SUBTREE`。 -- `FrontendVisibleValueResolverTest`:同一 `FOR_BODY` 在 `SUPPORTED + PUBLISHED` 后,body 内 iterator 和 body local lookup 返回 `FOUND_ALLOWED`,并继续保留 declaration-after-use filtered hit。 +- pending gate body lookup 必须是 `DEFERRED_UNSUPPORTED`,不能 fallback。 +- typed-dependent body scope 已存在但 gate 缺失、`PENDING`、`SUPPORTED + NOT_PUBLISHED`、`SUPPORTED + PUBLISHING`、`UNSUPPORTED` 时,body lookup 都必须是 `DEFERRED_UNSUPPORTED` 或对应 deferred domain。 +- `SUPPORTED + PUBLISHED` 后,body 内 gate-owned binding 和 body local lookup 返回 `FOUND_ALLOWED`,并继续保留 declaration-after-use filtered hit。 +- Request-domain gate 测试:PUBLISHED gate body lookup 必须由统一 request factory 进入 `EXECUTABLE_BODY`,或由同一 readiness policy normalization 后进入 lookup;未发布 gate、unsupported gate、无 owning gate 的 deferred-domain request 仍在 domain gate fail-closed。 +- AST boundary gate 测试:gate body edge 在非 `PUBLISHED` 状态继续返回 deferred boundary,`SUPPORTED + PUBLISHED` 后才放行;header edge 只在 header/classifier 上下文放行,不能提前打开 body edge。 +- Current-scope gate 测试:合成 `FOR_BODY` / typed-dependent body scope 即使没有 AST boundary,也必须在非 `PUBLISHED` 状态 fail-closed;`PUBLISHED` 后才允许 normal lookup。 +- 三闸同步测试:同一 synthetic gate 的 body lookup 必须覆盖 domain、AST boundary、current-scope 三处,防止只改其中一处导致“看似转正但实际仍被另一处封口”。 Local stabilization 测试: -- source-order alias chain 在 segmented runner 下保持稳定。 +- source-order alias chain 在 interface/body pipeline 下保持稳定。 - child block 读取 parent 前缀稳定 local。 -- exact type 不允许被后续 segment 改写为另一个 exact type。 -- `FrontendExprTypeAnalyzerTest` 中旧 backfill mutation / refresh 期望必须调整为 guard-only:inventory-seeded `Variant` 不被 expr phase narrowing,`symbolBindings()` payload 不刷新。 -- `FrontendExprTypeAnalyzerTest` 继续覆盖 guard:已稳定同类型 no-op,已稳定异类型 fail-fast,compiler-only initializer fact fail-fast。 +- exact type 不允许被后续 statement / overlay export 改写为另一个 exact type。 +- 同一 statement 内,local stabilization pending slot 对后续 chain binding / expr typing 可见,但不允许 initializer self-reference 借此通过过滤。 +- assignment initializer / bare `TYPE_META` / dynamic fallback 保持 `Variant`。 +- `FrontendExprTypeAnalyzerTest` 继续覆盖 backfill guard:inventory-seeded `Variant` 不被 expr phase narrowing,已稳定同类型 no-op,已稳定异类型 fail-fast,compiler-only initializer fact fail-fast。 Typed-dependent gate 测试: -- 前缀 `:=` local 稳定后,后续 gate classifier 能读取 typed fact。 +- 前缀 `:=` local 稳定后,后续合成 gate classifier 能读取 typed fact。 - gate 转正只产生 `SUPPORTED + NOT_PUBLISHED`,不得使 body resolver / binder 放行。 -- body inventory publication window 成功提交后,readiness 原子推进为 `PUBLISHED`,未来声明仍能被 filtered hit 捕获。 -- `FrontendExecutableInventorySupport` 或等价 readiness 测试:所有 callable-local inventory 消费者对 `FOR_BODY` 使用同一 readiness 查询,不允许各自直接判断 `BlockScopeKind.FOR_BODY`。 -- gate 未转正时旧 deferred / unsupported 行为保持。 -- `FrontendVariableAnalyzerTest` / `FrontendLocalTypeStabilizationAnalyzerTest` / `FrontendVarTypePostAnalyzerTest` / `FrontendCompileCheckAnalyzerTest`:`SUPPORTED + NOT_PUBLISHED` 的 `FOR_BODY` 不发布 local、slot type 或 lowering-ready fact;`SUPPORTED + PUBLISHED` 后才发布。 +- body inventory publication 成功后,readiness 原子推进为 `PUBLISHED`。 +- 所有 callable-local inventory 消费者对 typed-dependent body 使用同一 readiness 查询,不允许各自直接判断 `BlockScopeKind`。 +- feature-specific `GdCompilerType` 不出现在 `expressionTypes()` / source-facing `slotTypes()` / ordinary `ScopeValue.type()`。 Compile gate 测试: -- segmented facts 中残留 `DEFERRED` / `FAILED` / `UNSUPPORTED` 时仍被 compile gate 阻断。 -- upstream diagnostic 去重跨 segment 生效。 +- interface/body facts 中残留 `DEFERRED` / `FAILED` / `UNSUPPORTED` 时仍被 compile gate 阻断。 +- upstream diagnostic 去重跨 interface/body phase 生效。 - `analyze(...)` 与 `analyzeForCompile(...)` split 不变。 ## 7. 风险与缓解 ### R1:side-table 冲突被静默覆盖 -缓解:window 内 partial publication 只能写入 window-local scratch,window 结束后必须通过 patch merge;默认不同 value fail-fast。需要覆盖 scratch shadow stable、idempotent 与 conflict tests。 +缓解:overlay export 必须通过 per-owner patch merge API;默认不同 value fail-fast。需要覆盖 overlay shadow stable、idempotent、conflict tests 与 patch transaction 顺序 tests。 ### R2:resolver 看不到未来声明 -缓解:分段前必须为 supported block 发布完整 local inventory。禁止 resolver 自己扫描普通 `var` 弥补缺口。 +缓解:interface phase 必须基于基础结构层已发布的 inventory 为 supported suite 建立完整 local declaration index。禁止 resolver 自己扫描普通 `var` 弥补缺口;resolver 只能读取 index 与 readiness。参见第 3.2 节:完整 inventory 是 resolver filtered-hit 模型的前提,不是为了与 Godot source-order analysis 对立。 ### R3:scope slot mutation 与 published binding payload 脱节 -缓解:local slot rewrite 通过 `FrontendLocalSlotTypeUpdate` 统一应用,并同步刷新同 declaration 的 `symbolBindings()`。 +缓解:local slot rewrite 通过 `FrontendLocalTypeStabilizationPatch` 携带的 `FrontendLocalSlotTypeUpdate` 统一应用,并由同一个 commit helper 派生刷新同 declaration 的 `symbolBindings()`。禁止 local stabilization patch 同时携带独立 `symbolBindings()` delta。 ### R4:历史 backfill 路径恢复第二个 slot mutation owner @@ -658,33 +859,84 @@ Compile gate 测试: 缓解:`bodyInventoryReadiness` 是唯一可查询事实;`SUPPORTED` 只是 classifier 结果。resolver、variable analyzer、local stabilization、var type post、compile check 都必须通过共享 readiness 查询,测试覆盖 `SUPPORTED + NOT_PUBLISHED` 与 `SUPPORTED + PUBLISHING` 继续 fail-closed。 -### R6:diagnostics 重复或顺序漂移 +### R6:`SuiteResolver` 绕过 phase owner 边界 -缓解:stage patch 应用后刷新 diagnostics snapshot;新增跨 segment duplicate suppression tests。若顺序需要调整,必须更新 framework probe tests 与文档。 +缓解:`SuiteResolver` 只编排 owner 子过程,不直接写 owner side table。每个 side table 的写入 API 应保留 owner 意图,`FrontendSuiteContext` 必须校验当前 runner identity 与目标 overlay owner 匹配,per-owner patch 类型也必须编码 semantic owner identity 并在 merge-time 校验,不能复用 `ScopeOwnerKind`,测试覆盖错误 owner 写入 fail-fast 或不可达。 -### R7:unsupported subtree 被过早打开 +### R7:diagnostics 重复或顺序漂移 + +缓解:interface phase、body statement、suite export、diagnostics-only phase 都有明确 diagnostics snapshot 边界;新增跨 phase duplicate suppression tests。若顺序需要调整,必须更新 framework probe tests 与文档。 + +### R8:unsupported subtree 被过早打开 缓解:pending gate 默认 fail-closed;只有 classifier 明确返回 supported 且 `bodyInventoryReadiness == PUBLISHED`,resolver 才能把对应 body 当普通 executable body。 -### R8:compiler-only type 泄漏 +### R9:compiler-only type 泄漏 + +缓解:typed overlay、per-owner patch merge 与 local slot update 都做 `GdCompilerType` guard;feature-specific compiler state 只能通过专用 contract 给 CFG/lowering。当前 `checkPatchDoesNotLeakCompilerOnlyTypes(...)` 只完整覆盖 expression / slot / local update 路径,必须扩展到 `symbolBindings()`、`resolvedMembers()`、`resolvedCalls()` 的 type-bearing payload 后,本风险才算关闭。`FrontendWindowPublicationSurface` 不能作为 guard 完整性的基线,因为 VarTypePost window caller 在 surface guard 生效前已经直接写入 stable `slotTypes()`。 + +补充:如果保留 `updateXxx(...)` whole-table publish 或可变 stable side-table 引用,它们也必须被纳入同一风险面。只要 production body path 还能通过 `analysisData.xxx().put()/clear()/putAll()`、`updateXxx(...)` whole-table replace 或 window caller 中转直接触达 stable typed table,就不能宣称 overlay export 安全性已经被证明。 + +### R10:已实施过渡资产继续扩大影响面 + +缓解:阶段 A 必须冻结资产分类。`FrontendSegmentedSemanticScheduler` 与 `segmentedSemanticRunner` 只能作为迁移测试旁路或删除对象,不能继续承载新功能。 + +### R11:实现一次性改动过大 + +缓解:先冻结资产边界,再落地 interface 数据结构、overlay、suite skeleton、owner 子过程、typed-dependent gate、diagnostics、默认切换。每阶段都应有 targeted tests。 + +### R12:statement 内 owner 顺序或 overlay 导出时机漂移 + +缓解:第 4.3 节的 owner procedure 顺序不可重排;第 4.4 节的 pending -> committed -> stable export 时机不可折叠。Statement flush 不写 stable side table,suite export 只能通过按 owner 有序的 patch transaction 更新 stable facts,suite export 后 diagnostics-only phase 才能运行。测试必须覆盖 chain binding 读取 receiver local 时已经看到 local stabilization 的 exact slot fact,以及 suite export 前后 stable side table / `BlockScope` 状态。 + +### R13:resolver 三道封口只打开了一道 + +缓解:request-domain gate、AST boundary gate、current-scope gate 都必须由同一个 owner/body/domain readiness policy 条件化。PUBLISHED gate body 的 resolver request 不能继续裸传 `FOR_SUBTREE` 并被 domain gate 提前拒止;AST boundary 不能继续按 `ForStatement.body()` 恒定封口;current-scope 也不能只看 `BlockScopeKind.FOR_BODY`。Stage F synthetic gate tests 必须逐一覆盖三道封口。 + +### R14:retry 中间 expression type 被导出导致 patch 冲突 + +缓解:`expressionTypes()` stable merge 保持严格 `sameExpressionType` 判据,不增加 `Variant -> exact`、parent -> child 或 status upgrade 例外。Chain / argument retry 的中间 expression facts 只能存放在 `FrontendOwnerRetryMemo`;statement pending overlay、current-suite committed overlay 与 `FrontendExprTypePatch.expressionTypes()` 都只能包含每 key 最终单条 fact。测试必须覆盖 same key different value fail-fast,以及 retry 后 stable / committed table 不含 stale intermediate fact。 + +### R15:single-stage patch 被误用为 multi-owner suite export + +缓解:suite export 生产路径不得构造跨 owner `FrontendAnalysisPatch`。旧 `FrontendAnalysisPatch` 迁入 patch package 后只能作为 legacy single-stage patch / 测试兼容层或被拆解;`FrontendPatchTransaction` 必须按固定 owner 顺序 apply per-owner patches,并拒绝乱序、重复 owner 或跨 owner payload。 + +### R16:把 whole-module analyzer 包装成 body runner -缓解:window-local surface、patch merge 与 local slot update 都做 `GdCompilerType` guard;for iterator state 只能通过专用 contract 给 CFG/lowering。 +缓解:阶段 A 必须完成 walker-state inventory,阶段 D 必须建立不调用 `analyzeInWindow(...)` 的 root-bounded SuiteResolver skeleton,阶段 E 才能接入真实 owner procedure。任何 production SuiteResolver path 若调用 `analyzeInWindow(...)`、内部 `AstWalker...walk(sourceFile)` 或整表 `updateXxx(...)` 来表达 body result,都视为计划违约而非阶段性完成。 -### R9:实现一次性改动过大 +### R17:`FrontendWindowPublicationSurface` 被误认为纯 scratch 参考 -缓解:先做 patch infra,再独立落地 window-local surface,再做 window runner 等价改造,之后切 scheduler,最后接 typed-dependent gate。每阶段都应有 targeted tests。 +缓解:`FrontendWindowPublicationSurface` 的 direct API 可以表达 scratch-over-stable,但现有 analyzer caller 已经违反该模型:`FrontendVarTypePostAnalyzer.analyzeInWindow(...)` 直接 clear/write stable `slotTypes()`,再复制到 window scratch。阶段 A 必须把该类型降级为 legacy shim;阶段 C overlay 必须独立实现和验证;阶段 E var type post 重写不得复用旧 window path。任何以 `FrontendWindowPublicationSurfaceTest` 代替 overlay isolation test 的验收都无效。 ## 8. 完成定义 本计划完成时应满足: -- frontend shared semantic 默认使用 segmented runner,且现有 supported surface 行为等价。 -- `FrontendAnalysisData` 同时支持 whole-table publication 与安全 partial patch merge。 -- window 内 semantic fact 通过 scratch-over-stable effective view 即时可见,且 commit 前不会污染 stable side table。 -- `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 不再改写 `BlockScope`、不刷新 `symbolBindings()` payload、不产生 slot update,只保留 guard-only 协议检查。 -- supported block 的完整 local inventory 先于分段 resolver,declaration-after-use filtered hit 行为不退化。 +- frontend shared semantic 默认使用 interface/body pipeline,且现有 supported surface 行为等价。 +- 过渡用 `FrontendSegmentedSemanticScheduler` 与 `segmentedSemanticRunner` 已删除,或只作为明确隔离的测试辅助存在。 +- interface phase 建立完整 local declaration index 与 pending gate registry,但不做 body typed resolution。 +- `SuiteResolver` 按 source order 解析 supported body,并在 child body 前完成必要 readiness / inventory publication。 +- `SuiteResolver` 在每个 statement 内固定按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 调用 statement-local owner procedure。 +- Production SuiteResolver path 不调用 `analyzeInWindow(...)`、不从 module `SourceFile` root 启动内部 `AstWalker...walk(...)`,也不通过整表 `updateXxx(...)` 表达 body typed result。 +- 五个 owner analyzer 的 whole-module walker state inventory 已关闭,并已用 statement-local rewrite 替代 production body path;现有 walker 只可作为 legacy comparison path 或删除对象存在。 +- typed overlay 能区分 current statement pending facts 与 current suite committed facts,export 前不污染 stable side table 或 `BlockScope`;该标准明确排除旧 `FrontendVarTypePostAnalyzer.analyzeInWindow(...)` 的 stable `slotTypes()` clear/write 模式。 +- `FrontendAnalysisData` 支持安全 per-owner patch merge,并保持 stable reference 合同。 +- Suite export 使用按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 顺序 apply 的 `FrontendPatchTransaction` 或等价机制;生产路径不使用 single-stage `FrontendAnalysisPatch` 承载多 owner facts。 +- 所有 production patch 相关类型位于 `gd.script.gdcc.frontend.sema.patch` 包,包括旧 `FrontendAnalysisPatch`、`FrontendLocalSlotTypeUpdate`、新建 per-owner patch 类型、transaction 与 shared merge / guard helper。`FrontendWindowPublicationSurface` / `FrontendWindowAnalysisContext` 若保留或迁入该包,也必须标记为 legacy shim,不能作为 production overlay/export 参考。 +- patch commit 与 typed overlay 写入的 compiler-only guard 覆盖所有 user-visible type-bearing publication surfaces:`symbolBindings()`、`resolvedMembers()`、`resolvedCalls()`、`expressionTypes()`、`slotTypes()`、`localSlotTypeUpdates()`;guard 完整性不得以 `FrontendWindowPublicationSurface` 行为作为证明。 +- shared compiler-only walker 同时用于 patch commit、overlay pending write、overlay flush,以及任何保留的 source-facing whole-table publication API;新增 type-bearing payload 时必须同步更新该 walker 与对应 regression tests。 +- production body path 不通过 `FrontendAnalysisData.symbolBindings()/resolvedMembers()/resolvedCalls()/expressionTypes()/slotTypes()` 返回的可变 stable 引用直接 `put()` / `clear()` / `putAll()`,也不通过 `updateXxx(...)` whole-table replace 把 body typed facts 中转到 stable side table 后再校验。 +- `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 不改写 `BlockScope`、不刷新 `symbolBindings()` payload、不产生 slot update,只保留 guard-only 协议检查。 +- supported suite 的完整 local inventory 先于 body typed resolution,declaration-after-use filtered hit 行为不退化。 - local `:=` 的 source-order type stabilization 可被后续 statement / gate classifier 消费。 +- chain binding 消费 receiver local slot 时,必须看到 local stabilization 已发布到 overlay 的 exact type,而不是 interface baseline `Variant`。 - pending feature gate 能在 typed fact 就绪后安全转正,但 child body 只有在 `bodyInventoryReadiness == PUBLISHED` 后才可解析。 -- `FOR_BODY` body inventory readiness 由单一 registry/query 表达;scope 存在或 `SUPPORTED` 状态都不能被当作 readiness 替代品。 +- typed-dependent body inventory readiness 由单一 registry/query 表达;scope 存在或 `SUPPORTED` 状态都不能被当作 readiness 替代品。 +- resolver 的 request-domain gate、AST boundary gate、current-scope gate 都已接入同一 readiness policy;PUBLISHED gate body 能作为 executable body normal lookup,非 PUBLISHED / unsupported / 缺失 owner 的 body 继续 fail-closed。 +- gate header edge 与 body edge 在 resolver 中可区分:header classifier 可读取前缀 facts,但不会提前打开 body-local visibility。 +- nested chain / argument retry 不会产生 stable `expressionTypes()` narrowing rewrite 或 status upgrade;每个 expression / step key 在 overlay export 与 stable table 中最多有一个最终 fact。 +- `applyPatch` 对 `FrontendExprTypePatch.expressionTypes()` 的 conflict 检测保持 status + publishedType + detailReason 严格相等,只有 local slot update 保留 `Variant -> exact` 例外;如未来需要表达受控 expression narrowing,必须新增显式 merge/upgrade 机制,而不是复用当前 republish 路径。 - unsupported gate 仍 fail-closed,不能 fallback 或误发布 body facts。 - compile gate、lowering-ready fact 边界和 compiler-only type 隔离不变。 +- `doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md` 已根据最终实现同步更新,用作目标架构摘要,并与本完成定义中的 pipeline 层级、owner 顺序、overlay 生命周期、patch/export 合同、compiler-only guard 与 diagnostics / compile gate 边界保持一致。 From 54091054a07ea8f31cc41e6288c22e2a8d6810a7 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Tue, 7 Jul 2026 19:19:13 +0800 Subject: [PATCH 07/27] docs(frontend): close phase A of segmented type resolution pipeline plan - Mark all phase A checklist items as completed and record completion rationale in the plan - Add whole-module walker state inventory and compiler-only guard payload matrix as phase freeze artifacts - Demote the legacy segmented scheduler to a deprecated comparison entry with updated contract - Narrow the window publication surface test scope to legacy shim API only - Anchor the next phase on the documented patch migration, per-owner transaction order, and shared walker guard --- ...segmented_type_resolution_pipeline_plan.md | 52 +++++++++++++++---- .../FrontendSegmentedSemanticScheduler.java | 13 ++--- .../FrontendWindowPublicationSurfaceTest.java | 6 +++ 3 files changed, 55 insertions(+), 16 deletions(-) diff --git a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md index 8f6fdfa4..dd9b38f3 100644 --- a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md +++ b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md @@ -547,16 +547,48 @@ Feature-specific header state 仍属于 gate header 语义,不得因为 body r - 为五个 owner analyzer 建立 walker-state inventory:列出当前内部 visitor 的 traversal root、隐式字段状态、读取的 stable side table、写入的 side table、diagnostic emission 与可抽取 helper。该 inventory 是重写输入,不是迁移完成标志。 - 为 compiler-only guard 建立 payload matrix:逐项记录 `expressionTypes()`、`slotTypes()`、`localSlotTypeUpdates()`、`symbolBindings()`、`resolvedMembers()`、`resolvedCalls()` 当前由谁检查、谁未检查、哪些 API 仍可直接绕过 guard。该 matrix 必须与阶段 C shared walker 设计一起冻结。 -当前状态(2026-07-06): - -- [ ] A1 在文档中完成资产分类。 -- [ ] A2 在代码中将 `FrontendSegmentedSemanticScheduler` 标记为 deprecated 或 legacy comparison entry。 -- [ ] A3 保留 `FrontendAnalysisDataTest`,并将 `FrontendWindowPublicationSurfaceTest` 降级为 API-level / legacy contamination regression 测试,不再作为新 overlay 正确性的参考测试。 -- [ ] A4 移除或隔离 `segmentedSemanticRunner` 对生产路径的影响。 -- [ ] A5 确定 patch package 迁移计划、per-owner patch 命名与 transaction apply 顺序。 -- [ ] A6 完成 whole-module walker state inventory,并标记 `analyzeInWindow(...)` 只允许作为 legacy comparison path。 -- [ ] A7 记录 `FrontendVarTypePostAnalyzer.analyzeInWindow(...)` 的 stable `slotTypes()` 污染路径,并决定修复、隔离或删除该 legacy path。 -- [ ] A8 完成 compiler-only guard payload matrix,并明确 `updateXxx(...)` / direct stable side-table 引用哪些是 legacy-only,哪些必须在后续阶段接入 shared walker。 +当前状态(2026-07-07): + +- [x] A1 在文档中完成资产分类。 +- [x] A2 在代码中将 `FrontendSegmentedSemanticScheduler` 标记为 deprecated 或 legacy comparison entry。 +- [x] A3 保留 `FrontendAnalysisDataTest`,并将 `FrontendWindowPublicationSurfaceTest` 降级为 API-level / legacy contamination regression 测试,不再作为新 overlay 正确性的参考测试。 +- [x] A4 移除或隔离 `segmentedSemanticRunner` 对生产路径的影响。 +- [x] A5 确定 patch package 迁移计划、per-owner patch 命名与 transaction apply 顺序。 +- [x] A6 完成 whole-module walker state inventory,并标记 `analyzeInWindow(...)` 只允许作为 legacy comparison path。 +- [x] A7 记录 `FrontendVarTypePostAnalyzer.analyzeInWindow(...)` 的 stable `slotTypes()` 污染路径,并决定修复、隔离或删除该 legacy path。 +- [x] A8 完成 compiler-only guard payload matrix,并明确 `updateXxx(...)` / direct stable side-table 引用哪些是 legacy-only,哪些必须在后续阶段接入 shared walker。 + +阶段 A 完成记录: + +- A1:第 4.9 节的资产分类冻结为阶段 A 基线。`FrontendSemanticStage`、`FrontendAnalysisData.applyPatch(...)`、`refreshPublishedLocalBindingPayloads(...)`、backfill guard 与 `FrontendAnalysisDataTest` 保留;patch carrier 与 transaction 类型进入 `gd.script.gdcc.frontend.sema.patch` 迁移计划;现有 analyzer walker 只作为语义 helper / rewrite reference;`FrontendSegmentedSemanticScheduler`、`segmentedSemanticRunner` 和 production `analyzeInWindow(...)` 调用路径归类为可回退或废弃资产。 +- A2:`FrontendSegmentedSemanticScheduler` 已在代码中标记为 `@Deprecated` legacy comparison entry,类注释明确它不是 SuiteResolver production pipeline。IDE 对该类发出弃用警告是预期状态。 +- A3:`FrontendAnalysisDataTest` 继续作为 stable reference、conflict、idempotent 与 compiler-only guard 的 focused tests。`FrontendWindowPublicationSurfaceTest` 的类级注释已降级其解释范围:它只证明 legacy shim API 的 scratch write 隔离与 guard,不证明所有 legacy `analyzeInWindow(...)` 都 scratch-safe。 +- A4:生产构造路径保持 `segmentedSemanticRunner == false`;唯一启用入口仍是 `FrontendSemanticAnalyzer.withSegmentedSemanticRunnerForTesting()`。该入口只作为 legacy comparison / equivalence test hook,不作为 production path。 +- A5:patch package 迁移计划冻结为:新建 `gd.script.gdcc.frontend.sema.patch`;以 `FrontendOwnerPatch` 或等价 sealed interface 作为单 owner patch 根类型;新增 `FrontendTopBindingPatch`、`FrontendLocalTypeStabilizationPatch`、`FrontendChainBindingPatch`、`FrontendExprTypePatch`、`FrontendVarTypePostPatch`;新增 `FrontendPatchTransaction` 按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 顺序 apply。旧 `FrontendAnalysisPatch` 若迁移则只能作为 legacy single-stage compatibility carrier,不能继续作为 SuiteResolver export 的 multi-owner carrier;`FrontendLocalSlotTypeUpdate` 随 local stabilization patch 迁入 patch 包。 +- A6:whole-module walker state inventory 见下表。所有当前 `analyzeInWindow(...)` 都只允许作为 legacy comparison path;其 whole-module traversal root、隐式 visitor 字段和整表发布策略都不得直接复用为 SuiteResolver statement-local owner procedure。 +- A7:`FrontendVarTypePostAnalyzer.analyzeInWindow(...)` 的 legacy 污染路径为:读取 `analysisData.slotTypes()` 稳定表 -> `clear()` -> 将稳定表传入 `SlotTypePublisher` 逐项写入 -> 再复制到 `window.publications().slotTypes()`。阶段 A 决定是隔离而非修补该 legacy path:保留测试比较价值,但新 var type post procedure 必须从 statement-local scratch/overlay 写入重新实现。 +- A8:compiler-only guard payload matrix 见下表。阶段 C 的 shared type-bearing field walker 必须成为 overlay write、patch merge、overlay flush 与任何保留 source-facing `updateXxx(...)` 的共同 guard 基线;不能先写 overlay / stable 再在 export 时补查。 + +Whole-module walker state inventory: + +| Owner analyzer | 当前 traversal root | 隐式 walker state | 读取的 stable side table / state | 写入 side table / patch payload | Diagnostics owner | 可抽取 helper / 不可复用部分 | +| --- | --- | --- | --- | --- | --- | --- | +| `FrontendTopBindingAnalyzer` | 每个 `FrontendSourceClassRelation.unit().ast()` 的 `SourceFile` whole-module walk | `sourcePath`、`moduleSkeleton`、`visibleValueResolver`、`classRegistry`、`reportedUnsupportedRoots`、`ASTWalker` | `moduleSkeleton()`、`scopesByAst()`、parse/skeleton diagnostics snapshot、class registry | `symbolBindings()` | `sema.binding` 与 unsupported binding routes | binding 分类与 visible resolver provenance 可抽取;whole-module walk 和整表 `updateSymbolBindings(...)` 不可作为 SuiteResolver procedure | +| `FrontendLocalTypeStabilizationAnalyzer` | 每个 source file whole-module walk;按 visitor 状态维护 source-order probe | `SilentExpressionResolver`、`probes`、`writeBackStableSlots`、可选 `FrontendWindowAnalysisContext`、probe-local expression memo | `scopesByAst()`、`symbolBindings()`、`resolvedMembers()` / `resolvedCalls()` / `expressionTypes()` 已发布事实、当前 scope slot state | legacy path 直接更新 local `ScopeValue.type()`;window path 产生 `FrontendLocalSlotTypeUpdate` | 不拥有 source-facing diagnostics;只在写回边界 fail-fast 拒绝非法 slot type | initializer probe、compatibility 与 slot rewrite guard 可抽取;whole-module delayed probe 收集和直接 stable scope mutation 不可复用 | +| `FrontendChainBindingAnalyzer` | 每个 source file whole-module walk | `chainReduction`、assignment / expression semantic support、`reportedDeferredRoots`、`reportedUnsupportedRoots`、`ASTWalker` | `scopesByAst()`、`symbolBindings()`、stable slot types、已发布 member/call facts、class registry | `resolvedMembers()`、chain-owned `resolvedCalls()` | `sema.member_resolution`、`sema.call_resolution`、deferred / unsupported routes | chain reduction 与 call/member classification 可抽取;通过 analyzer-local callback 读取 dependency type 与 whole-module retry memo 不可复用 | +| `FrontendExprTypeAnalyzer` | 每个 source file whole-module walk | `chainReduction`、assignment / expression semantic support、`parentByNode`、reported expression/deferred/unsupported/discarded roots | `scopesByAst()`、`symbolBindings()`、`resolvedMembers()`、`resolvedCalls()`、slot types、class registry | `expressionTypes()`、bare-call `resolvedCalls()`;`backfillInferredLocalType(...)` 只能 guard-only | `sema.expression_resolution`、`sema.deferred_expression_resolution`、`sema.unsupported_expression_route`、`sema.discarded_expression` | expression support、type compatibility 与 discarded-expression rules 可抽取;backfill 不得重新成为 slot mutation owner,whole-module expression walk 不可复用 | +| `FrontendVarTypePostAnalyzer` | 每个 source file whole-module walk | `sourcePath`、`blockScopeStack`、`currentCallableOwner`、`supportedExecutableBlockDepth`、`ASTWalker` | `moduleSkeleton()`、`scopesByAst()`、callable/block scope inventory、current scope slot types | `slotTypes()` final callable-local snapshot | `sema.variable_slot_publication` fail-fast / invariant violations | slot publication eligibility rules 可抽取;legacy `analyzeInWindow(...)` 的 stable `slotTypes()` clear/write 与 whole-table snapshot 不可复用 | + +Compiler-only guard payload matrix: + +| Publication surface | User-visible type-bearing payload | 当前 guard 覆盖 | 当前绕过点 / legacy-only API | 后续 shared walker 要求 | +| --- | --- | --- | --- | --- | +| `expressionTypes()` | `FrontendExpressionType.publishedType()` | `FrontendAnalysisData.applyPatch(...)` 与 `FrontendWindowPublicationSurface.expressionTypes().put(...)` 检查 published type | `updateExpressionTypes(...)` 整表替换和 direct stable table mutation 是 legacy-only,当前不复用 shared walker | overlay write、patch commit、overlay flush、保留的 source-facing whole-table publish 均递归检查 published type | +| `slotTypes()` | local / parameter / iterator slot `GdType` value | `applyPatch(...)`、window `slotTypes().put(...)`、`FrontendLocalSlotTypeUpdate` 检查 compiler-only;local slot update 也拒绝 void | `updateSlotTypes(...)` 整表替换、`analysisData.slotTypes().put/clear` direct mutation、VarTypePost legacy contamination path 是 legacy-only | 所有 source-facing slot publication 必须先通过同一 field walker;VarTypePost 新 procedure 只能写 scratch/overlay | +| `localSlotTypeUpdates()` | `FrontendLocalSlotTypeUpdate.type()` | `FrontendWindowPublicationSurface.addLocalSlotTypeUpdate(...)` 与 `FrontendAnalysisData.validateLocalSlotTypeUpdates(...)` 检查 compiler-only / void / conflict | 旧 `FrontendAnalysisPatch` 可携带跨 owner payload,不能作为 SuiteResolver production carrier | `FrontendLocalTypeStabilizationPatch` 独占携带,并在 transaction apply 前后复用 shared guard | +| `symbolBindings()` | `FrontendBinding.resolvedValue().type()` | local slot update 后的 `refreshPublishedLocalBindingPayloads(...)` 覆盖刷新路径 | `updateSymbolBindings(...)`、`applyPatch(...)` 里的 direct binding merge、`analysisData.symbolBindings().put` 目前可绕过 compiler-only payload 检查 | 阶段 C 至少先关闭 `resolvedValue.type()` direct patch / overlay bypass,再允许 production SuiteResolver export | +| `resolvedMembers()` | `FrontendResolvedMember.receiverType()` / `resultType()` | 当前没有统一 compiler-only payload guard;只做 conflict equality | `updateResolvedMembers(...)`、`applyPatch(...)` merge、direct stable table mutation 都是 legacy-only publication surface | shared walker 必须递归检查 receiver/result type,且区分 hidden compiler state 与 user-visible result | +| `resolvedCalls()` | `FrontendResolvedCall.receiverType()` / `returnType()` / `argumentTypes()` / callable boundary parameter types | 当前没有统一 compiler-only payload guard;只做 conflict equality | `updateResolvedCalls(...)`、`applyPatch(...)` merge、direct stable table mutation 都是 legacy-only publication surface | shared walker 必须覆盖 call return、receiver、argument 与 exact callable boundary 参数类型 | 验收细则: diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSegmentedSemanticScheduler.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSegmentedSemanticScheduler.java index 0a2c7c3d..d32a18a1 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSegmentedSemanticScheduler.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSegmentedSemanticScheduler.java @@ -9,13 +9,14 @@ import java.util.Objects; -/// Coordinates the segmented semantic publication layer introduced after the window-capable -/// analyzer APIs. +/// Legacy comparison scheduler for the segmented semantic publication layer introduced after the +/// window-capable analyzer APIs. /// -/// This first scheduler keeps the current supported frontend behavior equivalent by committing one -/// owner stage at a time through `FrontendAnalysisData.applyPatch(...)`. That still exercises the -/// segmented publication path, preserves stable side-table references, and makes local slot updates -/// visible before chain binding consumes receiver slots. +/// This scheduler is not the planned SuiteResolver production pipeline: its `analyzeInWindow(...)` +/// calls still perform whole-module traversal, and local stabilization intentionally uses the stable +/// whole-phase path. Keep it only as a legacy comparison entry while the root-bounded body resolver +/// and per-owner patch transaction are implemented. +@Deprecated final class FrontendSegmentedSemanticScheduler { private final @NotNull FrontendTopBindingAnalyzer topBindingAnalyzer; private final @NotNull FrontendLocalTypeStabilizationAnalyzer localTypeStabilizationAnalyzer; diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendWindowPublicationSurfaceTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendWindowPublicationSurfaceTest.java index a2298d17..462989cb 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendWindowPublicationSurfaceTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendWindowPublicationSurfaceTest.java @@ -41,6 +41,12 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +/// API-level coverage for the legacy window publication shim. +/// +/// These tests prove direct `FrontendWindowPublicationSurface` scratch writes are isolated and +/// guarded. They must not be read as proof that every legacy `analyzeInWindow(...)` implementation +/// is scratch-safe; `FrontendVarTypePostAnalyzer.analyzeInWindow(...)` is documented separately as a +/// stable `slotTypes()` contamination path until the SuiteResolver rewrite replaces it. class FrontendWindowPublicationSurfaceTest { private static final Range RANGE = new Range(0, 1, new Point(0, 0), new Point(0, 1)); From 4ed8fdbf212505f17e1360b3ab3be1d3beb136ce Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Tue, 7 Jul 2026 21:29:49 +0800 Subject: [PATCH 08/27] feat(frontend): add interface phase coordinator and data structures - Introduce interface layer scaffolding that prepares body resolution without leaking typed facts to the stable side table. - Capture body declaration source order, gate registry, typed lexical baseline, and body entry roots as read-only baseline views. - Mark supporting body readiness, gate status, and declaration records as building blocks owned by the new phase. - Cover boundary contract with focused tests asserting no stable typed publication and fail-closed gate behavior. - Sync the pipeline plan and execution summary with the new surface naming and completed phase items. --- ...e_resolution_pipeline_execution_summary.md | 4 +- ...segmented_type_resolution_pipeline_plan.md | 22 +- .../sema/FrontendBodyDeclarationIndex.java | 45 +++ .../sema/FrontendBodyInventoryReadiness.java | 8 + .../sema/FrontendBodyLocalDeclaration.java | 29 ++ .../sema/FrontendInterfaceSurface.java | 20 + .../frontend/sema/FrontendInventoryGate.java | 45 +++ .../sema/FrontendInventoryGateRegistry.java | 70 ++++ .../sema/FrontendInventoryGateStatus.java | 8 + .../sema/FrontendSuiteEntryRoots.java | 45 +++ .../sema/FrontendTypedLexicalBaseline.java | 65 ++++ .../sema/analyzer/FrontendInterfacePhase.java | 361 ++++++++++++++++++ .../sema/FrontendInterfacePhaseTest.java | 357 +++++++++++++++++ 13 files changed, 1071 insertions(+), 8 deletions(-) create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyDeclarationIndex.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyInventoryReadiness.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyLocalDeclaration.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendInterfaceSurface.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGate.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateRegistry.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateStatus.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendSuiteEntryRoots.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalBaseline.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendInterfacePhase.java create mode 100644 src/test/java/gd/script/gdcc/frontend/sema/FrontendInterfacePhaseTest.java diff --git a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md index 793e393a..548fae06 100644 --- a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md +++ b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md @@ -7,7 +7,7 @@ 计划完成后,frontend shared semantic pipeline 分为四个层次: 1. 基础结构层:建立 module skeleton、scope graph 与 baseline inventory。 -2. Interface 层:建立 body 解析所需的 declaration index、gate registry、typed lexical baseline 与 suite plan。 +2. Interface 层:建立 body 解析所需的 declaration index、gate registry、typed lexical baseline 与 suite entry roots。 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。 @@ -44,7 +44,7 @@ Interface 层在基础结构层之后运行,负责准备 body `SuiteResolver` - `FrontendBodyDeclarationIndex`:记录每个 supported block 的完整 declaration 列表与 source order。 - `FrontendInventoryGateRegistry`:记录 typed-dependent subtree 的 gate owner、header root、body root、deferred domain 与 readiness。 - `FrontendTypedLexicalBaseline`:记录参数、显式 typed local 与 interface 层可静态确定的 source-facing slot baseline。 -- `FrontendSuitePlan`:列出 body layer 可进入的 callable、property initializer 与 supported block。 +- `FrontendSuiteEntryRoots`:列出 body layer 可进入的 callable、property initializer 与 supported block roots。 Interface 层不得发布 body typed facts。特别是不得发布 `expressionTypes()`、`resolvedMembers()` 或 `resolvedCalls()`,也不得把 `GdCompilerType` 写入 source-facing lexical baseline。 diff --git a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md index dd9b38f3..34adacae 100644 --- a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md +++ b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md @@ -225,7 +225,7 @@ Interface phase 输出: - `FrontendBodyDeclarationIndex`:每个 supported block 的完整 declaration 列表与 source order。 - `FrontendInventoryGateRegistry`:typed-dependent subtree 的 gate owner、header root、body root、deferred domain、readiness。 - `FrontendTypedLexicalBaseline`:参数、显式 typed local、已可静态确定的 interface-level source-facing slot baseline。 -- `FrontendSuitePlan`:body layer 可进入的 callable/property initializer/supported block 列表。 +- `FrontendSuiteEntryRoots`:body layer 可进入的 callable/property initializer/supported block 根列表。 Interface phase 不得: @@ -602,13 +602,22 @@ Compiler-only guard payload matrix: ### 阶段 B:建立 Interface phase 数据结构 +状态同步(2026-07-07): + +- [x] B1 新增 `FrontendInterfacePhase` coordinator;它在 skeleton / scope / variable inventory 之后构建独立 `FrontendInterfaceSurface`,不写入 stable typed side table,也不改变 legacy analyzer 顺序。 +- [x] B2 新增 `FrontendBodyDeclarationIndex`,按 supported body root 记录已发布 ordinary local declaration 与 body-local source order;该 index 是 baseline inventory 的只读 view,不重新发布 local。 +- [x] B3 新增 `FrontendInventoryGateRegistry`,记录 typed-dependent subtree 与 body readiness;Phase B gate 固定从 `PENDING + NOT_PUBLISHED` 起步,非 `SUPPORTED + PUBLISHED` 查询必须 fail-closed。 +- [x] B4 新增 `FrontendTypedLexicalBaseline`,记录 parameter / ordinary local source-facing slot baseline,并在写入时 fail-fast 拒绝 `GdCompilerType`。 +- [x] B5 新增 `FrontendSuiteEntryRoots`,列出 body layer 可进入的 callable / property initializer / supported block roots,并明确 typed-dependent body 在 gate 发布前不进入 entry roots。 +- [x] B6 保持 skeleton / scope / variable analyzer public contract 不变;新增 `FrontendInterfacePhaseTest` 只调用已发布基础结构层并断言 Interface phase 不写 typed stable side table,现有 phase-boundary / variable inventory focused tests 继续作为回归锚点。 + 实施内容: - 新增 `FrontendInterfacePhase` 或等价 coordinator。 - 新增 `FrontendBodyDeclarationIndex`,按 callable/block 记录完整 ordinary local declaration 与 source order。 - 新增 `FrontendInventoryGateRegistry`,记录 typed-dependent subtree 与 body readiness。 - 新增 `FrontendTypedLexicalBaseline`,记录 interface 层可确定的 source-facing typed baseline。 -- 新增 `FrontendSuitePlan`,列出 body layer 可进入的 callable/property initializer/supported block。 +- 新增 `FrontendSuiteEntryRoots`,列出 body layer 可进入的 callable/property initializer/supported block roots。 - 保持 skeleton/scope/variable analyzer 的 public contract 不变。 验收细则: @@ -657,7 +666,7 @@ Compiler-only guard payload matrix: 验收细则: -- `SuiteResolver` 只进入 `FrontendSuitePlan` 标记为可进入的 body。 +- `SuiteResolver` 只进入 `FrontendSuiteEntryRoots` 标记为可进入的 body。 - source-order traversal 与 AST statement order 一致。 - child block 递归顺序为 header 先解析,body 后解析。 - `FrontendVisibleValueResolver` 的 declaration-after-use 与 self-reference 测试继续通过。 @@ -814,9 +823,10 @@ Compiler-only guard payload matrix: Interface phase 测试: -- `FrontendInterfacePhaseTest`:构建 `FrontendBodyDeclarationIndex`,完整记录 supported block local declaration source order。 -- `FrontendInterfacePhaseTest`:记录 `FrontendInventoryGate(PENDING, NOT_PUBLISHED)`,但不发布 gate body inventory。 -- `FrontendInterfacePhaseTest`:`for` / `match` / lambda / block-local `const` 未转正时仍 fail-closed。 +- `FrontendInterfacePhaseTest.buildsSupportedBodyDeclarationIndexTypedBaselineAndSuiteEntryRoots`:构建 `FrontendBodyDeclarationIndex`,完整记录 supported block local declaration source order,同时锚定 typed baseline 与 suite entry roots 不写 stable typed side table。 +- `FrontendInterfacePhaseTest.keepsFutureDeclarationVisibleToResolverThroughCompleteBodyIndex`:`var first := second; var second := 1` 仍通过完整 inventory 被 resolver 过滤为 `DECLARATION_AFTER_USE_SITE`。 +- `FrontendInterfacePhaseTest.recordsPendingTypedDependentGatesWithoutOpeningTheirBodies`:记录 `FrontendInventoryGate(PENDING, NOT_PUBLISHED)`,但不发布 gate body inventory,且 `for` / `match` / lambda / block-local `const` 未转正时仍 fail-closed。 +- `FrontendInterfacePhaseTest.typedLexicalBaselineRejectsCompilerOnlySourceFacingTypes`:source-facing typed baseline 写入 `GdCompilerType` 时 fail-fast。 - `FrontendSemanticAnalyzerFrameworkTest`:skeleton / scope / variable diagnostics snapshot boundary 不漂移。 Suite/body pipeline 测试: 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..8c4b2a04 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyDeclarationIndex.java @@ -0,0 +1,45 @@ +package gd.script.gdcc.frontend.sema; + +import dev.superice.gdparser.frontend.ast.Node; +import org.jetbrains.annotations.NotNull; + +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 that already exist in baseline `BlockScope` inventory. +/// +/// The index is intentionally a view over published inventory, not a second declaration publisher. +/// Unsupported typed-dependent bodies therefore have no entries until their gate-owned inventory is +/// explicitly published by a later body phase. +public final class FrontendBodyDeclarationIndex { + private final @NotNull Map> declarationsByBodyRoot; + + public FrontendBodyDeclarationIndex( + @NotNull Map> declarationsByBodyRoot + ) { + Objects.requireNonNull(declarationsByBodyRoot, "declarationsByBodyRoot"); + var copiedDeclarations = new IdentityHashMap>(); + for (var entry : declarationsByBodyRoot.entrySet()) { + copiedDeclarations.put( + Objects.requireNonNull(entry.getKey(), "bodyRoot"), + List.copyOf(Objects.requireNonNull(entry.getValue(), "declarations")) + ); + } + this.declarationsByBodyRoot = Collections.unmodifiableMap(copiedDeclarations); + } + + 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")); + } + + public @NotNull Map> declarationsByBodyRoot() { + return declarationsByBodyRoot; + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyInventoryReadiness.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyInventoryReadiness.java new file mode 100644 index 00000000..f18fc02f --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyInventoryReadiness.java @@ -0,0 +1,8 @@ +package gd.script.gdcc.frontend.sema; + +/// Publication readiness of the full local inventory owned by a typed-dependent body gate. +public enum FrontendBodyInventoryReadiness { + NOT_PUBLISHED, + PUBLISHING, + PUBLISHED +} 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..73ba2e89 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyLocalDeclaration.java @@ -0,0 +1,29 @@ +package gd.script.gdcc.frontend.sema; + +import dev.superice.gdparser.frontend.ast.VariableDeclaration; +import gd.script.gdcc.scope.ScopeValue; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +/// One ordinary local declaration published by the baseline inventory layer for a supported body. +/// +/// `sourceOrder` is local to the owning body root. It preserves the complete-inventory plus +/// source-order model required by `FrontendVisibleValueResolver`: the resolver can see future locals +/// in the scope graph, then decide whether a use-site is before or after this declaration. +public record FrontendBodyLocalDeclaration( + @NotNull VariableDeclaration declaration, + @NotNull ScopeValue binding, + int sourceOrder +) { + public FrontendBodyLocalDeclaration { + Objects.requireNonNull(declaration, "declaration"); + Objects.requireNonNull(binding, "binding"); + 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"); + } + } +} 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..a8ebcd94 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendInterfaceSurface.java @@ -0,0 +1,20 @@ +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 FrontendInventoryGateRegistry inventoryGateRegistry, + @NotNull FrontendTypedLexicalBaseline typedLexicalBaseline, + @NotNull FrontendSuiteEntryRoots suiteEntryRoots +) { + public FrontendInterfaceSurface { + Objects.requireNonNull(bodyDeclarationIndex, "bodyDeclarationIndex"); + Objects.requireNonNull(inventoryGateRegistry, "inventoryGateRegistry"); + Objects.requireNonNull(typedLexicalBaseline, "typedLexicalBaseline"); + Objects.requireNonNull(suiteEntryRoots, "suiteEntryRoots"); + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGate.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGate.java new file mode 100644 index 00000000..95e961ec --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGate.java @@ -0,0 +1,45 @@ +package gd.script.gdcc.frontend.sema; + +import dev.superice.gdparser.frontend.ast.Node; +import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueDomain; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +/// Typed-dependent subtree gate discovered by the Interface phase. +/// +/// Phase B only creates `PENDING + NOT_PUBLISHED` gates. Later body phases may classify and publish +/// a gate, but consumers must keep body lookup fail-closed until readiness reaches `PUBLISHED`. +public record FrontendInventoryGate( + @NotNull Node owner, + @NotNull Node headerRoot, + @NotNull Node bodyRoot, + @NotNull FrontendVisibleValueDomain deferredDomain, + @NotNull FrontendInventoryGateStatus status, + @NotNull FrontendBodyInventoryReadiness bodyInventoryReadiness +) { + public FrontendInventoryGate { + Objects.requireNonNull(owner, "owner"); + Objects.requireNonNull(headerRoot, "headerRoot"); + Objects.requireNonNull(bodyRoot, "bodyRoot"); + Objects.requireNonNull(deferredDomain, "deferredDomain"); + Objects.requireNonNull(status, "status"); + Objects.requireNonNull(bodyInventoryReadiness, "bodyInventoryReadiness"); + } + + public static @NotNull FrontendInventoryGate pending( + @NotNull Node owner, + @NotNull Node headerRoot, + @NotNull Node bodyRoot, + @NotNull FrontendVisibleValueDomain deferredDomain + ) { + return new FrontendInventoryGate( + owner, + headerRoot, + bodyRoot, + deferredDomain, + FrontendInventoryGateStatus.PENDING, + FrontendBodyInventoryReadiness.NOT_PUBLISHED + ); + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateRegistry.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateRegistry.java new file mode 100644 index 00000000..17791e17 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateRegistry.java @@ -0,0 +1,70 @@ +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.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/// Interface-layer registry for typed-dependent body gates. +/// +/// The registry is the single body-readiness fact source for future resolver/binder consumers. A +/// missing gate or any state other than `SUPPORTED + PUBLISHED` must be interpreted as fail-closed. +public final class FrontendInventoryGateRegistry { + private final @NotNull List gates; + private final @NotNull Map gatesByBodyRoot; + + public FrontendInventoryGateRegistry(@NotNull List gates) { + Objects.requireNonNull(gates, "gates"); + var copiedGates = List.copyOf(gates); + var indexedGates = new IdentityHashMap(); + for (var gate : copiedGates) { + var previous = indexedGates.put(gate.bodyRoot(), gate); + if (previous != null) { + throw new IllegalArgumentException("duplicate inventory gate body root"); + } + } + this.gates = copiedGates; + gatesByBodyRoot = Collections.unmodifiableMap(indexedGates); + } + + public static @NotNull Builder builder() { + return new Builder(); + } + + public @NotNull List gates() { + return gates; + } + + public @Nullable FrontendInventoryGate gateForBodyRoot(@NotNull Node bodyRoot) { + return gatesByBodyRoot.get(Objects.requireNonNull(bodyRoot, "bodyRoot")); + } + + public boolean isBodyInventoryReady(@NotNull Node bodyRoot) { + var gate = gateForBodyRoot(bodyRoot); + return gate != null + && gate.status() == FrontendInventoryGateStatus.SUPPORTED + && gate.bodyInventoryReadiness() == FrontendBodyInventoryReadiness.PUBLISHED; + } + + public static final class Builder { + private final @NotNull List gates = new ArrayList<>(); + + private Builder() { + } + + public @NotNull Builder add(@NotNull FrontendInventoryGate gate) { + gates.add(Objects.requireNonNull(gate, "gate")); + return this; + } + + public @NotNull FrontendInventoryGateRegistry build() { + return new FrontendInventoryGateRegistry(gates); + } + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateStatus.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateStatus.java new file mode 100644 index 00000000..0bb604b1 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateStatus.java @@ -0,0 +1,8 @@ +package gd.script.gdcc.frontend.sema; + +/// Classification state for a typed-dependent body gate. +public enum FrontendInventoryGateStatus { + PENDING, + SUPPORTED, + UNSUPPORTED +} 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..c5958af7 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendSuiteEntryRoots.java @@ -0,0 +1,45 @@ +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 java.util.IdentityHashMap; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/// Body-entry roots produced by the Interface phase. +/// +/// Only roots listed here are legal for the future `SuiteResolver` to enter. Typed-dependent bodies +/// are deliberately excluded while their `FrontendInventoryGate` remains unpublished. +public record FrontendSuiteEntryRoots( + @NotNull List callableOwners, + @NotNull List propertyInitializers, + @NotNull List supportedBlocks +) { + public FrontendSuiteEntryRoots { + callableOwners = List.copyOf(Objects.requireNonNull(callableOwners, "callableOwners")); + propertyInitializers = List.copyOf(Objects.requireNonNull(propertyInitializers, "propertyInitializers")); + supportedBlocks = List.copyOf(Objects.requireNonNull(supportedBlocks, "supportedBlocks")); + } + + 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")); + } + + private static @NotNull Set identitySet(@NotNull List values) { + var set = java.util.Collections.newSetFromMap(new IdentityHashMap()); + set.addAll(values); + return set; + } +} 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/analyzer/FrontendInterfacePhase.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendInterfacePhase.java new file mode 100644 index 00000000..1afe243f --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendInterfacePhase.java @@ -0,0 +1,361 @@ +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.MatchSection; +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.FrontendInventoryGate; +import gd.script.gdcc.frontend.sema.FrontendInventoryGateRegistry; +import gd.script.gdcc.frontend.sema.FrontendSuiteEntryRoots; +import gd.script.gdcc.frontend.sema.FrontendTypedLexicalBaseline; +import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueDomain; +import gd.script.gdcc.scope.ClassRegistry; +import gd.script.gdcc.scope.Scope; +import org.jetbrains.annotations.NotNull; + +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, typed baseline slots, and pending +/// typed-dependent gates. +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().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 FrontendInventoryGateRegistry.Builder gateRegistryBuilder = + FrontendInventoryGateRegistry.builder(); + 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 int supportedBodyDepth; + + private InterfaceSurfaceBuilder(@NotNull FrontendAstSideTable scopesByAst) { + this.scopesByAst = Objects.requireNonNull(scopesByAst, "scopesByAst"); + astWalker = new ASTWalker(this); + } + + private void walk(@NotNull SourceFile sourceFile) { + astWalker.walk(sourceFile); + } + + private @NotNull FrontendInterfaceSurface build() { + return new FrontendInterfaceSurface( + new FrontendBodyDeclarationIndex(declarationsByBodyRoot), + gateRegistryBuilder.build(), + typedBaselineBuilder.build(), + new FrontendSuiteEntryRoots(callableOwners, propertyInitializers, supportedBlocks) + ); + } + + @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) { + if (!isNotPublished(lambdaExpression.body())) { + addPendingGate( + lambdaExpression, + lambdaExpression, + lambdaExpression.body(), + FrontendVisibleValueDomain.LAMBDA_SUBTREE + ); + } + 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()); + addPendingGate( + forStatement, + forStatement, + forStatement.body(), + FrontendVisibleValueDomain.FOR_SUBTREE + ); + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } + + @Override + public @NotNull FrontendASTTraversalDirective handleMatchStatement(@NotNull MatchStatement matchStatement) { + if (supportedBodyDepth <= 0 || isNotPublished(matchStatement)) { + return FrontendASTTraversalDirective.SKIP_CHILDREN; + } + astWalker.walk(matchStatement.value()); + for (var section : matchStatement.sections()) { + registerMatchSectionGate(matchStatement, section); + } + 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) { + addPendingGate( + variableDeclaration, + variableDeclaration, + variableDeclaration, + FrontendVisibleValueDomain.BLOCK_LOCAL_CONST_SUBTREE + ); + 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); + 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); + astWalker.walk(variableDeclaration.value()); + } + + private void enterSupportedBlock(@NotNull Block block) { + var scope = scopesByAst.get(block); + if (!(scope instanceof BlockScope blockScope) + || !FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(blockScope.kind())) { + return; + } + supportedBlocks.add(block); + var previousDeclarations = currentBodyDeclarations; + var bodyDeclarations = new ArrayList(); + currentBodyDeclarations = bodyDeclarations; + supportedBodyDepth++; + try { + 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; + } + currentBodyDeclarations.add(new FrontendBodyLocalDeclaration( + variableDeclaration, + binding, + currentBodyDeclarations.size() + )); + typedBaselineBuilder.put(variableDeclaration, binding.type()); + } + + private void registerMatchSectionGate( + @NotNull MatchStatement matchStatement, + @NotNull MatchSection section + ) { + for (var pattern : section.patterns()) { + astWalker.walk(pattern); + } + if (section.guard() != null) { + astWalker.walk(section.guard()); + } + addPendingGate( + matchStatement, + matchStatement, + section.body(), + FrontendVisibleValueDomain.MATCH_SUBTREE + ); + } + + private void addPendingGate( + @NotNull Node owner, + @NotNull Node headerRoot, + @NotNull Node bodyRoot, + @NotNull FrontendVisibleValueDomain deferredDomain + ) { + gateRegistryBuilder.add(FrontendInventoryGate.pending(owner, headerRoot, bodyRoot, deferredDomain)); + } + + 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/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..9e092382 --- /dev/null +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendInterfacePhaseTest.java @@ -0,0 +1,357 @@ +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 -> 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()); + var resolution = resolver.resolve(new FrontendVisibleValueResolveRequest( + "second", + secondUseSite, + FrontendVisibleValueDomain.EXECUTABLE_BODY + )); + + assertEquals(List.of("first", "second"), surface.bodyDeclarationIndex() + .declarationsFor(pingFunction.body()) + .stream() + .map(localDeclaration -> localDeclaration.declaration().name()) + .toList()); + assertEquals(FrontendVisibleValueStatus.NOT_FOUND, resolution.status()); + assertNull(resolution.visibleValue()); + assertEquals( + FrontendFilteredValueHitReason.DECLARATION_AFTER_USE_SITE, + primaryFilteredHitReason(resolution) + ); + } + + @Test + void recordsPendingTypedDependentGatesWithoutOpeningTheirBodies() 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 surface = new FrontendInterfacePhase().analyze(phaseInput.registry(), phaseInput.analysisData()); + var registry = surface.inventoryGateRegistry(); + + assertPendingGate(registry.gateForBodyRoot(forStatement.body()), FrontendVisibleValueDomain.FOR_SUBTREE); + assertPendingGate( + registry.gateForBodyRoot(matchStatement.sections().getFirst().body()), + FrontendVisibleValueDomain.MATCH_SUBTREE + ); + assertPendingGate(registry.gateForBodyRoot(lambda.body()), FrontendVisibleValueDomain.LAMBDA_SUBTREE); + assertPendingGate(registry.gateForBodyRoot(answer), FrontendVisibleValueDomain.BLOCK_LOCAL_CONST_SUBTREE); + assertFalse(registry.isBodyInventoryReady(forStatement.body())); + assertFalse(surface.suiteEntryRoots().containsSupportedBlock(forStatement.body())); + assertFalse(surface.suiteEntryRoots().containsSupportedBlock(matchStatement.sections().getFirst().body())); + assertFalse(surface.suiteEntryRoots().containsSupportedBlock(lambda.body())); + assertFalse(surface.bodyDeclarationIndex().containsBodyRoot(forStatement.body())); + assertFalse(surface.typedLexicalBaseline().containsDeclaration(answer)); + assertTrue(surface.typedLexicalBaseline().containsDeclaration(callback)); + } + + @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 void assertPendingGate( + FrontendInventoryGate maybeGate, + @NotNull FrontendVisibleValueDomain domain + ) { + if (maybeGate == null) { + fail("Expected pending gate for domain: " + domain); + } + var gate = maybeGate; + assertEquals(domain, gate.deferredDomain()); + assertEquals(FrontendInventoryGateStatus.PENDING, gate.status()); + assertEquals(FrontendBodyInventoryReadiness.NOT_PUBLISHED, gate.bodyInventoryReadiness()); + } + + 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 + ) { + } +} From 590287d8af22eff58a89f6f1be390953c3d1f65d Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Wed, 8 Jul 2026 14:42:21 +0800 Subject: [PATCH 09/27] feat(frontend): land typed overlay and suite resolver skeleton in main pipeline - Promote the typed lexical overlay to the main analysis flow with a shared guard covering all source-facing fact publication paths. - Move legacy patch carriers into a dedicated sema patch package and add typed owner-specific transaction primitives with shared walker enforcement. - Wire the interface phase and suite resolver skeleton into the main analyzer after skeleton, scope, and variable inventory stages. - Introduce the statement dispatcher, suite context, and owner retry memo to carry environment dependencies without leaking them through legacy paths. - Refresh the pipeline plan and targeted regressions to reflect the new phase C and D progress and shadow no-op body integration. --- ...segmented_type_resolution_pipeline_plan.md | 40 +- .../frontend/sema/FrontendAnalysisData.java | 99 ++-- .../frontend/sema/FrontendAnalysisPatch.java | 44 -- .../frontend/sema/FrontendOwnerRetryMemo.java | 56 ++ .../sema/FrontendSuiteEntryRoots.java | 31 +- .../sema/FrontendTypedLexicalEnvironment.java | 503 ++++++++++++++++++ .../sema/FrontendWindowAnalysisContext.java | 1 + .../FrontendWindowPublicationSurface.java | 21 +- .../sema/analyzer/FrontendInterfacePhase.java | 18 +- ...rontendLocalTypeStabilizationAnalyzer.java | 2 +- .../analyzer/FrontendSemanticAnalyzer.java | 41 ++ .../analyzer/FrontendStatementResolver.java | 159 ++++++ .../sema/analyzer/FrontendSuiteContext.java | 73 +++ .../sema/analyzer/FrontendSuiteResolver.java | 181 +++++++ .../sema/patch/FrontendAnalysisPatch.java | 46 ++ .../sema/patch/FrontendChainBindingPatch.java | 25 + .../sema/patch/FrontendExprTypePatch.java | 25 + .../FrontendLocalSlotTypeUpdate.java | 7 +- .../FrontendLocalTypeStabilizationPatch.java | 25 + .../sema/patch/FrontendOwnerPatch.java | 49 ++ .../sema/patch/FrontendPatchTables.java | 25 + .../sema/patch/FrontendPatchTransaction.java | 62 +++ .../patch/FrontendPublishedFactTypeGuard.java | 129 +++++ .../sema/patch/FrontendTopBindingPatch.java | 21 + .../sema/patch/FrontendVarTypePostPatch.java | 21 + .../FrontendVisibleValueResolver.java | 36 +- .../sema/FrontendAnalysisDataTest.java | 157 ++++++ .../sema/FrontendInterfacePhaseTest.java | 7 +- .../sema/FrontendSuiteResolverTest.java | 497 +++++++++++++++++ .../FrontendTypedLexicalEnvironmentTest.java | 275 ++++++++++ .../FrontendWindowPublicationSurfaceTest.java | 1 + .../FrontendVisibleValueResolverTest.java | 88 +++ 32 files changed, 2650 insertions(+), 115 deletions(-) delete mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendAnalysisPatch.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendOwnerRetryMemo.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendStatementResolver.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteContext.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolver.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendAnalysisPatch.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendChainBindingPatch.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendExprTypePatch.java rename src/main/java/gd/script/gdcc/frontend/sema/{ => patch}/FrontendLocalSlotTypeUpdate.java (75%) create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendLocalTypeStabilizationPatch.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendOwnerPatch.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendPatchTables.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendPatchTransaction.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendPublishedFactTypeGuard.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendTopBindingPatch.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendVarTypePostPatch.java create mode 100644 src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java create mode 100644 src/test/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironmentTest.java diff --git a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md index 34adacae..7d90b1ab 100644 --- a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md +++ b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md @@ -583,12 +583,12 @@ Compiler-only guard payload matrix: | Publication surface | User-visible type-bearing payload | 当前 guard 覆盖 | 当前绕过点 / legacy-only API | 后续 shared walker 要求 | | --- | --- | --- | --- | --- | -| `expressionTypes()` | `FrontendExpressionType.publishedType()` | `FrontendAnalysisData.applyPatch(...)` 与 `FrontendWindowPublicationSurface.expressionTypes().put(...)` 检查 published type | `updateExpressionTypes(...)` 整表替换和 direct stable table mutation 是 legacy-only,当前不复用 shared walker | overlay write、patch commit、overlay flush、保留的 source-facing whole-table publish 均递归检查 published type | -| `slotTypes()` | local / parameter / iterator slot `GdType` value | `applyPatch(...)`、window `slotTypes().put(...)`、`FrontendLocalSlotTypeUpdate` 检查 compiler-only;local slot update 也拒绝 void | `updateSlotTypes(...)` 整表替换、`analysisData.slotTypes().put/clear` direct mutation、VarTypePost legacy contamination path 是 legacy-only | 所有 source-facing slot publication 必须先通过同一 field walker;VarTypePost 新 procedure 只能写 scratch/overlay | -| `localSlotTypeUpdates()` | `FrontendLocalSlotTypeUpdate.type()` | `FrontendWindowPublicationSurface.addLocalSlotTypeUpdate(...)` 与 `FrontendAnalysisData.validateLocalSlotTypeUpdates(...)` 检查 compiler-only / void / conflict | 旧 `FrontendAnalysisPatch` 可携带跨 owner payload,不能作为 SuiteResolver production carrier | `FrontendLocalTypeStabilizationPatch` 独占携带,并在 transaction apply 前后复用 shared guard | -| `symbolBindings()` | `FrontendBinding.resolvedValue().type()` | local slot update 后的 `refreshPublishedLocalBindingPayloads(...)` 覆盖刷新路径 | `updateSymbolBindings(...)`、`applyPatch(...)` 里的 direct binding merge、`analysisData.symbolBindings().put` 目前可绕过 compiler-only payload 检查 | 阶段 C 至少先关闭 `resolvedValue.type()` direct patch / overlay bypass,再允许 production SuiteResolver export | -| `resolvedMembers()` | `FrontendResolvedMember.receiverType()` / `resultType()` | 当前没有统一 compiler-only payload guard;只做 conflict equality | `updateResolvedMembers(...)`、`applyPatch(...)` merge、direct stable table mutation 都是 legacy-only publication surface | shared walker 必须递归检查 receiver/result type,且区分 hidden compiler state 与 user-visible result | -| `resolvedCalls()` | `FrontendResolvedCall.receiverType()` / `returnType()` / `argumentTypes()` / callable boundary parameter types | 当前没有统一 compiler-only payload guard;只做 conflict equality | `updateResolvedCalls(...)`、`applyPatch(...)` merge、direct stable table mutation 都是 legacy-only publication surface | shared walker 必须覆盖 call return、receiver、argument 与 exact callable boundary 参数类型 | +| `expressionTypes()` | `FrontendExpressionType.publishedType()` | 阶段 C 起由 `FrontendPublishedFactTypeGuard` 统一覆盖 overlay write / flush / export、`applyPatch(...)`、legacy window put 与 `updateExpressionTypes(...)` | direct stable table mutation 仍是 legacy-only 可变引用 bypass,production SuiteResolver path 不得使用 | 新增 typed overlay 与 patch transaction 测试锚定 same walker 覆盖 | +| `slotTypes()` | local / parameter / iterator slot `GdType` value | 阶段 C 起由 `FrontendPublishedFactTypeGuard` 覆盖 overlay write / flush / export、owner patch、legacy patch、window put 与 `updateSlotTypes(...)`;local slot update 仍额外拒绝 void | `analysisData.slotTypes().put/clear` direct mutation 与 VarTypePost legacy contamination path 是 legacy-only bypass | VarTypePost 新 procedure 后续只能写 overlay,不得复用旧 `analyzeInWindow(...)` contamination path | +| `localSlotTypeUpdates()` | `FrontendLocalSlotTypeUpdate.type()` | 阶段 C 起迁入 `gd.script.gdcc.frontend.sema.patch`,由 `FrontendPublishedFactTypeGuard` 覆盖 overlay / owner patch / legacy patch;`FrontendAnalysisData` 继续负责 void / exact rewrite / declaration conflict | legacy `FrontendAnalysisPatch` 仍保留为 compatibility carrier,但 production suite export 使用 `FrontendLocalTypeStabilizationPatch` | `FrontendLocalTypeStabilizationPatch` 独占携带,并在 transaction apply 前后复用 shared guard | +| `symbolBindings()` | `FrontendBinding.resolvedValue().type()` | 阶段 C 起由 `FrontendPublishedFactTypeGuard` 覆盖 overlay write / flush / export、owner patch、legacy patch、window put 与 `updateSymbolBindings(...)`;local slot refresh 仍有额外 payload guard | `analysisData.symbolBindings().put` direct mutation 是 legacy-only 可变引用 bypass,production SuiteResolver path 不得使用 | `FrontendTopBindingPatch` 是 production suite export 的唯一 top-binding carrier | +| `resolvedMembers()` | `FrontendResolvedMember.receiverType()` / `resultType()` | 阶段 C 起由 `FrontendPublishedFactTypeGuard` 覆盖 overlay write / flush / export、owner patch、legacy patch、window put 与 `updateResolvedMembers(...)` | direct stable table mutation 仍是 legacy-only 可变引用 bypass,production SuiteResolver path 不得使用 | `FrontendChainBindingPatch` 是 production suite export 的 member carrier | +| `resolvedCalls()` | `FrontendResolvedCall.receiverType()` / `returnType()` / `argumentTypes()` / callable boundary parameter types | 阶段 C 起由 `FrontendPublishedFactTypeGuard` 覆盖 overlay write / flush / export、owner patch、legacy patch、window put 与 `updateResolvedCalls(...)` | direct stable table mutation 仍是 legacy-only 可变引用 bypass,production SuiteResolver path 不得使用 | chain-owned call 与 bare-call 由 `FrontendChainBindingPatch` / `FrontendExprTypePatch` 分 owner 携带 | 验收细则: @@ -619,6 +619,7 @@ Compiler-only guard payload matrix: - 新增 `FrontendTypedLexicalBaseline`,记录 interface 层可确定的 source-facing typed baseline。 - 新增 `FrontendSuiteEntryRoots`,列出 body layer 可进入的 callable/property initializer/supported block roots。 - 保持 skeleton/scope/variable analyzer 的 public contract 不变。 +- 阶段 B 的 `FrontendInterfaceSurface` 暂时仍是可手动构建的数据结构,不是 `FrontendSemanticAnalyzer.analyze()` 主 pipeline 的真实产物;阶段 D 引入 `SuiteResolver` 时必须补齐主流程集成。 验收细则: @@ -629,6 +630,17 @@ Compiler-only guard payload matrix: ### 阶段 C:引入 `TypedLexicalEnvironment` overlay +状态同步(2026-07-07): + +- [x] C1 新增 `FrontendTypedLexicalEnvironment`,包装当前 `Scope`、stable `FrontendAnalysisData`、parent environment、statement pending overlay 与 current-suite committed overlay;pending write / flush / export 均不提前修改 stable side table 或 `BlockScope`。 +- [x] C2 新增 `gd.script.gdcc.frontend.sema.patch` 包,迁入 legacy `FrontendAnalysisPatch` / `FrontendLocalSlotTypeUpdate`,并新增 `FrontendOwnerPatch`、`FrontendTopBindingPatch`、`FrontendLocalTypeStabilizationPatch`、`FrontendChainBindingPatch`、`FrontendExprTypePatch`、`FrontendVarTypePostPatch` 与 `FrontendPatchTransaction`。 +- [x] C3 新增 `FrontendPublishedFactTypeGuard` shared walker,覆盖 binding/member/call/expression/slot/update 六类 source-facing typed payload;`FrontendAnalysisData.applyPatch(...)`、owner patch、legacy window scratch put 与保留的 `updateXxx(...)` whole-table publish 均接入该 guard。 +- [x] C4 Overlay 写入 API 必须显式传入 `FrontendSemanticStage` owner metadata;错误 owner fail-fast,local slot overlay 继续执行 `Variant -> exact` / exact-same-only / no-void / no-compiler-only 规则。 +- [x] C5 `flushStatementFacts()` 只把 pending overlay 合并到 committed overlay;`exportPatchTransaction()` 只导出 fixed-order per-owner patch transaction,并由 `FrontendPatchTransaction` 拒绝 owner 顺序回退或重复 owner。 +- [x] C6 新增 `FrontendOwnerRetryMemo`,表达 owner-local retry 中间 fact 不属于 typed lexical environment,不参与 flush / export。 +- [x] C7 `FrontendVisibleValueResolver` 新增 overlay-aware `resolve(request, environment)` overload;它只在 declaration-order / self-reference filter 之后替换 returned local 的 effective type,不绕过 future-local filtered hit。 +- [ ] C8 Expression semantic support 与 chain reduction facade 的 dependency-type callback 尚未替换为 explicit environment lookup;这是阶段 E owner procedure 重写的一部分,本阶段只提供可接入入口。 + 实施内容: - 新增 `FrontendTypedLexicalEnvironment`,包装 `Scope`、suite-local typed facts、pending local slot updates。 @@ -652,12 +664,23 @@ Compiler-only guard payload matrix: ### 阶段 D:实现 body `SuiteResolver` 骨架 +状态同步(2026-07-08): + +- [x] D1 新增 `FrontendSuiteResolver` skeleton;默认 owner procedures 为 no-op,只从 `FrontendInterfaceSurface.suiteEntryRoots()` 进入 callable / property initializer / supported child block roots,并在 suite 收敛后通过 `FrontendTypedLexicalEnvironment.exportPatchTransaction()` apply stable facts。 +- [x] D2 `FrontendSemanticAnalyzer.analyze()` 已在 skeleton / scope / variable inventory 之后构建真实 `FrontendInterfaceSurface`,并在 legacy body analyzers 之前传入 `FrontendSuiteResolver`;legacy analyzer 顺序保留,SuiteResolver 目前是 shadow no-op body path。 +- [x] D3 新增 `FrontendSuiteContext`,显式携带 source path、callable owner、current block scope / scope、restriction、static context、property initializer context、interface surface、typed lexical environment、analysis data、diagnostic manager 与 class registry。 +- [x] D4 新增 `FrontendStatementResolver` dispatcher;supported roots 按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 固定顺序调用 injected owner hooks,并在每个 statement/header boundary flush pending overlay。 +- [x] D5 `if` / `elif` / `else` / `while` 的 Phase D traversal 已建立 header-first / child-suite-after-header 形状;`for` / `match` / block-local `const` 只触发 fail-closed unsupported hook,不进入 body。 +- [x] D6 新增并运行 `FrontendSuiteResolverTest` 及相关 targeted regressions,覆盖 source-order、owner order、header-before-body、unsupported body not entered、overlay export boundary、main pipeline surface hand-off;`FrontendInterfacePhaseTest`、`FrontendTypedLexicalEnvironmentTest`、`FrontendVisibleValueResolverTest`、`FrontendAnalysisDataTest`、`FrontendSemanticAnalyzerFrameworkTest`、`FrontendVariableAnalyzerTest`、`FrontendLocalTypeStabilizationAnalyzerTest`、`FrontendVarTypePostAnalyzerTest`、`FrontendChainBindingAnalyzerTest`、`FrontendExprTypeAnalyzerTest` 均通过。 + 实施内容: - 新增 `FrontendSuiteResolver`。 +- 在 `FrontendSemanticAnalyzer.analyze()` 主 pipeline 中,于 skeleton / scope / variable inventory 之后构建 `FrontendInterfaceSurface`,并把它作为 `FrontendSuiteResolver` 的输入;不能继续只依赖测试或手动 fixture 构造 interface surface。 - 新增 `FrontendSuiteContext`,携带 source path、callable owner、current block scope、restriction、static context、property initializer context、gate registry、typed lexical environment。 - 新增 `FrontendStatementResolver` 或等价 statement dispatcher。 - 新增 owner procedure registry / dispatch contract,但第一版只接线 no-op 或 fail-closed hook,不复用 whole-module `analyzeInWindow(...)`。 +- `FrontendSuiteContext` / owner procedure registry 必须把 `FrontendTypedLexicalEnvironment` 作为显式依赖暴露给后续 owner procedure;阶段 C8 的 expression semantic support 与 chain reduction facade 接入点由阶段 E 真正替换,阶段 D 只建立可传递该依赖的骨架。 - 第一版只遍历当前已支持 body 结构:ordinary statements、`if` / `elif` / `else`、`while`、property initializer。 - `for` / `match` / lambda / block-local `const` 继续 fail-closed。 - 暂不删除 legacy whole-phase analyzer wrappers。 @@ -667,6 +690,7 @@ Compiler-only guard payload matrix: 验收细则: - `SuiteResolver` 只进入 `FrontendSuiteEntryRoots` 标记为可进入的 body。 +- `FrontendSemanticAnalyzer.analyze()` 必须发布并传递真实的 `FrontendInterfaceSurface` 给 `SuiteResolver`;targeted test 必须证明 interface surface 是主 pipeline 中 skeleton / scope / variable inventory 之后、body suite 解析之前产生的。 - source-order traversal 与 AST statement order 一致。 - child block 递归顺序为 header 先解析,body 后解析。 - `FrontendVisibleValueResolver` 的 declaration-after-use 与 self-reference 测试继续通过。 @@ -684,6 +708,7 @@ Compiler-only guard payload matrix: - `FrontendStatementResolver` 必须按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 的顺序调用 owner procedure。 - 保留现有 analyzer class 名称和 owner 边界。 - `FrontendLocalTypeStabilizationAnalyzer` 不再通过整模块 legacy direct phase 表达 source-order 行为。 +- 完成阶段 C8:`FrontendExpressionSemanticSupport`、`FrontendChainReductionFacade` 与相关 chain-head / dependency-type callback 必须改为显式读取 `FrontendTypedLexicalEnvironment`,替换 stable-data / analyzer-local callback 的 dependency type 读取路径。 - `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 继续 guard-only。 - Chain / argument retry 的中间 expression facts 必须保存在 `FrontendOwnerRetryMemo` 或等价 owner-local memo,不得写入 `expressionTypes()` overlay 后再以 narrowing / status upgrade 方式覆盖。 @@ -693,6 +718,7 @@ Compiler-only guard payload matrix: - Local stabilization:把 `AstWalkerLocalTypeStabilizer` 的 eligible `:=` initializer 解析改为立即写 pending overlay,使同 statement 后续 owner 与后续 statement 可按 flush 规则读取;禁止继续依赖整模块 direct phase 更新 `BlockScope`。 - Chain binding:把 chain reduction 的 dependency type 回调改为读取 `FrontendTypedLexicalEnvironment` 与 owner-local memo,而不是 analyzer-local stable side table snapshot。 - Expr typing:把 expression fact 发布改为 statement-local final fact publication;父索引、duplicate-report state、retry state 都必须显式化,不能藏在 whole-module walker 字段里。 +- Expression semantic support / chain reduction facade:identifier binding、receiver type、argument type、bare-call callee type 与 nested dependency type 读取必须先走 owner-local memo / pending overlay / committed overlay 的 effective view,再回退 stable side table;不得继续把 `FrontendAnalysisData.symbolBindings()` / `expressionTypes()` 作为 body procedure 的第一读取源。 - Var type post:把 slot type publication 改为消费当前 statement / current-suite typed facts 的 statement-local publication,不再通过整表 `updateSlotTypes(...)` 表达 body 结果,也不得复用旧 `analyzeInWindow(...)` 的“stable `slotTypes()` clear/write 后再复制到 window”模式。 - Resolver request:request-domain gate、AST boundary gate 与 current-scope gate 的创建必须由 `FrontendSuiteContext` 统一完成,不能继续由各 analyzer 手写 deferred domain。 @@ -705,6 +731,8 @@ Compiler-only guard payload matrix: - rejected shadow declaration 不污染 parent slot。 - nested chain / argument retry 保持读己写能力,但这个能力只存在于 owner-local memo;同一 expression / step key 在 statement flush 和 suite export 中只产生一条最终 `expressionTypes()` fact。 - retry 过程中出现的任何非最终 expression fact(含 `DEFERRED` 代理类型、暂定 `Variant`、中间 status / detailReason)不得先发布到 pending overlay、committed overlay 或 stable side table 再被最终 fact 覆盖。 +- C8 回归测试必须证明:当 stable side table / baseline 仍是 `Variant` 而 pending 或 committed overlay 已有 exact local slot fact 时,`FrontendExpressionSemanticSupport` 与 `FrontendChainReductionFacade` 的 dependency-type callback 消费 overlay exact type,而不是 stable `Variant`。 +- C8 negative path 必须证明:support / facade 不能直接读取 retry memo 以外的非最终 expression fact,也不能绕过 `FrontendTypedLexicalEnvironment` 直接读取 stable-only side table 后发布 stale receiver / argument / bare-call result。 - owner 以外的 procedure 不能写对应 side table 或 slot update。 - 每个 owner 子过程的 suite export 以独立 per-owner patch 出现在 transaction 中;transaction coordinator 按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 顺序 apply。 - Var type post procedure 在 statement flush 前不得改变 stable `slotTypes()`;targeted test 必须在 procedure 运行、flush、suite export 三个点分别断言 stable table 只在 export/apply patch 后变化。 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 ca00c1ca..a5016dd3 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendAnalysisData.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendAnalysisData.java @@ -2,11 +2,14 @@ import dev.superice.gdparser.frontend.ast.Node; import gd.script.gdcc.exception.FrontendAnalysisPatchException; +import gd.script.gdcc.frontend.sema.patch.FrontendAnalysisPatch; +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.GdCompilerType; import gd.script.gdcc.type.GdType; import gd.script.gdcc.type.GdVariantType; import gd.script.gdcc.type.GdVoidType; @@ -99,24 +102,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, @@ -131,34 +139,69 @@ public void updateSlotTypes(@NotNull FrontendAstSideTable slotTypes) { /// the stable publication surface. public void applyPatch(@NotNull FrontendAnalysisPatch patch) { var checkedPatch = Objects.requireNonNull(patch, "patch must not be null"); - checkPatchDoesNotLeakCompilerOnlyTypes(checkedPatch); - var validatedLocalSlotUpdates = validateLocalSlotTypeUpdates(checkedPatch); - checkPatchConflicts(symbolBindings, checkedPatch.symbolBindings(), "symbolBindings", FrontendAnalysisData::sameBinding); + FrontendPublishedFactTypeGuard.checkAnalysisPatch(checkedPatch); + applyPatchFields( + checkedPatch.stage(), + checkedPatch.symbolBindings(), + checkedPatch.resolvedMembers(), + checkedPatch.resolvedCalls(), + checkedPatch.expressionTypes(), + checkedPatch.slotTypes(), + checkedPatch.localSlotTypeUpdates() + ); + } + + /// Applies one Phase C single-owner patch without replacing any stable side-table reference. + 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, - checkedPatch.resolvedMembers(), + patchResolvedMembers, "resolvedMembers", FrontendAnalysisData::sameResolvedMember ); checkPatchConflicts( resolvedCalls, - checkedPatch.resolvedCalls(), + patchResolvedCalls, "resolvedCalls", FrontendAnalysisData::sameResolvedCall ); checkPatchConflicts( expressionTypes, - checkedPatch.expressionTypes(), + patchExpressionTypes, "expressionTypes", FrontendAnalysisData::sameExpressionType ); - checkPatchConflicts(slotTypes, checkedPatch.slotTypes(), "slotTypes", FrontendAnalysisData::sameType); + checkPatchConflicts(slotTypes, patchSlotTypes, "slotTypes", FrontendAnalysisData::sameType); - mergeSideTable(symbolBindings, checkedPatch.symbolBindings()); - mergeSideTable(resolvedMembers, checkedPatch.resolvedMembers()); - mergeSideTable(resolvedCalls, checkedPatch.resolvedCalls()); - mergeSideTable(expressionTypes, checkedPatch.expressionTypes()); - mergeSideTable(slotTypes, checkedPatch.slotTypes()); + mergeSideTable(symbolBindings, patchSymbolBindings); + mergeSideTable(resolvedMembers, patchResolvedMembers); + mergeSideTable(resolvedCalls, patchResolvedCalls); + mergeSideTable(expressionTypes, patchExpressionTypes); + mergeSideTable(slotTypes, patchSlotTypes); for (var validatedUpdate : validatedLocalSlotUpdates) { applyLocalSlotTypeUpdate(validatedUpdate); } @@ -257,19 +300,20 @@ public void refreshPublishedLocalBindingPayloads( } private @NotNull List validateLocalSlotTypeUpdates( - @NotNull FrontendAnalysisPatch patch + @NotNull FrontendSemanticStage stage, + @NotNull List localSlotTypeUpdates ) { - if (patch.localSlotTypeUpdates().isEmpty()) { + if (localSlotTypeUpdates.isEmpty()) { return List.of(); } - if (patch.stage() != FrontendSemanticStage.LOCAL_TYPE_STABILIZATION) { + if (stage != FrontendSemanticStage.LOCAL_TYPE_STABILIZATION) { throw patchFailure( "Only LOCAL_TYPE_STABILIZATION patches may publish local slot type updates, but got " - + patch.stage() + + stage ); } - var validatedUpdates = new ArrayList(patch.localSlotTypeUpdates().size()); - for (var update : patch.localSlotTypeUpdates()) { + var validatedUpdates = new ArrayList(localSlotTypeUpdates.size()); + for (var update : localSlotTypeUpdates) { validatedUpdates.add(validateLocalSlotTypeUpdate(update, validatedUpdates)); } return List.copyOf(validatedUpdates); @@ -387,23 +431,8 @@ private static void mergeSideTable( } } - private void checkPatchDoesNotLeakCompilerOnlyTypes(@NotNull FrontendAnalysisPatch patch) { - for (var expressionType : patch.expressionTypes().values()) { - checkNoCompilerOnlyLeak(expressionType.publishedType(), "expressionTypes() published type"); - } - for (var slotType : patch.slotTypes().values()) { - checkNoCompilerOnlyLeak(slotType, "slotTypes() value"); - } - } - static void checkNoCompilerOnlyLeak(@Nullable GdType type, @NotNull String fieldName) { - if (type instanceof GdCompilerType compilerOnlyType) { - throw patchFailure( - fieldName - + " leaked compiler-only type " - + compilerOnlyType.getTypeName() - ); - } + FrontendPublishedFactTypeGuard.checkNoCompilerOnlyLeak(type, fieldName); } static void checkNoVoidLocalSlotType(@NotNull GdType type, @NotNull String localName) { diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendAnalysisPatch.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendAnalysisPatch.java deleted file mode 100644 index 186a3928..00000000 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendAnalysisPatch.java +++ /dev/null @@ -1,44 +0,0 @@ -package gd.script.gdcc.frontend.sema; - -import gd.script.gdcc.type.GdType; -import org.jetbrains.annotations.NotNull; - -import java.util.List; -import java.util.Objects; - -/// Incremental semantic facts published by one segmented frontend stage. -/// -/// Patch contents are copied on creation so later scratch mutations cannot silently change the -/// already-drained publication payload. -public record FrontendAnalysisPatch( - @NotNull FrontendSemanticStage stage, - @NotNull FrontendAstSideTable symbolBindings, - @NotNull FrontendAstSideTable resolvedMembers, - @NotNull FrontendAstSideTable resolvedCalls, - @NotNull FrontendAstSideTable expressionTypes, - @NotNull FrontendAstSideTable slotTypes, - @NotNull List localSlotTypeUpdates -) { - public FrontendAnalysisPatch { - Objects.requireNonNull(stage, "stage must not be null"); - symbolBindings = copySideTable(symbolBindings, "symbolBindings"); - resolvedMembers = copySideTable(resolvedMembers, "resolvedMembers"); - resolvedCalls = copySideTable(resolvedCalls, "resolvedCalls"); - expressionTypes = copySideTable(expressionTypes, "expressionTypes"); - slotTypes = copySideTable(slotTypes, "slotTypes"); - localSlotTypeUpdates = List.copyOf(Objects.requireNonNull( - localSlotTypeUpdates, - "localSlotTypeUpdates must not be null" - )); - } - - private static @NotNull FrontendAstSideTable copySideTable( - 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/FrontendOwnerRetryMemo.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendOwnerRetryMemo.java new file mode 100644 index 00000000..1bed9d3d --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendOwnerRetryMemo.java @@ -0,0 +1,56 @@ +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.Objects; + +/// Owner-local transient retry facts for chain/expression reduction. +/// +/// These facts are intentionally not part of `FrontendTypedLexicalEnvironment`: they are readable +/// only by the current owner procedure and are discarded before statement flush or suite export. +public final class FrontendOwnerRetryMemo { + private final @NotNull FrontendAstSideTable expressionTypes = new FrontendAstSideTable<>(); + private final @NotNull FrontendAstSideTable resolvedMembers = new FrontendAstSideTable<>(); + private final @NotNull FrontendAstSideTable resolvedCalls = new FrontendAstSideTable<>(); + + public void putExpressionType(@NotNull Node astNode, @NotNull FrontendExpressionType expressionType) { + expressionTypes.put( + Objects.requireNonNull(astNode, "astNode must not be null"), + Objects.requireNonNull(expressionType, "expressionType must not be null") + ); + } + + public @Nullable FrontendExpressionType expressionType(@NotNull Node astNode) { + return expressionTypes.get(Objects.requireNonNull(astNode, "astNode must not be null")); + } + + public void putResolvedMember(@NotNull Node astNode, @NotNull FrontendResolvedMember member) { + resolvedMembers.put( + Objects.requireNonNull(astNode, "astNode must not be null"), + Objects.requireNonNull(member, "member must not be null") + ); + } + + public @Nullable FrontendResolvedMember resolvedMember(@NotNull Node astNode) { + return resolvedMembers.get(Objects.requireNonNull(astNode, "astNode must not be null")); + } + + public void putResolvedCall(@NotNull Node astNode, @NotNull FrontendResolvedCall call) { + resolvedCalls.put( + Objects.requireNonNull(astNode, "astNode must not be null"), + Objects.requireNonNull(call, "call must not be null") + ); + } + + public @Nullable FrontendResolvedCall resolvedCall(@NotNull Node astNode) { + return resolvedCalls.get(Objects.requireNonNull(astNode, "astNode must not be null")); + } + + public void clear() { + expressionTypes.clear(); + resolvedMembers.clear(); + resolvedCalls.clear(); + } +} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendSuiteEntryRoots.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendSuiteEntryRoots.java index c5958af7..43b3c9c4 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendSuiteEntryRoots.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendSuiteEntryRoots.java @@ -4,9 +4,13 @@ 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; @@ -17,12 +21,22 @@ public record FrontendSuiteEntryRoots( @NotNull List callableOwners, @NotNull List propertyInitializers, - @NotNull List supportedBlocks + @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) { @@ -37,9 +51,24 @@ 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/FrontendTypedLexicalEnvironment.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java new file mode 100644 index 00000000..fc9814b4 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java @@ -0,0 +1,503 @@ +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 future 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`. +public final class FrontendTypedLexicalEnvironment { + private final @NotNull Scope scope; + private final @NotNull FrontendAnalysisData stableData; + private final @Nullable FrontendTypedLexicalEnvironment parent; + 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 = Objects.requireNonNull(scope, "scope must not be null"); + this.stableData = Objects.requireNonNull(stableData, "stableData must not be null"); + this.parent = parent; + } + + public @NotNull Scope scope() { + return scope; + } + + public @Nullable FrontendTypedLexicalEnvironment parent() { + return parent; + } + + public @Nullable FrontendBinding symbolBinding(@NotNull Node astNode) { + return firstNonNull( + pendingFacts.symbolBindings.get(astNode), + committedFacts.symbolBindings.get(astNode), + stableData.symbolBindings().get(astNode), + 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) { + return firstNonNull( + pendingFacts.slotTypes.get(astNode), + committedFacts.slotTypes.get(astNode), + stableData.slotTypes().get(astNode), + parent != null ? parent.slotType(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; + } + } + 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 current-statement facts into the suite overlay without touching stable data or scopes. + public void flushStatementFacts() { + pendingFacts.checkNoCompilerOnlyLeaks(); + committedFacts.mergeFrom(pendingFacts); + pendingFacts.clear(); + } + + /// Exports committed suite facts as ordered single-owner patches. Stable data remains unchanged. + 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 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/FrontendWindowAnalysisContext.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendWindowAnalysisContext.java index 920f6efc..e7df1b9f 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendWindowAnalysisContext.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendWindowAnalysisContext.java @@ -1,5 +1,6 @@ package gd.script.gdcc.frontend.sema; +import gd.script.gdcc.frontend.sema.patch.FrontendAnalysisPatch; import org.jetbrains.annotations.NotNull; import java.util.Objects; diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendWindowPublicationSurface.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendWindowPublicationSurface.java index deba2a36..2ad2742f 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendWindowPublicationSurface.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendWindowPublicationSurface.java @@ -1,6 +1,9 @@ package gd.script.gdcc.frontend.sema; import dev.superice.gdparser.frontend.ast.Node; +import gd.script.gdcc.frontend.sema.patch.FrontendAnalysisPatch; +import gd.script.gdcc.frontend.sema.patch.FrontendLocalSlotTypeUpdate; +import gd.script.gdcc.frontend.sema.patch.FrontendPublishedFactTypeGuard; import gd.script.gdcc.type.GdType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -29,34 +32,31 @@ public FrontendWindowPublicationSurface(@NotNull FrontendAnalysisData stableData checkedStableData.symbolBindings(), FrontendAnalysisData::sameBinding, "symbolBindings", - null + FrontendPublishedFactTypeGuard::checkBinding ); resolvedMembers = new WindowSideTableView<>( checkedStableData.resolvedMembers(), FrontendAnalysisData::sameResolvedMember, "resolvedMembers", - null + FrontendPublishedFactTypeGuard::checkResolvedMember ); resolvedCalls = new WindowSideTableView<>( checkedStableData.resolvedCalls(), FrontendAnalysisData::sameResolvedCall, "resolvedCalls", - null + FrontendPublishedFactTypeGuard::checkResolvedCall ); expressionTypes = new WindowSideTableView<>( checkedStableData.expressionTypes(), FrontendAnalysisData::sameExpressionType, "expressionTypes", - value -> FrontendAnalysisData.checkNoCompilerOnlyLeak( - value.publishedType(), - "expressionTypes() published type" - ) + FrontendPublishedFactTypeGuard::checkExpressionType ); slotTypes = new WindowSideTableView<>( checkedStableData.slotTypes(), FrontendAnalysisData::sameType, "slotTypes", - value -> FrontendAnalysisData.checkNoCompilerOnlyLeak(value, "slotTypes() value") + value -> FrontendPublishedFactTypeGuard.checkNoCompilerOnlyLeak(value, "slotTypes() value") ); } @@ -85,10 +85,7 @@ public void addLocalSlotTypeUpdate(@NotNull FrontendLocalSlotTypeUpdate update) requireOpen(); var checkedUpdate = Objects.requireNonNull(update, "update must not be null"); FrontendAnalysisData.checkNoVoidLocalSlotType(checkedUpdate.type(), checkedUpdate.name()); - FrontendAnalysisData.checkNoCompilerOnlyLeak( - checkedUpdate.type(), - "local slot update for '" + checkedUpdate.name() + "'" - ); + FrontendPublishedFactTypeGuard.checkLocalSlotTypeUpdate(checkedUpdate); localSlotTypeUpdates.add(checkedUpdate); } 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 index 1afe243f..b86497a1 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendInterfacePhase.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendInterfacePhase.java @@ -37,6 +37,7 @@ import gd.script.gdcc.scope.Scope; import org.jetbrains.annotations.NotNull; +import java.nio.file.Path; import java.util.ArrayList; import java.util.IdentityHashMap; import java.util.List; @@ -61,7 +62,7 @@ public class FrontendInterfacePhase { analysisData.diagnostics(); var builder = new InterfaceSurfaceBuilder(analysisData.scopesByAst()); for (var relation : moduleSkeleton.sourceClassRelations()) { - builder.walk(relation.unit().ast()); + builder.walk(relation.unit().path(), relation.unit().ast()); } return builder.build(); } @@ -71,6 +72,7 @@ private static final class InterfaceSurfaceBuilder implements ASTNodeHandler { private final @NotNull ASTWalker astWalker; private final @NotNull Map> declarationsByBodyRoot = new IdentityHashMap<>(); + private final @NotNull Map sourcePathsByEntryRoot = new IdentityHashMap<>(); private final @NotNull FrontendInventoryGateRegistry.Builder gateRegistryBuilder = FrontendInventoryGateRegistry.builder(); private final @NotNull FrontendTypedLexicalBaseline.Builder typedBaselineBuilder = @@ -79,6 +81,7 @@ private static final class InterfaceSurfaceBuilder implements ASTNodeHandler { 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) { @@ -86,7 +89,8 @@ private InterfaceSurfaceBuilder(@NotNull FrontendAstSideTable scopesByAst astWalker = new ASTWalker(this); } - private void walk(@NotNull SourceFile sourceFile) { + private void walk(@NotNull Path sourcePath, @NotNull SourceFile sourceFile) { + currentSourcePath = Objects.requireNonNull(sourcePath, "sourcePath must not be null"); astWalker.walk(sourceFile); } @@ -95,7 +99,12 @@ private void walk(@NotNull SourceFile sourceFile) { new FrontendBodyDeclarationIndex(declarationsByBodyRoot), gateRegistryBuilder.build(), typedBaselineBuilder.build(), - new FrontendSuiteEntryRoots(callableOwners, propertyInitializers, supportedBlocks) + new FrontendSuiteEntryRoots( + callableOwners, + propertyInitializers, + supportedBlocks, + sourcePathsByEntryRoot + ) ); } @@ -258,6 +267,7 @@ private void recordCallable( return; } callableOwners.add(callableOwner); + sourcePathsByEntryRoot.put(callableOwner, currentSourcePath); recordParameterBaselines(parameters); enterSupportedBlock(body); } @@ -280,6 +290,7 @@ private void recordPropertyInitializer(@NotNull VariableDeclaration variableDecl return; } propertyInitializers.add(variableDeclaration); + sourcePathsByEntryRoot.put(variableDeclaration, currentSourcePath); astWalker.walk(variableDeclaration.value()); } @@ -290,6 +301,7 @@ private void enterSupportedBlock(@NotNull Block block) { return; } supportedBlocks.add(block); + sourcePathsByEntryRoot.put(block, currentSourcePath); var previousDeclarations = currentBodyDeclarations; var bodyDeclarations = new ArrayList(); currentBodyDeclarations = bodyDeclarations; 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 index f51d6d98..75b81910 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendLocalTypeStabilizationAnalyzer.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendLocalTypeStabilizationAnalyzer.java @@ -37,7 +37,7 @@ 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.FrontendLocalSlotTypeUpdate; +import gd.script.gdcc.frontend.sema.patch.FrontendLocalSlotTypeUpdate; import gd.script.gdcc.frontend.sema.FrontendWindowAnalysisContext; import gd.script.gdcc.frontend.sema.analyzer.support.FrontendAssignmentSemanticSupport; import gd.script.gdcc.frontend.sema.analyzer.support.FrontendChainReductionFacade; 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 2927fb47..8cd27e4a 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 @@ -41,6 +41,8 @@ public final class FrontendSemanticAnalyzer { 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; private final boolean segmentedSemanticRunner; public FrontendSemanticAnalyzer() { @@ -61,6 +63,30 @@ public FrontendSemanticAnalyzer() { ); } + public FrontendSemanticAnalyzer( + @NotNull FrontendInterfacePhase interfacePhase, + @NotNull FrontendSuiteResolver suiteResolver + ) { + 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(), + interfacePhase, + suiteResolver, + false + ); + } + public FrontendSemanticAnalyzer(@NotNull FrontendClassSkeletonBuilder classSkeletonBuilder) { this( classSkeletonBuilder, @@ -422,6 +448,8 @@ public FrontendSemanticAnalyzer( typeCheckAnalyzer, loopControlFlowAnalyzer, compileCheckAnalyzer, + new FrontendInterfacePhase(), + new FrontendSuiteResolver(), false ); } @@ -440,6 +468,8 @@ private FrontendSemanticAnalyzer( @NotNull FrontendTypeCheckAnalyzer typeCheckAnalyzer, @NotNull FrontendLoopControlFlowAnalyzer loopControlFlowAnalyzer, @NotNull FrontendCompileCheckAnalyzer compileCheckAnalyzer, + @NotNull FrontendInterfacePhase interfacePhase, + @NotNull FrontendSuiteResolver suiteResolver, boolean segmentedSemanticRunner ) { this.classSkeletonBuilder = Objects.requireNonNull(classSkeletonBuilder, "classSkeletonBuilder must not be null"); @@ -470,6 +500,8 @@ private 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"); this.segmentedSemanticRunner = segmentedSemanticRunner; } @@ -488,6 +520,8 @@ private FrontendSemanticAnalyzer( new FrontendTypeCheckAnalyzer(), new FrontendLoopControlFlowAnalyzer(), new FrontendCompileCheckAnalyzer(), + new FrontendInterfacePhase(), + new FrontendSuiteResolver(), true ); } @@ -536,6 +570,13 @@ private FrontendSemanticAnalyzer( variableAnalyzer.analyze(analysisData, diagnosticManager); analysisData.updateDiagnostics(diagnosticManager.snapshot()); + // Phase D wires the Interface surface into the main pipeline before any body semantic + // publication runs. The suite resolver is still a no-op owner skeleton, so legacy analyzers + // remain the source of stable body facts until Phase E/H replace them. + var interfaceSurface = interfacePhase.analyze(classRegistry, analysisData); + suiteResolver.resolve(interfaceSurface, classRegistry, analysisData, diagnosticManager); + analysisData.updateDiagnostics(diagnosticManager.snapshot()); + runSharedSemanticPublication(classRegistry, diagnosticManager, analysisData); // 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..5e90d14b --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendStatementResolver.java @@ -0,0 +1,159 @@ +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 new body SuiteResolver skeleton. +/// +/// Phase D only establishes traversal and owner ordering. The owner procedures are intentionally +/// injectable no-op hooks until Phase E replaces the legacy whole-module analyzers. +public class FrontendStatementResolver { + private final @NotNull OwnerProcedures ownerProcedures; + + public FrontendStatementResolver() { + this(OwnerProcedures.noop()); + } + + 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 -> resolveUnsupportedRoot(context, forStatement); + 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 resolveSupportedRoot(@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); + flushStatementBoundary(context); + } + + private void resolveUnsupportedRoot(@NotNull FrontendSuiteContext context, @NotNull Node root) { + ownerProcedures.runUnsupported(context, root); + flushStatementBoundary(context); + } + + private void flushStatementBoundary(@NotNull FrontendSuiteContext context) { + context.typedEnvironment().flushStatementFacts(); + } + + @FunctionalInterface + public interface ChildSuiteResolver { + void resolveChildSuite(@NotNull FrontendSuiteContext parentContext, @NotNull Block childBlock); + } + + public interface OwnerProcedures { + static @NotNull OwnerProcedures noop() { + return new 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..bf34e41f --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteContext.java @@ -0,0 +1,73 @@ +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.FrontendInterfaceSurface; +import gd.script.gdcc.frontend.sema.FrontendTypedLexicalEnvironment; +import gd.script.gdcc.frontend.sema.analyzer.support.FrontendPropertyInitializerSupport; +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 new body SuiteResolver skeleton. +/// +/// 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. +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 +) { + 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 + ); + return new FrontendSuiteContext( + sourcePath, + callableOwner, + block, + blockScope, + blockScope, + restriction, + staticContext, + propertyInitializerContext, + interfaceSurface, + childEnvironment, + analysisData, + diagnosticManager, + classRegistry + ); + } +} 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..3dca4f4e --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolver.java @@ -0,0 +1,181 @@ +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.VariableDeclaration; +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.FrontendInterfaceSurface; +import gd.script.gdcc.frontend.sema.FrontendTypedLexicalEnvironment; +import gd.script.gdcc.frontend.sema.analyzer.support.FrontendPropertyInitializerSupport; +import gd.script.gdcc.scope.ClassRegistry; +import gd.script.gdcc.scope.ResolveRestriction; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.nio.file.Path; +import java.util.Objects; + +/// Body-suite coordinator for the staged semantic pipeline. +/// +/// Phase D runs no-op owner procedures by default. Its job is to prove that executable body roots are +/// entered only through `FrontendInterfaceSurface` and that overlay facts are exported through an +/// ordered patch transaction instead of direct stable side-table writes. +public class FrontendSuiteResolver { + private final @NotNull FrontendStatementResolver statementResolver; + + public FrontendSuiteResolver() { + this(new FrontendStatementResolver()); + } + + 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); + } + } + + 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"); + if (!context.interfaceSurface().suiteEntryRoots().containsSupportedBlock(block)) { + return; + } + for (var statement : block.statements()) { + statementResolver.resolveStatement(context, statement, this::resolveChildSuite); + } + context.typedEnvironment().exportPatchTransaction().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 || !interfaceSurface.suiteEntryRoots().containsSupportedBlock(body)) { + return; + } + var bodyScope = analysisData.scopesByAst().get(body); + if (!(bodyScope instanceof BlockScope blockScope)) { + return; + } + var environment = new FrontendTypedLexicalEnvironment(blockScope, analysisData); + var context = new FrontendSuiteContext( + sourcePathFor(interfaceSurface, callableOwner, analysisData), + callableOwner, + body, + blockScope, + blockScope, + restrictionForCallable(callableOwner), + isStaticCallable(callableOwner), + null, + interfaceSurface, + environment, + analysisData, + diagnosticManager, + classRegistry + ); + resolveSuite(context, body); + } + + 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); + var context = new FrontendSuiteContext( + sourcePathFor(interfaceSurface, propertyInitializer, analysisData), + propertyInitializer, + null, + classScope, + null, + FrontendPropertyInitializerSupport.restrictionFor(propertyInitializer), + propertyInitializer.isStatic(), + propertyContext, + interfaceSurface, + environment, + analysisData, + diagnosticManager, + classRegistry + ); + statementResolver.resolvePropertyInitializer(context, propertyInitializer); + context.typedEnvironment().exportPatchTransaction().applyTo(analysisData); + analysisData.updateDiagnostics(diagnosticManager.snapshot()); + } + + private void resolveChildSuite(@NotNull FrontendSuiteContext parentContext, @NotNull Block childBlock) { + if (!parentContext.interfaceSurface().suiteEntryRoots().containsSupportedBlock(childBlock)) { + return; + } + var childScope = parentContext.analysisData().scopesByAst().get(childBlock); + if (!(childScope instanceof BlockScope blockScope)) { + return; + } + resolveSuite(parentContext.withChildBlock(childBlock, blockScope), childBlock); + } + + 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 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/patch/FrontendAnalysisPatch.java b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendAnalysisPatch.java new file mode 100644 index 00000000..6376b5f5 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendAnalysisPatch.java @@ -0,0 +1,46 @@ +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; +import java.util.Objects; + +/// Legacy incremental semantic facts published by one segmented frontend stage. +/// +/// Phase C production export uses `FrontendOwnerPatch` and `FrontendPatchTransaction`. This record +/// remains as the compatibility carrier for pre-existing window tests and legacy segmented shims. +public record FrontendAnalysisPatch( + @NotNull FrontendSemanticStage stage, + @NotNull FrontendAstSideTable symbolBindings, + @NotNull FrontendAstSideTable resolvedMembers, + @NotNull FrontendAstSideTable resolvedCalls, + @NotNull FrontendAstSideTable expressionTypes, + @NotNull FrontendAstSideTable slotTypes, + @NotNull List localSlotTypeUpdates +) { + public FrontendAnalysisPatch { + Objects.requireNonNull(stage, "stage must not be null"); + symbolBindings = FrontendPatchTables.copySideTable(symbolBindings, "symbolBindings"); + resolvedMembers = FrontendPatchTables.copySideTable(resolvedMembers, "resolvedMembers"); + resolvedCalls = FrontendPatchTables.copySideTable(resolvedCalls, "resolvedCalls"); + expressionTypes = FrontendPatchTables.copySideTable(expressionTypes, "expressionTypes"); + slotTypes = FrontendPatchTables.copySideTable(slotTypes, "slotTypes"); + localSlotTypeUpdates = List.copyOf(Objects.requireNonNull( + localSlotTypeUpdates, + "localSlotTypeUpdates must not be null" + )); + FrontendPublishedFactTypeGuard.checkSymbolBindings(symbolBindings); + FrontendPublishedFactTypeGuard.checkResolvedMembers(resolvedMembers); + FrontendPublishedFactTypeGuard.checkResolvedCalls(resolvedCalls); + FrontendPublishedFactTypeGuard.checkExpressionTypes(expressionTypes); + FrontendPublishedFactTypeGuard.checkSlotTypes(slotTypes); + FrontendPublishedFactTypeGuard.checkLocalSlotTypeUpdates(localSlotTypeUpdates); + } +} 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/FrontendLocalSlotTypeUpdate.java b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendLocalSlotTypeUpdate.java similarity index 75% rename from src/main/java/gd/script/gdcc/frontend/sema/FrontendLocalSlotTypeUpdate.java rename to src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendLocalSlotTypeUpdate.java index 1468f120..a06edfd6 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendLocalSlotTypeUpdate.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendLocalSlotTypeUpdate.java @@ -1,4 +1,4 @@ -package gd.script.gdcc.frontend.sema; +package gd.script.gdcc.frontend.sema.patch; import gd.script.gdcc.frontend.scope.BlockScope; import gd.script.gdcc.type.GdType; @@ -8,8 +8,9 @@ /// One local-slot type rewrite produced by local type stabilization. /// -/// The update itself is stage-scoped metadata. `FrontendAnalysisData.applyPatch(...)` owns the -/// actual scope mutation rules, compiler-only guards, and published binding payload refresh. +/// 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, 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..74fdd449 --- /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 legacy `FrontendAnalysisPatch`. +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..6d022cbb --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendPatchTransaction.java @@ -0,0 +1,62 @@ +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. +/// +/// The constructor rejects duplicate or out-of-order owners so callers cannot accidentally recreate +/// the legacy multi-owner patch shape at suite export time. +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..b60389ac --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendPublishedFactTypeGuard.java @@ -0,0 +1,129 @@ +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 checkAnalysisPatch(@NotNull FrontendAnalysisPatch patch) { + checkSymbolBindings(patch.symbolBindings()); + checkResolvedMembers(patch.resolvedMembers()); + checkResolvedCalls(patch.resolvedCalls()); + checkExpressionTypes(patch.expressionTypes()); + checkSlotTypes(patch.slotTypes()); + checkLocalSlotTypeUpdates(patch.localSlotTypeUpdates()); + } + + 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/FrontendVisibleValueResolver.java b/src/main/java/gd/script/gdcc/frontend/sema/resolver/FrontendVisibleValueResolver.java index b79a3135..effb39c5 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 @@ -6,6 +6,7 @@ import gd.script.gdcc.frontend.sema.FrontendAnalysisData; import gd.script.gdcc.frontend.sema.FrontendExecutableInventorySupport; import gd.script.gdcc.frontend.sema.FrontendModuleSkeleton; +import gd.script.gdcc.frontend.sema.FrontendTypedLexicalEnvironment; import gd.script.gdcc.frontend.scope.CallableScopeKind; import gd.script.gdcc.scope.Scope; import gd.script.gdcc.scope.ScopeLookupResult; @@ -45,6 +46,14 @@ public FrontendVisibleValueResolver(@NotNull FrontendAnalysisData analysisData) /// 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) { @@ -83,13 +92,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 +117,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); @@ -167,10 +190,11 @@ case ForStatement forStatement when (forStatement.iteratorType() == childNode ); 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)) -> + new FrontendVisibleValueDeferredBoundary( + FrontendVisibleValueDomain.MATCH_SUBTREE, + FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED + ); default -> null; }; } 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 49aa246f..b983d617 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendAnalysisDataTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendAnalysisDataTest.java @@ -5,6 +5,11 @@ 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.FrontendAnalysisPatch; +import gd.script.gdcc.frontend.sema.patch.FrontendExprTypePatch; +import gd.script.gdcc.frontend.sema.patch.FrontendLocalSlotTypeUpdate; +import gd.script.gdcc.frontend.sema.patch.FrontendPatchTransaction; +import gd.script.gdcc.frontend.sema.patch.FrontendTopBindingPatch; import gd.script.gdcc.frontend.scope.BlockScope; import gd.script.gdcc.frontend.scope.BlockScopeKind; import gd.script.gdcc.frontend.scope.CallableScope; @@ -17,6 +22,7 @@ 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; @@ -590,6 +596,157 @@ void applyPatchRejectsWrongStageLocalSlotUpdatesAndSourceFacingCompilerOnlyLeaks ); } + @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, 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(); diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendInterfacePhaseTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendInterfacePhaseTest.java index 9e092382..d9f14171 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendInterfacePhaseTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendInterfacePhaseTest.java @@ -258,10 +258,9 @@ private static void assertPendingGate( if (maybeGate == null) { fail("Expected pending gate for domain: " + domain); } - var gate = maybeGate; - assertEquals(domain, gate.deferredDomain()); - assertEquals(FrontendInventoryGateStatus.PENDING, gate.status()); - assertEquals(FrontendBodyInventoryReadiness.NOT_PUBLISHED, gate.bodyInventoryReadiness()); + assertEquals(domain, maybeGate.deferredDomain()); + assertEquals(FrontendInventoryGateStatus.PENDING, maybeGate.status()); + assertEquals(FrontendBodyInventoryReadiness.NOT_PUBLISHED, maybeGate.bodyInventoryReadiness()); } private static @NotNull FrontendFilteredValueHitReason primaryFilteredHitReason( 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..b1155cc6 --- /dev/null +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java @@ -0,0 +1,497 @@ +package gd.script.gdcc.frontend.sema; + +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.LambdaExpression; +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.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.FrontendSemanticAnalyzer; +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.gdextension.ExtensionApiLoader; +import gd.script.gdcc.scope.ClassRegistry; +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.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.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class FrontendSuiteResolverTest { + @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 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 unsupportedTypedDependentBodiesRemainFailClosed() 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() + ); + + assertFalse(surface.suiteEntryRoots().containsSupportedBlock(forStatement.body())); + assertFalse(surface.suiteEntryRoots().containsSupportedBlock(matchStatement.sections().getFirst().body())); + assertFalse(surface.suiteEntryRoots().containsSupportedBlock(lambdaExpression.body())); + assertTrue(ownerProcedures.unsupportedRoots().contains(forStatement)); + assertTrue(ownerProcedures.unsupportedRoots().contains(matchStatement)); + assertTrue(ownerProcedures.unsupportedRoots().contains(answer)); + assertFalse(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 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()); + } + + private static void resolveWith( + @NotNull PhaseInput phaseInput, + @NotNull RecordingOwnerProcedures 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 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 @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 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 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 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(); + } + + 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..098beba7 --- /dev/null +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironmentTest.java @@ -0,0 +1,275 @@ +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.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.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 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.flushStatementFacts(); + + 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(patch -> patch.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.flushStatementFacts(); + 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 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.flushStatementFacts(); + assertThrows(FrontendAnalysisPatchException.class, () -> environment.putExpressionType( + FrontendSemanticStage.EXPR_TYPE, + expressionNode, + FrontendExpressionType.resolved(GdIntType.INT) + )); + } + + @Test + void retryMemoFactsDoNotEnterOverlayFlushOrExport() throws Exception { + var analysisData = FrontendAnalysisData.bootstrap(); + var environment = new FrontendTypedLexicalEnvironment(newBodyScope(), analysisData); + var memo = new FrontendOwnerRetryMemo(); + var expressionNode = identifier("temporary"); + + memo.putExpressionType(expressionNode, FrontendExpressionType.deferred("first retry pass")); + environment.flushStatementFacts(); + + var memoExpressionType = Objects.requireNonNull(memo.expressionType(expressionNode)); + assertSame(FrontendExpressionTypeStatus.DEFERRED, memoExpressionType.status()); + assertFalse(environment.hasPendingFacts()); + assertFalse(environment.hasCommittedFacts()); + assertTrue(environment.exportPatchTransaction().patches().isEmpty()); + memo.clear(); + assertNull(memo.expressionType(expressionNode)); + } + + 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/FrontendWindowPublicationSurfaceTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendWindowPublicationSurfaceTest.java index 462989cb..f7ddadf2 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendWindowPublicationSurfaceTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendWindowPublicationSurfaceTest.java @@ -13,6 +13,7 @@ import gd.script.gdcc.exception.FrontendAnalysisPatchException; import gd.script.gdcc.frontend.diagnostic.DiagnosticSnapshot; import gd.script.gdcc.frontend.diagnostic.FrontendDiagnostic; +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.frontend.scope.CallableScope; 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..0509cbc1 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 @@ -7,12 +7,17 @@ import gd.script.gdcc.frontend.parse.GdScriptParserService; import gd.script.gdcc.frontend.sema.FrontendAnalysisData; 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.IdentifierExpression; import dev.superice.gdparser.frontend.ast.Node; @@ -92,6 +97,89 @@ func ping(): assertEquals(ScopeValueKind.LOCAL, result.primaryFilteredHit().value().kind()); } + @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", """ From f5326ee9fff4bdb7e6599032c34ff8ef96d7f51b Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Thu, 9 Jul 2026 09:04:07 +0800 Subject: [PATCH 10/27] feat(frontend): land body owner procedures and refresh suite resolution pipeline - Remove the legacy owner retry memo carrier and route environment dependencies through the dedicated body owner layer - Introduce body owner procedures as the unified holder for body-level coordination, replacing ad-hoc context plumbing - Tighten suite resolver and chain reduction supports to consume the new owner-driven context with consistent guard semantics - Extend the typed lexical environment and analyzer pipeline wiring so body-bound facts publish through the shared owner surface - Refresh the pipeline plan and execution summary, adding focused regressions covering the new chain reduction and analyzer seams --- ...e_resolution_pipeline_execution_summary.md | 6 +- ...segmented_type_resolution_pipeline_plan.md | 46 +- .../frontend/sema/FrontendOwnerRetryMemo.java | 56 -- .../sema/FrontendTypedLexicalEnvironment.java | 22 +- .../analyzer/FrontendBodyOwnerProcedures.java | 611 ++++++++++++++++++ .../analyzer/FrontendSemanticAnalyzer.java | 18 +- .../sema/analyzer/FrontendSuiteResolver.java | 8 +- .../FrontendChainHeadReceiverSupport.java | 30 +- .../support/FrontendChainReductionFacade.java | 32 +- .../support/FrontendChainReductionHelper.java | 7 +- .../FrontendExpressionSemanticSupport.java | 37 +- ...FrontendSemanticAnalyzerFrameworkTest.java | 185 +++++- .../sema/FrontendSuiteResolverTest.java | 295 +++++++++ .../FrontendTypedLexicalEnvironmentTest.java | 20 - .../FrontendChainReductionFacadeTest.java | 51 ++ .../FrontendChainReductionHelperTest.java | 68 ++ ...FrontendExpressionSemanticSupportTest.java | 54 ++ 17 files changed, 1421 insertions(+), 125 deletions(-) delete mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendOwnerRetryMemo.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProcedures.java diff --git a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md index 548fae06..762855c5 100644 --- a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md +++ b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md @@ -121,14 +121,14 @@ Pending overlay 只对当前 statement 后续 owner 子过程可见。Statement 目标架构固定四层事实可见性模型: -1. `FrontendOwnerRetryMemo`:owner 子过程私有,只给当前 chain / expr reduction 的 retry 回调读取,owner 子过程结束即丢弃。 +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:由 statement flush 合并而来,后续 statement 与 gate classifier 可读,但仍不是 stable publication。 4. `FrontendAnalysisData` stable side tables / `BlockScope` stable slot:只在 suite export 的 per-owner patch apply / stable export helper 后更新。 -Nested chain / argument retry 的中间事实只能存在于 owner-local memo 中。Retry 中出现的临时 `DEFERRED`、暂定 `Variant`、中间 status 或 detailReason 不得写入 pending overlay、committed overlay 或 stable side table。 +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 memo 或专用非导出状态中。 +`expressionTypes()` 对同一 key 只能发布最终 fact 一次。如果一个 expression 在 reduction 过程中需要先得到临时 fact 再得到 exact result,中间状态必须留在 owner-local transient cache 或专用非导出状态中。 ## 8. Overlay 写入、Flush 与 Export diff --git a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md index 7d90b1ab..d7b167d7 100644 --- a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md +++ b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md @@ -269,9 +269,9 @@ Suite 收敛后,不能把 current-suite committed overlay 整体打包为单 不得重排 1-5。尤其是 chain binding 读取 receiver local slot 时,必须先看到 local stabilization 对前序 statement 或当前 statement 前序子过程写入的 exact slot fact;否则会重新打开 receiver 被误读成 `Variant` 的历史回归。 -Nested chain / argument retry 只能发生在当前 owner 子过程内部的 `FrontendOwnerRetryMemo`(实现命名可调整)或等价非导出 transient memo 中。Retry 可读取本次 reduction 已经推导出的 receiver / argument / step 临时事实,也可读取当前 statement 之前 owner 子过程已发布到 pending / committed overlay 的事实;但 retry 产生的中间 facts 不写入 `expressionTypes()` overlay、statement pending overlay、current-suite committed overlay 或 stable side table,也不能被 `TypedLexicalEnvironment` 的普通 lookup 读取。它们在当前 owner 子过程结束时丢弃。 +Nested chain / argument retry 只能发生在当前 owner 子过程内部的非导出 transient cache 中。当前实现由 `FrontendBodyOwnerProcedures.BodyExpressionResolver` 的 `expressionTypes` / `finalizedExpressionTypes` / `resolvedCalls` 缓存、`FrontendChainReductionFacade.reducedChains` 以及 `FrontendChainReductionHelper` 的 bounded `finalizeWindow` retry 共同承担。Retry 可读取本次 reduction 已经推导出的 receiver / argument / step 临时事实,也可读取当前 statement 之前 owner 子过程已发布到 pending / committed overlay 的事实;但 retry 产生的中间 facts 不写入 `expressionTypes()` overlay、statement pending overlay、current-suite committed overlay 或 stable side table,也不能被 `TypedLexicalEnvironment` 的普通 lookup 读取。它们在当前 owner 子过程结束时丢弃。 -`FrontendExprTypeAnalyzer` 对同一 expression / step key 只能在完成 retry、选定最终 `FrontendExpressionType` 后写入一次 expression type overlay。若同一 key 需要先得到 `DEFERRED`、暂定 `Variant` 或其他非最终状态,再得到 exact result,必须把中间值保存在 owner-local memo 或专用非导出状态里,而不是发布为 `expressionTypes()` 后再 narrowing / status upgrade。 +`FrontendExprTypeAnalyzer` 对同一 expression / step key 只能在完成 retry、选定最终 `FrontendExpressionType` 后写入一次 expression type overlay。若同一 key 需要先得到 `DEFERRED`、暂定 `Variant` 或其他非最终状态,再得到 exact result,必须把中间值保存在 owner-local transient cache 或专用非导出状态里,而不是发布为 `expressionTypes()` 后再 narrowing / status upgrade。 Gate classifier 属于 statement 结构处理的一部分,只能在其 header 所需的 top binding、local stabilization、chain binding 与 expr typing 子过程完成后运行。Classifier 可读取当前 statement pending overlay 与 current-suite committed overlay,但不能读取后续 statement facts。 @@ -312,12 +312,12 @@ Pending overlay 只对当前 statement 内后续 owner 子过程可见。Stateme - overlay fact 必须带 owner metadata,并在导出到 stable side table 前执行冲突检测、idempotent 检查和 compiler-only guard。 - compiler-only guard 必须检查该 fact 可达的每个 user-visible `GdType` payload;不能只检查 `expressionTypes()` / `slotTypes()` 两个表。 - compiler-only guard 必须在 pending overlay write 时就 fail-fast,而不是等 suite export 时才补救。任何 scratch 写入如果命中 `GdCompilerType`,必须在 write API 返回前拒绝该 fact,不能先写入 pending / committed overlay 再在 export 时回滚。 -- `expressionTypes()` overlay 只接受当前 statement 内每个 AST key 的最终 publication fact。retry 中间计算(包括首 pass 的 `DEFERRED`、暂定 `Variant`、临时 status / detailReason)必须留在 `FrontendOwnerRetryMemo`,不得作为 overlay fact 写入。 +- `expressionTypes()` overlay 只接受当前 statement 内每个 AST key 的最终 publication fact。retry 中间计算(包括首 pass 的 `DEFERRED`、暂定 `Variant`、临时 status / detailReason)必须留在 owner procedure 的非导出 transient cache,不得作为 overlay fact 写入。 - `expressionTypes()` overlay 不提供 `Variant -> exact`、parent -> child 或 terminal status -> success 的 narrowing 例外。这不是 overlay 的局部规则,而是对 `FrontendAnalysisData.sameExpressionType` 严格判据(status + publishedType + detailReason 全等,见第 4.6 节)的直接引用。需要 narrowing 的 local slot 变化必须走 `FrontendLocalSlotTypeUpdate`,不得绕道 `expressionTypes()` republish。 四层事实可见性模型固定为: -- `FrontendOwnerRetryMemo`:owner 子过程私有,只给当前 chain / expr reduction 的 retry 回调读取;不属于 `TypedLexicalEnvironment`,不参与 flush / export,owner 子过程结束即丢弃。 +- Owner procedure transient cache:owner 子过程私有,只给当前 chain / expr reduction 的 retry 回调读取;不属于 `TypedLexicalEnvironment`,不参与 flush / export,owner 子过程结束即丢弃。 - 当前 statement pending overlay:只给当前 statement 后续 owner 子过程读取;只接受每个 AST key 的最终 publication fact。 - current-suite committed overlay:由 statement flush 合并而来,给后续 statement 与 gate classifier 读取;仍不是 stable publication。 - `FrontendAnalysisData` stable side tables / `BlockScope` stable slot:只在 suite export 的 per-owner patch apply / stable export helper 后更新,供 diagnostics-only phase、compile gate 与 lowering 使用。 @@ -422,7 +422,7 @@ record FrontendInventoryGate( 可调整的是通信路径:body layer 可以先写 `TypedLexicalEnvironment` overlay,再在 suite / callable / module 边界导出 per-owner patch transaction。不得用 `updateXxx(...)` 表达部分提交,也不得用一个 single-stage patch 表达多 owner suite export。 -这意味着 Stage E 的 “nested chain / argument retry 读己写” 不是 stable side-table rewrite 能力。Retry 的读己写发生在 chain/expr owner 的 `FrontendOwnerRetryMemo` 与当前 statement effective view 中;stable `expressionTypes()` 仍只看到每个 key 的最终一次 publication。 +这意味着 Stage E 的 “nested chain / argument retry 读己写” 不是 stable side-table rewrite 能力。Retry 的读己写发生在 chain/expr owner 的 procedure-local transient cache 与当前 statement effective view 中;stable `expressionTypes()` 仍只看到每个 key 的最终一次 publication。 ### 4.7 Scope slot mutation @@ -510,7 +510,7 @@ Feature-specific header state 仍属于 gate header 语义,不得因为 body r - patch package 内的 shared merge helper / type-bearing compiler-only guard / owner-field validator。 - `FrontendSuiteResolver` / `FrontendStatementResolver` 的 root-bounded statement dispatch 子框架。 - statement-local owner procedure interface 或等价显式调用约定,接收 `FrontendSuiteContext`、当前 statement root 与 `FrontendTypedLexicalEnvironment`。 -- 每个 owner 的显式上下文状态 record / stack,例如 restriction、static context、property initializer context、block scope stack、diagnostic suppression state、owner-local retry memo。 +- 每个 owner 的显式上下文状态 record / stack,例如 restriction、static context、property initializer context、block scope stack、diagnostic suppression state、procedure-local transient retry cache。 移动 / 兼容资产: @@ -524,7 +524,7 @@ Feature-specific header state 仍属于 gate header 语义,不得因为 body r - `FrontendTopBindingAnalyzer.AstWalkerTopBindingBinder`、`FrontendLocalTypeStabilizationAnalyzer.AstWalkerLocalTypeStabilizer`、`FrontendChainBindingAnalyzer.AstWalkerChainBinder`、`FrontendExprTypeAnalyzer.AstWalkerExprTypePublisher`、`FrontendVarTypePostAnalyzer.SlotTypePublisher` 等内部 walker 只能作为语义规则参考。它们的 whole-module `walk(sourceFile)` 入口、隐式字段状态与整表发布策略不得作为 production SuiteResolver procedure 复用。 - 各 analyzer 的 `analyzeInWindow(...)` 方法名称具有误导性:它只改变 publication surface,不改变 traversal root。它可以保留为 legacy / comparison test path,但不能被归类为可迁移 runner。 - `FrontendVarTypePostAnalyzer.analyzeInWindow(...)` 更不能作为参考:它直接清空并写入 stable `slotTypes()`,再事后复制到 window scratch。新 var type post procedure 必须从这个实现重新写起,而不是修饰调用路径。 -- `FrontendChainReductionFacade` 与 `FrontendExpressionSemanticSupport` 可保留算法价值,但它们当前通过 analyzer-local 回调读取 dependency type;SuiteResolver 下必须改为从 `FrontendTypedLexicalEnvironment` 与 owner-local memo 读取。 +- `FrontendChainReductionFacade` 与 `FrontendExpressionSemanticSupport` 可保留算法价值,但它们当前通过 analyzer-local 回调读取 dependency type;SuiteResolver 下必须改为从 `FrontendTypedLexicalEnvironment` 与 owner-local transient cache 读取。 可回退或废弃资产: @@ -575,7 +575,7 @@ Whole-module walker state inventory: | --- | --- | --- | --- | --- | --- | --- | | `FrontendTopBindingAnalyzer` | 每个 `FrontendSourceClassRelation.unit().ast()` 的 `SourceFile` whole-module walk | `sourcePath`、`moduleSkeleton`、`visibleValueResolver`、`classRegistry`、`reportedUnsupportedRoots`、`ASTWalker` | `moduleSkeleton()`、`scopesByAst()`、parse/skeleton diagnostics snapshot、class registry | `symbolBindings()` | `sema.binding` 与 unsupported binding routes | binding 分类与 visible resolver provenance 可抽取;whole-module walk 和整表 `updateSymbolBindings(...)` 不可作为 SuiteResolver procedure | | `FrontendLocalTypeStabilizationAnalyzer` | 每个 source file whole-module walk;按 visitor 状态维护 source-order probe | `SilentExpressionResolver`、`probes`、`writeBackStableSlots`、可选 `FrontendWindowAnalysisContext`、probe-local expression memo | `scopesByAst()`、`symbolBindings()`、`resolvedMembers()` / `resolvedCalls()` / `expressionTypes()` 已发布事实、当前 scope slot state | legacy path 直接更新 local `ScopeValue.type()`;window path 产生 `FrontendLocalSlotTypeUpdate` | 不拥有 source-facing diagnostics;只在写回边界 fail-fast 拒绝非法 slot type | initializer probe、compatibility 与 slot rewrite guard 可抽取;whole-module delayed probe 收集和直接 stable scope mutation 不可复用 | -| `FrontendChainBindingAnalyzer` | 每个 source file whole-module walk | `chainReduction`、assignment / expression semantic support、`reportedDeferredRoots`、`reportedUnsupportedRoots`、`ASTWalker` | `scopesByAst()`、`symbolBindings()`、stable slot types、已发布 member/call facts、class registry | `resolvedMembers()`、chain-owned `resolvedCalls()` | `sema.member_resolution`、`sema.call_resolution`、deferred / unsupported routes | chain reduction 与 call/member classification 可抽取;通过 analyzer-local callback 读取 dependency type 与 whole-module retry memo 不可复用 | +| `FrontendChainBindingAnalyzer` | 每个 source file whole-module walk | `chainReduction`、assignment / expression semantic support、`reportedDeferredRoots`、`reportedUnsupportedRoots`、`ASTWalker` | `scopesByAst()`、`symbolBindings()`、stable slot types、已发布 member/call facts、class registry | `resolvedMembers()`、chain-owned `resolvedCalls()` | `sema.member_resolution`、`sema.call_resolution`、deferred / unsupported routes | chain reduction 与 call/member classification 可抽取;通过 analyzer-local callback 读取 dependency type 与 whole-module transient retry cache 不可复用 | | `FrontendExprTypeAnalyzer` | 每个 source file whole-module walk | `chainReduction`、assignment / expression semantic support、`parentByNode`、reported expression/deferred/unsupported/discarded roots | `scopesByAst()`、`symbolBindings()`、`resolvedMembers()`、`resolvedCalls()`、slot types、class registry | `expressionTypes()`、bare-call `resolvedCalls()`;`backfillInferredLocalType(...)` 只能 guard-only | `sema.expression_resolution`、`sema.deferred_expression_resolution`、`sema.unsupported_expression_route`、`sema.discarded_expression` | expression support、type compatibility 与 discarded-expression rules 可抽取;backfill 不得重新成为 slot mutation owner,whole-module expression walk 不可复用 | | `FrontendVarTypePostAnalyzer` | 每个 source file whole-module walk | `sourcePath`、`blockScopeStack`、`currentCallableOwner`、`supportedExecutableBlockDepth`、`ASTWalker` | `moduleSkeleton()`、`scopesByAst()`、callable/block scope inventory、current scope slot types | `slotTypes()` final callable-local snapshot | `sema.variable_slot_publication` fail-fast / invariant violations | slot publication eligibility rules 可抽取;legacy `analyzeInWindow(...)` 的 stable `slotTypes()` clear/write 与 whole-table snapshot 不可复用 | @@ -637,7 +637,7 @@ Compiler-only guard payload matrix: - [x] C3 新增 `FrontendPublishedFactTypeGuard` shared walker,覆盖 binding/member/call/expression/slot/update 六类 source-facing typed payload;`FrontendAnalysisData.applyPatch(...)`、owner patch、legacy window scratch put 与保留的 `updateXxx(...)` whole-table publish 均接入该 guard。 - [x] C4 Overlay 写入 API 必须显式传入 `FrontendSemanticStage` owner metadata;错误 owner fail-fast,local slot overlay 继续执行 `Variant -> exact` / exact-same-only / no-void / no-compiler-only 规则。 - [x] C5 `flushStatementFacts()` 只把 pending overlay 合并到 committed overlay;`exportPatchTransaction()` 只导出 fixed-order per-owner patch transaction,并由 `FrontendPatchTransaction` 拒绝 owner 顺序回退或重复 owner。 -- [x] C6 新增 `FrontendOwnerRetryMemo`,表达 owner-local retry 中间 fact 不属于 typed lexical environment,不参与 flush / export。 +- [x] C6 移除未接入生产路径的 `FrontendOwnerRetryMemo`,并把合同明确为 owner procedure 内部非导出 transient cache:`BodyExpressionResolver` 双 expression cache / call cache、`FrontendChainReductionFacade.reducedChains` 与 helper bounded retry 均不属于 typed lexical environment,不参与 flush / export。 - [x] C7 `FrontendVisibleValueResolver` 新增 overlay-aware `resolve(request, environment)` overload;它只在 declaration-order / self-reference filter 之后替换 returned local 的 effective type,不绕过 future-local filtered hit。 - [ ] C8 Expression semantic support 与 chain reduction facade 的 dependency-type callback 尚未替换为 explicit environment lookup;这是阶段 E owner procedure 重写的一部分,本阶段只提供可接入入口。 @@ -710,18 +710,28 @@ Compiler-only guard payload matrix: - `FrontendLocalTypeStabilizationAnalyzer` 不再通过整模块 legacy direct phase 表达 source-order 行为。 - 完成阶段 C8:`FrontendExpressionSemanticSupport`、`FrontendChainReductionFacade` 与相关 chain-head / dependency-type callback 必须改为显式读取 `FrontendTypedLexicalEnvironment`,替换 stable-data / analyzer-local callback 的 dependency type 读取路径。 - `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 继续 guard-only。 -- Chain / argument retry 的中间 expression facts 必须保存在 `FrontendOwnerRetryMemo` 或等价 owner-local memo,不得写入 `expressionTypes()` overlay 后再以 narrowing / status upgrade 方式覆盖。 +- Chain / argument retry 的中间 expression facts 必须保存在 owner procedure 内部非导出 transient cache,不得写入 `expressionTypes()` overlay 后再以 narrowing / status upgrade 方式覆盖。 每个 owner 的重写范围必须显式记录: - Top binding:把 `AstWalkerTopBindingBinder` 的 use-site binding 规则拆为 statement-local binding procedure,并把 restriction、static context、property initializer context 由 `FrontendSuiteContext` 显式传入。 - Local stabilization:把 `AstWalkerLocalTypeStabilizer` 的 eligible `:=` initializer 解析改为立即写 pending overlay,使同 statement 后续 owner 与后续 statement 可按 flush 规则读取;禁止继续依赖整模块 direct phase 更新 `BlockScope`。 -- Chain binding:把 chain reduction 的 dependency type 回调改为读取 `FrontendTypedLexicalEnvironment` 与 owner-local memo,而不是 analyzer-local stable side table snapshot。 +- Chain binding:把 chain reduction 的 dependency type 回调改为读取 `FrontendTypedLexicalEnvironment` 与 owner-local transient cache,而不是 analyzer-local stable side table snapshot。 - Expr typing:把 expression fact 发布改为 statement-local final fact publication;父索引、duplicate-report state、retry state 都必须显式化,不能藏在 whole-module walker 字段里。 -- Expression semantic support / chain reduction facade:identifier binding、receiver type、argument type、bare-call callee type 与 nested dependency type 读取必须先走 owner-local memo / pending overlay / committed overlay 的 effective view,再回退 stable side table;不得继续把 `FrontendAnalysisData.symbolBindings()` / `expressionTypes()` 作为 body procedure 的第一读取源。 +- Expression semantic support / chain reduction facade:identifier binding、receiver type、argument type、bare-call callee type 与 nested dependency type 读取必须先走 owner-local transient cache / pending overlay / committed overlay 的 effective view,再回退 stable side table;不得继续把 `FrontendAnalysisData.symbolBindings()` / `expressionTypes()` 作为 body procedure 的第一读取源。 - Var type post:把 slot type publication 改为消费当前 statement / current-suite typed facts 的 statement-local publication,不再通过整表 `updateSlotTypes(...)` 表达 body 结果,也不得复用旧 `analyzeInWindow(...)` 的“stable `slotTypes()` clear/write 后再复制到 window”模式。 - Resolver request:request-domain gate、AST boundary gate 与 current-scope gate 的创建必须由 `FrontendSuiteContext` 统一完成,不能继续由各 analyzer 手写 deferred domain。 +阶段 E 状态同步: + +- [x] E0 重新读取 `AGENTS.md`,用 MCP 列出 `doc` 与 `doc/module_impl`,并并行子代理调研阶段 E 相关文档、owner analyzer 代码与测试基线。 +- [x] E1 完成 C8 overlay-aware dependency lookup:`FrontendExpressionSemanticSupport` / `FrontendChainReductionFacade` / chain-head receiver 可通过注入 lookup 读取 `FrontendTypedLexicalEnvironment` 的 effective binding 与 exact local slot fact;`FrontendChainReductionHelper` 的 argument dependency lookup 不再先读 stable `expressionTypes()`,而是委托注入 resolver 保持 overlay-first 合同;旧构造器继续保持 stable-table 兼容。 +- [x] E2 新增 `FrontendBodyOwnerProcedures`,以 root-bounded DFS 实现 top binding、local stabilization、chain binding、expr typing、var type post 的核心 statement-local publication,不调用 whole-module analyzer entrypoint。 +- [x] E3 `FrontendSuiteResolver` 默认接入真实 owner procedure;`FrontendSemanticAnalyzer` 默认仍注入 legacy-compatible no-op suite resolver,避免阶段 H 之前提前写入 body facts 干扰 legacy whole-phase fallback。 +- [x] E4 新增正反向 targeted tests,锚定 source-order alias、child-prefix visibility、chain receiver exactness、transient cache isolation、var type post export boundary、C8 overlay lookup 与 framework-level real owner path hand-off。 +- [x] E5 已运行格式化、IDE 增量编译与问题检查、`FrontendSuiteResolverTest`、`FrontendExpressionSemanticSupportTest`、`FrontendChainReductionFacadeTest`、`FrontendChainReductionHelperTest`、阶段 E 相关 targeted regression batch,以及 `git diff --check`。 +- [x] E6 补充 `FrontendSemanticAnalyzerFrameworkTest.injectedRealBodyOwnerSuiteResolverRunsDAndEOwnerPathBeforeLegacyPublication`,通过显式注入真实 `FrontendBodyOwnerProcedures` 的 `FrontendSuiteResolver` 证明 D/E body owner path 在 framework hand-off 中先于 legacy whole-phase publication 执行;原 `withSegmentedSemanticRunnerForTesting()` 等价测试已重命名为 deprecated scheduler coverage,不再作为 real body owner path 证据。本轮已运行 `FrontendSemanticAnalyzerFrameworkTest`、suite/body/support focused batch、阶段 E 相关 15 类 targeted regression batch 与 `git diff --check`。 + 验收细则: - `var a := typed_value; var b := a; var c := b` 在 body resolver 下稳定为同一 exact type。 @@ -729,10 +739,10 @@ Compiler-only guard payload matrix: - 对 `var b := a`,`b` 的 local stabilization 必须读取前一 statement 已 committed 的 `a` exact slot fact。 - 对 `var x := receiver.member`,chain binding 必须在 local stabilization 子过程之后运行,并消费已稳定的 `receiver` slot fact;不能直接读取 interface baseline `Variant`。 - rejected shadow declaration 不污染 parent slot。 -- nested chain / argument retry 保持读己写能力,但这个能力只存在于 owner-local memo;同一 expression / step key 在 statement flush 和 suite export 中只产生一条最终 `expressionTypes()` fact。 +- nested chain / argument retry 保持读己写能力,但这个能力只存在于 owner-local transient cache;同一 expression / step key 在 statement flush 和 suite export 中只产生一条最终 `expressionTypes()` fact。 - retry 过程中出现的任何非最终 expression fact(含 `DEFERRED` 代理类型、暂定 `Variant`、中间 status / detailReason)不得先发布到 pending overlay、committed overlay 或 stable side table 再被最终 fact 覆盖。 - C8 回归测试必须证明:当 stable side table / baseline 仍是 `Variant` 而 pending 或 committed overlay 已有 exact local slot fact 时,`FrontendExpressionSemanticSupport` 与 `FrontendChainReductionFacade` 的 dependency-type callback 消费 overlay exact type,而不是 stable `Variant`。 -- C8 negative path 必须证明:support / facade 不能直接读取 retry memo 以外的非最终 expression fact,也不能绕过 `FrontendTypedLexicalEnvironment` 直接读取 stable-only side table 后发布 stale receiver / argument / bare-call result。 +- C8 negative path 必须证明:support / facade 不能直接读取 owner-local transient cache 以外的非最终 expression fact,也不能绕过 `FrontendTypedLexicalEnvironment` 直接读取 stable-only side table 后发布 stale receiver / argument / bare-call result。 - owner 以外的 procedure 不能写对应 side table 或 slot update。 - 每个 owner 子过程的 suite export 以独立 per-owner patch 出现在 transaction 中;transaction coordinator 按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 顺序 apply。 - Var type post procedure 在 statement flush 前不得改变 stable `slotTypes()`;targeted test 必须在 procedure 运行、flush、suite export 三个点分别断言 stable table 只在 export/apply patch 后变化。 @@ -759,7 +769,7 @@ Compiler-only guard payload matrix: - classifier 已返回 supported 但 body readiness 仍为 `NOT_PUBLISHED` / `PUBLISHING` 时,body lookup 仍必须是 `DEFERRED_UNSUPPORTED` 或对应 deferred domain。 - Classifier 只能在 header statement 的 top binding、local stabilization、chain binding 与 expr typing 子过程完成后读取 overlay。 - Classifier 可读取前序 statement committed typed fact,但不能读取后续 statement 或未运行子过程的 fact。 -- Classifier 只能读取 expr typing 已最终发布到 overlay 的 expression facts,不能读取 retry memo 中未导出的中间 expression type。 +- Classifier 只能读取 expr typing 已最终发布到 overlay 的 expression facts,不能读取 owner-local transient cache 中未导出的中间 expression type。 - body inventory publication 成功后,readiness 原子推进为 `PUBLISHED`,resolver 才允许进入该合成 body。 - 合成 gate 的 body use-site 在 `PUBLISHED` 后必须同时通过 request-domain gate、AST boundary gate 与 current-scope gate;任一 gate 仍按旧常量逻辑封口都应有测试失败。 - 合成 gate 的 header use-site 可在 header/classifier 上下文读取前缀 overlay fact,但 header 放行不能让 body lookup 提前通过。 @@ -840,7 +850,7 @@ Compiler-only guard payload matrix: - `FrontendTypedLexicalEnvironmentTest`:suite export 前 stable side table 与 `BlockScope` 不变,export 后只通过 patch apply 更新。 - `FrontendTypedLexicalEnvironmentTest`:suite export 生成 per-owner patch transaction,而不是单个 multi-owner patch。 - `FrontendTypedLexicalEnvironmentTest`:`expressionTypes()` overlay 同 key 同值幂等,不同值 fail-fast;retry 中间 fact 不能作为可导出的 expression type fact 留存。 -- `FrontendTypedLexicalEnvironmentTest`:`FrontendOwnerRetryMemo` 中的临时 facts 不会进入 pending overlay、committed overlay 或 stable side table。 +- `FrontendSuiteResolverTest` / `FrontendChainReductionHelperTest`:owner procedure transient caches 与 bounded `finalizeWindow` retry 中的临时 facts 不会进入 pending overlay、committed overlay 或 stable side table;只有 owner 显式 publication 后才进入 typed overlay。 - `FrontendTypedLexicalEnvironmentTest`:overlay 写入拒绝 `symbolBindings()`、`resolvedMembers()`、`resolvedCalls()` 中所有 type-bearing payload 的 `GdCompilerType`;不得用 `FrontendWindowPublicationSurfaceTest` 代替该覆盖。 - `FrontendTypedLexicalEnvironmentTest`:pending write、statement flush、suite export 三个时点对 compiler-only payload 的拒绝集合一致;不能出现 pending 接受、export 才拒绝的 coverage drift。 - `FrontendWindowPublicationSurfaceTest`:保留为 API-level / legacy shim 测试,只验证 surface 自身 direct API 的 scratch / discard / conflict 语义;不得声明所有 `analyzeInWindow(...)` caller 都满足 scratch-over-stable。 @@ -869,7 +879,7 @@ Suite/body pipeline 测试: - `FrontendSuiteResolverTest`:local stabilization patch apply 后由 commit helper 派生刷新同 declaration 的 `symbolBindings()` payload,而不是通过同一个 patch 携带 binding delta。 - `FrontendSuiteResolverTest`:`if` / `elif` / `else` / `while` header 先解析,body 后递归。 - `FrontendSuiteResolverTest`:unsupported body 不进入 resolver。 -- `FrontendSemanticAnalyzerFrameworkTest`:legacy pipeline 与 interface/body pipeline side-table 等价。 +- `FrontendSemanticAnalyzerFrameworkTest`:legacy pipeline 与显式注入真实 body owner procedures 的 interface/body pipeline side-table 等价;`withSegmentedSemanticRunnerForTesting()` 只覆盖 deprecated whole-module segmented scheduler 兼容性,不得作为 D/E real body owner path 证据。 - `FrontendSemanticAnalyzerFrameworkTest`:等价基线使用 guard-only backfill 合同,不允许恢复旧 expr-phase slot mutation。 Resolver 测试: @@ -965,7 +975,7 @@ Compile gate 测试: ### R14:retry 中间 expression type 被导出导致 patch 冲突 -缓解:`expressionTypes()` stable merge 保持严格 `sameExpressionType` 判据,不增加 `Variant -> exact`、parent -> child 或 status upgrade 例外。Chain / argument retry 的中间 expression facts 只能存放在 `FrontendOwnerRetryMemo`;statement pending overlay、current-suite committed overlay 与 `FrontendExprTypePatch.expressionTypes()` 都只能包含每 key 最终单条 fact。测试必须覆盖 same key different value fail-fast,以及 retry 后 stable / committed table 不含 stale intermediate fact。 +缓解:`expressionTypes()` stable merge 保持严格 `sameExpressionType` 判据,不增加 `Variant -> exact`、parent -> child 或 status upgrade 例外。Chain / argument retry 的中间 expression facts 只能存放在 owner procedure 内部非导出 transient cache;statement pending overlay、current-suite committed overlay 与 `FrontendExprTypePatch.expressionTypes()` 都只能包含每 key 最终单条 fact。测试必须覆盖 same key different value fail-fast,以及 retry 后 stable / committed table 不含 stale intermediate fact。 ### R15:single-stage patch 被误用为 multi-owner suite export diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendOwnerRetryMemo.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendOwnerRetryMemo.java deleted file mode 100644 index 1bed9d3d..00000000 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendOwnerRetryMemo.java +++ /dev/null @@ -1,56 +0,0 @@ -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.Objects; - -/// Owner-local transient retry facts for chain/expression reduction. -/// -/// These facts are intentionally not part of `FrontendTypedLexicalEnvironment`: they are readable -/// only by the current owner procedure and are discarded before statement flush or suite export. -public final class FrontendOwnerRetryMemo { - private final @NotNull FrontendAstSideTable expressionTypes = new FrontendAstSideTable<>(); - private final @NotNull FrontendAstSideTable resolvedMembers = new FrontendAstSideTable<>(); - private final @NotNull FrontendAstSideTable resolvedCalls = new FrontendAstSideTable<>(); - - public void putExpressionType(@NotNull Node astNode, @NotNull FrontendExpressionType expressionType) { - expressionTypes.put( - Objects.requireNonNull(astNode, "astNode must not be null"), - Objects.requireNonNull(expressionType, "expressionType must not be null") - ); - } - - public @Nullable FrontendExpressionType expressionType(@NotNull Node astNode) { - return expressionTypes.get(Objects.requireNonNull(astNode, "astNode must not be null")); - } - - public void putResolvedMember(@NotNull Node astNode, @NotNull FrontendResolvedMember member) { - resolvedMembers.put( - Objects.requireNonNull(astNode, "astNode must not be null"), - Objects.requireNonNull(member, "member must not be null") - ); - } - - public @Nullable FrontendResolvedMember resolvedMember(@NotNull Node astNode) { - return resolvedMembers.get(Objects.requireNonNull(astNode, "astNode must not be null")); - } - - public void putResolvedCall(@NotNull Node astNode, @NotNull FrontendResolvedCall call) { - resolvedCalls.put( - Objects.requireNonNull(astNode, "astNode must not be null"), - Objects.requireNonNull(call, "call must not be null") - ); - } - - public @Nullable FrontendResolvedCall resolvedCall(@NotNull Node astNode) { - return resolvedCalls.get(Objects.requireNonNull(astNode, "astNode must not be null")); - } - - public void clear() { - expressionTypes.clear(); - resolvedMembers.clear(); - resolvedCalls.clear(); - } -} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java index fc9814b4..0d95f821 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java @@ -61,12 +61,15 @@ public FrontendTypedLexicalEnvironment( } public @Nullable FrontendBinding symbolBinding(@NotNull Node astNode) { - return firstNonNull( + var localBinding = firstNonNull( pendingFacts.symbolBindings.get(astNode), committedFacts.symbolBindings.get(astNode), - stableData.symbolBindings().get(astNode), - parent != null ? parent.symbolBinding(astNode) : null + stableData.symbolBindings().get(astNode) ); + if (localBinding != null) { + return effectiveBinding(localBinding); + } + return parent != null ? parent.symbolBinding(astNode) : null; } public @Nullable FrontendResolvedMember resolvedMember(@NotNull Node astNode) { @@ -309,6 +312,19 @@ private void validateLocalSlotTypeUpdate(@NotNull FrontendLocalSlotTypeUpdate up 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(), 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..841610ac --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProcedures.java @@ -0,0 +1,611 @@ +package gd.script.gdcc.frontend.sema.analyzer; + +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.CallExpression; +import dev.superice.gdparser.frontend.ast.DeclarationKind; +import dev.superice.gdparser.frontend.ast.Expression; +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.Node; +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.scope.BlockScope; +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.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.FrontendResolvedCall; +import gd.script.gdcc.frontend.sema.FrontendSemanticStage; +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.patch.FrontendLocalSlotTypeUpdate; +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.ExtensionUtilityFunction; +import gd.script.gdcc.scope.FunctionDef; +import gd.script.gdcc.scope.Scope; +import gd.script.gdcc.scope.ScopeLookupStatus; +import gd.script.gdcc.scope.ScopeValueKind; +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.util.IdentityHashMap; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +/// Statement-local owner procedures used by the new 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 call any legacy 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 { + private FrontendAnalysisData cachedAnalysisData; + private FrontendVisibleValueResolver cachedVisibleValueResolver; + + @Override + public void runTopBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { + forEachExpression(root, expression -> { + if (expression instanceof IdentifierExpression identifierExpression) { + bindIdentifier(context, identifierExpression); + } + }); + } + + @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); + } + } + }); + } + + @Override + public void runExprType(@NotNull FrontendSuiteContext context, @NotNull Node root) { + var resolver = new BodyExpressionResolver(context); + forEachExpression(root, expression -> publishExpressionType(context, resolver, expression)); + } + + @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) { + return; + } + var effectiveSlot = context.typedEnvironment().effectiveScopeValue(slot, blockScope); + context.typedEnvironment().putSlotType( + FrontendSemanticStage.VAR_TYPE_POST, + variableDeclaration, + effectiveSlot.type() + ); + } + + 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); + 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) + ); + } + + 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(new FrontendVisibleValueResolveRequest( + identifierExpression.name(), + identifierExpression, + context.restriction(), + FrontendVisibleValueDomain.EXECUTABLE_BODY + ), 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) + || !FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(blockScope.kind())) { + 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() + ); + } + if (trace.suggestedCall() != null) { + context.typedEnvironment().putResolvedCall( + FrontendSemanticStage.CHAIN_BINDING, + trace.step(), + trace.suggestedCall() + ); + } + } + } + + private static void publishExpressionType( + @NotNull FrontendSuiteContext context, + @NotNull BodyExpressionResolver resolver, + @NotNull Expression expression + ) { + var expressionType = resolver.resolveExpressionType(expression, true); + context.typedEnvironment().putExpressionType( + FrontendSemanticStage.EXPR_TYPE, + expression, + expressionType + ); + if (expression instanceof CallExpression callExpression) { + var resolvedCall = resolver.resolvedCall(callExpression); + if (resolvedCall != null) { + context.typedEnvironment().putResolvedCall( + FrontendSemanticStage.EXPR_TYPE, + callExpression, + resolvedCall + ); + } + } + } + + private @NotNull FrontendVisibleValueResolver visibleValueResolver(@NotNull FrontendSuiteContext context) { + if (cachedAnalysisData != context.analysisData() || cachedVisibleValueResolver == null) { + cachedAnalysisData = context.analysisData(); + cachedVisibleValueResolver = new FrontendVisibleValueResolver(context.analysisData()); + } + 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 resolvedCalls = + new IdentityHashMap<>(); + private final @NotNull FrontendChainReductionFacade chainReduction; + private final @NotNull FrontendExpressionSemanticSupport expressionSemanticSupport; + + 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) + ); + 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; + } + + 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); + case AssignmentExpression _ -> FrontendExpressionType.failed( + "Assignment expression typing is not part of the statement-local value contract yet" + ); + 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 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 + ) { + 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 FrontendResolvedCall resolvedCall(@NotNull CallExpression callExpression) { + return resolvedCalls.get(callExpression); + } + + 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 8cd27e4a..b4ac5010 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 @@ -449,7 +449,7 @@ public FrontendSemanticAnalyzer( loopControlFlowAnalyzer, compileCheckAnalyzer, new FrontendInterfacePhase(), - new FrontendSuiteResolver(), + legacyCompatibleSuiteResolver(), false ); } @@ -506,6 +506,9 @@ private FrontendSemanticAnalyzer( } public static @NotNull FrontendSemanticAnalyzer withSegmentedSemanticRunnerForTesting() { + // This factory keeps the deprecated whole-module segmented scheduler covered. It does not + // exercise the Phase D/E root-bounded body owner path; tests for that path should inject a + // `FrontendSuiteResolver` wired with real owner procedures through the public constructor. return new FrontendSemanticAnalyzer( new FrontendClassSkeletonBuilder(), new FrontendScopeAnalyzer(), @@ -521,11 +524,15 @@ private FrontendSemanticAnalyzer( new FrontendLoopControlFlowAnalyzer(), new FrontendCompileCheckAnalyzer(), new FrontendInterfacePhase(), - new FrontendSuiteResolver(), + legacyCompatibleSuiteResolver(), true ); } + private static @NotNull FrontendSuiteResolver legacyCompatibleSuiteResolver() { + return new FrontendSuiteResolver(new FrontendStatementResolver()); + } + /// Runs the current frontend analyzer framework against one module using a shared /// `DiagnosticManager`. /// @@ -570,9 +577,10 @@ private FrontendSemanticAnalyzer( variableAnalyzer.analyze(analysisData, diagnosticManager); analysisData.updateDiagnostics(diagnosticManager.snapshot()); - // Phase D wires the Interface surface into the main pipeline before any body semantic - // publication runs. The suite resolver is still a no-op owner skeleton, so legacy analyzers - // remain the source of stable body facts until Phase E/H replace them. + // The Interface/Body hand-off runs before legacy whole-phase publication. The default + // analyzer still injects a legacy-compatible no-op suite resolver until Phase H switches the + // shared pipeline. Phase D/E tests that need the real body owner path inject a + // `FrontendSuiteResolver` with real owner procedures through the public constructor. var interfaceSurface = interfacePhase.analyze(classRegistry, analysisData); suiteResolver.resolve(interfaceSurface, classRegistry, analysisData, diagnosticManager); analysisData.updateDiagnostics(diagnosticManager.snapshot()); 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 index 3dca4f4e..c318f01c 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolver.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolver.java @@ -21,14 +21,14 @@ /// Body-suite coordinator for the staged semantic pipeline. /// -/// Phase D runs no-op owner procedures by default. Its job is to prove that executable body roots are -/// entered only through `FrontendInterfaceSurface` and that overlay facts are exported through an -/// ordered patch transaction instead of direct stable side-table writes. +/// The default resolver now uses statement-local owner procedures. Tests may still 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 final @NotNull FrontendStatementResolver statementResolver; public FrontendSuiteResolver() { - this(new FrontendStatementResolver()); + this(new FrontendStatementResolver(new FrontendBodyOwnerProcedures())); } public FrontendSuiteResolver(@NotNull FrontendStatementResolver statementResolver) { 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..14d6a4ff 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,6 +27,7 @@ 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. /// @@ -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/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/test/java/gd/script/gdcc/frontend/sema/FrontendSemanticAnalyzerFrameworkTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSemanticAnalyzerFrameworkTest.java index 503037fc..344d6de7 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSemanticAnalyzerFrameworkTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSemanticAnalyzerFrameworkTest.java @@ -7,14 +7,19 @@ 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.FrontendBodyOwnerProcedures; 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.FrontendInterfacePhase; 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.FrontendStatementResolver; +import gd.script.gdcc.frontend.sema.analyzer.FrontendSuiteContext; +import gd.script.gdcc.frontend.sema.analyzer.FrontendSuiteResolver; import gd.script.gdcc.frontend.sema.analyzer.FrontendTopBindingAnalyzer; import gd.script.gdcc.frontend.sema.analyzer.FrontendTypeCheckAnalyzer; import gd.script.gdcc.frontend.sema.analyzer.FrontendVirtualOverrideAnalyzer; @@ -30,6 +35,7 @@ import gd.script.gdcc.lir.LirClassDef; 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; @@ -57,6 +63,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; @@ -976,7 +983,81 @@ func ping(value: Point) -> int: } @Test - void segmentedRunnerProducesEquivalentSharedSemanticSideTablesAndDiagnostics() throws Exception { + void injectedRealBodyOwnerSuiteResolverRunsDAndEOwnerPathBeforeLegacyPublication() 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 ownerProcedures = new FrameworkBodyOwnerProcedures( + aliasDeclaration, + numberDeclaration, + markerStep, + numberInitializer + ); + var legacyDiagnostics = new DiagnosticManager(); + var realBodyDiagnostics = new DiagnosticManager(); + + var legacy = analyzeModule( + new FrontendSemanticAnalyzer(), + "test_module", + List.of(unit), + new ClassRegistry(ExtensionApiLoader.loadDefault()), + legacyDiagnostics + ); + var realBody = analyzeModule( + new FrontendSemanticAnalyzer( + new FrontendInterfacePhase(), + new FrontendSuiteResolver(new FrontendStatementResolver(ownerProcedures)) + ), + "test_module", + List.of(unit), + new ClassRegistry(ExtensionApiLoader.loadDefault()), + realBodyDiagnostics + ); + + var aliasSlotType = realBody.slotTypes().get(aliasDeclaration); + var numberSlotType = realBody.slotTypes().get(numberDeclaration); + var markerMember = realBody.resolvedMembers().get(markerStep); + var initializerType = realBody.expressionTypes().get(numberInitializer); + assertAll( + () -> assertTrue(ownerProcedures.aliasSlotWasPublishedBeforeLegacy()), + () -> assertTrue(ownerProcedures.memberFactWasPublishedBeforeLegacy()), + () -> assertTrue(ownerProcedures.expressionFactWasPublishedBeforeLegacy()), + () -> assertTrue(ownerProcedures.varPostFactWasPublishedBeforeLegacy()), + () -> 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()) + ); + assertEquivalentSharedSemanticFacts(legacy, realBody); + assertEquals(legacyDiagnostics.snapshot(), realBodyDiagnostics.snapshot()); + } + + @Test + void deprecatedSegmentedSchedulerProducesEquivalentSharedSemanticSideTablesAndDiagnostics() throws Exception { var parserService = new GdScriptParserService(); var parseDiagnostics = new DiagnosticManager(); var unit = parserService.parseUnit(Path.of("tmp", "segmented_equivalence.gd"), """ @@ -1019,7 +1100,7 @@ func ping(value: Point) -> int: } @Test - void segmentedRunnerKeepsExistingUnsupportedSubtreeBehaviorEquivalent() throws Exception { + void deprecatedSegmentedSchedulerKeepsExistingUnsupportedSubtreeBehaviorEquivalent() throws Exception { var parserService = new GdScriptParserService(); var unit = parserService.parseUnit(Path.of("tmp", "segmented_unsupported_equivalence.gd"), """ class_name SegmentedUnsupportedEquivalence @@ -1733,6 +1814,106 @@ private ClassRegistry createRegistryWithSingleton(String singletonName) { )); } + private static boolean hasTypeNameSuffix(Object type, String suffix) { + return type instanceof GdType gdType && gdType.getTypeName().endsWith(suffix); + } + + private static final class FrameworkBodyOwnerProcedures implements FrontendStatementResolver.OwnerProcedures { + private final @NotNull FrontendBodyOwnerProcedures delegate = new FrontendBodyOwnerProcedures(); + private final @NotNull VariableDeclaration aliasDeclaration; + private final @NotNull VariableDeclaration numberDeclaration; + private final @NotNull AttributePropertyStep markerStep; + private final @NotNull Node numberInitializer; + private boolean aliasSlotWasPublishedBeforeLegacy; + private boolean memberFactWasPublishedBeforeLegacy; + private boolean expressionFactWasPublishedBeforeLegacy; + private boolean varPostFactWasPublishedBeforeLegacy; + + private FrameworkBodyOwnerProcedures( + @NotNull VariableDeclaration aliasDeclaration, + @NotNull VariableDeclaration numberDeclaration, + @NotNull AttributePropertyStep markerStep, + @NotNull Node numberInitializer + ) { + this.aliasDeclaration = aliasDeclaration; + this.numberDeclaration = numberDeclaration; + this.markerStep = markerStep; + this.numberInitializer = numberInitializer; + } + + @Override + public void runTopBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { + delegate.runTopBinding(context, root); + } + + @Override + public void runLocalTypeStabilization(@NotNull FrontendSuiteContext context, @NotNull Node root) { + delegate.runLocalTypeStabilization(context, root); + if (root == aliasDeclaration) { + var currentBlockScope = context.currentBlockScope(); + aliasSlotWasPublishedBeforeLegacy = context.analysisData().slotTypes().get(aliasDeclaration) == null + && currentBlockScope != null + && hasTypeNameSuffix(context.typedEnvironment().localSlotType( + currentBlockScope, + aliasDeclaration.name(), + aliasDeclaration + ), "Point"); + } + } + + @Override + public void runChainBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { + delegate.runChainBinding(context, root); + if (root == numberDeclaration) { + var markerMember = context.typedEnvironment().resolvedMember(markerStep); + memberFactWasPublishedBeforeLegacy = context.analysisData().resolvedMembers().get(markerStep) == null + && markerMember != null + && markerMember.status() == FrontendMemberResolutionStatus.RESOLVED + && hasTypeNameSuffix(markerMember.receiverType(), "Point") + && "int".equals(markerMember.resultType().getTypeName()); + } + } + + @Override + public void runExprType(@NotNull FrontendSuiteContext context, @NotNull Node root) { + delegate.runExprType(context, root); + if (root == numberDeclaration) { + var initializerType = context.typedEnvironment().expressionType(numberInitializer); + expressionFactWasPublishedBeforeLegacy = context.analysisData().expressionTypes().get(numberInitializer) == null + && initializerType != null + && initializerType.status() == FrontendExpressionTypeStatus.RESOLVED + && "int".equals(initializerType.publishedType().getTypeName()); + } + } + + @Override + public void runVarTypePost(@NotNull FrontendSuiteContext context, @NotNull Node root) { + delegate.runVarTypePost(context, root); + if (root == numberDeclaration) { + var slotType = context.typedEnvironment().slotType(numberDeclaration); + varPostFactWasPublishedBeforeLegacy = context.analysisData().slotTypes().get(numberDeclaration) == null + && slotType != null + && "int".equals(slotType.getTypeName()); + } + } + + private boolean aliasSlotWasPublishedBeforeLegacy() { + return aliasSlotWasPublishedBeforeLegacy; + } + + private boolean memberFactWasPublishedBeforeLegacy() { + return memberFactWasPublishedBeforeLegacy; + } + + private boolean expressionFactWasPublishedBeforeLegacy() { + return expressionFactWasPublishedBeforeLegacy; + } + + private boolean varPostFactWasPublishedBeforeLegacy() { + return varPostFactWasPublishedBeforeLegacy; + } + } + /// Test double that records the diagnostics snapshot and published skeleton boundary visible /// to scope analysis. private static final class RecordingScopeAnalyzer extends FrontendScopeAnalyzer { diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java index b1155cc6..46219507 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java @@ -1,5 +1,7 @@ package gd.script.gdcc.frontend.sema; +import dev.superice.gdparser.frontend.ast.AttributePropertyStep; +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.IfStatement; @@ -17,13 +19,16 @@ 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.gdextension.ExtensionApiLoader; import gd.script.gdcc.scope.ClassRegistry; +import gd.script.gdcc.type.GdType; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.Test; import java.nio.file.Path; @@ -31,10 +36,12 @@ import java.util.List; 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.assertTrue; @@ -272,6 +279,145 @@ func ping(value): 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 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()) + ); + } + private static void resolveWith( @NotNull PhaseInput phaseInput, @NotNull RecordingOwnerProcedures ownerProcedures @@ -285,6 +431,42 @@ private static void resolveWith( ); } + 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()); @@ -343,6 +525,46 @@ private static T findStatement( .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, @@ -426,6 +648,79 @@ private boolean stableWasEmptyDuringOwnerProcedure() { } } + 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 RecordingInterfacePhase extends FrontendInterfacePhase { private boolean invoked; private boolean variableInventoryWasPublished; diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironmentTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironmentTest.java index 098beba7..0c061353 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironmentTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironmentTest.java @@ -33,7 +33,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -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; @@ -196,25 +195,6 @@ void overlayRejectsExactLocalSlotRewriteAndExpressionFactNarrowing() throws Exce )); } - @Test - void retryMemoFactsDoNotEnterOverlayFlushOrExport() throws Exception { - var analysisData = FrontendAnalysisData.bootstrap(); - var environment = new FrontendTypedLexicalEnvironment(newBodyScope(), analysisData); - var memo = new FrontendOwnerRetryMemo(); - var expressionNode = identifier("temporary"); - - memo.putExpressionType(expressionNode, FrontendExpressionType.deferred("first retry pass")); - environment.flushStatementFacts(); - - var memoExpressionType = Objects.requireNonNull(memo.expressionType(expressionNode)); - assertSame(FrontendExpressionTypeStatus.DEFERRED, memoExpressionType.status()); - assertFalse(environment.hasPendingFacts()); - assertFalse(environment.hasCommittedFacts()); - assertTrue(environment.exportPatchTransaction().patches().isEmpty()); - memo.clear(); - assertNull(memo.expressionType(expressionNode)); - } - private static @NotNull IdentifierExpression identifier(@NotNull String name) { return new IdentifierExpression(name, RANGE); } 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( From 866c11b3b3c2d69e184656ccad40e20d9ac6dbb7 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Thu, 9 Jul 2026 11:36:00 +0800 Subject: [PATCH 11/27] feat(frontend): advance body inventory gate lifecycle to support typed-dependent executable resolution - Promote the inventory gate to a two-axis lifecycle with combined readiness checks so consumers share a single fail-closed fact source. - Index the registry by owner and header root with explicit state transition helpers for support, publish, and unsupported outcomes. - Add a gate classifier hook to the statement resolver and route body owner context through a dedicated suite context and request helper. - Make the visible value resolver consume the shared gate registry across request, boundary, and current-scope lookups instead of ad-hoc probes. - Refresh the pipeline plan and execution summary, and add focused regressions for the new gate registry, suite resolver, and value resolver seams. --- ...e_resolution_pipeline_execution_summary.md | 11 +- ...segmented_type_resolution_pipeline_plan.md | 16 +- .../FrontendExecutableInventorySupport.java | 18 ++ .../frontend/sema/FrontendInventoryGate.java | 36 ++- .../sema/FrontendInventoryGateRegistry.java | 121 +++++++-- .../analyzer/FrontendBodyOwnerProcedures.java | 23 +- .../analyzer/FrontendStatementResolver.java | 7 + .../sema/analyzer/FrontendSuiteContext.java | 36 +++ .../sema/analyzer/FrontendSuiteResolver.java | 31 ++- .../FrontendVisibleValueResolver.java | 148 ++++++++--- .../FrontendInventoryGateRegistryTest.java | 185 +++++++++++++ .../sema/FrontendSuiteResolverTest.java | 247 ++++++++++++++++++ .../FrontendVisibleValueResolverTest.java | 137 ++++++++++ 13 files changed, 932 insertions(+), 84 deletions(-) create mode 100644 src/test/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateRegistryTest.java diff --git a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md index 762855c5..1153a26e 100644 --- a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md +++ b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md @@ -60,7 +60,7 @@ resolveSuite(context, block): resolvePendingBodiesIfAllowed(context) ``` -`resolveStatement(...)` 不是新的 semantic owner。它只按 statement 结构编排已有 owner 的 body-aware 子过程:top binding、local stabilization、chain binding、expr typing 与 var type post。 +`resolveStatement(...)` 不是新的 semantic owner。它只按 statement 结构编排已有 owner 的 body-aware 子过程:top binding、local stabilization、chain binding、expr typing、gate classifier 与 var type post。 Body procedure 必须是 root-bounded、statement-local 的实现。每个 owner 子过程只处理 `SuiteResolver` 传入的 statement、header 或 expression root 及其允许的子表达式。实现可以复用纯语义 helper,但 owner 子过程不能依赖 whole-module traversal 建立隐式上下文。 @@ -84,8 +84,9 @@ Body procedure 必须是 root-bounded、statement-local 的实现。每个 owner 2. Local stabilization runner。 3. Chain binding runner。 4. Expr typing runner。 -5. Var type post procedure。 -6. Statement flush。 +5. Gate classifier hook。 +6. Var type post procedure。 +7. Statement flush。 Top binding runner 为当前 statement 内的 bare identifier 与 chain head use-site 写入 binding overlay。 @@ -95,6 +96,8 @@ Chain binding runner 消费 top binding overlay 与 local stabilization pending Expr typing runner 消费 binding、member、call 与 local slot overlay,发布 `expressionTypes()` 与 bare-call `resolvedCalls()` overlay。`backfillInferredLocalType(...)` 在目标架构中仍只保留 guard-only 协议检查。 +Gate classifier hook 在 header / synthetic fixture 已完成 top binding、local stabilization、chain binding 与 expr typing 后运行。当前 Phase F 只使用该 hook 推进 generic gate lifecycle,不实现任何 for-range feature rule,也不发布 iterator binding。 + Var type post procedure 消费 expression type 与 source-facing local slot overlay,发布 final `slotTypes()` overlay。 Statement flush 把当前 statement pending overlay 转入 current-suite committed overlay,供后续 statement 与 gate classifier 读取。Flush 不得写入 stable side table 或 stable `BlockScope` slot。 @@ -237,6 +240,8 @@ Typed-dependent gate 使用统一 readiness policy。三道封口必须同步条 只有 owning gate 达到 `SUPPORTED + PUBLISHED` 后,body lookup 才能作为普通 executable body lookup 进入 resolver。`PENDING`、`SUPPORTED + NOT_PUBLISHED`、`PUBLISHING`、`UNSUPPORTED`、缺失 owning gate 或找不到 owner 的情况都必须 fail-closed。 +Phase F 当前实现以 `FrontendInventoryGateRegistry` 作为 gate lifecycle 事实源。`FrontendVisibleValueResolver` 的 request-domain gate、AST boundary gate 与 current-scope gate 均读取同一个 registry readiness;`FrontendSuiteContext.visibleValueResolveRequest(...)` 统一创建 body owner lookup request;`FrontendSuiteResolver` 与 body-local stabilization 通过 shared readiness helper 只接受 unconditional supported block 或 `SUPPORTED + PUBLISHED` gate body。 + ## 13. Diagnostics 与 Compile Gate Diagnostics-only phase 在 suite export 后运行,消费 stable facts,不发布新的 semantic side table。 diff --git a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md index d7b167d7..5d0f35e6 100644 --- a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md +++ b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md @@ -753,14 +753,14 @@ Compiler-only guard payload matrix: 实施内容: -- 建立 typed-dependent inventory gate 的注册、lookup、status update 与 readiness update API。 -- 使用合成 fixture 或最小测试 gate 验证 `PENDING + NOT_PUBLISHED`、`SUPPORTED + NOT_PUBLISHED`、`PUBLISHING`、`SUPPORTED + PUBLISHED`、`UNSUPPORTED` 的转换。 -- 支持由前缀 statement typed fact 驱动的测试 classifier,但 classifier 只服务于 gate lifecycle,不实现任何 for-range 规则。 -- `SUPPORTED` 只表示 header/classifier 通过,不能使 body resolver / binder 放行。 -- body inventory publication 成功后才原子推进为 `PUBLISHED`。 -- 建立 resolver gate readiness policy,并接入 request-domain gate、AST boundary gate、current-scope gate 三处判断。 -- 统一 resolver request 创建路径:gate header / gate body lookup 都必须由 `SuiteResolver` / `FrontendSuiteContext` 构造,不能由 analyzer 直接决定 deferred domain。 -- unsupported gate 继续保留对应 deferred / unsupported boundary。 +- [x] F1 建立 typed-dependent inventory gate 的注册、lookup、status update 与 readiness update API。当前实现以 `FrontendInventoryGate` 的 immutable state transition helper、`FrontendInventoryGateRegistry` 的 mutable lifecycle API,以及 `FrontendExecutableInventorySupport.isCallableLocalValueInventoryReady(...)` 作为 shared readiness 入口;非 `SUPPORTED + PUBLISHED` 的 gate 仍统一 fail-closed。 +- [x] F2 使用合成 fixture 或最小测试 gate 验证 `PENDING + NOT_PUBLISHED`、`SUPPORTED + NOT_PUBLISHED`、`PUBLISHING`、`SUPPORTED + PUBLISHED`、`UNSUPPORTED` 的转换。`FrontendInventoryGateRegistryTest.registryTransitionsOnlySupportedPublishedGateToReady` 锚定所有有效生命周期状态,invalid-state tests 锚定 pending / unsupported 不得携带 published readiness。 +- [x] F3 支持由前缀 statement typed fact 驱动的测试 classifier,但 classifier 只服务于 gate lifecycle,不实现任何 for-range 规则。`FrontendStatementResolver.OwnerProcedures.runGateClassifier(...)` 接在 top/local/chain/expr 后与 flush 前,`FrontendSuiteResolverTest.classifierReadsPrefixOverlayAndDoesNotOpenUnpublishedBody` 证明 classifier 可读取前缀 `:=` local exact overlay 与 expr typing 已最终发布的 expression overlay;同测例证明 local stabilization 的 transient expression cache 不会提前进入 overlay,并且 classifier 只推进指定 synthetic gate。 +- [x] F4 `SUPPORTED` 只表示 header/classifier 通过,不能使 body resolver / binder 放行。`FrontendVisibleValueResolverTest.resolveKeepsSupportedButUnpublishedGateBodyFailClosed` 覆盖 `SUPPORTED + NOT_PUBLISHED` / `SUPPORTED + PUBLISHING`,`FrontendSuiteResolverTest.classifierReadsPrefixOverlayAndDoesNotOpenUnpublishedBody` 覆盖 binder 不进入未发布 body。 +- [x] F5 body inventory publication 成功后才原子推进为 `PUBLISHED`。`FrontendInventoryGateRegistry.markBodyInventoryPublished(...)` 是唯一 readiness published transition,resolver / suite entry 只接受 `SUPPORTED + PUBLISHED`;`FrontendSuiteResolverTest.publishedGateBodyIsResolvedBySuiteResolver` 证明 published 后 body facts 可发布。 +- [x] F6 建立 resolver gate readiness policy,并接入 request-domain gate、AST boundary gate、current-scope gate 三处判断。`FrontendVisibleValueResolver` 三处封口、`FrontendSuiteResolver` body entry 与 body-local stabilization 均复用 `FrontendExecutableInventorySupport.isCallableLocalValueInventoryReady(...)` / `FrontendInventoryGateRegistry` readiness;`resolvePublishedGateBodyPassesRequestAstAndCurrentScopeGates` 使用 deferred request domain 证明任一旧常量封口未接入都会失败。 +- [x] F7 统一 resolver request 创建路径:gate header / gate body lookup 都必须由 `SuiteResolver` / `FrontendSuiteContext` 构造,不能由 analyzer 直接决定 deferred domain。body owner path 已改为通过 `FrontendSuiteContext.visibleValueResolveRequest(...)` 创建请求,并随 current body readiness 选择 `EXECUTABLE_BODY` 或 gate deferred domain;`missingOwningGateBodyContextUsesDeferredDomainAndStaysFailClosed` 锚定缺失 gate 的 typed-dependent body 不回退为 executable lookup。 +- [x] F8 unsupported gate 继续保留对应 deferred / unsupported boundary。`FrontendVisibleValueResolverTest.resolveUnsupportedGateBodyDoesNotFallBackToOuterBinding` 证明 `UNSUPPORTED` body lookup 不回退到外层同名 binding。 - 明确非目标:不实现 `range(...)` AST shape 识别、不实现 `INT_SHORTHAND`、不发布 iterator binding、不解封 supported for-range body、不调整 for-range compile gate。 验收细则: 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..3265a255 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendExecutableInventorySupport.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendExecutableInventorySupport.java @@ -1,7 +1,12 @@ 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.scope.BlockScopeKind; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; /// Shared semantic contract for executable block kinds whose callable-local value inventory is supported being /// published by `FrontendVariableAnalyzer`. @@ -21,4 +26,17 @@ public static boolean canPublishCallableLocalValueInventory(@NotNull BlockScopeK default -> false; }; } + + public static boolean isCallableLocalValueInventoryReady( + @NotNull BlockScope blockScope, + @Nullable Node bodyRoot, + @NotNull FrontendInventoryGateRegistry gateRegistry + ) { + Objects.requireNonNull(blockScope, "blockScope"); + Objects.requireNonNull(gateRegistry, "gateRegistry"); + if (canPublishCallableLocalValueInventory(blockScope.kind())) { + return true; + } + return bodyRoot != null && gateRegistry.isBodyInventoryReady(bodyRoot); + } } diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGate.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGate.java index 95e961ec..4842088b 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGate.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGate.java @@ -8,8 +8,9 @@ /// Typed-dependent subtree gate discovered by the Interface phase. /// -/// Phase B only creates `PENDING + NOT_PUBLISHED` gates. Later body phases may classify and publish -/// a gate, but consumers must keep body lookup fail-closed until readiness reaches `PUBLISHED`. +/// Interface discovery creates `PENDING + NOT_PUBLISHED` gates. Later body phases can classify the +/// header and publish the body inventory, but consumers must keep body lookup fail-closed until the +/// combined state reaches `SUPPORTED + PUBLISHED`. public record FrontendInventoryGate( @NotNull Node owner, @NotNull Node headerRoot, @@ -25,6 +26,10 @@ public record FrontendInventoryGate( Objects.requireNonNull(deferredDomain, "deferredDomain"); Objects.requireNonNull(status, "status"); Objects.requireNonNull(bodyInventoryReadiness, "bodyInventoryReadiness"); + if (status != FrontendInventoryGateStatus.SUPPORTED + && bodyInventoryReadiness != FrontendBodyInventoryReadiness.NOT_PUBLISHED) { + throw new IllegalArgumentException("only supported inventory gates can publish body inventory"); + } } public static @NotNull FrontendInventoryGate pending( @@ -42,4 +47,31 @@ public record FrontendInventoryGate( FrontendBodyInventoryReadiness.NOT_PUBLISHED ); } + + public @NotNull FrontendInventoryGate withStatus(@NotNull FrontendInventoryGateStatus newStatus) { + Objects.requireNonNull(newStatus, "newStatus"); + // Unsupported and pending gates never own a partially published body inventory. + var nextReadiness = newStatus == FrontendInventoryGateStatus.SUPPORTED + ? bodyInventoryReadiness + : FrontendBodyInventoryReadiness.NOT_PUBLISHED; + return new FrontendInventoryGate(owner, headerRoot, bodyRoot, deferredDomain, newStatus, nextReadiness); + } + + public @NotNull FrontendInventoryGate withBodyInventoryReadiness( + @NotNull FrontendBodyInventoryReadiness newReadiness + ) { + return new FrontendInventoryGate( + owner, + headerRoot, + bodyRoot, + deferredDomain, + status, + Objects.requireNonNull(newReadiness, "newReadiness") + ); + } + + public boolean isBodyInventoryReady() { + return status == FrontendInventoryGateStatus.SUPPORTED + && bodyInventoryReadiness == FrontendBodyInventoryReadiness.PUBLISHED; + } } diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateRegistry.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateRegistry.java index 17791e17..3b64c01a 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateRegistry.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateRegistry.java @@ -5,7 +5,6 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; -import java.util.Collections; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; @@ -16,21 +15,23 @@ /// The registry is the single body-readiness fact source for future resolver/binder consumers. A /// missing gate or any state other than `SUPPORTED + PUBLISHED` must be interpreted as fail-closed. public final class FrontendInventoryGateRegistry { - private final @NotNull List gates; + private final @NotNull List gates = new ArrayList<>(); private final @NotNull Map gatesByBodyRoot; + private final @NotNull Map> gatesByOwner; + private final @NotNull Map> gatesByHeaderRoot; public FrontendInventoryGateRegistry(@NotNull List gates) { Objects.requireNonNull(gates, "gates"); - var copiedGates = List.copyOf(gates); - var indexedGates = new IdentityHashMap(); - for (var gate : copiedGates) { - var previous = indexedGates.put(gate.bodyRoot(), gate); - if (previous != null) { - throw new IllegalArgumentException("duplicate inventory gate body root"); - } + gatesByBodyRoot = new IdentityHashMap<>(); + gatesByOwner = new IdentityHashMap<>(); + gatesByHeaderRoot = new IdentityHashMap<>(); + for (var gate : gates) { + register(gate); } - this.gates = copiedGates; - gatesByBodyRoot = Collections.unmodifiableMap(indexedGates); + } + + public static @NotNull FrontendInventoryGateRegistry empty() { + return new FrontendInventoryGateRegistry(List.of()); } public static @NotNull Builder builder() { @@ -38,18 +39,108 @@ public FrontendInventoryGateRegistry(@NotNull List gates) } public @NotNull List gates() { - return gates; + return List.copyOf(gates); } public @Nullable FrontendInventoryGate gateForBodyRoot(@NotNull Node bodyRoot) { return gatesByBodyRoot.get(Objects.requireNonNull(bodyRoot, "bodyRoot")); } + public @NotNull List gatesForOwner(@NotNull Node owner) { + return copyIndexedGates(gatesByOwner, Objects.requireNonNull(owner, "owner")); + } + + public @NotNull List gatesForHeaderRoot(@NotNull Node headerRoot) { + return copyIndexedGates(gatesByHeaderRoot, Objects.requireNonNull(headerRoot, "headerRoot")); + } + public boolean isBodyInventoryReady(@NotNull Node bodyRoot) { var gate = gateForBodyRoot(bodyRoot); - return gate != null - && gate.status() == FrontendInventoryGateStatus.SUPPORTED - && gate.bodyInventoryReadiness() == FrontendBodyInventoryReadiness.PUBLISHED; + return gate != null && gate.isBodyInventoryReady(); + } + + public void register(@NotNull FrontendInventoryGate gate) { + Objects.requireNonNull(gate, "gate"); + if (gatesByBodyRoot.containsKey(gate.bodyRoot())) { + throw new IllegalArgumentException("duplicate inventory gate body root"); + } + gates.add(gate); + gatesByBodyRoot.put(gate.bodyRoot(), gate); + gatesByOwner.computeIfAbsent(gate.owner(), _ -> new ArrayList<>()).add(gate); + gatesByHeaderRoot.computeIfAbsent(gate.headerRoot(), _ -> new ArrayList<>()).add(gate); + } + + public @NotNull FrontendInventoryGate updateStatus( + @NotNull Node bodyRoot, + @NotNull FrontendInventoryGateStatus status + ) { + return replaceGate(requireGateForBodyRoot(bodyRoot).withStatus(status)); + } + + public @NotNull FrontendInventoryGate updateBodyInventoryReadiness( + @NotNull Node bodyRoot, + @NotNull FrontendBodyInventoryReadiness readiness + ) { + return replaceGate(requireGateForBodyRoot(bodyRoot).withBodyInventoryReadiness(readiness)); + } + + public @NotNull FrontendInventoryGate markSupported(@NotNull Node bodyRoot) { + return updateStatus(bodyRoot, FrontendInventoryGateStatus.SUPPORTED); + } + + public @NotNull FrontendInventoryGate markUnsupported(@NotNull Node bodyRoot) { + return updateStatus(bodyRoot, FrontendInventoryGateStatus.UNSUPPORTED); + } + + public @NotNull FrontendInventoryGate markBodyInventoryPublishing(@NotNull Node bodyRoot) { + return updateBodyInventoryReadiness(bodyRoot, FrontendBodyInventoryReadiness.PUBLISHING); + } + + public @NotNull FrontendInventoryGate markBodyInventoryPublished(@NotNull Node bodyRoot) { + return updateBodyInventoryReadiness(bodyRoot, FrontendBodyInventoryReadiness.PUBLISHED); + } + + private @NotNull FrontendInventoryGate requireGateForBodyRoot(@NotNull Node bodyRoot) { + var gate = gateForBodyRoot(bodyRoot); + if (gate == null) { + throw new IllegalArgumentException("missing inventory gate body root"); + } + return gate; + } + + private @NotNull FrontendInventoryGate replaceGate(@NotNull FrontendInventoryGate nextGate) { + var previousGate = requireGateForBodyRoot(nextGate.bodyRoot()); + var gateIndex = gates.indexOf(previousGate); + gates.set(gateIndex, nextGate); + removeIndexedGate(gatesByOwner, previousGate.owner(), previousGate); + removeIndexedGate(gatesByHeaderRoot, previousGate.headerRoot(), previousGate); + gatesByBodyRoot.put(nextGate.bodyRoot(), nextGate); + gatesByOwner.computeIfAbsent(nextGate.owner(), _ -> new ArrayList<>()).add(nextGate); + gatesByHeaderRoot.computeIfAbsent(nextGate.headerRoot(), _ -> new ArrayList<>()).add(nextGate); + return nextGate; + } + + private static @NotNull List copyIndexedGates( + @NotNull Map> index, + @NotNull Node key + ) { + var indexedGates = index.get(key); + return indexedGates == null ? List.of() : List.copyOf(indexedGates); + } + + private static void removeIndexedGate( + @NotNull Map> index, + @NotNull Node key, + @NotNull FrontendInventoryGate gate + ) { + var indexedGates = index.get(key); + if (indexedGates == null) { + return; + } + indexedGates.remove(gate); + if (indexedGates.isEmpty()) { + index.remove(key); + } } public static final class Builder { 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 index 841610ac..29acec72 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProcedures.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProcedures.java @@ -29,8 +29,6 @@ import gd.script.gdcc.frontend.sema.analyzer.support.FrontendChainStatusBridge; import gd.script.gdcc.frontend.sema.analyzer.support.FrontendExpressionSemanticSupport; import gd.script.gdcc.frontend.sema.patch.FrontendLocalSlotTypeUpdate; -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; @@ -184,12 +182,10 @@ private void bindIdentifier( case NOT_FOUND -> FrontendVisibleValueResolution.notFound(List.of()); }; } - return visibleValueResolver(context).resolve(new FrontendVisibleValueResolveRequest( - identifierExpression.name(), - identifierExpression, - context.restriction(), - FrontendVisibleValueDomain.EXECUTABLE_BODY - ), context.typedEnvironment()); + return visibleValueResolver(context).resolve( + context.visibleValueResolveRequest(identifierExpression.name(), identifierExpression), + context.typedEnvironment() + ); } private void publishScopeValueBinding( @@ -281,7 +277,11 @@ private boolean tryPublishTypeMetaBinding( } var declarationScope = context.analysisData().scopesByAst().get(variableDeclaration); if (!(declarationScope instanceof BlockScope blockScope) - || !FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(blockScope.kind())) { + || !FrontendExecutableInventorySupport.isCallableLocalValueInventoryReady( + blockScope, + context.currentBlockRoot(), + context.interfaceSurface().inventoryGateRegistry() + )) { return null; } var survivingLocal = blockScope.resolveValueHere(variableDeclaration.name().trim()); @@ -384,7 +384,10 @@ private static void publishExpressionType( private @NotNull FrontendVisibleValueResolver visibleValueResolver(@NotNull FrontendSuiteContext context) { if (cachedAnalysisData != context.analysisData() || cachedVisibleValueResolver == null) { cachedAnalysisData = context.analysisData(); - cachedVisibleValueResolver = new FrontendVisibleValueResolver(context.analysisData()); + cachedVisibleValueResolver = new FrontendVisibleValueResolver( + context.analysisData(), + context.interfaceSurface().inventoryGateRegistry() + ); } return cachedVisibleValueResolver; } 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 index 5e90d14b..42ca50d5 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendStatementResolver.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendStatementResolver.java @@ -114,6 +114,7 @@ private void resolveSupportedRoot(@NotNull FrontendSuiteContext context, @NotNul ownerProcedures.runLocalTypeStabilization(context, root); ownerProcedures.runChainBinding(context, root); ownerProcedures.runExprType(context, root); + ownerProcedures.runGateClassifier(context, root); ownerProcedures.runVarTypePost(context, root); flushStatementBoundary(context); } @@ -150,6 +151,12 @@ default void runChainBinding(@NotNull FrontendSuiteContext context, @NotNull Nod default void runExprType(@NotNull FrontendSuiteContext context, @NotNull Node root) { } + /// Runs after header expression facts are finalized into the current overlay but before the + /// statement boundary is flushed. Phase F test classifiers use this hook to advance synthetic + /// gate lifecycle without implementing any feature-specific rules. + default void runGateClassifier(@NotNull FrontendSuiteContext context, @NotNull Node root) { + } + default void runVarTypePost(@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 index bf34e41f..056ae539 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteContext.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteContext.java @@ -4,10 +4,14 @@ 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.scope.BlockScopeKind; import gd.script.gdcc.frontend.sema.FrontendAnalysisData; +import gd.script.gdcc.frontend.sema.FrontendExecutableInventorySupport; 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.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; @@ -70,4 +74,36 @@ public record FrontendSuiteContext( classRegistry ); } + + 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; + } + var gateRegistry = interfaceSurface.inventoryGateRegistry(); + if (FrontendExecutableInventorySupport.isCallableLocalValueInventoryReady( + currentBlockScope, + currentBlockRoot, + gateRegistry + )) { + return FrontendVisibleValueDomain.EXECUTABLE_BODY; + } + var gate = gateRegistry.gateForBodyRoot(currentBlockRoot); + return gate != null ? gate.deferredDomain() : deferredDomainForBlockKind(currentBlockScope.kind()); + } + + private static @NotNull FrontendVisibleValueDomain deferredDomainForBlockKind(@NotNull BlockScopeKind kind) { + return switch (kind) { + case FOR_BODY -> FrontendVisibleValueDomain.FOR_SUBTREE; + case MATCH_SECTION_BODY -> FrontendVisibleValueDomain.MATCH_SUBTREE; + case LAMBDA_BODY -> FrontendVisibleValueDomain.LAMBDA_SUBTREE; + default -> FrontendVisibleValueDomain.EXECUTABLE_BODY; + }; + } } 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 index c318f01c..e166cd59 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolver.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolver.java @@ -8,6 +8,7 @@ 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.FrontendExecutableInventorySupport; import gd.script.gdcc.frontend.sema.FrontendInterfaceSurface; import gd.script.gdcc.frontend.sema.FrontendTypedLexicalEnvironment; import gd.script.gdcc.frontend.sema.analyzer.support.FrontendPropertyInitializerSupport; @@ -57,7 +58,8 @@ public void resolve( 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"); - if (!context.interfaceSurface().suiteEntryRoots().containsSupportedBlock(block)) { + var blockScope = blockScopeFor(context, block); + if (blockScope == null || !isCallableLocalValueInventoryReady(context, block, blockScope)) { return; } for (var statement : block.statements()) { @@ -138,16 +140,33 @@ private void resolvePropertyInitializer( } private void resolveChildSuite(@NotNull FrontendSuiteContext parentContext, @NotNull Block childBlock) { - if (!parentContext.interfaceSurface().suiteEntryRoots().containsSupportedBlock(childBlock)) { - return; - } - var childScope = parentContext.analysisData().scopesByAst().get(childBlock); - if (!(childScope instanceof BlockScope blockScope)) { + var blockScope = blockScopeFor(parentContext, childBlock); + if (blockScope == null || !isCallableLocalValueInventoryReady(parentContext, childBlock, blockScope)) { return; } resolveSuite(parentContext.withChildBlock(childBlock, blockScope), childBlock); } + private static boolean isCallableLocalValueInventoryReady( + @NotNull FrontendSuiteContext context, + @NotNull Block block, + @NotNull BlockScope blockScope + ) { + return FrontendExecutableInventorySupport.isCallableLocalValueInventoryReady( + blockScope, + block, + context.interfaceSurface().inventoryGateRegistry() + ); + } + + private static @Nullable BlockScope blockScopeFor( + @NotNull FrontendSuiteContext context, + @NotNull Block block + ) { + var scope = context.analysisData().scopesByAst().get(block); + return scope instanceof BlockScope blockScope ? blockScope : null; + } + private static @Nullable Block callableBody(@NotNull Node callableOwner) { return switch (callableOwner) { case FunctionDeclaration functionDeclaration -> functionDeclaration.body(); 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 effb39c5..29c0fe43 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,10 +1,11 @@ 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.sema.FrontendAnalysisData; import gd.script.gdcc.frontend.sema.FrontendExecutableInventorySupport; +import gd.script.gdcc.frontend.sema.FrontendInventoryGate; +import gd.script.gdcc.frontend.sema.FrontendInventoryGateRegistry; import gd.script.gdcc.frontend.sema.FrontendModuleSkeleton; import gd.script.gdcc.frontend.sema.FrontendTypedLexicalEnvironment; import gd.script.gdcc.frontend.scope.CallableScopeKind; @@ -36,11 +37,20 @@ /// explicit deferred-boundary sealing for domains whose variable inventory is not published yet public final class FrontendVisibleValueResolver { private final @NotNull FrontendAnalysisData analysisData; + private final @NotNull FrontendInventoryGateRegistry gateRegistry; private final @NotNull IdentityHashMap parentByNode = new IdentityHashMap<>(); private final @NotNull IdentityHashMap indexedAstNodes = new IdentityHashMap<>(); public FrontendVisibleValueResolver(@NotNull FrontendAnalysisData analysisData) { + this(analysisData, FrontendInventoryGateRegistry.empty()); + } + + public FrontendVisibleValueResolver( + @NotNull FrontendAnalysisData analysisData, + @NotNull FrontendInventoryGateRegistry gateRegistry + ) { this.analysisData = Objects.requireNonNull(analysisData, "analysisData must not be null"); + this.gateRegistry = Objects.requireNonNull(gateRegistry, "gateRegistry must not be null"); indexSourceAstParents(analysisData.moduleSkeleton()); } @@ -56,11 +66,9 @@ public FrontendVisibleValueResolver(@NotNull FrontendAnalysisData analysisData) ) { 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(); @@ -78,7 +86,7 @@ public FrontendVisibleValueResolver(@NotNull FrontendAnalysisData analysisData) FrontendVisibleValueDeferredReason.MISSING_SCOPE_OR_SKIPPED_SUBTREE ); } - var currentScopeBoundary = classifyUnsupportedCurrentScopeBoundary(currentScope); + var currentScopeBoundary = classifyUnsupportedCurrentScopeBoundary(currentScope, useSite); if (currentScopeBoundary != null) { return FrontendVisibleValueResolution.deferredUnsupported(currentScopeBoundary); } @@ -171,25 +179,30 @@ private void indexSubtree(@NotNull Node node, @Nullable Node parentNode) { 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 - ); + case LambdaExpression lambdaExpression when lambdaExpression.body() == childNode -> gateBodyBoundary( + lambdaExpression.body(), + FrontendVisibleValueDomain.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) -> gateBodyBoundary( + variableDeclaration, + FrontendVisibleValueDomain.BLOCK_LOCAL_CONST_SUBTREE + ); + case ForStatement forStatement when forStatement.body() == childNode -> gateBodyBoundary( + forStatement.body(), + FrontendVisibleValueDomain.FOR_SUBTREE ); case ForStatement forStatement when (forStatement.iteratorType() == childNode - || forStatement.iterable() == childNode - || forStatement.body() == childNode) -> new FrontendVisibleValueDeferredBoundary( + || forStatement.iterable() == childNode) -> new FrontendVisibleValueDeferredBoundary( FrontendVisibleValueDomain.FOR_SUBTREE, FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED ); + case MatchSection matchSection when matchSection.body() == childNode -> gateBodyBoundary( + matchSection.body(), + FrontendVisibleValueDomain.MATCH_SUBTREE + ); case MatchSection matchSection when (matchSection.guard() == childNode - || matchSection.body() == childNode || containsNodeIdentity(matchSection.patterns(), childNode)) -> new FrontendVisibleValueDeferredBoundary( FrontendVisibleValueDomain.MATCH_SUBTREE, @@ -199,6 +212,37 @@ case MatchSection matchSection when (matchSection.guard() == childNode }; } + private @Nullable FrontendVisibleValueDeferredBoundary classifyRequestDomainBoundary( + @NotNull FrontendVisibleValueResolveRequest request + ) { + if (request.domain() == FrontendVisibleValueDomain.EXECUTABLE_BODY) { + return null; + } + // Transitional callers may still pass a deferred domain for a gate body. A published owning + // gate normalizes that request into ordinary executable-body lookup; every other case stays + // fail-closed at the request-domain boundary. + if (isNearestOwningGateReady(request.useSite(), request.domain())) { + return null; + } + return new FrontendVisibleValueDeferredBoundary( + request.domain(), + FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN + ); + } + + private @Nullable FrontendVisibleValueDeferredBoundary gateBodyBoundary( + @NotNull Node bodyRoot, + @NotNull FrontendVisibleValueDomain deferredDomain + ) { + if (gateRegistry.isBodyInventoryReady(bodyRoot)) { + return null; + } + return new FrontendVisibleValueDeferredBoundary( + deferredDomain, + FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED + ); + } + /// 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) { @@ -256,7 +300,8 @@ private boolean isDeferredBlockLocalConst(@NotNull VariableDeclaration variableD if (!(statement instanceof VariableDeclaration variableDeclaration) || variableDeclaration.kind() != DeclarationKind.CONST || !variableDeclaration.name().trim().equals(name) - || !isVisibleLocal(variableDeclaration, useSite)) { + || !isVisibleLocal(variableDeclaration, useSite) + || gateRegistry.isBodyInventoryReady(variableDeclaration)) { return null; } return new FrontendVisibleValueDeferredBoundary( @@ -269,15 +314,13 @@ private boolean isDeferredBlockLocalConst(@NotNull VariableDeclaration variableD /// that still reuse one outer supported scope. This current-scope gate exists as the fail-closed /// backstop for unsupported bodies whose own scope is already published in `scopesByAst()`. private @Nullable FrontendVisibleValueDeferredBoundary classifyUnsupportedCurrentScopeBoundary( - @NotNull Scope currentScope + @NotNull Scope currentScope, + @NotNull Node useSite ) { return switch (currentScope) { - case BlockScope blockScope -> classifyUnsupportedCurrentBlockScopeBoundary(blockScope.kind()); + case BlockScope blockScope -> classifyUnsupportedCurrentBlockScopeBoundary(blockScope, useSite); case CallableScope callableScope when callableScope.kind() == CallableScopeKind.LAMBDA_EXPRESSION -> - new FrontendVisibleValueDeferredBoundary( - FrontendVisibleValueDomain.LAMBDA_SUBTREE, - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED - ); + unsupportedScopeBoundary(useSite, FrontendVisibleValueDomain.LAMBDA_SUBTREE); default -> new FrontendVisibleValueDeferredBoundary( FrontendVisibleValueDomain.EXECUTABLE_BODY, FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN @@ -297,24 +340,16 @@ private boolean containsNodeIdentity(@NotNull List nodes, @NotNu } private @Nullable FrontendVisibleValueDeferredBoundary classifyUnsupportedCurrentBlockScopeBoundary( - @NotNull BlockScopeKind kind + @NotNull BlockScope blockScope, + @NotNull Node useSite ) { - if (FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(kind)) { + if (FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(blockScope.kind())) { 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 - ); + return switch (blockScope.kind()) { + case LAMBDA_BODY -> unsupportedScopeBoundary(useSite, FrontendVisibleValueDomain.LAMBDA_SUBTREE); + case FOR_BODY -> unsupportedScopeBoundary(useSite, FrontendVisibleValueDomain.FOR_SUBTREE); + case MATCH_SECTION_BODY -> unsupportedScopeBoundary(useSite, FrontendVisibleValueDomain.MATCH_SUBTREE); default -> new FrontendVisibleValueDeferredBoundary( FrontendVisibleValueDomain.EXECUTABLE_BODY, FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN @@ -322,6 +357,39 @@ private boolean containsNodeIdentity(@NotNull List nodes, @NotNu }; } + private @Nullable FrontendVisibleValueDeferredBoundary unsupportedScopeBoundary( + @NotNull Node useSite, + @NotNull FrontendVisibleValueDomain deferredDomain + ) { + if (isNearestOwningGateReady(useSite, deferredDomain)) { + return null; + } + return new FrontendVisibleValueDeferredBoundary( + deferredDomain, + FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED + ); + } + + private boolean isNearestOwningGateReady( + @NotNull Node useSite, + @NotNull FrontendVisibleValueDomain deferredDomain + ) { + var gate = nearestOwningGate(useSite); + return gate != null && gate.deferredDomain() == deferredDomain && gate.isBodyInventoryReady(); + } + + private @Nullable FrontendInventoryGate nearestOwningGate(@NotNull Node useSite) { + Node currentNode = useSite; + while (currentNode != null) { + var gate = gateRegistry.gateForBodyRoot(currentNode); + if (gate != null) { + return gate; + } + currentNode = parentByNode.get(currentNode); + } + return null; + } + private boolean publishesCallableLocalValueInventory(@NotNull Block block) { var blockScope = analysisData.scopesByAst().get(block); return blockScope instanceof BlockScope typedBlockScope diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateRegistryTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateRegistryTest.java new file mode 100644 index 00000000..18f0a002 --- /dev/null +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateRegistryTest.java @@ -0,0 +1,185 @@ +package gd.script.gdcc.frontend.sema; + +import dev.superice.gdparser.frontend.ast.ForStatement; +import dev.superice.gdparser.frontend.ast.Node; +import gd.script.gdcc.frontend.diagnostic.DiagnosticManager; +import gd.script.gdcc.frontend.parse.GdScriptParserService; +import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueDomain; +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.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class FrontendInventoryGateRegistryTest { + @Test + void pendingGateFactoryStartsFailClosed() throws Exception { + var forStatement = parseForStatement(); + + var gate = FrontendInventoryGate.pending( + forStatement, + forStatement, + forStatement.body(), + FrontendVisibleValueDomain.FOR_SUBTREE + ); + + assertEquals(FrontendInventoryGateStatus.PENDING, gate.status()); + assertEquals(FrontendBodyInventoryReadiness.NOT_PUBLISHED, gate.bodyInventoryReadiness()); + assertFalse(gate.isBodyInventoryReady()); + } + + @Test + void registryTransitionsOnlySupportedPublishedGateToReady() throws Exception { + var forStatement = parseForStatement(); + var registry = FrontendInventoryGateRegistry.builder() + .add(FrontendInventoryGate.pending( + forStatement, + forStatement, + forStatement.body(), + FrontendVisibleValueDomain.FOR_SUBTREE + )) + .build(); + + assertFalse(registry.isBodyInventoryReady(forStatement.body())); + + var supported = registry.markSupported(forStatement.body()); + assertEquals(FrontendInventoryGateStatus.SUPPORTED, supported.status()); + assertEquals(FrontendBodyInventoryReadiness.NOT_PUBLISHED, supported.bodyInventoryReadiness()); + assertFalse(registry.isBodyInventoryReady(forStatement.body())); + + var publishing = registry.markBodyInventoryPublishing(forStatement.body()); + assertEquals(FrontendBodyInventoryReadiness.PUBLISHING, publishing.bodyInventoryReadiness()); + assertFalse(registry.isBodyInventoryReady(forStatement.body())); + + var published = registry.markBodyInventoryPublished(forStatement.body()); + assertEquals(FrontendInventoryGateStatus.SUPPORTED, published.status()); + assertEquals(FrontendBodyInventoryReadiness.PUBLISHED, published.bodyInventoryReadiness()); + assertTrue(registry.isBodyInventoryReady(forStatement.body())); + + var unsupported = registry.markUnsupported(forStatement.body()); + assertEquals(FrontendInventoryGateStatus.UNSUPPORTED, unsupported.status()); + assertEquals(FrontendBodyInventoryReadiness.NOT_PUBLISHED, unsupported.bodyInventoryReadiness()); + assertFalse(registry.isBodyInventoryReady(forStatement.body())); + } + + @Test + void invalidReadinessStatesAreRejected() throws Exception { + var forStatement = parseForStatement(); + var pending = FrontendInventoryGate.pending( + forStatement, + forStatement, + forStatement.body(), + FrontendVisibleValueDomain.FOR_SUBTREE + ); + + assertThrows( + IllegalArgumentException.class, + () -> pending.withBodyInventoryReadiness(FrontendBodyInventoryReadiness.PUBLISHING) + ); + assertThrows( + IllegalArgumentException.class, + () -> new FrontendInventoryGate( + forStatement, + forStatement, + forStatement.body(), + FrontendVisibleValueDomain.FOR_SUBTREE, + FrontendInventoryGateStatus.UNSUPPORTED, + FrontendBodyInventoryReadiness.PUBLISHED + ) + ); + } + + @Test + void registryRejectsDuplicateBodyRootAndReturnsImmutableSnapshot() throws Exception { + var forStatement = parseForStatement(); + var gate = FrontendInventoryGate.pending( + forStatement, + forStatement, + forStatement.body(), + FrontendVisibleValueDomain.FOR_SUBTREE + ); + + assertThrows( + IllegalArgumentException.class, + () -> FrontendInventoryGateRegistry.builder().add(gate).add(gate).build() + ); + var registry = FrontendInventoryGateRegistry.builder().add(gate).build(); + + assertSame(gate, registry.gateForBodyRoot(forStatement.body())); + assertEquals(List.of(gate), registry.gatesForOwner(forStatement)); + assertEquals(List.of(gate), registry.gatesForHeaderRoot(forStatement)); + assertThrows(UnsupportedOperationException.class, () -> registry.gates().add(gate)); + } + + @Test + void missingGateStaysFailClosed() throws Exception { + var forStatement = parseForStatement(); + + assertFalse(FrontendInventoryGateRegistry.empty().isBodyInventoryReady(forStatement.body())); + assertThrows( + IllegalArgumentException.class, + () -> FrontendInventoryGateRegistry.empty().markSupported(forStatement.body()) + ); + } + + private static @NotNull ForStatement parseForStatement() throws Exception { + var diagnostics = new DiagnosticManager(); + var unit = new GdScriptParserService().parseUnit(Path.of("tmp", "gate_registry.gd"), """ + class_name GateRegistry + extends Node + + func ping(values): + for value in values: + print(value) + """, diagnostics); + assertTrue(diagnostics.isEmpty(), () -> "Unexpected parse diagnostics: " + diagnostics.snapshot()); + return findNode(unit.ast(), ForStatement.class, _ -> true); + } + + private static @NotNull 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; + } +} diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java index 46219507..194d9480 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java @@ -1,9 +1,11 @@ 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; @@ -16,6 +18,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.sema.analyzer.FrontendInterfacePhase; import gd.script.gdcc.frontend.sema.analyzer.FrontendScopeAnalyzer; import gd.script.gdcc.frontend.sema.analyzer.FrontendSemanticAnalyzer; @@ -24,8 +27,11 @@ 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.resolver.FrontendVisibleValueDomain; 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 org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -220,6 +226,133 @@ func ping(items, choice): assertTrue(hasOwnerEvent(ownerProcedures.events(), after)); } + @Test + void classifierReadsPrefixOverlayAndDoesNotOpenUnpublishedBody() throws Exception { + var phaseInput = phaseInput("suite_gate_classifier_prefix.gd", """ + class_name SuiteGateClassifierPrefix + extends Node + + func ping(values, choice): + var limit := 1 + for item in values: + var from_for := item + match choice: + 0: + var from_match := choice + """); + 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 matchStatement = findStatement(pingFunction.body().statements(), MatchStatement.class, _ -> true); + var surface = new FrontendInterfacePhase().analyze(phaseInput.registry(), phaseInput.analysisData()); + var ownerProcedures = new PrefixGateClassifierOwnerProcedures(limit, forStatement.body()); + + new FrontendSuiteResolver(new FrontendStatementResolver(ownerProcedures)).resolve( + surface, + phaseInput.registry(), + phaseInput.analysisData(), + phaseInput.diagnostics() + ); + + var forGate = surface.inventoryGateRegistry().gateForBodyRoot(forStatement.body()); + assertNotNull(forGate); + assertEquals(FrontendInventoryGateStatus.SUPPORTED, forGate.status()); + assertEquals(FrontendBodyInventoryReadiness.NOT_PUBLISHED, forGate.bodyInventoryReadiness()); + assertFalse(surface.inventoryGateRegistry().isBodyInventoryReady(forStatement.body())); + assertTrue(ownerProcedures.classifierSawPrefixExactType()); + assertTrue(ownerProcedures.localStageCouldNotSeeTransientExpressionFact()); + assertTrue(ownerProcedures.classifierSawFinalExpressionFact()); + assertFalse(ownerProcedures.events().stream().anyMatch(event -> event.root() == fromFor)); + + var matchGate = surface.inventoryGateRegistry().gateForBodyRoot(matchStatement.sections().getFirst().body()); + assertNotNull(matchGate); + assertEquals(FrontendInventoryGateStatus.PENDING, matchGate.status()); + } + + @Test + void publishedGateBodyIsResolvedBySuiteResolver() 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()); + surface.inventoryGateRegistry().markSupported(forStatement.body()); + surface.inventoryGateRegistry().markBodyInventoryPublishing(forStatement.body()); + surface.inventoryGateRegistry().markBodyInventoryPublished(forStatement.body()); + 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 missingOwningGateBodyContextUsesDeferredDomainAndStaysFailClosed() throws Exception { + var phaseInput = phaseInput("suite_gate_body_missing_owner.gd", """ + class_name SuiteGateBodyMissingOwner + extends Node + + func ping(values, seed: int): + for item in values: + print(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 seedUseSite = findNode(forStatement.body(), IdentifierExpression.class, identifier -> identifier.name().equals("seed")); + var originalSurface = new FrontendInterfacePhase().analyze(phaseInput.registry(), phaseInput.analysisData()); + var surfaceWithoutGate = new FrontendInterfaceSurface( + originalSurface.bodyDeclarationIndex(), + FrontendInventoryGateRegistry.empty(), + originalSurface.typedLexicalBaseline(), + originalSurface.suiteEntryRoots() + ); + var forBodyContext = contextForBlock(phaseInput, surfaceWithoutGate, pingFunction, forStatement.body()); + + var request = forBodyContext.visibleValueResolveRequest(seedUseSite.name(), seedUseSite); + new FrontendSuiteResolver().resolveSuite(forBodyContext, forStatement.body()); + + assertEquals(FrontendVisibleValueDomain.FOR_SUBTREE, request.domain()); + assertNull(phaseInput.analysisData().symbolBindings().get(seedUseSite)); + } + @Test void suiteExportKeepsStableSideTableUnchangedUntilTransactionApply() throws Exception { var phaseInput = phaseInput("suite_export_boundary.gd", """ @@ -493,6 +626,30 @@ private static boolean hasOwnerEvent(@NotNull List events, @NotNull return events.stream().anyMatch(event -> event.root() == root); } + private static @NotNull FrontendSuiteContext contextForBlock( + @NotNull PhaseInput phaseInput, + @NotNull FrontendInterfaceSurface surface, + @NotNull Node callableOwner, + @NotNull Block block + ) { + var blockScope = assertInstanceOf(BlockScope.class, phaseInput.analysisData().scopesByAst().get(block)); + return new FrontendSuiteContext( + Path.of("tmp", "synthetic_gate_body.gd"), + callableOwner, + block, + blockScope, + blockScope, + ResolveRestriction.instanceContext(), + false, + null, + surface, + new FrontendTypedLexicalEnvironment(blockScope, phaseInput.analysisData()), + phaseInput.analysisData(), + phaseInput.diagnostics(), + phaseInput.registry() + ); + } + private static @NotNull PhaseInput phaseInput(@NotNull String fileName, @NotNull String source) throws Exception { var parserService = new GdScriptParserService(); var diagnostics = new DiagnosticManager(); @@ -576,6 +733,96 @@ private record PhaseInput( private record OwnerEvent(@NotNull String stage, @NotNull Node root) { } + private static final class PrefixGateClassifierOwnerProcedures implements FrontendStatementResolver.OwnerProcedures { + private final @NotNull FrontendBodyOwnerProcedures delegate = new FrontendBodyOwnerProcedures(); + private final @NotNull VariableDeclaration prefixLocal; + private final @NotNull Block targetBody; + private final @NotNull List events = new ArrayList<>(); + private boolean classifierSawPrefixExactType; + private boolean localStageCouldNotSeeTransientExpressionFact; + private boolean classifierSawFinalExpressionFact; + + private PrefixGateClassifierOwnerProcedures( + @NotNull VariableDeclaration prefixLocal, + @NotNull Block targetBody + ) { + this.prefixLocal = prefixLocal; + this.targetBody = targetBody; + } + + @Override + public void runTopBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { + events.add(new OwnerEvent("top", root)); + delegate.runTopBinding(context, root); + } + + @Override + public void runLocalTypeStabilization(@NotNull FrontendSuiteContext context, @NotNull Node root) { + events.add(new OwnerEvent("local", root)); + delegate.runLocalTypeStabilization(context, root); + if (root == prefixLocal && prefixLocal.value() != null) { + localStageCouldNotSeeTransientExpressionFact = context.typedEnvironment() + .expressionType(prefixLocal.value()) == null; + } + } + + @Override + public void runChainBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { + events.add(new OwnerEvent("chain", root)); + delegate.runChainBinding(context, root); + } + + @Override + public void runExprType(@NotNull FrontendSuiteContext context, @NotNull Node root) { + events.add(new OwnerEvent("expr", root)); + delegate.runExprType(context, root); + } + + @Override + public void runGateClassifier(@NotNull FrontendSuiteContext context, @NotNull Node root) { + if (root != prefixLocal || context.currentBlockScope() == null) { + return; + } + var prefixType = context.typedEnvironment().localSlotType( + context.currentBlockScope(), + prefixLocal.name(), + prefixLocal + ); + var prefixExpressionType = prefixLocal.value() != null + ? context.typedEnvironment().expressionType(prefixLocal.value()) + : null; + classifierSawPrefixExactType = prefixType == GdIntType.INT; + classifierSawFinalExpressionFact = prefixExpressionType != null + && prefixExpressionType.status() == FrontendExpressionTypeStatus.RESOLVED + && prefixExpressionType.publishedType() == GdIntType.INT; + if (classifierSawPrefixExactType) { + context.interfaceSurface().inventoryGateRegistry().markSupported(targetBody); + } + } + + @Override + public void runVarTypePost(@NotNull FrontendSuiteContext context, @NotNull Node root) { + events.add(new OwnerEvent("var_post", root)); + delegate.runVarTypePost(context, root); + } + + private @NotNull List events() { + return events; + } + + private boolean classifierSawPrefixExactType() { + return classifierSawPrefixExactType; + } + + private boolean localStageCouldNotSeeTransientExpressionFact() { + return localStageCouldNotSeeTransientExpressionFact; + } + + private boolean classifierSawFinalExpressionFact() { + return classifierSawFinalExpressionFact; + } + } + private static final class RecordingOwnerProcedures implements FrontendStatementResolver.OwnerProcedures { private final boolean publishTopBinding; private final @NotNull List events = new ArrayList<>(); 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 0509cbc1..6b4c2976 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,6 +6,10 @@ 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.FrontendBodyInventoryReadiness; +import gd.script.gdcc.frontend.sema.FrontendInventoryGate; +import gd.script.gdcc.frontend.sema.FrontendInventoryGateRegistry; +import gd.script.gdcc.frontend.sema.FrontendInventoryGateStatus; import gd.script.gdcc.frontend.sema.analyzer.FrontendSemanticAnalyzer; import gd.script.gdcc.frontend.sema.FrontendSemanticStage; import gd.script.gdcc.frontend.sema.FrontendTypedLexicalEnvironment; @@ -19,6 +23,7 @@ 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; @@ -448,6 +453,113 @@ func ping(values): ); } + @Test + void resolveKeepsSupportedButUnpublishedGateBodyFailClosed() throws Exception { + var analyzedInput = analyzedInput("for_body_supported_not_published.gd", """ + class_name ForBodySupportedNotPublished + 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 notPublished = resolveWithGate( + analyzedInput, + forStatement, + useSite, + FrontendInventoryGateStatus.SUPPORTED, + FrontendBodyInventoryReadiness.NOT_PUBLISHED, + FrontendVisibleValueDomain.FOR_SUBTREE + ); + var publishing = resolveWithGate( + analyzedInput, + forStatement, + useSite, + FrontendInventoryGateStatus.SUPPORTED, + FrontendBodyInventoryReadiness.PUBLISHING, + FrontendVisibleValueDomain.FOR_SUBTREE + ); + + assertEquals(FrontendVisibleValueStatus.DEFERRED_UNSUPPORTED, notPublished.status()); + assertEquals(FrontendVisibleValueDomain.FOR_SUBTREE, notPublished.deferredBoundary().domain()); + assertEquals( + FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN, + notPublished.deferredBoundary().reason() + ); + assertEquals(FrontendVisibleValueStatus.DEFERRED_UNSUPPORTED, publishing.status()); + assertEquals(FrontendVisibleValueDomain.FOR_SUBTREE, publishing.deferredBoundary().domain()); + assertEquals( + FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN, + publishing.deferredBoundary().reason() + ); + } + + @Test + void resolvePublishedGateBodyPassesRequestAstAndCurrentScopeGates() throws Exception { + var analyzedInput = analyzedInput("for_body_published.gd", """ + class_name ForBodyPublished + 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 result = resolveWithGate( + analyzedInput, + forStatement, + useSite, + FrontendInventoryGateStatus.SUPPORTED, + FrontendBodyInventoryReadiness.PUBLISHED, + FrontendVisibleValueDomain.FOR_SUBTREE + ); + + assertEquals(FrontendVisibleValueStatus.FOUND_ALLOWED, result.status()); + assertNotNull(result.visibleValue()); + assertEquals(ScopeValueKind.PARAMETER, result.visibleValue().kind()); + assertTrue(result.filteredHits().isEmpty()); + assertNull(result.deferredBoundary()); + } + + @Test + void resolveUnsupportedGateBodyDoesNotFallBackToOuterBinding() throws Exception { + var analyzedInput = analyzedInput("for_body_unsupported_no_fallback.gd", """ + class_name ForBodyUnsupportedNoFallback + extends Node + + var item = 100 + + func ping(values): + for item in values: + print(item) + """); + var forStatement = findNode(analyzedInput.unit().ast(), ForStatement.class, _ -> true); + var useSite = findIdentifierExpression(forStatement.body(), "item"); + + var result = resolveWithGate( + analyzedInput, + forStatement, + useSite, + FrontendInventoryGateStatus.UNSUPPORTED, + FrontendBodyInventoryReadiness.NOT_PUBLISHED, + FrontendVisibleValueDomain.EXECUTABLE_BODY + ); + + assertEquals(FrontendVisibleValueStatus.DEFERRED_UNSUPPORTED, result.status()); + assertNull(result.visibleValue()); + assertTrue(result.filteredHits().isEmpty()); + assertEquals(FrontendVisibleValueDomain.FOR_SUBTREE, result.deferredBoundary().domain()); + assertEquals( + FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED, + result.deferredBoundary().reason() + ); + } + @Test void resolveSealsForIterableAsDeferredUnsupported() throws Exception { var analyzedInput = analyzedInput("for_iterable_deferred.gd", """ @@ -706,6 +818,31 @@ func ping(): ); } + private static @NotNull FrontendVisibleValueResolution resolveWithGate( + @NotNull AnalyzedInput analyzedInput, + @NotNull ForStatement forStatement, + @NotNull IdentifierExpression useSite, + @NotNull FrontendInventoryGateStatus status, + @NotNull FrontendBodyInventoryReadiness readiness, + @NotNull FrontendVisibleValueDomain requestDomain + ) { + var gate = FrontendInventoryGate.pending( + forStatement, + forStatement, + forStatement.body(), + FrontendVisibleValueDomain.FOR_SUBTREE + ) + .withStatus(status) + .withBodyInventoryReadiness(readiness); + var registry = FrontendInventoryGateRegistry.builder().add(gate).build(); + var resolver = new FrontendVisibleValueResolver(analyzedInput.analysisData(), registry); + return resolver.resolve(new FrontendVisibleValueResolveRequest( + useSite.name(), + useSite, + requestDomain + )); + } + private static @NotNull T findNode( @NotNull Node root, @NotNull Class nodeType, From ceea3359799f01c76732a07862e264c0f6b3c172 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Thu, 9 Jul 2026 14:53:19 +0800 Subject: [PATCH 12/27] feat(frontend): tighten compile-gate diagnostic snapshot and statement boundary flush - Freeze the live diagnostic manager at compile-gate entry so upstream facts are captured before the gate runs. - Share the statement boundary flush with the typed fact flush to propagate diagnostics across suite statements. - Add focused regressions covering the snapshot, suite resolver, and analyzer seams. - Refresh the diagnostic manager and compile check analyzer docs to reflect the new boundary contract. - Update the segmented type resolution pipeline plan and execution summary to track the new progress. --- ...e_resolution_pipeline_execution_summary.md | 4 +- .../frontend/diagnostic_manager.md | 2 + ...d_compile_check_analyzer_implementation.md | 6 +- ...segmented_type_resolution_pipeline_plan.md | 10 +- .../FrontendCompileCheckAnalyzer.java | 8 +- .../analyzer/FrontendStatementResolver.java | 3 + .../sema/FrontendSuiteResolverTest.java | 117 ++++++++++++++++++ .../FrontendCompileCheckAnalyzerTest.java | 28 +++++ 8 files changed, 169 insertions(+), 9 deletions(-) diff --git a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md index 1153a26e..bd587b1b 100644 --- a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md +++ b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md @@ -260,7 +260,9 @@ Diagnostic owner 保持单一: 如果同一根源错误已经有 upstream diagnostic,下游 analyzer 只能保留 side-table status,不得补第二条同级错误。 -`FrontendCompileCheckAnalyzer` 只运行在 compile-only 入口。默认 shared semantic `analyze(...)`、inspection 与未来 LSP 入口不得隐式运行 compile-only gate。Lowering 只能以 `analyzeForCompile(...)` 且 diagnostics 无 error 的结果作为最低前置条件。 +Diagnostics snapshot 在 segmented 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。 Compile gate 的 generic published-fact blocker 仍基于 final stable facts:`BLOCKED`、`DEFERRED`、`FAILED`、`UNSUPPORTED` 阻断编译,`DYNAMIC` 是 frontend 已认可的 runtime-open fact,不得误判为 blocker。 diff --git a/doc/module_impl/frontend/diagnostic_manager.md b/doc/module_impl/frontend/diagnostic_manager.md index 4da55333..0264201a 100644 --- a/doc/module_impl/frontend/diagnostic_manager.md +++ b/doc/module_impl/frontend/diagnostic_manager.md @@ -330,6 +330,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 +349,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_compile_check_analyzer_implementation.md b/doc/module_impl/frontend/frontend_compile_check_analyzer_implementation.md index fde6dd65..9c61f59b 100644 --- a/doc/module_impl/frontend/frontend_compile_check_analyzer_implementation.md +++ b/doc/module_impl/frontend/frontend_compile_check_analyzer_implementation.md @@ -39,11 +39,11 @@ 1. `analyze(...)` - 共享 frontend 语义入口 - - 负责发布 8 个稳定 frontend phase 的 semantic facts + - 负责发布 skeleton/scope/variable、interface/body suite、shared semantic publication 与 diagnostics-only phase 的 frontend facts / diagnostics snapshots - 不保证 lowering-ready 2. `analyzeForCompile(...)` - compile-only 入口 - - 先运行共享 8 phase + - 先运行共享 semantic pipeline - 再运行 `FrontendCompileCheckAnalyzer` - 最后刷新最终 diagnostics snapshot @@ -54,6 +54,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 @@ -335,6 +336,7 @@ compile gate 当前统一使用: 当前“已有 upstream error”按以下条件判定: +- upstream 诊断集合来自 compile gate 入口处冻结的 live `DiagnosticManager.snapshot()`,而不是之后会被本 gate 继续追加的 mutable manager - 同一 `sourcePath` - 同一 `FrontendRange` - severity 为 `ERROR` diff --git a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md index 5d0f35e6..02028d2c 100644 --- a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md +++ b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md @@ -781,11 +781,11 @@ Compiler-only guard payload matrix: 实施内容: -- 确定 interface phase、body suite statement、suite export、diagnostics-only phase 的 diagnostics snapshot 刷新点。 -- 重新定义 diagnostics 可见性:body statement 解析期间的诊断写入仍进入 `DiagnosticManager`,但 duplicate suppression 必须能区分 statement-local upstream、current-suite upstream 与稳定 phase upstream;不能继续假设每个 whole-module analyzer 结束后才刷新一次 snapshot。 -- 保持 compile gate 只在 `analyzeForCompile(...)` 运行。 -- 检查 compile gate 的 duplicate suppression 是否仍能识别 interface/body upstream diagnostics。 -- 对缺失 `slotTypes()`、`DEFERRED`、`FAILED`、`UNSUPPORTED` 的 final facts 保持现有 compile blocking 规则。 +- [x] G1 确定 interface phase、body suite statement、suite export、diagnostics-only phase 的 diagnostics snapshot 刷新点。`FrontendStatementResolver.flushStatementBoundary(...)` 在每个 body statement flush typed facts 后同步刷新 diagnostics snapshot;`FrontendSuiteResolver` 保留 suite export snapshot;`FrontendSemanticAnalyzer` 保留 interface/suite hand-off 后、annotation/virtual/type/loop diagnostics-only phase 后、compile-only gate 后的 snapshot。 +- [x] G2 重新定义 diagnostics 可见性:body statement 解析期间的诊断写入仍进入 `DiagnosticManager`,但 duplicate suppression 必须能区分 statement-local upstream、current-suite upstream 与稳定 phase upstream;不能继续假设每个 whole-module analyzer 结束后才刷新一次 snapshot。`FrontendSuiteResolverTest.statementBoundaryPublishesDiagnosticsSnapshotForLaterStatements` 锚定同 statement 内只能通过 live manager snapshot 读取 statement-local upstream,后一 statement 可通过 `analysisData.diagnostics()` 读取 current-suite snapshot。 +- [x] G3 保持 compile gate 只在 `analyzeForCompile(...)` 运行。代码路径未把 `FrontendCompileCheckAnalyzer` 接入 shared `analyze(...)`、suite resolver 或 inspection;既有 `FrontendCompileCheckAnalyzerTest` / `FrontendSemanticAnalyzerFrameworkTest` / `FrontendAnalysisInspectionToolTest` split 覆盖继续作为锚点。 +- [x] G4 检查 compile gate 的 duplicate suppression 是否仍能识别 interface/body upstream diagnostics。`FrontendCompileCheckAnalyzer` 在 gate 入口先要求 stable diagnostics boundary 已发布,再冻结 live `DiagnosticManager.snapshot()` 作为 upstream 去重输入;`FrontendCompileCheckAnalyzerTest.analyzeDeduplicatesAgainstLiveManagerSnapshotWhenAnalysisDataSnapshotIsStale` 锚定 manager 中存在但 stable snapshot 尚未刷新的 upstream error 仍会抑制同 anchor `sema.compile_check`。 +- [x] G5 对缺失 `slotTypes()`、`DEFERRED`、`FAILED`、`UNSUPPORTED` 的 final facts 保持现有 compile blocking 规则。compile gate 仅调整 upstream snapshot 来源,generic published-fact blocker 与 non-error slot publication blocker 逻辑未改变;`FrontendCompileCheckAnalyzerTest` 中 slot publication、deferred/failed/unsupported/dynamic 覆盖继续通过。 验收细则: 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..beed95e8 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 @@ -103,7 +103,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()) { 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 index 42ca50d5..e585004c 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendStatementResolver.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendStatementResolver.java @@ -126,6 +126,9 @@ private void resolveUnsupportedRoot(@NotNull FrontendSuiteContext context, @NotN private void flushStatementBoundary(@NotNull FrontendSuiteContext context) { context.typedEnvironment().flushStatementFacts(); + // 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 diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java index 194d9480..a6486f27 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java @@ -15,6 +15,9 @@ 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; @@ -52,6 +55,8 @@ 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", """ @@ -89,6 +94,55 @@ func ping(value): 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", """ @@ -626,6 +680,15 @@ private static boolean hasOwnerEvent(@NotNull List events, @NotNull return events.stream().anyMatch(event -> event.root() == root); } + 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, @@ -733,6 +796,60 @@ private record PhaseInput( 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 PrefixGateClassifierOwnerProcedures implements FrontendStatementResolver.OwnerProcedures { private final @NotNull FrontendBodyOwnerProcedures delegate = new FrontendBodyOwnerProcedures(); private final @NotNull VariableDeclaration prefixLocal; 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..c4f7fb2b 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 @@ -703,6 +703,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", """ From 13cd34cbc28297aa80d6ec65d55cad53daf34ab5 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Thu, 9 Jul 2026 17:09:38 +0800 Subject: [PATCH 13/27] feat(frontend): promote interface/body pipeline to default and broaden body owner coverage - Remove the production switch so the new pipeline is the only entry point and move the legacy whole-phase publication to a package-private test bridge - Expand body owner procedures to cover attribute, identifier, literal, and self bindings, parameter slots, and assignment semantic context with shared resolver injection - Report unsupported subtrees, recovery boundary diagnostics, and unsafe builtin constructor warnings through the suite resolver - Suppress redundant static self-binding diagnostics at the compile gate and align body owner facts with the legacy baseline - Refresh the pipeline plan and broaden regressions to cover nested, unsupported, and dual-role type-meta routes --- ...segmented_type_resolution_pipeline_plan.md | 19 +- .../analyzer/FrontendBodyOwnerProcedures.java | 680 +++++++++++++++++- .../FrontendCompileCheckAnalyzer.java | 28 +- .../analyzer/FrontendSemanticAnalyzer.java | 94 +-- .../analyzer/FrontendStatementResolver.java | 5 +- .../sema/analyzer/FrontendSuiteResolver.java | 69 ++ .../analyzer/FrontendTopBindingAnalyzer.java | 227 +----- .../FrontendAssignmentSemanticSupport.java | 28 +- .../FrontendDualRoleTypeMetaRouteSupport.java | 236 ++++++ ...FrontendSemanticAnalyzerFrameworkTest.java | 133 ++-- .../sema/FrontendSuiteResolverTest.java | 19 +- .../FrontendCompileCheckAnalyzerTest.java | 16 +- .../FrontendExprTypeAnalyzerTest.java | 3 +- .../FrontendSemanticAnalyzerTestAccess.java | 22 + 14 files changed, 1200 insertions(+), 379 deletions(-) create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendDualRoleTypeMetaRouteSupport.java create mode 100644 src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSemanticAnalyzerTestAccess.java diff --git a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md index 02028d2c..501a243a 100644 --- a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md +++ b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md @@ -517,7 +517,7 @@ Feature-specific header state 仍属于 gate header 语义,不得因为 body r - `FrontendAnalysisPatch` 迁入 patch package;若保留,它只能是 legacy single-stage patch / 测试兼容层,不能作为 suite export 生产路径的 multi-owner carrier。 - `FrontendLocalSlotTypeUpdate` 迁入 patch package,并只由 `FrontendLocalTypeStabilizationPatch` 携带。 - `FrontendWindowPublicationSurface` 与 `FrontendWindowAnalysisContext` 若迁入 patch package,也只能作为 legacy comparison shim。它们不再是 overlay/export 参考实现;若最终删除这两个类型,必须把仍然有效的 surface API tests 拆到 overlay 与 per-owner patch transaction 测试,并新增 VarTypePost window contamination regression test 记录旧路径问题。 -- `FrontendSemanticAnalyzer.withSegmentedSemanticRunnerForTesting()`,可改造为新 pipeline 等价测试入口。 +- `FrontendSemanticAnalyzer.analyzeWithLegacySharedSemanticPublication(...)` package-private 测试旁路,作为阶段 H 后 legacy whole-phase baseline 的等价测试入口。 重写参考资产: @@ -529,7 +529,7 @@ Feature-specific header state 仍属于 gate header 语义,不得因为 body r 可回退或废弃资产: - `FrontendSegmentedSemanticScheduler` 当前实现。它是阶段 D 的行为等价过渡调度器,仍包含 legacy local stabilization direct phase,不是新计划的主体。 -- `FrontendSemanticAnalyzer.segmentedSemanticRunner` 生产路径开关。新计划最终应只有默认新 pipeline,legacy whole-phase 只作为迁移测试旁路存在。 +- 已删除的 `FrontendSemanticAnalyzer.segmentedSemanticRunner` 生产路径开关。阶段 H 后默认入口只运行新 pipeline,legacy whole-phase 只作为 package-private 迁移测试旁路存在。 - 把 `analyzeInWindow(...)` 作为 production body procedure 的任何调用路径。 ## 5. 分步骤实施 @@ -563,7 +563,7 @@ Feature-specific header state 仍属于 gate header 语义,不得因为 body r - A1:第 4.9 节的资产分类冻结为阶段 A 基线。`FrontendSemanticStage`、`FrontendAnalysisData.applyPatch(...)`、`refreshPublishedLocalBindingPayloads(...)`、backfill guard 与 `FrontendAnalysisDataTest` 保留;patch carrier 与 transaction 类型进入 `gd.script.gdcc.frontend.sema.patch` 迁移计划;现有 analyzer walker 只作为语义 helper / rewrite reference;`FrontendSegmentedSemanticScheduler`、`segmentedSemanticRunner` 和 production `analyzeInWindow(...)` 调用路径归类为可回退或废弃资产。 - A2:`FrontendSegmentedSemanticScheduler` 已在代码中标记为 `@Deprecated` legacy comparison entry,类注释明确它不是 SuiteResolver production pipeline。IDE 对该类发出弃用警告是预期状态。 - A3:`FrontendAnalysisDataTest` 继续作为 stable reference、conflict、idempotent 与 compiler-only guard 的 focused tests。`FrontendWindowPublicationSurfaceTest` 的类级注释已降级其解释范围:它只证明 legacy shim API 的 scratch write 隔离与 guard,不证明所有 legacy `analyzeInWindow(...)` 都 scratch-safe。 -- A4:生产构造路径保持 `segmentedSemanticRunner == false`;唯一启用入口仍是 `FrontendSemanticAnalyzer.withSegmentedSemanticRunnerForTesting()`。该入口只作为 legacy comparison / equivalence test hook,不作为 production path。 +- A4/H3:阶段 A 先隔离 `segmentedSemanticRunner` 的生产影响;阶段 H 已删除该字段、构造参数和 `withSegmentedSemanticRunnerForTesting()` 工厂。legacy comparison 现在通过 package-private `FrontendSemanticAnalyzer.analyzeWithLegacySharedSemanticPublication(...)` 及 test-source bridge 进入,不再经过 deprecated segmented scheduler。 - A5:patch package 迁移计划冻结为:新建 `gd.script.gdcc.frontend.sema.patch`;以 `FrontendOwnerPatch` 或等价 sealed interface 作为单 owner patch 根类型;新增 `FrontendTopBindingPatch`、`FrontendLocalTypeStabilizationPatch`、`FrontendChainBindingPatch`、`FrontendExprTypePatch`、`FrontendVarTypePostPatch`;新增 `FrontendPatchTransaction` 按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 顺序 apply。旧 `FrontendAnalysisPatch` 若迁移则只能作为 legacy single-stage compatibility carrier,不能继续作为 SuiteResolver export 的 multi-owner carrier;`FrontendLocalSlotTypeUpdate` 随 local stabilization patch 迁入 patch 包。 - A6:whole-module walker state inventory 见下表。所有当前 `analyzeInWindow(...)` 都只允许作为 legacy comparison path;其 whole-module traversal root、隐式 visitor 字段和整表发布策略都不得直接复用为 SuiteResolver statement-local owner procedure。 - A7:`FrontendVarTypePostAnalyzer.analyzeInWindow(...)` 的 legacy 污染路径为:读取 `analysisData.slotTypes()` 稳定表 -> `clear()` -> 将稳定表传入 `SlotTypePublisher` 逐项写入 -> 再复制到 `window.publications().slotTypes()`。阶段 A 决定是隔离而非修补该 legacy path:保留测试比较价值,但新 var type post procedure 必须从 statement-local scratch/overlay 写入重新实现。 @@ -730,7 +730,7 @@ Compiler-only guard payload matrix: - [x] E3 `FrontendSuiteResolver` 默认接入真实 owner procedure;`FrontendSemanticAnalyzer` 默认仍注入 legacy-compatible no-op suite resolver,避免阶段 H 之前提前写入 body facts 干扰 legacy whole-phase fallback。 - [x] E4 新增正反向 targeted tests,锚定 source-order alias、child-prefix visibility、chain receiver exactness、transient cache isolation、var type post export boundary、C8 overlay lookup 与 framework-level real owner path hand-off。 - [x] E5 已运行格式化、IDE 增量编译与问题检查、`FrontendSuiteResolverTest`、`FrontendExpressionSemanticSupportTest`、`FrontendChainReductionFacadeTest`、`FrontendChainReductionHelperTest`、阶段 E 相关 targeted regression batch,以及 `git diff --check`。 -- [x] E6 补充 `FrontendSemanticAnalyzerFrameworkTest.injectedRealBodyOwnerSuiteResolverRunsDAndEOwnerPathBeforeLegacyPublication`,通过显式注入真实 `FrontendBodyOwnerProcedures` 的 `FrontendSuiteResolver` 证明 D/E body owner path 在 framework hand-off 中先于 legacy whole-phase publication 执行;原 `withSegmentedSemanticRunnerForTesting()` 等价测试已重命名为 deprecated scheduler coverage,不再作为 real body owner path 证据。本轮已运行 `FrontendSemanticAnalyzerFrameworkTest`、suite/body/support focused batch、阶段 E 相关 15 类 targeted regression batch 与 `git diff --check`。 +- [x] E6 原通过显式注入真实 `FrontendBodyOwnerProcedures` 的 `FrontendSuiteResolver` 证明 D/E body owner path 在 framework hand-off 中先于 legacy whole-phase publication 执行;阶段 H 后该证据已升级为 `FrontendSemanticAnalyzerFrameworkTest.defaultInterfaceBodyPipelineMatchesLegacyWholePhaseSideTables` 与 nested/unsupported 等价测试,默认 analyzer 已直接运行 real body owner path。 验收细则: @@ -800,10 +800,11 @@ Compiler-only guard payload matrix: 实施内容: -- `FrontendSemanticAnalyzer.analyze(...)` 默认运行新 interface/body pipeline。 -- legacy whole-phase runner 只保留为 package-private 测试旁路。 -- 移除 `segmentedSemanticRunner` 生产路径开关。 -- 新增 legacy vs interface/body pipeline 等价测试,等价基线以 guard-only backfill 合同为准。 +- [x] H1 `FrontendSemanticAnalyzer.analyze(...)` 默认运行新 interface/body pipeline。当前生产 `analyze(...)` 在 interface phase 后只调用真实 `FrontendSuiteResolver`,不再追加 legacy whole-phase owner publication,避免 body facts 双重发布。 +- [x] H2 legacy whole-phase runner 只保留为 package-private 测试旁路。当前 `FrontendSemanticAnalyzer.analyzeWithLegacySharedSemanticPublication(...)` 为 package-private comparison hook,供测试桥接 legacy baseline,不暴露给生产入口。 +- [x] H3 移除 `segmentedSemanticRunner` 生产路径开关。当前已删除 `segmentedSemanticRunner` 字段、构造参数与 `withSegmentedSemanticRunnerForTesting()` 工厂,生产路径不再能切到 deprecated segmented scheduler。 +- [x] H4 新增 legacy vs interface/body pipeline 等价测试,等价基线以 guard-only backfill 合同为准。当前 `FrontendSemanticAnalyzerFrameworkTest` 通过 package-private legacy test bridge 比较默认 interface/body pipeline 与 legacy whole-phase baseline 的五张共享 side table;unsupported `for` / `match` / block-local `const` 负向测试同时锚定 fail-closed fact surface,并用顺序无关 diagnostics 断言记录 legacy whole-phase 与 statement-local suite boundary 的诊断顺序差异。 +- [x] H5 broad regression parity follow-up:body owner top-binding 现在与 legacy 共享 dual-role singleton/type-meta 路偏和 global enum type-meta preference,body expression publication 补齐 builtin Variant constructor unsafe-call warning,missing / blocked binding diagnostics 与 legacy binding owner 对齐;legacy owner-specific `FrontendExprTypeAnalyzerTest` 通过 test bridge 显式进入 legacy whole-phase baseline。 验收细则: @@ -879,7 +880,7 @@ Suite/body pipeline 测试: - `FrontendSuiteResolverTest`:local stabilization patch apply 后由 commit helper 派生刷新同 declaration 的 `symbolBindings()` payload,而不是通过同一个 patch 携带 binding delta。 - `FrontendSuiteResolverTest`:`if` / `elif` / `else` / `while` header 先解析,body 后递归。 - `FrontendSuiteResolverTest`:unsupported body 不进入 resolver。 -- `FrontendSemanticAnalyzerFrameworkTest`:legacy pipeline 与显式注入真实 body owner procedures 的 interface/body pipeline side-table 等价;`withSegmentedSemanticRunnerForTesting()` 只覆盖 deprecated whole-module segmented scheduler 兼容性,不得作为 D/E real body owner path 证据。 +- `FrontendSemanticAnalyzerFrameworkTest`:legacy whole-phase baseline 与默认 interface/body pipeline side-table 等价;legacy baseline 通过 package-private `analyzeWithLegacySharedSemanticPublication(...)` test bridge 进入,不再依赖 `withSegmentedSemanticRunnerForTesting()` 或 deprecated scheduler。 - `FrontendSemanticAnalyzerFrameworkTest`:等价基线使用 guard-only backfill 合同,不允许恢复旧 expr-phase slot mutation。 Resolver 测试: 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 index 29acec72..514c0fa3 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProcedures.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProcedures.java @@ -1,44 +1,66 @@ 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.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.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.FrontendCallResolutionKind; +import gd.script.gdcc.frontend.sema.FrontendCallResolutionStatus; 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.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; @@ -55,18 +77,93 @@ /// 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 { + 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 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 FrontendVisibleValueResolver cachedVisibleValueResolver; @Override public void runTopBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { forEachExpression(root, expression -> { - if (expression instanceof IdentifierExpression identifierExpression) { + 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)) { @@ -108,6 +205,8 @@ public void runChainBinding(@NotNull FrontendSuiteContext context, @NotNull Node if (reduced != null) { publishReduction(context, reduced); } + } else if (expression instanceof LambdaExpression lambdaExpression) { + reportUnsupportedChain(context, lambdaExpression, "lambda subtree"); } }); } @@ -115,7 +214,7 @@ public void runChainBinding(@NotNull FrontendSuiteContext context, @NotNull Node @Override public void runExprType(@NotNull FrontendSuiteContext context, @NotNull Node root) { var resolver = new BodyExpressionResolver(context); - forEachExpression(root, expression -> publishExpressionType(context, resolver, expression)); + publishRootExpressionTypes(context, resolver, root); } @Override @@ -131,6 +230,7 @@ public void runVarTypePost(@NotNull FrontendSuiteContext context, @NotNull Node 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); @@ -141,6 +241,138 @@ public void runVarTypePost(@NotNull FrontendSuiteContext context, @NotNull Node ); } + 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( + FrontendVarTypePostAnalyzer.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; + } + 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; + } + + 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 ForStatement forStatement -> { + reportUnsupportedBinding(context, forStatement, "for subtree"); + reportUnsupportedChain(context, forStatement, "for subtree"); + } + case MatchStatement matchStatement -> { + reportUnsupportedBinding(context, matchStatement, "match subtree"); + reportUnsupportedChain(context, matchStatement, "match subtree"); + // `match` sections remain sealed until their gate is supported, 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 @@ -152,6 +384,13 @@ private void bindIdentifier( 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) { @@ -168,6 +407,49 @@ private void bindIdentifier( 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( @@ -347,6 +629,7 @@ private static void publishReduction( trace.step(), trace.suggestedMember() ); + reportMemberTrace(context, trace); } if (trace.suggestedCall() != null) { context.typedEnvironment().putResolvedCall( @@ -354,33 +637,303 @@ private static void publishReduction( 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 + @NotNull Expression expression, + boolean allowStatementResult + ) { + resolver.populateRootExpressionTransientCaches(expression, allowStatementResult); + for (var entry : resolver.finalizedExpressionTypes().entrySet()) { + if (resolver.isRouteHeadOnlyTypeMeta(entry.getKey())) { + continue; + } + 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)); + } + } + 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 reportUnsafeCallArgumentWarning( + @NotNull FrontendSuiteContext context, + @NotNull CallExpression callExpression, + @NotNull FrontendResolvedCall publishedCall ) { - var expressionType = resolver.resolveExpressionType(expression, true); - context.typedEnvironment().putExpressionType( - FrontendSemanticStage.EXPR_TYPE, - expression, - expressionType + 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()) ); - if (expression instanceof CallExpression callExpression) { - var resolvedCall = resolver.resolvedCall(callExpression); - if (resolvedCall != null) { - context.typedEnvironment().putResolvedCall( - FrontendSemanticStage.EXPR_TYPE, - callExpression, - resolvedCall - ); + } + + 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) { if (cachedAnalysisData != context.analysisData() || cachedVisibleValueResolver == null) { cachedAnalysisData = context.analysisData(); @@ -459,10 +1012,17 @@ private static final class BodyExpressionResolver { new IdentityHashMap<>(); private final @NotNull IdentityHashMap finalizedExpressionTypes = 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"); @@ -476,6 +1036,15 @@ private BodyExpressionResolver(@NotNull FrontendSuiteContext context) { 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(), @@ -507,6 +1076,23 @@ private BodyExpressionResolver(@NotNull FrontendSuiteContext context) { 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 @@ -521,10 +1107,20 @@ private BodyExpressionResolver(@NotNull FrontendSuiteContext context) { case IdentifierExpression identifierExpression -> expressionSemanticSupport .resolveIdentifierExpressionType(identifierExpression) .expressionType(); - case AttributeExpression attributeExpression -> resolveAttributeExpressionType(attributeExpression); - case AssignmentExpression _ -> FrontendExpressionType.failed( - "Assignment expression typing is not part of the statement-local value contract yet" - ); + case AttributeExpression attributeExpression -> + resolveAttributeExpressionType(attributeExpression, finalizeWindow); + case AssignmentExpression assignmentExpression -> FrontendAssignmentSemanticSupport + .resolveAssignmentExpressionType( + assignmentSemanticContext, + assignmentExpression, + assignmentUsages.getOrDefault( + assignmentExpression, + FrontendAssignmentSemanticSupport.AssignmentUsage.VALUE_REQUIRED + ), + this::resolveExpressionType, + finalizeWindow + ) + .expressionType(); case CallExpression callExpression -> resolveCallExpressionType(callExpression, finalizeWindow); case SubscriptExpression subscriptExpression -> expressionSemanticSupport .resolveSubscriptExpressionType( @@ -583,8 +1179,26 @@ private BodyExpressionResolver(@NotNull FrontendSuiteContext context) { } private @NotNull FrontendExpressionType resolveAttributeExpressionType( - @NotNull AttributeExpression attributeExpression + @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( @@ -600,8 +1214,28 @@ private BodyExpressionResolver(@NotNull FrontendSuiteContext context) { return chainReduction.reduce(attributeExpression).result(); } - private @Nullable FrontendResolvedCall resolvedCall(@NotNull CallExpression callExpression) { - return resolvedCalls.get(callExpression); + private @NotNull IdentityHashMap finalizedExpressionTypes() { + return finalizedExpressionTypes; + } + + 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( 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 beed95e8..bd87b96e 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 @@ -548,7 +548,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); @@ -574,6 +577,9 @@ private void scanResolvedMemberCompileBlocks() { if (!isCompileBlocking(publishedMember.status()) || !compileSurfaceNodes.contains(anchor)) { continue; } + if (isCoveredByStaticSelfBindingDiagnostic(publishedMember.detailReason())) { + continue; + } reportCompileBlock( anchor, publishedCompileBlockedMessage( @@ -684,10 +690,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; } @@ -695,13 +698,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().equals(FrontendDiagnostic.sourcePathText(sourcePath)) + ); } private static @Nullable SelfExpression directExplicitSelfAssignmentTargetPrefixOrNull( 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 b4ac5010..b068b579 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 @@ -43,7 +40,6 @@ public final class FrontendSemanticAnalyzer { private final @NotNull FrontendCompileCheckAnalyzer compileCheckAnalyzer; private final @NotNull FrontendInterfacePhase interfacePhase; private final @NotNull FrontendSuiteResolver suiteResolver; - private final boolean segmentedSemanticRunner; public FrontendSemanticAnalyzer() { this( @@ -82,8 +78,7 @@ public FrontendSemanticAnalyzer( new FrontendLoopControlFlowAnalyzer(), new FrontendCompileCheckAnalyzer(), interfacePhase, - suiteResolver, - false + suiteResolver ); } @@ -449,8 +444,7 @@ public FrontendSemanticAnalyzer( loopControlFlowAnalyzer, compileCheckAnalyzer, new FrontendInterfacePhase(), - legacyCompatibleSuiteResolver(), - false + new FrontendSuiteResolver() ); } @@ -469,8 +463,7 @@ private FrontendSemanticAnalyzer( @NotNull FrontendLoopControlFlowAnalyzer loopControlFlowAnalyzer, @NotNull FrontendCompileCheckAnalyzer compileCheckAnalyzer, @NotNull FrontendInterfacePhase interfacePhase, - @NotNull FrontendSuiteResolver suiteResolver, - boolean segmentedSemanticRunner + @NotNull FrontendSuiteResolver suiteResolver ) { this.classSkeletonBuilder = Objects.requireNonNull(classSkeletonBuilder, "classSkeletonBuilder must not be null"); this.scopeAnalyzer = Objects.requireNonNull(scopeAnalyzer, "scopeAnalyzer must not be null"); @@ -502,35 +495,6 @@ private FrontendSemanticAnalyzer( 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"); - this.segmentedSemanticRunner = segmentedSemanticRunner; - } - - public static @NotNull FrontendSemanticAnalyzer withSegmentedSemanticRunnerForTesting() { - // This factory keeps the deprecated whole-module segmented scheduler covered. It does not - // exercise the Phase D/E root-bounded body owner path; tests for that path should inject a - // `FrontendSuiteResolver` wired with real owner procedures through the public constructor. - return new FrontendSemanticAnalyzer( - 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 FrontendInterfacePhase(), - legacyCompatibleSuiteResolver(), - true - ); - } - - private static @NotNull FrontendSuiteResolver legacyCompatibleSuiteResolver() { - return new FrontendSuiteResolver(new FrontendStatementResolver()); } /// Runs the current frontend analyzer framework against one module using a shared @@ -547,6 +511,26 @@ private FrontendSemanticAnalyzer( @NotNull FrontendModule module, @NotNull ClassRegistry classRegistry, @NotNull DiagnosticManager diagnosticManager + ) { + return analyzeShared(module, classRegistry, diagnosticManager, false); + } + + /// Test-only compatibility hook for comparing the retired whole-phase owner publication path + /// with the default interface/body pipeline. Production callers must use `analyze(...)` so body + /// facts are published by `FrontendSuiteResolver` and not double-written by legacy analyzers. + @NotNull FrontendAnalysisData analyzeWithLegacySharedSemanticPublication( + @NotNull FrontendModule module, + @NotNull ClassRegistry classRegistry, + @NotNull DiagnosticManager diagnosticManager + ) { + return analyzeShared(module, classRegistry, diagnosticManager, true); + } + + private @NotNull FrontendAnalysisData analyzeShared( + @NotNull FrontendModule module, + @NotNull ClassRegistry classRegistry, + @NotNull DiagnosticManager diagnosticManager, + boolean useLegacySharedSemanticPublication ) { Objects.requireNonNull(module, "module must not be null"); Objects.requireNonNull(classRegistry, "classRegistry must not be null"); @@ -577,16 +561,17 @@ private FrontendSemanticAnalyzer( variableAnalyzer.analyze(analysisData, diagnosticManager); analysisData.updateDiagnostics(diagnosticManager.snapshot()); - // The Interface/Body hand-off runs before legacy whole-phase publication. The default - // analyzer still injects a legacy-compatible no-op suite resolver until Phase H switches the - // shared pipeline. Phase D/E tests that need the real body owner path inject a - // `FrontendSuiteResolver` with real owner procedures through the public constructor. + // Interface analysis freezes callable/property entry roots, then exactly one body owner + // publication path runs. The legacy path is reserved for comparison tests so production never + // publishes the same side-table facts twice. var interfaceSurface = interfacePhase.analyze(classRegistry, analysisData); - suiteResolver.resolve(interfaceSurface, classRegistry, analysisData, diagnosticManager); + if (useLegacySharedSemanticPublication) { + runLegacySharedSemanticPublication(classRegistry, diagnosticManager, analysisData); + } else { + suiteResolver.resolve(interfaceSurface, classRegistry, analysisData, diagnosticManager); + } analysisData.updateDiagnostics(diagnosticManager.snapshot()); - runSharedSemanticPublication(classRegistry, diagnosticManager, analysisData); - // Annotation-usage validation consumes retained annotations plus the published class/scope // facts, but still stays diagnostics-only and does not mutate semantic side tables. annotationUsageAnalyzer.analyze(classRegistry, analysisData, diagnosticManager); @@ -609,22 +594,11 @@ private FrontendSemanticAnalyzer( return analysisData; } - private void runSharedSemanticPublication( + private void runLegacySharedSemanticPublication( @NotNull ClassRegistry classRegistry, @NotNull DiagnosticManager diagnosticManager, @NotNull FrontendAnalysisData analysisData ) { - if (segmentedSemanticRunner) { - new FrontendSegmentedSemanticScheduler( - topBindingAnalyzer, - localTypeStabilizationAnalyzer, - chainBindingAnalyzer, - exprTypeAnalyzer, - varTypePostAnalyzer - ).run(classRegistry, analysisData, diagnosticManager); - return; - } - // 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. 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 index e585004c..21749290 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendStatementResolver.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendStatementResolver.java @@ -22,8 +22,9 @@ /// Root-bounded statement dispatcher for the new body SuiteResolver skeleton. /// -/// Phase D only establishes traversal and owner ordering. The owner procedures are intentionally -/// injectable no-op hooks until Phase E replaces the legacy whole-module analyzers. +/// The no-op constructor is retained for traversal tests and explicit legacy shims. Production +/// wiring reaches this dispatcher through `FrontendSuiteResolver`, which supplies real owner +/// procedures and publishes facts through the typed lexical environment. public class FrontendStatementResolver { private final @NotNull OwnerProcedures ownerProcedures; 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 index e166cd59..76b1bc89 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolver.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolver.java @@ -4,20 +4,26 @@ 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.FrontendExecutableInventorySupport; 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.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. @@ -26,6 +32,10 @@ /// `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() { @@ -100,9 +110,60 @@ private void resolveCallableOwner( diagnosticManager, classRegistry ); + publishCallableParameterSlots(context, callableOwner); resolveSuite(context, body); } + private void publishCallableParameterSlots( + @NotNull FrontendSuiteContext context, + @NotNull Node callableOwner + ) { + var parameters = callableParameters(callableOwner); + for (var parameter : parameters) { + publishParameterSlotType(context, parameter); + reportUnsupportedParameterDefault(context, parameter); + } + // Parameters are callable-entry facts rather than statement facts, but they still need to + // enter the suite overlay before export and before the first statement consumes them. + context.typedEnvironment().flushStatementFacts(); + } + + private void publishParameterSlotType( + @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"); + } + context.typedEnvironment().putSlotType(FrontendSemanticStage.VAR_TYPE_POST, parameter, slot.type()); + } + + 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, @@ -175,6 +236,14 @@ private static boolean isCallableLocalValueInventoryReady( }; } + 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(); 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 index a2e09d78..ff4e19be 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTopBindingAnalyzer.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTopBindingAnalyzer.java @@ -9,6 +9,7 @@ import gd.script.gdcc.frontend.sema.FrontendModuleSkeleton; import gd.script.gdcc.frontend.sema.FrontendSemanticStage; import gd.script.gdcc.frontend.sema.FrontendWindowAnalysisContext; +import gd.script.gdcc.frontend.sema.analyzer.support.FrontendDualRoleTypeMetaRouteSupport; 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; @@ -17,23 +18,18 @@ 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; @@ -63,9 +59,8 @@ 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.IdentityHashMap; import java.util.List; import java.util.Objects; @@ -1020,26 +1015,14 @@ private void publishScopeValueBinding( } private boolean supportsTopLevelTypeMeta(@NotNull ScopeTypeMeta typeMeta) { - return switch (typeMeta.kind()) { - case GDCC_CLASS, ENGINE_CLASS, BUILTIN -> !typeMeta.pseudoType(); - case GLOBAL_ENUM -> typeMeta.declaration() != null; - }; + return FrontendDualRoleTypeMetaRouteSupport.supportsTopLevelTypeMeta(typeMeta); } 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()); + return FrontendDualRoleTypeMetaRouteSupport.shouldPreferGlobalEnumTypeMeta(valueResolution, typeMetaResult); } /// Dual-role chain-head route bias entry point. @@ -1088,198 +1071,24 @@ private boolean tryApplyDualRoleTypeMetaBias(@NotNull AttributeExpression attrib 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( + var biasedTypeMeta = FrontendDualRoleTypeMetaRouteSupport.resolveBiasedTypeMeta( + attributeExpression, + resolveVisibleValue(identifierExpression), currentScope, - name, - currentRestriction + currentRestriction, + moduleSkeleton, + classRegistry ); - 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; - } + if (biasedTypeMeta == null) { 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; + publishBinding( + identifierExpression, + name, + FrontendBindingKind.TYPE_META, + biasedTypeMeta.declaration() + ); + return true; } private @Nullable Scope findCurrentScope(@NotNull IdentifierExpression identifierExpression) { 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/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/test/java/gd/script/gdcc/frontend/sema/FrontendSemanticAnalyzerFrameworkTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSemanticAnalyzerFrameworkTest.java index 344d6de7..23ab80b9 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSemanticAnalyzerFrameworkTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSemanticAnalyzerFrameworkTest.java @@ -17,6 +17,7 @@ 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.FrontendSemanticAnalyzerTestAccess; import gd.script.gdcc.frontend.sema.analyzer.FrontendStatementResolver; import gd.script.gdcc.frontend.sema.analyzer.FrontendSuiteContext; import gd.script.gdcc.frontend.sema.analyzer.FrontendSuiteResolver; @@ -666,7 +667,7 @@ func _ready( } @Test - void analyzePublishesPhaseBoundariesThroughVirtualOverridePhaseAndRefreshesDiagnosticsAfterEachPhase() throws Exception { + void legacyWholePhaseBypassPublishesPhaseBoundariesThroughVirtualOverridePhaseAndRefreshesDiagnosticsAfterEachPhase() throws Exception { var parserService = new GdScriptParserService(); var diagnostics = new DiagnosticManager(); var unit = parserService.parseUnit(Path.of("tmp", "variable_phase_probe.gd"), """ @@ -704,7 +705,13 @@ func ping(value): new FrontendCompileCheckAnalyzer() ); - var result = analyzeModule(analyzer, "test_module", List.of(unit), registry, diagnostics); + var result = analyzeModuleWithLegacySharedSemanticPublication( + analyzer, + "test_module", + List.of(unit), + registry, + diagnostics + ); assertTrue(probeScopeAnalyzer.invoked); assertTrue(probeScopeAnalyzer.moduleSkeletonPublished); @@ -983,7 +990,7 @@ func ping(value: Point) -> int: } @Test - void injectedRealBodyOwnerSuiteResolverRunsDAndEOwnerPathBeforeLegacyPublication() throws Exception { + void defaultInterfaceBodyPipelineMatchesLegacyWholePhaseSideTables() throws Exception { var parserService = new GdScriptParserService(); var parseDiagnostics = new DiagnosticManager(); var unit = parserService.parseUnit(Path.of("tmp", "real_body_owner_framework_path.gd"), """ @@ -1004,42 +1011,29 @@ func ping(value: Point) -> int: var markerStep = findNode(pingFunction.body(), AttributePropertyStep.class, step -> step.name().equals("marker")); var numberInitializer = numberDeclaration.value(); assertNotNull(numberInitializer); - var ownerProcedures = new FrameworkBodyOwnerProcedures( - aliasDeclaration, - numberDeclaration, - markerStep, - numberInitializer - ); var legacyDiagnostics = new DiagnosticManager(); - var realBodyDiagnostics = new DiagnosticManager(); + var interfaceBodyDiagnostics = new DiagnosticManager(); - var legacy = analyzeModule( + var legacy = analyzeModuleWithLegacySharedSemanticPublication( new FrontendSemanticAnalyzer(), "test_module", List.of(unit), new ClassRegistry(ExtensionApiLoader.loadDefault()), legacyDiagnostics ); - var realBody = analyzeModule( - new FrontendSemanticAnalyzer( - new FrontendInterfacePhase(), - new FrontendSuiteResolver(new FrontendStatementResolver(ownerProcedures)) - ), + var interfaceBody = analyzeModule( + new FrontendSemanticAnalyzer(), "test_module", List.of(unit), new ClassRegistry(ExtensionApiLoader.loadDefault()), - realBodyDiagnostics + interfaceBodyDiagnostics ); - var aliasSlotType = realBody.slotTypes().get(aliasDeclaration); - var numberSlotType = realBody.slotTypes().get(numberDeclaration); - var markerMember = realBody.resolvedMembers().get(markerStep); - var initializerType = realBody.expressionTypes().get(numberInitializer); + 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( - () -> assertTrue(ownerProcedures.aliasSlotWasPublishedBeforeLegacy()), - () -> assertTrue(ownerProcedures.memberFactWasPublishedBeforeLegacy()), - () -> assertTrue(ownerProcedures.expressionFactWasPublishedBeforeLegacy()), - () -> assertTrue(ownerProcedures.varPostFactWasPublishedBeforeLegacy()), () -> assertNotNull(aliasSlotType), () -> assertTrue(aliasSlotType.getTypeName().endsWith("Point")), () -> assertNotNull(numberSlotType), @@ -1052,12 +1046,12 @@ func ping(value: Point) -> int: () -> assertEquals(FrontendExpressionTypeStatus.RESOLVED, initializerType.status()), () -> assertEquals("int", initializerType.publishedType().getTypeName()) ); - assertEquivalentSharedSemanticFacts(legacy, realBody); - assertEquals(legacyDiagnostics.snapshot(), realBodyDiagnostics.snapshot()); + assertEquivalentSharedSemanticFacts(legacy, interfaceBody); + assertEquals(legacyDiagnostics.snapshot(), interfaceBodyDiagnostics.snapshot()); } @Test - void deprecatedSegmentedSchedulerProducesEquivalentSharedSemanticSideTablesAndDiagnostics() throws Exception { + void defaultInterfaceBodyPipelineKeepsNestedSharedSemanticFactsEquivalentToLegacy() throws Exception { var parserService = new GdScriptParserService(); var parseDiagnostics = new DiagnosticManager(); var unit = parserService.parseUnit(Path.of("tmp", "segmented_equivalence.gd"), """ @@ -1077,30 +1071,30 @@ func ping(value: Point) -> int: return alias.marker """, parseDiagnostics); var legacyDiagnostics = new DiagnosticManager(); - var segmentedDiagnostics = new DiagnosticManager(); + var interfaceBodyDiagnostics = new DiagnosticManager(); - var legacy = analyzeModule( + var legacy = analyzeModuleWithLegacySharedSemanticPublication( new FrontendSemanticAnalyzer(), "test_module", List.of(unit), new ClassRegistry(ExtensionApiLoader.loadDefault()), legacyDiagnostics ); - var segmented = analyzeModule( - FrontendSemanticAnalyzer.withSegmentedSemanticRunnerForTesting(), + var interfaceBody = analyzeModule( + new FrontendSemanticAnalyzer(), "test_module", List.of(unit), new ClassRegistry(ExtensionApiLoader.loadDefault()), - segmentedDiagnostics + interfaceBodyDiagnostics ); - assertEquivalentSharedSemanticFacts(legacy, segmented); - assertEquals(legacy.diagnostics(), segmented.diagnostics()); - assertEquals(legacyDiagnostics.snapshot(), segmentedDiagnostics.snapshot()); + assertEquivalentSharedSemanticFacts(legacy, interfaceBody); + assertDiagnosticsEquivalentIgnoringOrder(legacy.diagnostics(), interfaceBody.diagnostics()); + assertEquals(legacyDiagnostics.snapshot(), interfaceBodyDiagnostics.snapshot()); } @Test - void deprecatedSegmentedSchedulerKeepsExistingUnsupportedSubtreeBehaviorEquivalent() throws Exception { + void defaultInterfaceBodyPipelineKeepsUnsupportedSubtreesFailClosed() throws Exception { var parserService = new GdScriptParserService(); var unit = parserService.parseUnit(Path.of("tmp", "segmented_unsupported_equivalence.gd"), """ class_name SegmentedUnsupportedEquivalence @@ -1115,29 +1109,40 @@ func ping(values): return values """, new DiagnosticManager()); var legacyDiagnostics = new DiagnosticManager(); - var segmentedDiagnostics = new DiagnosticManager(); + var interfaceBodyDiagnostics = new DiagnosticManager(); - var legacy = analyzeModule( + var legacy = analyzeModuleWithLegacySharedSemanticPublication( new FrontendSemanticAnalyzer(), "test_module", List.of(unit), new ClassRegistry(ExtensionApiLoader.loadDefault()), legacyDiagnostics ); - var segmented = analyzeModule( - FrontendSemanticAnalyzer.withSegmentedSemanticRunnerForTesting(), + var interfaceBody = analyzeModule( + new FrontendSemanticAnalyzer(), "test_module", List.of(unit), new ClassRegistry(ExtensionApiLoader.loadDefault()), - segmentedDiagnostics + 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()); - assertEquivalentSharedSemanticFacts(legacy, segmented); + assertEquivalentSharedSemanticFacts(legacy, interfaceBody); + assertNull(interfaceBody.slotTypes().get(blockedConst)); + assertNull(interfaceBody.slotTypes().get(hiddenLocal)); + assertNull(interfaceBody.symbolBindings().get(hiddenUseSite)); assertEquals( diagnosticsByCategory(legacy.diagnostics(), "sema.unsupported_binding_subtree"), - diagnosticsByCategory(segmented.diagnostics(), "sema.unsupported_binding_subtree") + diagnosticsByCategory(interfaceBody.diagnostics(), "sema.unsupported_binding_subtree") ); - assertEquals(legacy.diagnostics(), segmented.diagnostics()); + assertDiagnosticsEquivalentIgnoringOrder(legacy.diagnostics(), interfaceBody.diagnostics()); } @Test @@ -1549,6 +1554,21 @@ private FrontendAnalysisData analyzeModule( return analyzer.analyze(new FrontendModule(moduleName, units), registry, diagnostics); } + private FrontendAnalysisData analyzeModuleWithLegacySharedSemanticPublication( + @NotNull FrontendSemanticAnalyzer analyzer, + @NotNull String moduleName, + @NotNull List units, + @NotNull ClassRegistry registry, + @NotNull DiagnosticManager diagnostics + ) { + return FrontendSemanticAnalyzerTestAccess.analyzeWithLegacySharedSemanticPublication( + analyzer, + new FrontendModule(moduleName, units), + registry, + diagnostics + ); + } + private FrontendAnalysisData analyzeModuleForCompile( @NotNull String moduleName, @NotNull List units, @@ -1692,7 +1712,6 @@ private void assertEquivalentSideTable( @NotNull BiPredicate sameValue, @NotNull String tableName ) { - assertEquals(expected.size(), actual.size(), tableName + " size changed"); for (var entry : expected.entrySet()) { assertTrue(actual.containsKey(entry.getKey()), tableName + " lost key " + entry.getKey()); var actualValue = actual.get(entry.getKey()); @@ -1703,6 +1722,10 @@ private void assertEquivalentSideTable( + ", actual " + actualValue ); } + for (var entry : actual.entrySet()) { + assertTrue(expected.containsKey(entry.getKey()), tableName + " gained key " + entry.getKey()); + } + assertEquals(expected.size(), actual.size(), tableName + " size changed"); } private FunctionDeclaration findFunction(List statements, String name) { @@ -1796,6 +1819,16 @@ private List diagnosticsByCategory( .toList(); } + 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"), @@ -1854,10 +1887,10 @@ public void runLocalTypeStabilization(@NotNull FrontendSuiteContext context, @No aliasSlotWasPublishedBeforeLegacy = context.analysisData().slotTypes().get(aliasDeclaration) == null && currentBlockScope != null && hasTypeNameSuffix(context.typedEnvironment().localSlotType( - currentBlockScope, - aliasDeclaration.name(), - aliasDeclaration - ), "Point"); + currentBlockScope, + aliasDeclaration.name(), + aliasDeclaration + ), "Point"); } } diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java index a6486f27..23299af0 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java @@ -99,7 +99,7 @@ void statementBoundaryPublishesDiagnosticsSnapshotForLaterStatements() throws Ex var phaseInput = phaseInput("suite_statement_diagnostics_snapshot.gd", """ class_name SuiteStatementDiagnosticsSnapshot extends Node - + func ping(value): var first := value var second := first @@ -285,7 +285,7 @@ void classifierReadsPrefixOverlayAndDoesNotOpenUnpublishedBody() throws Exceptio var phaseInput = phaseInput("suite_gate_classifier_prefix.gd", """ class_name SuiteGateClassifierPrefix extends Node - + func ping(values, choice): var limit := 1 for item in values: @@ -341,7 +341,7 @@ void publishedGateBodyIsResolvedBySuiteResolver() 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 @@ -379,7 +379,7 @@ void missingOwningGateBodyContextUsesDeferredDomainAndStaysFailClosed() throws E var phaseInput = phaseInput("suite_gate_body_missing_owner.gd", """ class_name SuiteGateBodyMissingOwner extends Node - + func ping(values, seed: int): for item in values: print(seed) @@ -471,10 +471,10 @@ void bodyOwnerProceduresStabilizeSourceOrderAliasChainBeforeLegacyAnalyzers() th 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 @@ -514,10 +514,10 @@ void childBlockReadsParentPrefixAndRejectedShadowDoesNotPolluteParentSlot() thro 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: @@ -565,7 +565,7 @@ void transientExpressionCacheAndVarPostFactsStayOverlayLocalUntilSuiteExport() t var phaseInput = phaseInput("suite_stage_e_export_boundary.gd", """ class_name SuiteStageEExportBoundary extends RefCounted - + func ping(seed: int): var first := seed var second := first @@ -1139,6 +1139,7 @@ public void resolve( && analysisData.resolvedCalls().isEmpty() && analysisData.expressionTypes().isEmpty() && analysisData.slotTypes().isEmpty(); + super.resolve(interfaceSurface, classRegistry, analysisData, diagnosticManager); } private boolean invoked() { 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 c4f7fb2b..762e4568 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 @@ -148,7 +148,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")); } @@ -518,7 +518,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 +549,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 @@ -708,7 +714,7 @@ void analyzeDeduplicatesAgainstLiveManagerSnapshotWhenAnalysisDataSnapshotIsStal var preparedInput = prepareCompileCheckInput("compile_check_live_manager_upstream.gd", """ class_name CompileCheckLiveManagerUpstream extends Node - + func ping(): [1] """); @@ -1167,7 +1173,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 diff --git a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzerTest.java b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzerTest.java index 17e2302c..6e1cc1c0 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzerTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzerTest.java @@ -2753,7 +2753,8 @@ 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 = FrontendSemanticAnalyzerTestAccess.analyzeWithLegacySharedSemanticPublication( + new FrontendSemanticAnalyzer(), new FrontendModule("test_module", List.of(unit), topLevelCanonicalNameMap), registry, diagnostics diff --git a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSemanticAnalyzerTestAccess.java b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSemanticAnalyzerTestAccess.java new file mode 100644 index 00000000..27207b91 --- /dev/null +++ b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSemanticAnalyzerTestAccess.java @@ -0,0 +1,22 @@ +package gd.script.gdcc.frontend.sema.analyzer; + +import gd.script.gdcc.frontend.diagnostic.DiagnosticManager; +import gd.script.gdcc.frontend.parse.FrontendModule; +import gd.script.gdcc.frontend.sema.FrontendAnalysisData; +import gd.script.gdcc.scope.ClassRegistry; +import org.jetbrains.annotations.NotNull; + +/// Test-source bridge for package-private semantic analyzer compatibility hooks. +public final class FrontendSemanticAnalyzerTestAccess { + private FrontendSemanticAnalyzerTestAccess() { + } + + public static @NotNull FrontendAnalysisData analyzeWithLegacySharedSemanticPublication( + @NotNull FrontendSemanticAnalyzer analyzer, + @NotNull FrontendModule module, + @NotNull ClassRegistry classRegistry, + @NotNull DiagnosticManager diagnosticManager + ) { + return analyzer.analyzeWithLegacySharedSemanticPublication(module, classRegistry, diagnosticManager); + } +} From 339ef8dce697c36b7cdd1c588f02ca2d07c49480 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Thu, 9 Jul 2026 18:54:05 +0800 Subject: [PATCH 14/27] feat(frontend): retire segmented scheduler and finalize body owner pipeline - Drop the legacy segmented semantic scheduler and the test-only whole-phase publication hook so the interface/body pipeline is the sole entry point. - Route assignment target prefix expressions through body owner procedures with shared resolver guard semantics. - Suppress duplicate diagnostics for receiver-rooted assignment targets and align body facts with the legacy baseline. - Refresh the pipeline plan, execution summary, and related module docs to reflect the retired scheduler and expanded target coverage. - Tighten regressions around target resolution, suite resolver, and analyzer seams. --- ...e_resolution_pipeline_execution_summary.md | 5 +- ..._chain_binding_expr_type_implementation.md | 25 +- ...d_compile_check_analyzer_implementation.md | 5 +- ...tend_for_range_loop_implementation_plan.md | 3 +- ...local_type_stabilization_implementation.md | 29 +- ...segmented_type_resolution_pipeline_plan.md | 42 +- ...ontend_variable_analyzer_implementation.md | 5 +- ...d_visible_value_resolver_implementation.md | 8 +- .../analyzer/FrontendBodyOwnerProcedures.java | 133 +++++- .../FrontendSegmentedSemanticScheduler.java | 120 ------ .../analyzer/FrontendSemanticAnalyzer.java | 83 +--- ...FrontendSemanticAnalyzerFrameworkTest.java | 378 +++++------------- .../FrontendExprTypeAnalyzerTest.java | 40 +- .../FrontendSemanticAnalyzerTestAccess.java | 22 - 14 files changed, 329 insertions(+), 569 deletions(-) delete mode 100644 src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSegmentedSemanticScheduler.java delete mode 100644 src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSemanticAnalyzerTestAccess.java diff --git a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md index bd587b1b..35630b71 100644 --- a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md +++ b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md @@ -2,6 +2,8 @@ 本文总结 `doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md` 执行完成后的前端分析流水线形态。内容只描述目标架构、执行顺序与不变量,不展开旧 whole-module 流水线或过渡实现资产。 +最近同步:2026-07-09(Phase I:shared analyzer legacy whole-phase bypass、test bridge 与 `FrontendSegmentedSemanticScheduler` 已删除)。 + ## 1. 总体形态 计划完成后,frontend shared semantic pipeline 分为四个层次: @@ -260,7 +262,7 @@ Diagnostic owner 保持单一: 如果同一根源错误已经有 upstream diagnostic,下游 analyzer 只能保留 side-table status,不得补第二条同级错误。 -Diagnostics snapshot 在 segmented 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。 +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。 @@ -294,6 +296,7 @@ Top binding 先绑定 `receiver` use-site。Local stabilization 随后把 `recei 计划完成后应满足以下不变量: - shared semantic 默认使用 interface/body pipeline。 +- shared analyzer 不再提供 legacy whole-phase body publication bypass;`FrontendSegmentedSemanticScheduler` 不再是代码资产。 - body typed resolution 按 source order 运行。 - 每个 statement 内 owner 顺序固定为 top binding -> local stabilization -> chain binding -> expr typing -> var type post。 - pending overlay 只对当前 statement 后续 owner 可见。 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..f6f3e33d 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 @@ -4,8 +4,8 @@ ## 文档状态 -- 状态:事实源维护中(`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、typed overlay-aware 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-07-09(Phase I:production shared analyzer 不再提供 legacy whole-phase chain/expr bypass) - 适用范围: - `src/main/java/gd/script/gdcc/frontend/sema/**` - `src/main/java/gd/script/gdcc/frontend/sema/analyzer/**` @@ -40,7 +40,7 @@ ### 1.1 主链路位置 -当前 `FrontendSemanticAnalyzer` 的稳定顺序是: +当前 production `FrontendSemanticAnalyzer` 的稳定顺序是: 1. `FrontendClassSkeletonBuilder.build(...)` 2. `analysisData.updateModuleSkeleton(...)` @@ -49,21 +49,16 @@ 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 +- standalone `FrontendChainBindingAnalyzer.analyze(...)` / `FrontendExprTypeAnalyzer.analyze(...)` 保留为 focused analyzer / legacy shim 测试参考,不再由 shared analyzer 的 legacy whole-phase bypass 调用 ### 1.2 当前 owner 边界 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 9c61f59b..9c754945 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、显式 AST 封口、generic published-fact blocker、explicit self assignment-target prefix 去重、shared/compile 分流边界、interface/body SuiteResolver facts、unary/binary 非 blocker 合同已落地) +- 更新时间:2026-07-09(Phase I:compile gate 消费最终 shared pipeline 导出的 stable facts,不依赖 legacy whole-phase publication) - 适用范围: - `src/main/java/gd/script/gdcc/frontend/sema/**` - `src/main/java/gd/script/gdcc/frontend/sema/analyzer/**` @@ -40,6 +40,7 @@ 1. `analyze(...)` - 共享 frontend 语义入口 - 负责发布 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 入口 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 index a7c404b6..87df75ff 100644 --- a/doc/module_impl/frontend/frontend_for_range_loop_implementation_plan.md +++ b/doc/module_impl/frontend/frontend_for_range_loop_implementation_plan.md @@ -6,6 +6,7 @@ - 状态:计划中 - 创建日期:2026-07-03 +- 最近校对:2026-07-09(Phase I:后续 for-range 解封必须基于最终 interface/body + SuiteResolver shared semantic pipeline) - 适用范围: - `src/main/java/gd/script/gdcc/frontend/sema/**` - `src/main/java/gd/script/gdcc/frontend/lowering/**` @@ -58,7 +59,7 @@ - `GdScriptParserService` 只是外部 parser adapter,本仓库没有本地 parser grammar 可改;若 parse smoke 发现 `range` loop AST 形态不足,应先升级或修复外部 parser,而不是在 frontend analyzer 中猜文本。 - `FrontendScopeAnalyzer` 已为 `ForStatement` 建立 `FOR_BODY` scope,并保证 `iteratorType` 与 `iterable` 在外层 scope 下遍历,`body` 在独立 `FOR_BODY` scope 下遍历。 - `FrontendLoopControlFlowAnalyzer` 已把 `for` 与 `while` 一样视为 loop boundary,`break` / `continue` 在 `for` body 内不再报 `sema.loop_control_flow`。 -- `FrontendVisibleValueResolver`、`FrontendVariableAnalyzer`、`FrontendTopBindingAnalyzer`、`FrontendChainBindingAnalyzer`、`FrontendExprTypeAnalyzer`、`FrontendLocalTypeStabilizationAnalyzer`、`FrontendVarTypePostAnalyzer` 与 `FrontendCompileCheckAnalyzer` 仍分别把 `for` 作为 deferred / unsupported boundary。 +- 最终 shared semantic pipeline 已固定为 interface/body + SuiteResolver:`FrontendVisibleValueResolver`、`FrontendVariableAnalyzer`、body owner procedures 与 `FrontendCompileCheckAnalyzer` 当前仍分别把 `for` 作为 deferred / unsupported boundary;后续 for-range 解封必须在 SuiteResolver owner procedures 与 shared readiness policy 中闭环,不能恢复 legacy whole-phase analyzer bypass。 - `FrontendCfgGraphBuilder.processStatement(...)` 当前没有 `ForStatement` 分支;`break` / `continue` lowering 依赖 active `LoopFrame`。 - `FrontendCfgRegion` 当前只允许 `BlockRegion`、`FrontendIfRegion`、`FrontendElifRegion`、`FrontendWhileRegion`。 - `GdccForRangeIterType.FOR_RANGE_ITER` 已作为唯一 `GdCompilerType` 子类型存在。 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..ab3d25ba 100644 --- a/doc/module_impl/frontend/frontend_local_type_stabilization_implementation.md +++ b/doc/module_impl/frontend/frontend_local_type_stabilization_implementation.md @@ -4,8 +4,8 @@ ## 文档状态 -- 状态:事实源维护中(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、parameter/local alias 传播、复杂 initializer 求型、assignment initializer 与 bare `TYPE_META` fail-closed、parent/child block 边界合同、SuiteResolver body-owner overlay/export 路径已落地) +- 更新时间:2026-07-09(Phase I:production shared analyzer 不再调用 legacy whole-phase stabilization bypass) - 适用范围: - `src/main/java/gd/script/gdcc/frontend/sema/**` - `src/main/java/gd/script/gdcc/frontend/sema/analyzer/**` @@ -37,29 +37,26 @@ ### 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 之前。Standalone `FrontendLocalTypeStabilizationAnalyzer.analyze(...)` / `analyzeInWindow(...)` 只保留为 focused analyzer / legacy shim 测试参考,不再由 shared analyzer 的 legacy whole-phase bypass 调用。 ### 1.2 当前职责 -`FrontendLocalTypeStabilizationAnalyzer` 当前只负责一件事:在已发布的 callable executable body 中,按源码顺序稳定符合条件的 local `var := initializer` slot type。 +local stabilization 当前只负责一件事:在已发布的 callable executable body 中,按源码顺序稳定符合条件的 local `var := initializer` slot type。 它当前稳定负责: diff --git a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md index 501a243a..710020cf 100644 --- a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md +++ b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md @@ -4,7 +4,7 @@ - 性质:重启后的实施计划。 - 目标模块:`src/main/java/gd/script/gdcc/frontend/sema/**`,并影响 `frontend/scope/**` 与 frontend lowering 的 analysis contract。 -- 直接动机:用 Godot 风格的 interface/body 分层与 source-order suite 解析替代原先 statement-window segmented runner 路线,使 `var limit := 3; for i in limit:` 这类依赖前缀 typed fact 的语义支持拥有稳定架构基础。 +- 直接动机:用 Godot 风格的 interface/body 分层与 source-order suite 解析替代原先 statement-window segmented runner 路线,使 `var limit := 3; for i in limit:` 这类依赖前缀 typed fact 的语义支持拥有稳定架构基础。阶段 I 后,旧 runner 与 shared analyzer legacy whole-phase bypass 已从代码中删除。 - 主体方案:以 interface phase + body `SuiteResolver` 为主线,即先建立 class/callable/block 的 lexical 与 signature/interface 事实,再按 body suite 源码顺序解析 statement。 - 辅助方案:引入 `TypedLexicalEnvironment` overlay,使当前 statement / 当前 suite 内的 local slot typed fact 能被后续 semantic step 读取,而不提前污染最终 stable side table。 - 实施策略:允许把阶段 A-D 的已有实现视为“可保留资产 / 可重写参考 / 可回退资产”。本文不假设当前 segmented scheduler 已经是新路线的可继续扩展基础,也不把现有 `analyzeInWindow(...)` 当作可抽取的 statement runner。 @@ -59,7 +59,7 @@ 现有 analyzer 不是“window runner”。它们的 `analyze(...)` / `analyzeInWindow(...)` 入口仍以 `moduleSkeleton.sourceClassRelations()` 为根,创建内部 `AstWalker...` / visitor 并遍历完整 `SourceFile` AST。`FrontendWindowAnalysisContext` 只把发布目标换成 scratch surface;它没有把遍历 root 限制到当前 statement、当前 suite 或当前 body。因此,新的 body `SuiteResolver` 不能通过简单迁移这些入口获得。它需要在现有语义规则与 owner 合同之上,重写每个 owner 的 statement-local 调度、显式上下文状态和发布逻辑。 -这也是一次执行框架重写,而不是 statement-window 方案的续作:`FrontendSegmentedSemanticScheduler` 只能作为证明 patch merge / stable reference 行为的过渡资产。它仍按 whole-module owner stage 调度,并且 local type stabilization 仍走 legacy direct phase 以避免延迟 slot update 破坏 source-order alias chain。 +这也是一次执行框架重写,而不是 statement-window 方案的续作:`FrontendSegmentedSemanticScheduler` 曾只作为证明 patch merge / stable reference 行为的过渡资产,阶段 I 后已删除。最终 pipeline 不再保留 shared analyzer 级别的 legacy whole-phase body semantic bypass。 因此,`FrontendWindowPublicationSurface` / `FrontendWindowAnalysisContext` 不能作为新 overlay/export 的正确性参考。类型本身的 API 试图表达 scratch view,但现有 analyzer 使用方式已经破坏该承诺;计划只能把它们作为 legacy comparison / targeted regression 的输入,不能把它们当作可复用设计。 @@ -517,7 +517,7 @@ Feature-specific header state 仍属于 gate header 语义,不得因为 body r - `FrontendAnalysisPatch` 迁入 patch package;若保留,它只能是 legacy single-stage patch / 测试兼容层,不能作为 suite export 生产路径的 multi-owner carrier。 - `FrontendLocalSlotTypeUpdate` 迁入 patch package,并只由 `FrontendLocalTypeStabilizationPatch` 携带。 - `FrontendWindowPublicationSurface` 与 `FrontendWindowAnalysisContext` 若迁入 patch package,也只能作为 legacy comparison shim。它们不再是 overlay/export 参考实现;若最终删除这两个类型,必须把仍然有效的 surface API tests 拆到 overlay 与 per-owner patch transaction 测试,并新增 VarTypePost window contamination regression test 记录旧路径问题。 -- `FrontendSemanticAnalyzer.analyzeWithLegacySharedSemanticPublication(...)` package-private 测试旁路,作为阶段 H 后 legacy whole-phase baseline 的等价测试入口。 +- 已删除的 `FrontendSemanticAnalyzer.analyzeWithLegacySharedSemanticPublication(...)` package-private 测试旁路。阶段 I 后不再通过 shared analyzer 进入 legacy whole-phase baseline;需要 owner-level legacy 行为时只能在 focused legacy shim tests 中直接使用对应 analyzer/window API。 重写参考资产: @@ -528,8 +528,8 @@ Feature-specific header state 仍属于 gate header 语义,不得因为 body r 可回退或废弃资产: -- `FrontendSegmentedSemanticScheduler` 当前实现。它是阶段 D 的行为等价过渡调度器,仍包含 legacy local stabilization direct phase,不是新计划的主体。 -- 已删除的 `FrontendSemanticAnalyzer.segmentedSemanticRunner` 生产路径开关。阶段 H 后默认入口只运行新 pipeline,legacy whole-phase 只作为 package-private 迁移测试旁路存在。 +- 已删除的 `FrontendSegmentedSemanticScheduler` 过渡实现。阶段 I 后它不再作为代码资产存在。 +- 已删除的 `FrontendSemanticAnalyzer.segmentedSemanticRunner` 生产路径开关与 `analyzeWithLegacySharedSemanticPublication(...)` 测试旁路。默认入口只运行 interface/body pipeline。 - 把 `analyzeInWindow(...)` 作为 production body procedure 的任何调用路径。 ## 5. 分步骤实施 @@ -539,15 +539,15 @@ Feature-specific header state 仍属于 gate header 语义,不得因为 body r 实施内容: - 明确阶段 A-D 已实施代码的处理方式:保留、移动、重写参考、废弃或回退。 -- 将 `FrontendSegmentedSemanticScheduler` 当前实现标记为过渡实现,不再作为新路线的主体。 +- 将 `FrontendSegmentedSemanticScheduler` 标记为过渡实现并在最终阶段删除,不再作为新路线的主体。 - 保留 `FrontendAnalysisPatch`、`FrontendLocalSlotTypeUpdate`、`FrontendAnalysisData.applyPatch(...)` 的测试价值。`FrontendWindowPublicationSurface` 只保留 API-level / legacy comparison 测试价值,不能作为 overlay 隔离参考;patch 相关类型是否迁入 `gd.script.gdcc.frontend.sema.patch` 包需与其 legacy shim 定位一致。 - 明确 per-owner patch 类型与 `FrontendPatchTransaction` 命名方案,旧 `FrontendAnalysisPatch` 不再作为 suite export 生产路径的 multi-owner carrier。 -- 明确 `FrontendSemanticAnalyzer.segmentedSemanticRunner` 生产路径开关最终要移除。 +- 明确 `FrontendSemanticAnalyzer.segmentedSemanticRunner` 生产路径开关最终要移除;阶段 I 后该开关及 shared analyzer legacy bypass 均已删除。 - 明确 `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` guard-only 合同不可回退。 - 为五个 owner analyzer 建立 walker-state inventory:列出当前内部 visitor 的 traversal root、隐式字段状态、读取的 stable side table、写入的 side table、diagnostic emission 与可抽取 helper。该 inventory 是重写输入,不是迁移完成标志。 - 为 compiler-only guard 建立 payload matrix:逐项记录 `expressionTypes()`、`slotTypes()`、`localSlotTypeUpdates()`、`symbolBindings()`、`resolvedMembers()`、`resolvedCalls()` 当前由谁检查、谁未检查、哪些 API 仍可直接绕过 guard。该 matrix 必须与阶段 C shared walker 设计一起冻结。 -当前状态(2026-07-07): +当前状态(2026-07-09): - [x] A1 在文档中完成资产分类。 - [x] A2 在代码中将 `FrontendSegmentedSemanticScheduler` 标记为 deprecated 或 legacy comparison entry。 @@ -561,9 +561,9 @@ Feature-specific header state 仍属于 gate header 语义,不得因为 body r 阶段 A 完成记录: - A1:第 4.9 节的资产分类冻结为阶段 A 基线。`FrontendSemanticStage`、`FrontendAnalysisData.applyPatch(...)`、`refreshPublishedLocalBindingPayloads(...)`、backfill guard 与 `FrontendAnalysisDataTest` 保留;patch carrier 与 transaction 类型进入 `gd.script.gdcc.frontend.sema.patch` 迁移计划;现有 analyzer walker 只作为语义 helper / rewrite reference;`FrontendSegmentedSemanticScheduler`、`segmentedSemanticRunner` 和 production `analyzeInWindow(...)` 调用路径归类为可回退或废弃资产。 -- A2:`FrontendSegmentedSemanticScheduler` 已在代码中标记为 `@Deprecated` legacy comparison entry,类注释明确它不是 SuiteResolver production pipeline。IDE 对该类发出弃用警告是预期状态。 +- A2/I2:`FrontendSegmentedSemanticScheduler` 在阶段 A 被标记为 `@Deprecated` legacy comparison entry;阶段 I 已删除该过渡实现,SuiteResolver 成为唯一 shared body publication path。 - A3:`FrontendAnalysisDataTest` 继续作为 stable reference、conflict、idempotent 与 compiler-only guard 的 focused tests。`FrontendWindowPublicationSurfaceTest` 的类级注释已降级其解释范围:它只证明 legacy shim API 的 scratch write 隔离与 guard,不证明所有 legacy `analyzeInWindow(...)` 都 scratch-safe。 -- A4/H3:阶段 A 先隔离 `segmentedSemanticRunner` 的生产影响;阶段 H 已删除该字段、构造参数和 `withSegmentedSemanticRunnerForTesting()` 工厂。legacy comparison 现在通过 package-private `FrontendSemanticAnalyzer.analyzeWithLegacySharedSemanticPublication(...)` 及 test-source bridge 进入,不再经过 deprecated segmented scheduler。 +- A4/H3/I1:阶段 A 先隔离 `segmentedSemanticRunner` 的生产影响;阶段 H 已删除该字段、构造参数和 `withSegmentedSemanticRunnerForTesting()` 工厂;阶段 I 已删除 shared analyzer 的 package-private legacy whole-phase bypass 与 test-source bridge。 - A5:patch package 迁移计划冻结为:新建 `gd.script.gdcc.frontend.sema.patch`;以 `FrontendOwnerPatch` 或等价 sealed interface 作为单 owner patch 根类型;新增 `FrontendTopBindingPatch`、`FrontendLocalTypeStabilizationPatch`、`FrontendChainBindingPatch`、`FrontendExprTypePatch`、`FrontendVarTypePostPatch`;新增 `FrontendPatchTransaction` 按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 顺序 apply。旧 `FrontendAnalysisPatch` 若迁移则只能作为 legacy single-stage compatibility carrier,不能继续作为 SuiteResolver export 的 multi-owner carrier;`FrontendLocalSlotTypeUpdate` 随 local stabilization patch 迁入 patch 包。 - A6:whole-module walker state inventory 见下表。所有当前 `analyzeInWindow(...)` 都只允许作为 legacy comparison path;其 whole-module traversal root、隐式 visitor 字段和整表发布策略都不得直接复用为 SuiteResolver statement-local owner procedure。 - A7:`FrontendVarTypePostAnalyzer.analyzeInWindow(...)` 的 legacy 污染路径为:读取 `analysisData.slotTypes()` 稳定表 -> `clear()` -> 将稳定表传入 `SlotTypePublisher` 逐项写入 -> 再复制到 `window.publications().slotTypes()`。阶段 A 决定是隔离而非修补该 legacy path:保留测试比较价值,但新 var type post procedure 必须从 statement-local scratch/overlay 写入重新实现。 @@ -727,10 +727,10 @@ Compiler-only guard payload matrix: - [x] E0 重新读取 `AGENTS.md`,用 MCP 列出 `doc` 与 `doc/module_impl`,并并行子代理调研阶段 E 相关文档、owner analyzer 代码与测试基线。 - [x] E1 完成 C8 overlay-aware dependency lookup:`FrontendExpressionSemanticSupport` / `FrontendChainReductionFacade` / chain-head receiver 可通过注入 lookup 读取 `FrontendTypedLexicalEnvironment` 的 effective binding 与 exact local slot fact;`FrontendChainReductionHelper` 的 argument dependency lookup 不再先读 stable `expressionTypes()`,而是委托注入 resolver 保持 overlay-first 合同;旧构造器继续保持 stable-table 兼容。 - [x] E2 新增 `FrontendBodyOwnerProcedures`,以 root-bounded DFS 实现 top binding、local stabilization、chain binding、expr typing、var type post 的核心 statement-local publication,不调用 whole-module analyzer entrypoint。 -- [x] E3 `FrontendSuiteResolver` 默认接入真实 owner procedure;`FrontendSemanticAnalyzer` 默认仍注入 legacy-compatible no-op suite resolver,避免阶段 H 之前提前写入 body facts 干扰 legacy whole-phase fallback。 +- [x] E3 `FrontendSuiteResolver` 默认接入真实 owner procedure;阶段 I 后 `FrontendSemanticAnalyzer` 默认无条件运行真实 SuiteResolver,不再保留 legacy-compatible no-op / whole-phase fallback 分支。 - [x] E4 新增正反向 targeted tests,锚定 source-order alias、child-prefix visibility、chain receiver exactness、transient cache isolation、var type post export boundary、C8 overlay lookup 与 framework-level real owner path hand-off。 - [x] E5 已运行格式化、IDE 增量编译与问题检查、`FrontendSuiteResolverTest`、`FrontendExpressionSemanticSupportTest`、`FrontendChainReductionFacadeTest`、`FrontendChainReductionHelperTest`、阶段 E 相关 targeted regression batch,以及 `git diff --check`。 -- [x] E6 原通过显式注入真实 `FrontendBodyOwnerProcedures` 的 `FrontendSuiteResolver` 证明 D/E body owner path 在 framework hand-off 中先于 legacy whole-phase publication 执行;阶段 H 后该证据已升级为 `FrontendSemanticAnalyzerFrameworkTest.defaultInterfaceBodyPipelineMatchesLegacyWholePhaseSideTables` 与 nested/unsupported 等价测试,默认 analyzer 已直接运行 real body owner path。 +- [x] E6 原通过显式注入真实 `FrontendBodyOwnerProcedures` 的 `FrontendSuiteResolver` 证明 D/E body owner path 在 framework hand-off 中先于 legacy whole-phase publication 执行;阶段 I 后该证据已收口为 `FrontendSemanticAnalyzerFrameworkTest` 中的默认 SuiteResolver body publication、nested source-order facts 与 unsupported fail-closed tests,且不再依赖 legacy baseline。 验收细则: @@ -801,10 +801,10 @@ Compiler-only guard payload matrix: 实施内容: - [x] H1 `FrontendSemanticAnalyzer.analyze(...)` 默认运行新 interface/body pipeline。当前生产 `analyze(...)` 在 interface phase 后只调用真实 `FrontendSuiteResolver`,不再追加 legacy whole-phase owner publication,避免 body facts 双重发布。 -- [x] H2 legacy whole-phase runner 只保留为 package-private 测试旁路。当前 `FrontendSemanticAnalyzer.analyzeWithLegacySharedSemanticPublication(...)` 为 package-private comparison hook,供测试桥接 legacy baseline,不暴露给生产入口。 +- [x] H2 legacy whole-phase runner 在阶段 H 只保留为 package-private 测试旁路;阶段 I 已删除该旁路与 test bridge。 - [x] H3 移除 `segmentedSemanticRunner` 生产路径开关。当前已删除 `segmentedSemanticRunner` 字段、构造参数与 `withSegmentedSemanticRunnerForTesting()` 工厂,生产路径不再能切到 deprecated segmented scheduler。 -- [x] H4 新增 legacy vs interface/body pipeline 等价测试,等价基线以 guard-only backfill 合同为准。当前 `FrontendSemanticAnalyzerFrameworkTest` 通过 package-private legacy test bridge 比较默认 interface/body pipeline 与 legacy whole-phase baseline 的五张共享 side table;unsupported `for` / `match` / block-local `const` 负向测试同时锚定 fail-closed fact surface,并用顺序无关 diagnostics 断言记录 legacy whole-phase 与 statement-local suite boundary 的诊断顺序差异。 -- [x] H5 broad regression parity follow-up:body owner top-binding 现在与 legacy 共享 dual-role singleton/type-meta 路偏和 global enum type-meta preference,body expression publication 补齐 builtin Variant constructor unsafe-call warning,missing / blocked binding diagnostics 与 legacy binding owner 对齐;legacy owner-specific `FrontendExprTypeAnalyzerTest` 通过 test bridge 显式进入 legacy whole-phase baseline。 +- [x] H4 新增 legacy vs interface/body pipeline 等价测试,等价基线以 guard-only backfill 合同为准;阶段 I 后这些测试已迁移为默认 interface/body pipeline 的 body fact、nested source-order 与 unsupported fail-closed 合同测试。 +- [x] H5 broad regression parity follow-up:body owner top-binding 现在与 legacy 共享 dual-role singleton/type-meta 路偏和 global enum type-meta preference,body expression publication 补齐 builtin Variant constructor unsafe-call warning,missing / blocked binding diagnostics 与 legacy binding owner 对齐;阶段 I 后 owner-specific `FrontendExprTypeAnalyzerTest` 使用测试内显式 owner-analyzer baseline helper,不再通过 shared analyzer test bridge 进入 legacy whole-phase baseline。 验收细则: @@ -816,9 +816,11 @@ Compiler-only guard payload matrix: 实施内容: -- 删除 legacy whole-phase body semantic 旁路。 -- 删除或重构 `FrontendSegmentedSemanticScheduler` 过渡实现。 -- 保留 patch/overlay/export 基础设施,并移除 single-stage `FrontendAnalysisPatch` 作为 suite export 生产路径的用途。 +- [x] I1 删除 legacy whole-phase body semantic 旁路。`FrontendSemanticAnalyzer.analyze(...)` 现在无条件执行 interface phase + `FrontendSuiteResolver`;`analyzeWithLegacySharedSemanticPublication(...)` 与 test bridge 已删除。 +- [x] I2 删除 `FrontendSegmentedSemanticScheduler` 过渡实现。代码中不再存在 scheduler 文件或 runner 开关。 +- [x] I3 保留 patch/overlay/export 基础设施,并移除 single-stage `FrontendAnalysisPatch` 作为 suite export 生产路径的用途。Suite export 继续通过 `FrontendTypedLexicalEnvironment.exportPatchTransaction()` 产生 per-owner transaction;`FrontendAnalysisPatch` 仅保留为 legacy shim / focused tests 兼容载体。 +- [x] I4 更新 variable analyzer、visible resolver、local stabilization、chain/expr typing、compile check、for-range plan,明确最终 production shared analyzer 只通过 interface/body + SuiteResolver 发布 body facts。 +- [x] I5 更新 `doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md`,记录 Phase I 删除项、final owner 顺序、fact 生命周期、per-owner patch/export、compiler-only guard 与 diagnostics / compile gate 边界。 - 更新 variable analyzer、visible resolver、local stabilization、chain/expr typing、compile check、for-range plan。 - 更新 `doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md`,使其反映最终实现的层级职责、owner 顺序、fact 生命周期、patch/export 合同、compiler-only guard 与 diagnostics / compile gate 边界,并继续保持为目标架构摘要而非旧流水线或过渡资产说明。 @@ -880,8 +882,8 @@ Suite/body pipeline 测试: - `FrontendSuiteResolverTest`:local stabilization patch apply 后由 commit helper 派生刷新同 declaration 的 `symbolBindings()` payload,而不是通过同一个 patch 携带 binding delta。 - `FrontendSuiteResolverTest`:`if` / `elif` / `else` / `while` header 先解析,body 后递归。 - `FrontendSuiteResolverTest`:unsupported body 不进入 resolver。 -- `FrontendSemanticAnalyzerFrameworkTest`:legacy whole-phase baseline 与默认 interface/body pipeline side-table 等价;legacy baseline 通过 package-private `analyzeWithLegacySharedSemanticPublication(...)` test bridge 进入,不再依赖 `withSegmentedSemanticRunnerForTesting()` 或 deprecated scheduler。 -- `FrontendSemanticAnalyzerFrameworkTest`:等价基线使用 guard-only backfill 合同,不允许恢复旧 expr-phase slot mutation。 +- `FrontendSemanticAnalyzerFrameworkTest`:默认 interface/body pipeline 发布 body facts、nested source-order facts,并保持 unsupported `for` / `match` / block-local `const` fail-closed;测试不再通过 shared analyzer legacy whole-phase baseline。 +- `FrontendExprTypeAnalyzerTest`:owner-specific baseline 只能在测试内显式串联 focused owner analyzers;不得恢复 shared analyzer legacy bridge 或旧 expr-phase slot mutation。 Resolver 测试: diff --git a/doc/module_impl/frontend/frontend_variable_analyzer_implementation.md b/doc/module_impl/frontend/frontend_variable_analyzer_implementation.md index bce73de6..d0669497 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-09(Phase I:默认 shared analyzer 只走 interface/body + SuiteResolver body publication) - 适用范围: - `src/main/java/gd/script/gdcc/frontend/sema/**` - `src/main/java/gd/script/gdcc/frontend/sema/analyzer/**` @@ -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 当前职责 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..39a4efc3 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 封口、current-scope fail-closed hardening、typed overlay-aware resolve overload、shared support matrix 与核心单元测试已落地) +- 更新时间:2026-07-09(Phase I:resolver 服务最终 interface/body + SuiteResolver pipeline,不依赖 legacy whole-phase bypass) - 适用范围: - `src/main/java/gd/script/gdcc/frontend/sema/**` - `src/main/java/gd/script/gdcc/frontend/sema/analyzer/**` @@ -82,6 +82,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、AST boundary 与 current-scope 三道 fail-closed gate;overlay 只改变可见事实来源,不允许绕过 declaration-order、initializer self-reference 或 typed-dependent body readiness 判定。 + --- ## 3. 支持域与封口域 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 index 514c0fa3..dd699d6b 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProcedures.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProcedures.java @@ -752,7 +752,9 @@ private static void publishExpressionType( if (resolver.isRouteHeadOnlyTypeMeta(entry.getKey())) { continue; } - reportExpressionDiagnostic(context, resolver, entry.getKey(), entry.getValue()); + if (!resolver.isAssignmentTargetPrefixExpression(entry.getKey())) { + reportExpressionDiagnostic(context, resolver, entry.getKey(), entry.getValue()); + } context.typedEnvironment().putExpressionType( FrontendSemanticStage.EXPR_TYPE, entry.getKey(), @@ -762,6 +764,9 @@ private static void publishExpressionType( 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, @@ -772,6 +777,16 @@ private static void publishExpressionType( } } + 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, @@ -1012,6 +1027,10 @@ private static final class BodyExpressionResolver { 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 = @@ -1109,18 +1128,10 @@ private void populateRootExpressionTransientCaches( .expressionType(); case AttributeExpression attributeExpression -> resolveAttributeExpressionType(attributeExpression, finalizeWindow); - case AssignmentExpression assignmentExpression -> FrontendAssignmentSemanticSupport - .resolveAssignmentExpressionType( - assignmentSemanticContext, - assignmentExpression, - assignmentUsages.getOrDefault( - assignmentExpression, - FrontendAssignmentSemanticSupport.AssignmentUsage.VALUE_REQUIRED - ), - this::resolveExpressionType, - finalizeWindow - ) - .expressionType(); + case AssignmentExpression assignmentExpression -> resolveAssignmentExpressionType( + assignmentExpression, + finalizeWindow + ); case CallExpression callExpression -> resolveCallExpressionType(callExpression, finalizeWindow); case SubscriptExpression subscriptExpression -> expressionSemanticSupport .resolveSubscriptExpressionType( @@ -1162,6 +1173,92 @@ private void populateRootExpressionTransientCaches( }; } + 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 @@ -1214,10 +1311,20 @@ private void populateRootExpressionTransientCaches( 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); } diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSegmentedSemanticScheduler.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSegmentedSemanticScheduler.java deleted file mode 100644 index d32a18a1..00000000 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSegmentedSemanticScheduler.java +++ /dev/null @@ -1,120 +0,0 @@ -package gd.script.gdcc.frontend.sema.analyzer; - -import gd.script.gdcc.frontend.diagnostic.DiagnosticManager; -import gd.script.gdcc.frontend.sema.FrontendAnalysisData; -import gd.script.gdcc.frontend.sema.FrontendSemanticStage; -import gd.script.gdcc.frontend.sema.FrontendWindowAnalysisContext; -import gd.script.gdcc.scope.ClassRegistry; -import org.jetbrains.annotations.NotNull; - -import java.util.Objects; - -/// Legacy comparison scheduler for the segmented semantic publication layer introduced after the -/// window-capable analyzer APIs. -/// -/// This scheduler is not the planned SuiteResolver production pipeline: its `analyzeInWindow(...)` -/// calls still perform whole-module traversal, and local stabilization intentionally uses the stable -/// whole-phase path. Keep it only as a legacy comparison entry while the root-bounded body resolver -/// and per-owner patch transaction are implemented. -@Deprecated -final class FrontendSegmentedSemanticScheduler { - 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; - - FrontendSegmentedSemanticScheduler( - @NotNull FrontendTopBindingAnalyzer topBindingAnalyzer, - @NotNull FrontendLocalTypeStabilizationAnalyzer localTypeStabilizationAnalyzer, - @NotNull FrontendChainBindingAnalyzer chainBindingAnalyzer, - @NotNull FrontendExprTypeAnalyzer exprTypeAnalyzer, - @NotNull FrontendVarTypePostAnalyzer varTypePostAnalyzer - ) { - 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"); - } - - void run( - @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"); - - runTopBinding(classRegistry, analysisData, diagnosticManager); - runLocalTypeStabilization(classRegistry, analysisData, diagnosticManager); - runChainBinding(classRegistry, analysisData, diagnosticManager); - runExprType(classRegistry, analysisData, diagnosticManager); - runVarTypePost(analysisData, diagnosticManager); - } - - private void runTopBinding( - @NotNull ClassRegistry classRegistry, - @NotNull FrontendAnalysisData analysisData, - @NotNull DiagnosticManager diagnosticManager - ) { - var window = new FrontendWindowAnalysisContext(analysisData); - topBindingAnalyzer.analyzeInWindow(classRegistry, window, diagnosticManager); - commit(window, FrontendSemanticStage.TOP_BINDING, analysisData, diagnosticManager); - } - - private void runLocalTypeStabilization( - @NotNull ClassRegistry classRegistry, - @NotNull FrontendAnalysisData analysisData, - @NotNull DiagnosticManager diagnosticManager - ) { - // This phase must keep its existing source-order scope writes until the scheduler grows - // true root-bounded statement windows. A whole-module local window would delay every slot - // update until commit and break alias chains such as `var b := a` after `var a := typed`. - localTypeStabilizationAnalyzer.analyze(classRegistry, analysisData, diagnosticManager); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - } - - private void runChainBinding( - @NotNull ClassRegistry classRegistry, - @NotNull FrontendAnalysisData analysisData, - @NotNull DiagnosticManager diagnosticManager - ) { - var window = new FrontendWindowAnalysisContext(analysisData); - chainBindingAnalyzer.analyzeInWindow(classRegistry, window, diagnosticManager); - commit(window, FrontendSemanticStage.CHAIN_BINDING, analysisData, diagnosticManager); - } - - private void runExprType( - @NotNull ClassRegistry classRegistry, - @NotNull FrontendAnalysisData analysisData, - @NotNull DiagnosticManager diagnosticManager - ) { - var window = new FrontendWindowAnalysisContext(analysisData); - exprTypeAnalyzer.analyzeInWindow(classRegistry, window, diagnosticManager); - commit(window, FrontendSemanticStage.EXPR_TYPE, analysisData, diagnosticManager); - } - - private void runVarTypePost( - @NotNull FrontendAnalysisData analysisData, - @NotNull DiagnosticManager diagnosticManager - ) { - var window = new FrontendWindowAnalysisContext(analysisData); - varTypePostAnalyzer.analyzeInWindow(window, diagnosticManager); - commit(window, FrontendSemanticStage.VAR_TYPE_POST, analysisData, diagnosticManager); - } - - private void commit( - @NotNull FrontendWindowAnalysisContext window, - @NotNull FrontendSemanticStage stage, - @NotNull FrontendAnalysisData analysisData, - @NotNull DiagnosticManager diagnosticManager - ) { - analysisData.applyPatch(window.drainPatch(stage)); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - } -} 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 b068b579..1aa89beb 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 @@ -28,11 +28,6 @@ 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; @@ -468,17 +463,11 @@ private FrontendSemanticAnalyzer( 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" - ); + Objects.requireNonNull(topBindingAnalyzer, "topBindingAnalyzer must not be null"); + Objects.requireNonNull(localTypeStabilizationAnalyzer, "localTypeStabilizationAnalyzer must not be null"); + Objects.requireNonNull(chainBindingAnalyzer, "chainBindingAnalyzer must not be null"); + Objects.requireNonNull(exprTypeAnalyzer, "exprTypeAnalyzer must not be null"); + Objects.requireNonNull(varTypePostAnalyzer, "varTypePostAnalyzer must not be null"); this.annotationUsageAnalyzer = Objects.requireNonNull( annotationUsageAnalyzer, "annotationUsageAnalyzer must not be null" @@ -512,25 +501,13 @@ private FrontendSemanticAnalyzer( @NotNull ClassRegistry classRegistry, @NotNull DiagnosticManager diagnosticManager ) { - return analyzeShared(module, classRegistry, diagnosticManager, false); - } - - /// Test-only compatibility hook for comparing the retired whole-phase owner publication path - /// with the default interface/body pipeline. Production callers must use `analyze(...)` so body - /// facts are published by `FrontendSuiteResolver` and not double-written by legacy analyzers. - @NotNull FrontendAnalysisData analyzeWithLegacySharedSemanticPublication( - @NotNull FrontendModule module, - @NotNull ClassRegistry classRegistry, - @NotNull DiagnosticManager diagnosticManager - ) { - return analyzeShared(module, classRegistry, diagnosticManager, true); + return analyzeShared(module, classRegistry, diagnosticManager); } private @NotNull FrontendAnalysisData analyzeShared( @NotNull FrontendModule module, @NotNull ClassRegistry classRegistry, - @NotNull DiagnosticManager diagnosticManager, - boolean useLegacySharedSemanticPublication + @NotNull DiagnosticManager diagnosticManager ) { Objects.requireNonNull(module, "module must not be null"); Objects.requireNonNull(classRegistry, "classRegistry must not be null"); @@ -561,15 +538,11 @@ private FrontendSemanticAnalyzer( variableAnalyzer.analyze(analysisData, diagnosticManager); analysisData.updateDiagnostics(diagnosticManager.snapshot()); - // Interface analysis freezes callable/property entry roots, then exactly one body owner - // publication path runs. The legacy path is reserved for comparison tests so production never - // publishes the same side-table facts twice. + // Interface analysis freezes callable/property entry roots before the only body owner + // publication path runs. Phase I removes the legacy whole-phase bypass so shared facts can + // only enter stable storage through SuiteResolver's per-owner export transaction. var interfaceSurface = interfacePhase.analyze(classRegistry, analysisData); - if (useLegacySharedSemanticPublication) { - runLegacySharedSemanticPublication(classRegistry, diagnosticManager, analysisData); - } else { - suiteResolver.resolve(interfaceSurface, classRegistry, analysisData, diagnosticManager); - } + suiteResolver.resolve(interfaceSurface, classRegistry, analysisData, diagnosticManager); analysisData.updateDiagnostics(diagnosticManager.snapshot()); // Annotation-usage validation consumes retained annotations plus the published class/scope @@ -594,40 +567,6 @@ private FrontendSemanticAnalyzer( return analysisData; } - private void runLegacySharedSemanticPublication( - @NotNull ClassRegistry classRegistry, - @NotNull DiagnosticManager diagnosticManager, - @NotNull FrontendAnalysisData analysisData - ) { - // 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); - analysisData.updateDiagnostics(diagnosticManager.snapshot()); - } - /// Runs the shared semantic pipeline plus the compile-only final gate. /// /// This split keeps the default semantic entrypoint reusable for inspection/LSP-style tooling 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 23ab80b9..1362635e 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSemanticAnalyzerFrameworkTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSemanticAnalyzerFrameworkTest.java @@ -11,16 +11,13 @@ 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.FrontendInterfacePhase; 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.FrontendSemanticAnalyzerTestAccess; 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.FrontendTopBindingAnalyzer; import gd.script.gdcc.frontend.sema.analyzer.FrontendTypeCheckAnalyzer; import gd.script.gdcc.frontend.sema.analyzer.FrontendVirtualOverrideAnalyzer; @@ -34,6 +31,7 @@ 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; @@ -50,14 +48,13 @@ 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; import gd.script.gdcc.scope.ClassRegistry; import gd.script.gdcc.scope.ClassDef; -import gd.script.gdcc.scope.FunctionDef; import gd.script.gdcc.scope.PropertyDef; -import gd.script.gdcc.scope.ScopeValue; import gd.script.gdcc.scope.ScopeValueKind; import gd.script.gdcc.scope.resolver.ScopeResolvedMethod; import gd.script.gdcc.type.GdArrayType; @@ -75,7 +72,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.function.BiPredicate; import java.util.function.Predicate; import static org.junit.jupiter.api.Assertions.assertAll; @@ -667,7 +663,7 @@ func _ready( } @Test - void legacyWholePhaseBypassPublishesPhaseBoundariesThroughVirtualOverridePhaseAndRefreshesDiagnosticsAfterEachPhase() throws Exception { + void analyzeUsesSuiteResolverBodyPublicationAndDoesNotCallLegacyWholePhaseAnalyzers() throws Exception { var parserService = new GdScriptParserService(); var diagnostics = new DiagnosticManager(); var unit = parserService.parseUnit(Path.of("tmp", "variable_phase_probe.gd"), """ @@ -705,7 +701,7 @@ func ping(value): new FrontendCompileCheckAnalyzer() ); - var result = analyzeModuleWithLegacySharedSemanticPublication( + var result = analyzeModule( analyzer, "test_module", List.of(unit), @@ -719,32 +715,11 @@ 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); + assertFalse(probeTopBindingAnalyzer.invoked); + assertFalse(probeLocalTypeStabilizationAnalyzer.invoked); + assertFalse(probeChainBindingAnalyzer.invoked); + assertFalse(probeExprTypeAnalyzer.invoked); + assertFalse(probeVarTypePostAnalyzer.invoked); assertTrue(probeAnnotationUsageAnalyzer.invoked); assertTrue(probeAnnotationUsageAnalyzer.varTypeBoundaryPublished); assertTrue(probeAnnotationUsageAnalyzer.preAnnotationUsageDiagnosticsMatchedManager); @@ -766,45 +741,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, @@ -834,24 +774,14 @@ 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") )); + assertTrue(diagnosticsByCategory(result.diagnostics(), "sema.top_binding_phase_probe").isEmpty()); + assertTrue(diagnosticsByCategory(result.diagnostics(), "sema.local_type_stabilization_phase_probe").isEmpty()); + assertTrue(diagnosticsByCategory(result.diagnostics(), "sema.chain_binding_phase_probe").isEmpty()); + assertTrue(diagnosticsByCategory(result.diagnostics(), "sema.expr_type_phase_probe").isEmpty()); + assertTrue(diagnosticsByCategory(result.diagnostics(), "sema.var_type_post_phase_probe").isEmpty()); assertTrue(result.diagnostics().asList().stream().anyMatch(diagnostic -> diagnostic.category().equals("sema.virtual_override_phase_probe") )); @@ -990,7 +920,7 @@ func ping(value: Point) -> int: } @Test - void defaultInterfaceBodyPipelineMatchesLegacyWholePhaseSideTables() throws Exception { + 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"), """ @@ -1011,16 +941,8 @@ func ping(value: Point) -> int: var markerStep = findNode(pingFunction.body(), AttributePropertyStep.class, step -> step.name().equals("marker")); var numberInitializer = numberDeclaration.value(); assertNotNull(numberInitializer); - var legacyDiagnostics = new DiagnosticManager(); var interfaceBodyDiagnostics = new DiagnosticManager(); - var legacy = analyzeModuleWithLegacySharedSemanticPublication( - new FrontendSemanticAnalyzer(), - "test_module", - List.of(unit), - new ClassRegistry(ExtensionApiLoader.loadDefault()), - legacyDiagnostics - ); var interfaceBody = analyzeModule( new FrontendSemanticAnalyzer(), "test_module", @@ -1046,12 +968,11 @@ func ping(value: Point) -> int: () -> assertEquals(FrontendExpressionTypeStatus.RESOLVED, initializerType.status()), () -> assertEquals("int", initializerType.publishedType().getTypeName()) ); - assertEquivalentSharedSemanticFacts(legacy, interfaceBody); - assertEquals(legacyDiagnostics.snapshot(), interfaceBodyDiagnostics.snapshot()); + assertEquals(interfaceBodyDiagnostics.snapshot(), interfaceBody.diagnostics()); } @Test - void defaultInterfaceBodyPipelineKeepsNestedSharedSemanticFactsEquivalentToLegacy() throws Exception { + void defaultInterfaceBodyPipelinePublishesNestedHeaderAndBodyFactsInSourceOrder() throws Exception { var parserService = new GdScriptParserService(); var parseDiagnostics = new DiagnosticManager(); var unit = parserService.parseUnit(Path.of("tmp", "segmented_equivalence.gd"), """ @@ -1059,10 +980,9 @@ void defaultInterfaceBodyPipelineKeepsNestedSharedSemanticFactsEquivalentToLegac extends Node class Point: var marker: int = 1 - var ready_value := 1 func ping(value: Point) -> int: var alias := value - var number := ready_value + var number := 1 if number > 0: var nested := alias number = nested.marker @@ -1070,16 +990,7 @@ func ping(value: Point) -> int: number += 1 return alias.marker """, parseDiagnostics); - var legacyDiagnostics = new DiagnosticManager(); var interfaceBodyDiagnostics = new DiagnosticManager(); - - var legacy = analyzeModuleWithLegacySharedSemanticPublication( - new FrontendSemanticAnalyzer(), - "test_module", - List.of(unit), - new ClassRegistry(ExtensionApiLoader.loadDefault()), - legacyDiagnostics - ); var interfaceBody = analyzeModule( new FrontendSemanticAnalyzer(), "test_module", @@ -1087,10 +998,29 @@ func ping(value: Point) -> int: 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); - assertEquivalentSharedSemanticFacts(legacy, interfaceBody); - assertDiagnosticsEquivalentIgnoringOrder(legacy.diagnostics(), interfaceBody.diagnostics()); - assertEquals(legacyDiagnostics.snapshot(), interfaceBodyDiagnostics.snapshot()); + 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 @@ -1108,16 +1038,8 @@ func ping(values): pass return values """, new DiagnosticManager()); - var legacyDiagnostics = new DiagnosticManager(); var interfaceBodyDiagnostics = new DiagnosticManager(); - var legacy = analyzeModuleWithLegacySharedSemanticPublication( - new FrontendSemanticAnalyzer(), - "test_module", - List.of(unit), - new ClassRegistry(ExtensionApiLoader.loadDefault()), - legacyDiagnostics - ); var interfaceBody = analyzeModule( new FrontendSemanticAnalyzer(), "test_module", @@ -1134,15 +1056,64 @@ func ping(values): ); var hiddenUseSite = assertInstanceOf(IdentifierExpression.class, hiddenLocal.value()); - assertEquivalentSharedSemanticFacts(legacy, interfaceBody); assertNull(interfaceBody.slotTypes().get(blockedConst)); assertNull(interfaceBody.slotTypes().get(hiddenLocal)); assertNull(interfaceBody.symbolBindings().get(hiddenUseSite)); - assertEquals( - diagnosticsByCategory(legacy.diagnostics(), "sema.unsupported_binding_subtree"), - diagnosticsByCategory(interfaceBody.diagnostics(), "sema.unsupported_binding_subtree") + assertFalse(diagnosticsByCategory(interfaceBody.diagnostics(), "sema.unsupported_binding_subtree").isEmpty()); + 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() + ) ); - assertDiagnosticsEquivalentIgnoringOrder(legacy.diagnostics(), interfaceBody.diagnostics()); + assertEquals(diagnostics.snapshot(), result.diagnostics()); } @Test @@ -1554,21 +1525,6 @@ private FrontendAnalysisData analyzeModule( return analyzer.analyze(new FrontendModule(moduleName, units), registry, diagnostics); } - private FrontendAnalysisData analyzeModuleWithLegacySharedSemanticPublication( - @NotNull FrontendSemanticAnalyzer analyzer, - @NotNull String moduleName, - @NotNull List units, - @NotNull ClassRegistry registry, - @NotNull DiagnosticManager diagnostics - ) { - return FrontendSemanticAnalyzerTestAccess.analyzeWithLegacySharedSemanticPublication( - analyzer, - new FrontendModule(moduleName, units), - registry, - diagnostics - ); - } - private FrontendAnalysisData analyzeModuleForCompile( @NotNull String moduleName, @NotNull List units, @@ -1588,146 +1544,6 @@ private FrontendAnalysisData analyzeModuleForCompile( return analyzer.analyzeForCompile(new FrontendModule(moduleName, units), registry, diagnostics); } - private void assertEquivalentSharedSemanticFacts( - @NotNull FrontendAnalysisData expected, - @NotNull FrontendAnalysisData actual - ) { - assertEquivalentSideTable( - expected.symbolBindings(), - actual.symbolBindings(), - this::sameBindingAcrossIndependentRuns, - "symbolBindings" - ); - assertEquivalentSideTable( - expected.resolvedMembers(), - actual.resolvedMembers(), - this::sameResolvedMemberAcrossIndependentRuns, - "resolvedMembers" - ); - assertEquivalentSideTable( - expected.resolvedCalls(), - actual.resolvedCalls(), - this::sameResolvedCallAcrossIndependentRuns, - "resolvedCalls" - ); - assertEquivalentSideTable( - expected.expressionTypes(), - actual.expressionTypes(), - FrontendAnalysisData::sameExpressionType, - "expressionTypes" - ); - assertEquivalentSideTable( - expected.slotTypes(), - actual.slotTypes(), - FrontendAnalysisData::sameType, - "slotTypes" - ); - } - - private boolean sameBindingAcrossIndependentRuns( - @NotNull FrontendBinding first, - @NotNull FrontendBinding second - ) { - return first.kind() == second.kind() - && first.symbolName().equals(second.symbolName()) - && sameDeclarationAcrossIndependentRuns(first.declarationSite(), second.declarationSite()) - && first.valueAccessStatus() == second.valueAccessStatus() - && sameScopeValueAcrossIndependentRuns(first.resolvedValue(), second.resolvedValue()); - } - - private boolean sameResolvedMemberAcrossIndependentRuns( - @NotNull FrontendResolvedMember first, - @NotNull FrontendResolvedMember second - ) { - return first.bindingKind() == second.bindingKind() - && first.status() == second.status() - && first.receiverKind() == second.receiverKind() - && first.ownerKind() == second.ownerKind() - && sameDeclarationAcrossIndependentRuns(first.declarationSite(), second.declarationSite()) - && first.memberName().equals(second.memberName()) - && FrontendAnalysisData.sameType(first.receiverType(), second.receiverType()) - && FrontendAnalysisData.sameType(first.resultType(), second.resultType()) - && Objects.equals(first.detailReason(), second.detailReason()); - } - - private boolean sameResolvedCallAcrossIndependentRuns( - @NotNull FrontendResolvedCall first, - @NotNull FrontendResolvedCall second - ) { - return first.callKind() == second.callKind() - && first.status() == second.status() - && first.receiverKind() == second.receiverKind() - && first.ownerKind() == second.ownerKind() - && sameDeclarationAcrossIndependentRuns(first.declarationSite(), second.declarationSite()) - && first.callableName().equals(second.callableName()) - && FrontendAnalysisData.sameType(first.receiverType(), second.receiverType()) - && FrontendAnalysisData.sameType(first.returnType(), second.returnType()) - && FrontendAnalysisData.sameTypeList(first.argumentTypes(), second.argumentTypes()) - && FrontendAnalysisData.sameExactCallableBoundary( - first.exactCallableBoundary(), - second.exactCallableBoundary() - ) - && Objects.equals(first.detailReason(), second.detailReason()); - } - - private boolean sameScopeValueAcrossIndependentRuns(ScopeValue first, 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() - && sameDeclarationAcrossIndependentRuns(first.declaration(), second.declaration()) - && first.name().equals(second.name()) - && FrontendAnalysisData.sameType(first.type(), second.type()); - } - - private boolean sameDeclarationAcrossIndependentRuns(Object first, Object second) { - if (first == second) { - return true; - } - if (first == null || second == null || first instanceof Node || second instanceof Node) { - return false; - } - return declarationFingerprint(first).equals(declarationFingerprint(second)); - } - - private String declarationFingerprint(@NotNull Object declaration) { - if (declaration instanceof ClassDef classDef) { - return "class:" + classDef.getName(); - } - if (declaration instanceof PropertyDef propertyDef) { - return "property:" + propertyDef.getName(); - } - if (declaration instanceof FunctionDef functionDef) { - return "function:" + functionDef.getName(); - } - return declaration.getClass().getName() + ':' + declaration; - } - - private void assertEquivalentSideTable( - @NotNull FrontendAstSideTable expected, - @NotNull FrontendAstSideTable actual, - @NotNull BiPredicate sameValue, - @NotNull String tableName - ) { - for (var entry : expected.entrySet()) { - assertTrue(actual.containsKey(entry.getKey()), tableName + " lost key " + entry.getKey()); - var actualValue = actual.get(entry.getKey()); - assertTrue( - sameValue.test(entry.getValue(), actualValue), - tableName + " changed value for " + entry.getKey() - + ": expected " + entry.getValue() - + ", actual " + actualValue - ); - } - for (var entry : actual.entrySet()) { - assertTrue(expected.containsKey(entry.getKey()), tableName + " gained key " + entry.getKey()); - } - assertEquals(expected.size(), actual.size(), tableName + " size changed"); - } - private FunctionDeclaration findFunction(List statements, String name) { return statements.stream() .filter(FunctionDeclaration.class::isInstance) @@ -1819,6 +1635,10 @@ 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 diff --git a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzerTest.java b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzerTest.java index 6e1cc1c0..c5f49cf9 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzerTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzerTest.java @@ -2753,13 +2753,43 @@ func ping(): var diagnostics = new DiagnosticManager(); var parserService = new GdScriptParserService(); var unit = parserService.parseUnit(Path.of("tmp", fileName), source, diagnostics); - var analysisData = FrontendSemanticAnalyzerTestAccess.analyzeWithLegacySharedSemanticPublication( - new FrontendSemanticAnalyzer(), + var analysisData = analyzeWithOwnerAnalyzerBaseline(unit, registry, diagnostics, topLevelCanonicalNameMap); + return new AnalyzedScript(unit.ast(), analysisData); + } + + private static @NotNull FrontendAnalysisData analyzeWithOwnerAnalyzerBaseline( + @NotNull FrontendSourceUnit unit, + @NotNull ClassRegistry classRegistry, + @NotNull DiagnosticManager diagnostics, + @NotNull Map topLevelCanonicalNameMap + ) { + var analysisData = FrontendAnalysisData.bootstrap(); + var moduleSkeleton = new FrontendClassSkeletonBuilder().build( new FrontendModule("test_module", List.of(unit), topLevelCanonicalNameMap), - registry, - diagnostics + classRegistry, + diagnostics, + analysisData ); - return new AnalyzedScript(unit.ast(), analysisData); + analysisData.updateModuleSkeleton(moduleSkeleton); + analysisData.updateDiagnostics(diagnostics.snapshot()); + new FrontendScopeAnalyzer().analyze(classRegistry, analysisData, diagnostics); + analysisData.updateDiagnostics(diagnostics.snapshot()); + new FrontendVariableAnalyzer().analyze(analysisData, diagnostics); + analysisData.updateDiagnostics(diagnostics.snapshot()); + new FrontendTopBindingAnalyzer().analyze(classRegistry, analysisData, diagnostics); + analysisData.updateDiagnostics(diagnostics.snapshot()); + new FrontendLocalTypeStabilizationAnalyzer().analyze(classRegistry, analysisData, diagnostics); + analysisData.updateDiagnostics(diagnostics.snapshot()); + new FrontendChainBindingAnalyzer().analyze(classRegistry, analysisData, diagnostics); + analysisData.updateDiagnostics(diagnostics.snapshot()); + // These focused tests exercise the standalone owner analyzers after Phase I removed the + // shared FrontendSemanticAnalyzer legacy bypass. Production body publication remains covered + // by SuiteResolver/framework tests and must not call this helper. + new FrontendExprTypeAnalyzer().analyze(classRegistry, analysisData, diagnostics); + analysisData.updateDiagnostics(diagnostics.snapshot()); + new FrontendVarTypePostAnalyzer().analyze(analysisData, diagnostics); + analysisData.updateDiagnostics(diagnostics.snapshot()); + return analysisData; } private static @NotNull PreparedExpressionInput prepareInputBeforeExpressionTyping( diff --git a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSemanticAnalyzerTestAccess.java b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSemanticAnalyzerTestAccess.java deleted file mode 100644 index 27207b91..00000000 --- a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSemanticAnalyzerTestAccess.java +++ /dev/null @@ -1,22 +0,0 @@ -package gd.script.gdcc.frontend.sema.analyzer; - -import gd.script.gdcc.frontend.diagnostic.DiagnosticManager; -import gd.script.gdcc.frontend.parse.FrontendModule; -import gd.script.gdcc.frontend.sema.FrontendAnalysisData; -import gd.script.gdcc.scope.ClassRegistry; -import org.jetbrains.annotations.NotNull; - -/// Test-source bridge for package-private semantic analyzer compatibility hooks. -public final class FrontendSemanticAnalyzerTestAccess { - private FrontendSemanticAnalyzerTestAccess() { - } - - public static @NotNull FrontendAnalysisData analyzeWithLegacySharedSemanticPublication( - @NotNull FrontendSemanticAnalyzer analyzer, - @NotNull FrontendModule module, - @NotNull ClassRegistry classRegistry, - @NotNull DiagnosticManager diagnosticManager - ) { - return analyzer.analyzeWithLegacySharedSemanticPublication(module, classRegistry, diagnosticManager); - } -} From 8bacc3ad4fc26f08018bea1f5374858d70cce00b Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Fri, 10 Jul 2026 13:13:59 +0800 Subject: [PATCH 15/27] feat(frontend): bind suite resolver to interface typed baseline and inventory guard - Route source-facing typed slot lookups through the Interface-layer baseline instead of scope slot types. - Cross-check resolved scope locals against published body inventory by declaration identity. - Plumb the baseline and index through the suite environment, owner procedures, and resolver seams. - Cover the new baseline, inventory guard, and resolver wiring with focused regressions. - Refresh the pipeline plan and execution summary to track the new binding. --- ...e_resolution_pipeline_execution_summary.md | 11 ++-- ...segmented_type_resolution_pipeline_plan.md | 17 +++--- .../sema/FrontendBodyDeclarationIndex.java | 27 ++++++++- .../sema/FrontendTypedLexicalEnvironment.java | 37 ++++++++++-- .../analyzer/FrontendBodyOwnerProcedures.java | 11 +++- .../sema/analyzer/FrontendSuiteContext.java | 3 +- .../sema/analyzer/FrontendSuiteResolver.java | 20 ++++++- .../FrontendVisibleValueResolver.java | 33 +++++++++- .../FrontendTypedLexicalEnvironmentTest.java | 21 +++++++ .../FrontendVisibleValueResolverTest.java | 60 +++++++++++++++++++ 10 files changed, 215 insertions(+), 25 deletions(-) diff --git a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md index 35630b71..c750ac9f 100644 --- a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md +++ b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md @@ -43,9 +43,9 @@ Interface 层在基础结构层之后运行,负责准备 body `SuiteResolver` 输出包括: -- `FrontendBodyDeclarationIndex`:记录每个 supported block 的完整 declaration 列表与 source order。 +- `FrontendBodyDeclarationIndex`:记录每个 supported block 的完整 declaration 列表与 source order;生产 resolver 用 declaration identity 验证 scope 命中的 ordinary local 属于已发布 inventory,但不替代 declaration-order / self-reference filtered-hit。 - `FrontendInventoryGateRegistry`:记录 typed-dependent subtree 的 gate owner、header root、body root、deferred domain 与 readiness。 -- `FrontendTypedLexicalBaseline`:记录参数、显式 typed local 与 interface 层可静态确定的 source-facing slot baseline。 +- `FrontendTypedLexicalBaseline`:记录参数、显式 typed local 与 interface 层可静态确定的 source-facing slot baseline;`TypedLexicalEnvironment` 在 overlay、已发布 slot fact 与 parent environment 都没有事实时读取该冻结 fallback。 - `FrontendSuiteEntryRoots`:列出 body layer 可进入的 callable、property initializer 与 supported block roots。 Interface 层不得发布 body typed facts。特别是不得发布 `expressionTypes()`、`resolvedMembers()` 或 `resolvedCalls()`,也不得把 `GdCompilerType` 写入 source-facing lexical baseline。 @@ -66,6 +66,8 @@ resolveSuite(context, block): 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`。 @@ -114,9 +116,10 @@ Statement flush 把当前 statement pending overlay 转入 current-suite committ 1. 当前 statement pending overlay。 2. 当前 suite committed overlay。 -3. `BlockScope` / `CallableScope` 中的 stable lexical inventory 与 stable side tables。 +3. 已发布 stable slot facts。 4. parent typed lexical environment。 -5. class / global / singleton / type-meta lookup。 +5. `FrontendTypedLexicalBaseline` 提供的冻结 source-facing fallback;`BlockScope` / `CallableScope` 仍负责 lexical inventory 名称查找。 +6. class / global / singleton / type-meta lookup。 Pending overlay 只对当前 statement 后续 owner 子过程可见。Statement flush 后,pending facts 才进入 current-suite committed overlay,并对后续 statement 与 gate classifier 可见。Committed overlay 仍不是 stable publication。 diff --git a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md index 710020cf..2daaadb4 100644 --- a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md +++ b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md @@ -104,7 +104,7 @@ GDCC 的完整 lexical inventory 要求不是因为 Godot 缺少前向 local 检 正确模型是: -- 对已支持的 block,基础结构层沿用现有 `FrontendVariableAnalyzer` + scope graph 发布完整 ordinary local inventory;interface 层建立 body declaration index / typed baseline view,而不是另起一套重复发布通道。 +- 对已支持的 block,基础结构层沿用现有 `FrontendVariableAnalyzer` + scope graph 发布完整 ordinary local inventory;interface 层建立 body declaration index / typed baseline view,而不是另起一套重复发布通道。生产 resolver 仍由 scope 完成按名称查找,并以 index 的 declaration identity 验证命中的 ordinary local 属于已发布 inventory;现有 source byte range filter 继续负责 declaration-order 与 initializer self-reference 语义。 - local `:=` 初始类型仍是 `Variant`。 - body `SuiteResolver` 只负责按源码顺序稳定类型、发布 use-site facts、推进 gate readiness。 - 若某个 child feature gate 后续转正,必须先发布该 child body 的 gate-owned bindings 与完整 local inventory,再解析 body suite。 @@ -222,9 +222,9 @@ Interface phase 输入: Interface phase 输出: -- `FrontendBodyDeclarationIndex`:每个 supported block 的完整 declaration 列表与 source order。 +- `FrontendBodyDeclarationIndex`:每个 supported block 的完整 declaration 列表与 source order;production resolver 用它验证 scope local 的 published-inventory identity。 - `FrontendInventoryGateRegistry`:typed-dependent subtree 的 gate owner、header root、body root、deferred domain、readiness。 -- `FrontendTypedLexicalBaseline`:参数、显式 typed local、已可静态确定的 interface-level source-facing slot baseline。 +- `FrontendTypedLexicalBaseline`:参数、显式 typed local、已可静态确定的 interface-level source-facing slot baseline;production `TypedLexicalEnvironment` 将其作为冻结 fallback。 - `FrontendSuiteEntryRoots`:body layer 可进入的 callable/property initializer/supported block 根列表。 Interface phase 不得: @@ -265,6 +265,8 @@ resolveSuite(context, block): 5. Var type post procedure:消费 expression type 与 source-facing local slot overlay,发布 final `slotTypes()` overlay。 6. Statement flush:把当前 statement pending overlay 转入 current-suite committed overlay,供后续 statement / gate classifier 读取;不得在这一步写 stable side table。 +`SuiteResolver` 必须把 interface surface 的 declaration index 交给 visible-value resolver,以检查 scope 选中的 ordinary local 的 declaration identity 已由 baseline inventory 发布;这不是重新扫描普通 `var`,也不改变 resolver 现有 filtered-hit 规则。它还必须把 typed lexical baseline 传给 root 与 child typed environment,使参数和 ordinary local 在没有更新 overlay / stable slot fact 时仍有 immutable source-facing 初始类型。 + Suite 收敛后,不能把 current-suite committed overlay 整体打包为单个多 owner `FrontendAnalysisPatch`。Stable export 必须构造按 owner 有序的 patch transaction,并按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 顺序依次 apply 每个 owner patch。这样既保留 source-order suite 的前缀可见性,又不破坏 `FrontendAnalysisPatch` 现有单 stage / 单 owner 约束。 不得重排 1-5。尤其是 chain binding 读取 receiver local slot 时,必须先看到 local stabilization 对前序 statement 或当前 statement 前序子过程写入的 exact slot fact;否则会重新打开 receiver 被误读成 `Variant` 的历史回归。 @@ -296,9 +298,10 @@ Gate classifier 属于 statement 结构处理的一部分,只能在其 header 1. 当前 statement pending overlay。 2. 当前 suite committed overlay。 -3. `BlockScope` / `CallableScope` 中的 stable lexical inventory 与 stable side tables。 +3. 已发布 stable slot facts。 4. parent typed lexical environment。 -5. class/global/singleton/type-meta lookup。 +5. interface typed baseline 提供的冻结 source-facing fallback;`BlockScope` / `CallableScope` 继续提供 lexical inventory 名称查找。 +6. class/global/singleton/type-meta lookup。 Pending overlay 只对当前 statement 内后续 owner 子过程可见。Statement flush 后,它才变成 current-suite committed overlay,并对后续 statement / gate classifier 可见。Committed overlay 仍不是 stable publication。 @@ -928,7 +931,7 @@ Compile gate 测试: ### R2:resolver 看不到未来声明 -缓解:interface phase 必须基于基础结构层已发布的 inventory 为 supported suite 建立完整 local declaration index。禁止 resolver 自己扫描普通 `var` 弥补缺口;resolver 只能读取 index 与 readiness。参见第 3.2 节:完整 inventory 是 resolver filtered-hit 模型的前提,不是为了与 Godot source-order analysis 对立。 +缓解:interface phase 必须基于基础结构层已发布的 inventory 为 supported suite 建立完整 local declaration index。禁止 resolver 自己扫描普通 `var` 弥补缺口;resolver 通过 scope 的已发布 inventory 完成名称查找,并读取 index 与 readiness 验证该命中属于可解析 body。现有 source byte range filter 继续产出 declaration-after-use 与 initializer self-reference provenance;仅有 local declaration `sourceOrder` 不能替代任意 use-site 的位置模型。参见第 3.2 节:完整 inventory 是 resolver filtered-hit 模型的前提,不是为了与 Godot source-order analysis 对立。 ### R3:scope slot mutation 与 published binding payload 脱节 @@ -1011,7 +1014,7 @@ Compile gate 测试: - shared compiler-only walker 同时用于 patch commit、overlay pending write、overlay flush,以及任何保留的 source-facing whole-table publication API;新增 type-bearing payload 时必须同步更新该 walker 与对应 regression tests。 - production body path 不通过 `FrontendAnalysisData.symbolBindings()/resolvedMembers()/resolvedCalls()/expressionTypes()/slotTypes()` 返回的可变 stable 引用直接 `put()` / `clear()` / `putAll()`,也不通过 `updateXxx(...)` whole-table replace 把 body typed facts 中转到 stable side table 后再校验。 - `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 不改写 `BlockScope`、不刷新 `symbolBindings()` payload、不产生 slot update,只保留 guard-only 协议检查。 -- supported suite 的完整 local inventory 先于 body typed resolution,declaration-after-use filtered hit 行为不退化。 +- supported suite 的完整 local inventory 先于 body typed resolution,production resolver 使用 declaration index 验证 scope local identity,且 declaration-after-use filtered hit 行为不退化。 - local `:=` 的 source-order type stabilization 可被后续 statement / gate classifier 消费。 - chain binding 消费 receiver local slot 时,必须看到 local stabilization 已发布到 overlay 的 exact type,而不是 interface baseline `Variant`。 - pending feature gate 能在 typed fact 就绪后安全转正,但 child body 只有在 `bodyInventoryReadiness == PUBLISHED` 后才可解析。 diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyDeclarationIndex.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyDeclarationIndex.java index 8c4b2a04..c9fd6980 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyDeclarationIndex.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyDeclarationIndex.java @@ -1,7 +1,9 @@ package gd.script.gdcc.frontend.sema; 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.util.Collections; import java.util.IdentityHashMap; @@ -12,23 +14,39 @@ /// Interface-layer index of ordinary locals that already exist in baseline `BlockScope` inventory. /// /// The index is intentionally a view over published inventory, not a second declaration publisher. +/// 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. /// Unsupported typed-dependent bodies therefore have no entries until their gate-owned inventory is /// explicitly published by a later body phase. 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( - Objects.requireNonNull(entry.getKey(), "bodyRoot"), - List.copyOf(Objects.requireNonNull(entry.getValue(), "declarations")) + 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) { @@ -39,6 +57,11 @@ public boolean containsBodyRoot(@NotNull Node bodyRoot) { return declarationsByBodyRoot.containsKey(Objects.requireNonNull(bodyRoot, "bodyRoot")); } + /// Returns the published inventory entry for one ordinary local declaration, if any. + public @Nullable FrontendBodyLocalDeclaration declarationFor(@NotNull VariableDeclaration 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/FrontendTypedLexicalEnvironment.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java index 0d95f821..673b62b5 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java @@ -23,15 +23,17 @@ import java.util.List; import java.util.Objects; -/// Effective typed view used by the future body suite resolver. +/// 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`. +/// 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(); @@ -46,10 +48,20 @@ 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() { @@ -100,12 +112,21 @@ public FrontendTypedLexicalEnvironment( } public @Nullable GdType slotType(@NotNull Node astNode) { - return firstNonNull( + var localSlotType = firstNonNull( pendingFacts.slotTypes.get(astNode), committedFacts.slotTypes.get(astNode), - stableData.slotTypes().get(astNode), - parent != null ? parent.slotType(astNode) : null + 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. @@ -142,6 +163,12 @@ public FrontendTypedLexicalEnvironment( 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(); 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 index dd699d6b..7d960c44 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProcedures.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProcedures.java @@ -29,6 +29,7 @@ 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.FrontendBodyDeclarationIndex; import gd.script.gdcc.frontend.sema.FrontendCallResolutionKind; import gd.script.gdcc.frontend.sema.FrontendCallResolutionStatus; import gd.script.gdcc.frontend.sema.FrontendDeclaredTypeSupport; @@ -91,6 +92,7 @@ public final class FrontendBodyOwnerProcedures implements FrontendStatementResol private static final @NotNull String UNSUPPORTED_EXPRESSION_ROUTE_CATEGORY = "sema.unsupported_expression_route"; private FrontendAnalysisData cachedAnalysisData; + private FrontendBodyDeclarationIndex cachedBodyDeclarationIndex; private FrontendVisibleValueResolver cachedVisibleValueResolver; @Override @@ -950,11 +952,16 @@ private static void publishAttributeStepExpressionTypes( } private @NotNull FrontendVisibleValueResolver visibleValueResolver(@NotNull FrontendSuiteContext context) { - if (cachedAnalysisData != context.analysisData() || cachedVisibleValueResolver == null) { + var bodyDeclarationIndex = context.interfaceSurface().bodyDeclarationIndex(); + if (cachedAnalysisData != context.analysisData() + || cachedBodyDeclarationIndex != bodyDeclarationIndex + || cachedVisibleValueResolver == null) { cachedAnalysisData = context.analysisData(); + cachedBodyDeclarationIndex = bodyDeclarationIndex; cachedVisibleValueResolver = new FrontendVisibleValueResolver( context.analysisData(), - context.interfaceSurface().inventoryGateRegistry() + context.interfaceSurface().inventoryGateRegistry(), + bodyDeclarationIndex ); } return cachedVisibleValueResolver; 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 index 056ae539..4fad3795 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteContext.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteContext.java @@ -56,7 +56,8 @@ public record FrontendSuiteContext( var childEnvironment = new FrontendTypedLexicalEnvironment( blockScope, analysisData, - typedEnvironment + typedEnvironment, + interfaceSurface.typedLexicalBaseline() ); return new FrontendSuiteContext( sourcePath, 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 index 76b1bc89..ae02ca82 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolver.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolver.java @@ -94,7 +94,12 @@ private void resolveCallableOwner( if (!(bodyScope instanceof BlockScope blockScope)) { return; } - var environment = new FrontendTypedLexicalEnvironment(blockScope, analysisData); + var environment = new FrontendTypedLexicalEnvironment( + blockScope, + analysisData, + null, + interfaceSurface.typedLexicalBaseline() + ); var context = new FrontendSuiteContext( sourcePathFor(interfaceSurface, callableOwner, analysisData), callableOwner, @@ -140,7 +145,11 @@ private void publishParameterSlotType( if (slot == null || slot.kind() != ScopeValueKind.PARAMETER || slot.declaration() != parameter) { throw new IllegalStateException("Parameter '" + parameter.name().trim() + "' inventory slot drifted"); } - context.typedEnvironment().putSlotType(FrontendSemanticStage.VAR_TYPE_POST, parameter, slot.type()); + 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( @@ -179,7 +188,12 @@ private void resolvePropertyInitializer( return; } var classScope = propertyContext.declaringClassScope(); - var environment = new FrontendTypedLexicalEnvironment(classScope, analysisData); + var environment = new FrontendTypedLexicalEnvironment( + classScope, + analysisData, + null, + interfaceSurface.typedLexicalBaseline() + ); var context = new FrontendSuiteContext( sourcePathFor(interfaceSurface, propertyInitializer, analysisData), propertyInitializer, 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 29c0fe43..dd8e019b 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 @@ -3,6 +3,7 @@ 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.FrontendBodyDeclarationIndex; import gd.script.gdcc.frontend.sema.FrontendExecutableInventorySupport; import gd.script.gdcc.frontend.sema.FrontendInventoryGate; import gd.script.gdcc.frontend.sema.FrontendInventoryGateRegistry; @@ -38,19 +39,31 @@ public final class FrontendVisibleValueResolver { private final @NotNull FrontendAnalysisData analysisData; private final @NotNull FrontendInventoryGateRegistry gateRegistry; + 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, FrontendInventoryGateRegistry.empty()); + this(analysisData, FrontendInventoryGateRegistry.empty(), null); } public FrontendVisibleValueResolver( @NotNull FrontendAnalysisData analysisData, @NotNull FrontendInventoryGateRegistry gateRegistry + ) { + this(analysisData, gateRegistry, null); + } + + /// `bodyDeclarationIndex` is supplied by the production SuiteResolver path to validate that a + /// selected local belongs to Interface-phase inventory. Legacy callers retain range-only filtering. + public FrontendVisibleValueResolver( + @NotNull FrontendAnalysisData analysisData, + @NotNull FrontendInventoryGateRegistry gateRegistry, + @Nullable FrontendBodyDeclarationIndex bodyDeclarationIndex ) { this.analysisData = Objects.requireNonNull(analysisData, "analysisData must not be null"); this.gateRegistry = Objects.requireNonNull(gateRegistry, "gateRegistry must not be null"); + this.bodyDeclarationIndex = bodyDeclarationIndex; indexSourceAstParents(analysisData.moduleSkeleton()); } @@ -406,6 +419,7 @@ private boolean publishesCallableLocalValueInventory(@NotNull Block block) { return null; } if (declaration instanceof VariableDeclaration variableDeclaration) { + checkPublishedLocalInventory(variableDeclaration, value); if (isVisibleLocal(variableDeclaration, useSite)) { return null; } @@ -418,6 +432,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(); } diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironmentTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironmentTest.java index 0c061353..e32c35e2 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironmentTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironmentTest.java @@ -102,6 +102,27 @@ void childEnvironmentReadsParentCommittedLocalSlotOverlay() throws Exception { 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(); 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 6b4c2976..8b98e46f 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,7 +6,9 @@ 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.FrontendBodyInventoryReadiness; +import gd.script.gdcc.frontend.sema.FrontendBodyLocalDeclaration; import gd.script.gdcc.frontend.sema.FrontendInventoryGate; import gd.script.gdcc.frontend.sema.FrontendInventoryGateRegistry; import gd.script.gdcc.frontend.sema.FrontendInventoryGateStatus; @@ -27,12 +29,14 @@ 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; @@ -102,6 +106,62 @@ 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, 0)) + )); + var resolver = new FrontendVisibleValueResolver( + analyzedInput.analysisData(), + FrontendInventoryGateRegistry.empty(), + 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(), + FrontendInventoryGateRegistry.empty(), + 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", """ From 838eab4328ce12c537e3f0a15844a24e231071ac Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Fri, 10 Jul 2026 16:23:57 +0800 Subject: [PATCH 16/27] feat(frontend): align parameter pre-publication with the body owner lifecycle - Reframe callable parameter slot publication as a callable-entry step sharing the statement-local owner stage. - Rename the overlay flush and parameter helpers to reflect the boundary-agnostic overlay lifecycle. - Update the pipeline plan and execution summary to document the callable-entry pre-publication contract. - Cover the callable-entry behavior with focused regressions and refresh the existing overlay tests. --- ...e_resolution_pipeline_execution_summary.md | 33 ++++++-- ...segmented_type_resolution_pipeline_plan.md | 33 +++++--- .../sema/FrontendTypedLexicalEnvironment.java | 6 +- .../analyzer/FrontendStatementResolver.java | 2 +- .../sema/analyzer/FrontendSuiteResolver.java | 18 +++-- .../sema/FrontendSuiteResolverTest.java | 79 +++++++++++++++++++ .../FrontendTypedLexicalEnvironmentTest.java | 6 +- 7 files changed, 148 insertions(+), 29 deletions(-) diff --git a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md index c750ac9f..8ed57daf 100644 --- a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md +++ b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md @@ -55,13 +55,28 @@ Interface 层不得发布 body typed facts。特别是不得发布 `expressionTy Body 层由 `FrontendSuiteResolver` 或等价 coordinator 驱动。它按源码顺序进入 supported suite,形态如下: ```text +resolveCallableOwner(context, callableOwner): + runCallableEntryVarTypePost(context, callableOwner) + resolveSuite(context, callableOwner.body) + +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) - flushStatementFacts(context, statement) + context.typedEnvironment().flushPendingFacts() resolvePendingBodiesIfAllowed(context) ``` +参数是 callable-entry `VAR_TYPE_POST` facts,不是 statement root。因此预发布是与 +statement-local var type post procedure 互补的正规步骤,而不是绕过或例外:两条路径都使用 +同一 owner stage、typed overlay 与 suite-export transaction。参数 facts 在进入 statement +循环前 flush 到 committed overlay,使第一个 statement 可读取它们,同时 stable side table +仍保持不变。 + `resolveStatement(...)` 不是新的 semantic owner。它只按 statement 结构编排已有 owner 的 body-aware 子过程:top binding、local stabilization、chain binding、expr typing、gate classifier 与 var type post。 Body procedure 必须是 root-bounded、statement-local 的实现。每个 owner 子过程只处理 `SuiteResolver` 传入的 statement、header 或 expression root 及其允许的子表达式。实现可以复用纯语义 helper,但 owner 子过程不能依赖 whole-module traversal 建立隐式上下文。 @@ -90,7 +105,11 @@ Body procedure 必须是 root-bounded、statement-local 的实现。每个 owner 4. Expr typing runner。 5. Gate classifier hook。 6. Var type post procedure。 -7. Statement flush。 +7. Pending fact flush。 + +在上述 statement-local 顺序之前,callable-entry var type post 会为每个参数发布 +`VAR_TYPE_POST` slot fact 并 flush 到 committed overlay。它不属于第 1-7 步的 statement +procedure,但使用相同的 owner stage 和发布协议。 Top binding runner 为当前 statement 内的 bare identifier 与 chain head use-site 写入 binding overlay。 @@ -104,7 +123,7 @@ Gate classifier hook 在 header / synthetic fixture 已完成 top binding、loca Var type post procedure 消费 expression type 与 source-facing local slot overlay,发布 final `slotTypes()` overlay。 -Statement flush 把当前 statement pending overlay 转入 current-suite committed overlay,供后续 statement 与 gate classifier 读取。Flush 不得写入 stable side table 或 stable `BlockScope` slot。 +Pending fact flush 把当前 statement pending overlay 转入 current-suite committed overlay,供后续 statement 与 gate classifier 读取。Flush 不得写入 stable side table 或 stable `BlockScope` slot。 这个顺序不能重排。尤其 chain binding 读取 receiver local slot 时,必须先看到 local stabilization 对前序 statement 或当前 statement 前序子过程写入的 exact slot fact。 @@ -121,7 +140,7 @@ Statement flush 把当前 statement pending overlay 转入 current-suite committ 5. `FrontendTypedLexicalBaseline` 提供的冻结 source-facing fallback;`BlockScope` / `CallableScope` 仍负责 lexical inventory 名称查找。 6. class / global / singleton / type-meta lookup。 -Pending overlay 只对当前 statement 后续 owner 子过程可见。Statement flush 后,pending facts 才进入 current-suite committed overlay,并对后续 statement 与 gate classifier 可见。Committed overlay 仍不是 stable publication。 +Pending overlay 只对当前 statement 后续 owner 子过程可见。`flushPendingFacts()` 后,pending facts 才进入 current-suite committed overlay,并对后续 statement 与 gate classifier 可见。Committed overlay 仍不是 stable publication。 `TypedLexicalEnvironment` 的目标是模拟 source-order body resolution 中“前缀 statement 已解析出的类型可被后续 statement 使用”的行为,同时避免提前污染 stable semantic facts。 @@ -131,7 +150,7 @@ Pending overlay 只对当前 statement 后续 owner 子过程可见。Statement 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:由 statement flush 合并而来,后续 statement 与 gate classifier 可读,但仍不是 stable publication。 +3. Current-suite committed overlay:由 pending fact flush 合并而来,后续 statement 与 gate classifier 可读,但仍不是 stable publication。 4. `FrontendAnalysisData` stable side tables / `BlockScope` stable slot:只在 suite export 的 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。 @@ -140,9 +159,9 @@ Nested chain / argument retry 的中间事实只能存在于 owner-local transie ## 8. Overlay 写入、Flush 与 Export -Owner runner 只能写当前 statement pending overlay。Overlay write 必须携带 owner metadata,并在写入时执行 owner、conflict、idempotent、exact-type 与 compiler-only guard。 +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。 -`flushStatementFacts(...)` 只把 pending overlay 合并到 current-suite committed overlay。Flush 必须复用 suite export 使用的同一个 type-bearing field walker,不能在 scratch 层接受 export 层会拒绝的 payload。 +`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。Stable side table 与 `BlockScope.resetLocalType(...)` 只能在 per-owner patch apply 或 stable export helper 中更新。 diff --git a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md index 2daaadb4..782209fb 100644 --- a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md +++ b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md @@ -239,13 +239,27 @@ Interface phase 不得: 新增 `FrontendSuiteResolver` 或等价 body coordinator。它按 Godot `resolve_suite()` 的形状处理 body: ```text +resolveCallableOwner(context, callableOwner): + runCallableEntryVarTypePost(context, callableOwner) + resolveSuite(context, callableOwner.body) + +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) - flushStatementFacts(context, statement) + context.typedEnvironment().flushPendingFacts() resolvePendingBodiesIfAllowed(context) ``` +参数是 callable-entry `VAR_TYPE_POST` facts。它们不是 statement root,因而必须在进入 +statement 循环前写入 pending overlay 并 flush 到 current-suite committed overlay。该步骤与 +statement-local var type post procedure 互补而非绕过:二者共享 `VAR_TYPE_POST` owner stage、 +冲突检查、compiler-only guard 与 suite-export transaction;此前不得写 stable `slotTypes()`。 + `resolveStatement(...)` 不是新的 owner。它只负责按 statement 结构调用 body-aware owner 子过程: - identifier / route head 绑定仍由 top binding owner 产出。 @@ -258,12 +272,13 @@ resolveSuite(context, block): `resolveStatement(...)` 内部的 owner 子过程顺序是新的硬不变量,必须保持 legacy whole-phase 的可见性顺序: +0. Callable-entry var type post pre-publication:在进入 statement 循环前,为每个 parameter 发布 `VAR_TYPE_POST` slot fact,并调用 `flushPendingFacts()` 使其进入 current-suite committed overlay。它不是 statement-local procedure 的例外。 1. Top binding runner:为当前 statement 内的 bare identifier / chain head use-site 写入 binding overlay。 2. Local stabilization runner:在 top binding overlay、当前 suite 已提交 typed fact、stable lexical inventory 之上解析 eligible `:=` initializer,并写入当前 statement 的 local slot pending overlay。 3. Chain binding runner:消费 top binding overlay 与 local stabilization pending / committed slot fact,发布 `resolvedMembers()` 与 chain-owned `resolvedCalls()` overlay。 4. Expr typing runner:消费 binding、member、call 与 local slot overlay,发布 `expressionTypes()` 与 bare-call `resolvedCalls()` overlay;`backfillInferredLocalType(...)` 仍只做 guard-only 检查。 5. Var type post procedure:消费 expression type 与 source-facing local slot overlay,发布 final `slotTypes()` overlay。 -6. Statement flush:把当前 statement pending overlay 转入 current-suite committed overlay,供后续 statement / gate classifier 读取;不得在这一步写 stable side table。 +6. Pending fact flush:把当前 statement pending overlay 转入 current-suite committed overlay,供后续 statement / gate classifier 读取;不得在这一步写 stable side table。 `SuiteResolver` 必须把 interface surface 的 declaration index 交给 visible-value resolver,以检查 scope 选中的 ordinary local 的 declaration identity 已由 baseline inventory 发布;这不是重新扫描普通 `var`,也不改变 resolver 现有 filtered-hit 规则。它还必须把 typed lexical baseline 传给 root 与 child typed environment,使参数和 ordinary local 在没有更新 overlay / stable slot fact 时仍有 immutable source-facing 初始类型。 @@ -303,7 +318,7 @@ Gate classifier 属于 statement 结构处理的一部分,只能在其 header 5. interface typed baseline 提供的冻结 source-facing fallback;`BlockScope` / `CallableScope` 继续提供 lexical inventory 名称查找。 6. class/global/singleton/type-meta lookup。 -Pending overlay 只对当前 statement 内后续 owner 子过程可见。Statement flush 后,它才变成 current-suite committed overlay,并对后续 statement / gate classifier 可见。Committed overlay 仍不是 stable publication。 +Pending overlay 只对当前 statement 内后续 owner 子过程可见。`flushPendingFacts()` 后,它才变成 current-suite committed overlay,并对后续 statement / gate classifier 可见。Committed overlay 仍不是 stable publication。 写入规则: @@ -311,7 +326,7 @@ Pending overlay 只对当前 statement 内后续 owner 子过程可见。Stateme - 只有 `FrontendTopBindingAnalyzer` 可写 binding overlay。 - 只有 `FrontendChainBindingAnalyzer` 可写 member / chain-call overlay。 - 只有 `FrontendExprTypeAnalyzer` 可写 expression type / bare-call overlay。 -- 只有 `FrontendVarTypePostAnalyzer` 可写 source-facing slot type overlay。 +- 只有 var type post owner 可写 source-facing slot type overlay;它包括 callable-entry 参数预发布与 statement-local local-`var` publication,两者都必须使用 `VAR_TYPE_POST` stage。 - overlay fact 必须带 owner metadata,并在导出到 stable side table 前执行冲突检测、idempotent 检查和 compiler-only guard。 - compiler-only guard 必须检查该 fact 可达的每个 user-visible `GdType` payload;不能只检查 `expressionTypes()` / `slotTypes()` 两个表。 - compiler-only guard 必须在 pending overlay write 时就 fail-fast,而不是等 suite export 时才补救。任何 scratch 写入如果命中 `GdCompilerType`,必须在 write API 返回前拒绝该 fact,不能先写入 pending / committed overlay 再在 export 时回滚。 @@ -322,16 +337,16 @@ Pending overlay 只对当前 statement 内后续 owner 子过程可见。Stateme - Owner procedure transient cache:owner 子过程私有,只给当前 chain / expr reduction 的 retry 回调读取;不属于 `TypedLexicalEnvironment`,不参与 flush / export,owner 子过程结束即丢弃。 - 当前 statement pending overlay:只给当前 statement 后续 owner 子过程读取;只接受每个 AST key 的最终 publication fact。 -- current-suite committed overlay:由 statement flush 合并而来,给后续 statement 与 gate classifier 读取;仍不是 stable publication。 +- current-suite committed overlay:由 pending fact flush 合并而来,给后续 statement 与 gate classifier 读取;仍不是 stable publication。 - `FrontendAnalysisData` stable side tables / `BlockScope` stable slot:只在 suite export 的 per-owner patch apply / stable export helper 后更新,供 diagnostics-only phase、compile gate 与 lowering 使用。 Overlay 的目标是模拟 Godot “当前 statement 已解析出的类型可被后续 statement 使用”的效果,同时避免提前污染 `FrontendAnalysisData` stable tables。 写入与导出时机: -- Owner runner 只能写当前 statement pending overlay。 -- `flushStatementFacts(...)` 只把 pending overlay 合并到 current-suite committed overlay,并执行 owner、conflict、idempotent、exact-type 与 compiler-only guard。该 guard 必须与 suite export 使用同一 type-bearing field walker,避免 scratch 层接受 stable merge 层会拒绝的 compiler-only payload。 -- `flushStatementFacts(...)` 不得通过 stable side table 的临时 whole-table publish 再“读回 overlay”。如果当前实现为了复用旧 analyzer helper 需要 `updateXxx(...)` / stable side table 作为中转,则该 helper 必须先被改写或隔离为 legacy path,不能把中转污染当作阶段性可接受状态。 +- Statement owner runner 只能写当前 statement pending overlay;callable-entry var type post 在 statement 循环前写参数 pending overlay。 +- `flushPendingFacts(...)` 只把 pending overlay 合并到 current-suite committed overlay,并执行 owner、conflict、idempotent、exact-type 与 compiler-only guard。该 guard 必须与 suite export 使用同一 type-bearing field walker,避免 scratch 层接受 stable merge 层会拒绝的 compiler-only payload。 +- `flushPendingFacts(...)` 不得通过 stable side table 的临时 whole-table publish 再“读回 overlay”。如果当前实现为了复用旧 analyzer helper 需要 `updateXxx(...)` / stable side table 作为中转,则该 helper 必须先被改写或隔离为 legacy path,不能把中转污染当作阶段性可接受状态。 - Suite 收敛时,current-suite committed overlay 只能导出为按 owner 有序的 patch transaction,不能导出为一个跨 owner `FrontendAnalysisPatch`。 - Patch transaction 固定按 top binding -> local stabilization -> chain binding -> expr typing -> var type post apply;每个 step 只包含该 owner 的 facts。 - Stable side table 与 `BlockScope.resetLocalType(...)` 只能在 per-owner patch apply / stable export helper 中更新。 @@ -639,7 +654,7 @@ Compiler-only guard payload matrix: - [x] C2 新增 `gd.script.gdcc.frontend.sema.patch` 包,迁入 legacy `FrontendAnalysisPatch` / `FrontendLocalSlotTypeUpdate`,并新增 `FrontendOwnerPatch`、`FrontendTopBindingPatch`、`FrontendLocalTypeStabilizationPatch`、`FrontendChainBindingPatch`、`FrontendExprTypePatch`、`FrontendVarTypePostPatch` 与 `FrontendPatchTransaction`。 - [x] C3 新增 `FrontendPublishedFactTypeGuard` shared walker,覆盖 binding/member/call/expression/slot/update 六类 source-facing typed payload;`FrontendAnalysisData.applyPatch(...)`、owner patch、legacy window scratch put 与保留的 `updateXxx(...)` whole-table publish 均接入该 guard。 - [x] C4 Overlay 写入 API 必须显式传入 `FrontendSemanticStage` owner metadata;错误 owner fail-fast,local slot overlay 继续执行 `Variant -> exact` / exact-same-only / no-void / no-compiler-only 规则。 -- [x] C5 `flushStatementFacts()` 只把 pending overlay 合并到 committed overlay;`exportPatchTransaction()` 只导出 fixed-order per-owner patch transaction,并由 `FrontendPatchTransaction` 拒绝 owner 顺序回退或重复 owner。 +- [x] C5 `flushPendingFacts()` 只把 pending overlay 合并到 committed overlay;`exportPatchTransaction()` 只导出 fixed-order per-owner patch transaction,并由 `FrontendPatchTransaction` 拒绝 owner 顺序回退或重复 owner。 - [x] C6 移除未接入生产路径的 `FrontendOwnerRetryMemo`,并把合同明确为 owner procedure 内部非导出 transient cache:`BodyExpressionResolver` 双 expression cache / call cache、`FrontendChainReductionFacade.reducedChains` 与 helper bounded retry 均不属于 typed lexical environment,不参与 flush / export。 - [x] C7 `FrontendVisibleValueResolver` 新增 overlay-aware `resolve(request, environment)` overload;它只在 declaration-order / self-reference filter 之后替换 returned local 的 effective type,不绕过 future-local filtered hit。 - [ ] C8 Expression semantic support 与 chain reduction facade 的 dependency-type callback 尚未替换为 explicit environment lookup;这是阶段 E owner procedure 重写的一部分,本阶段只提供可接入入口。 diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java index 673b62b5..4ad78ada 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java @@ -270,8 +270,10 @@ public void putSlotType( ); } - /// Moves current-statement facts into the suite overlay without touching stable data or scopes. - public void flushStatementFacts() { + /// 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(); 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 index 21749290..790434c0 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendStatementResolver.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendStatementResolver.java @@ -126,7 +126,7 @@ private void resolveUnsupportedRoot(@NotNull FrontendSuiteContext context, @NotN } private void flushStatementBoundary(@NotNull FrontendSuiteContext context) { - context.typedEnvironment().flushStatementFacts(); + 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()); 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 index ae02ca82..7c1bac00 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolver.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolver.java @@ -115,25 +115,29 @@ private void resolveCallableOwner( diagnosticManager, classRegistry ); - publishCallableParameterSlots(context, callableOwner); + runCallableEntryVarTypePost(context, callableOwner); resolveSuite(context, body); } - private void publishCallableParameterSlots( + /// 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 suite-export transaction. + private void runCallableEntryVarTypePost( @NotNull FrontendSuiteContext context, @NotNull Node callableOwner ) { var parameters = callableParameters(callableOwner); for (var parameter : parameters) { - publishParameterSlotType(context, parameter); + publishCallableEntryParameterSlotType(context, parameter); reportUnsupportedParameterDefault(context, parameter); } - // Parameters are callable-entry facts rather than statement facts, but they still need to - // enter the suite overlay before export and before the first statement consumes them. - context.typedEnvironment().flushStatementFacts(); + // Make every parameter visible to the first statement without publishing stable facts. + context.typedEnvironment().flushPendingFacts(); } - private void publishParameterSlotType( + private void publishCallableEntryParameterSlotType( @NotNull FrontendSuiteContext context, @NotNull Parameter parameter ) { diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java index 23299af0..8e9ee4e1 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java @@ -10,6 +10,7 @@ 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; @@ -30,6 +31,7 @@ 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.frontend.sema.resolver.FrontendVisibleValueDomain; import gd.script.gdcc.gdextension.ExtensionApiLoader; import gd.script.gdcc.scope.ClassRegistry; @@ -605,6 +607,43 @@ func ping(seed: int): ); } + @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 RecordingOwnerProcedures ownerProcedures @@ -1085,6 +1124,46 @@ private boolean 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; diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironmentTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironmentTest.java index e32c35e2..66df7251 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironmentTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironmentTest.java @@ -61,7 +61,7 @@ void pendingFlushExportAndApplyPreserveStableDataUntilTransactionApply() throws assertSame(GdVariantType.VARIANT, requireLocal(bodyScope, "local").type()); assertSame(GdVariantType.VARIANT, Objects.requireNonNull(originalBinding.resolvedValue()).type()); - environment.flushStatementFacts(); + environment.flushPendingFacts(); assertFalse(environment.hasPendingFacts()); assertTrue(environment.hasCommittedFacts()); @@ -92,7 +92,7 @@ void childEnvironmentReadsParentCommittedLocalSlotOverlay() throws Exception { FrontendSemanticStage.LOCAL_TYPE_STABILIZATION, new FrontendLocalSlotTypeUpdate(parentScope, "parent_local", declaration, GdIntType.INT) ); - parentEnvironment.flushStatementFacts(); + parentEnvironment.flushPendingFacts(); var childScope = new BlockScope(parentScope, BlockScopeKind.IF_BODY); var childEnvironment = new FrontendTypedLexicalEnvironment(childScope, analysisData, parentEnvironment); @@ -208,7 +208,7 @@ void overlayRejectsExactLocalSlotRewriteAndExpressionFactNarrowing() throws Exce expressionNode, FrontendExpressionType.resolved(GdIntType.INT) )); - environment.flushStatementFacts(); + environment.flushPendingFacts(); assertThrows(FrontendAnalysisPatchException.class, () -> environment.putExpressionType( FrontendSemanticStage.EXPR_TYPE, expressionNode, From bc6a30dcc75ae233e3bd6e9746277d6120188a9c Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Fri, 10 Jul 2026 17:08:29 +0800 Subject: [PATCH 17/27] feat(frontend): defer nested suite publication to callable export batch - Introduce a callable-scoped export batch that collects per-owner patch transactions from root and nested suites and applies them only after the root callable returns. - Keep child overlays isolated from the parent environment so cross-suite stable publication is impossible mid-resolution. - Treat property initializers as independent roots that apply their transactions directly without joining a callable batch. - Update the pipeline plan and execution summary to document the deferred-publication contract and refresh regressions to cover the new boundary. --- ...e_resolution_pipeline_execution_summary.md | 23 ++++-- ...segmented_type_resolution_pipeline_plan.md | 20 +++-- .../sema/FrontendTypedLexicalEnvironment.java | 3 + .../sema/analyzer/FrontendSuiteContext.java | 11 ++- .../sema/analyzer/FrontendSuiteResolver.java | 25 ++++++- .../patch/FrontendCallableExportBatch.java | 27 +++++++ .../sema/patch/FrontendPatchTransaction.java | 3 +- .../sema/FrontendSuiteResolverTest.java | 74 ++++++++++++++++++- 8 files changed, 164 insertions(+), 22 deletions(-) create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendCallableExportBatch.java diff --git a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md index 8ed57daf..6f33709e 100644 --- a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md +++ b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md @@ -56,8 +56,11 @@ Body 层由 `FrontendSuiteResolver` 或等价 coordinator 驱动。它按源码 ```text resolveCallableOwner(context, callableOwner): + exportBatch = new FrontendCallableExportBatch() + context = newSuiteContext(callableOwner, exportBatch) runCallableEntryVarTypePost(context, callableOwner) resolveSuite(context, callableOwner.body) + exportBatch.applyTo(context.analysisData()) runCallableEntryVarTypePost(context, callableOwner): for parameter in callableOwner.parameters(): @@ -67,16 +70,19 @@ runCallableEntryVarTypePost(context, callableOwner): resolveSuite(context, block): for statement in block.statements(): resolveStatement(context, statement) - context.typedEnvironment().flushPendingFacts() - resolvePendingBodiesIfAllowed(context) + context.exportBatch().accumulate(context.typedEnvironment().exportPatchTransaction()) ``` 参数是 callable-entry `VAR_TYPE_POST` facts,不是 statement root。因此预发布是与 statement-local var type post procedure 互补的正规步骤,而不是绕过或例外:两条路径都使用 -同一 owner stage、typed overlay 与 suite-export transaction。参数 facts 在进入 statement +同一 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。 + `resolveStatement(...)` 不是新的 semantic owner。它只按 statement 结构编排已有 owner 的 body-aware 子过程:top binding、local stabilization、chain binding、expr typing、gate classifier 与 var type post。 Body procedure 必须是 root-bounded、statement-local 的实现。每个 owner 子过程只处理 `SuiteResolver` 传入的 statement、header 或 expression root 及其允许的子表达式。实现可以复用纯语义 helper,但 owner 子过程不能依赖 whole-module traversal 建立隐式上下文。 @@ -144,6 +150,10 @@ Pending overlay 只对当前 statement 后续 owner 子过程可见。`flushPend `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 生命周期 目标架构固定四层事实可见性模型: @@ -151,7 +161,7 @@ Pending overlay 只对当前 statement 后续 owner 子过程可见。`flushPend 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 与 gate classifier 可读,但仍不是 stable publication。 -4. `FrontendAnalysisData` stable side tables / `BlockScope` stable slot:只在 suite export 的 per-owner patch apply / stable export helper 后更新。 +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。 @@ -163,7 +173,7 @@ Statement owner runner 只能写当前 statement pending overlay;callable-entr `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。Stable side table 与 `BlockScope.resetLocalType(...)` 只能在 per-owner patch apply 或 stable export helper 中更新。 +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 中更新。 Diagnostics-only phase、compile gate 与 lowering 只能读取 suite export 后的 stable facts。 @@ -178,6 +188,7 @@ Diagnostics-only phase、compile gate 与 lowering 只能读取 suite export 后 - `FrontendExprTypePatch`。 - `FrontendVarTypePostPatch`。 - `FrontendPatchTransaction`。 +- `FrontendCallableExportBatch`。 - `FrontendLocalSlotTypeUpdate`。 每个 per-owner patch 只能携带该 owner 允许发布的 payload: @@ -190,6 +201,8 @@ Diagnostics-only phase、compile gate 与 lowering 只能读取 suite export 后 `FrontendPatchTransaction` 按固定 owner 顺序 apply:top binding -> local stabilization -> chain binding -> expr typing -> var type post。 +`FrontendCallableExportBatch` 按 suite 收敛顺序保存 root callable 与 nested suite 的 transaction,且仅在 root callable suite 返回后依次 apply。Property initializer 是独立 root,不加入 callable-scoped batch。 + Merge 规则如下: - 新 key 直接写入 stable side table。 diff --git a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md index 782209fb..faa610ad 100644 --- a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md +++ b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md @@ -240,8 +240,11 @@ Interface phase 不得: ```text resolveCallableOwner(context, callableOwner): + exportBatch = new FrontendCallableExportBatch() + context = newSuiteContext(callableOwner, exportBatch) runCallableEntryVarTypePost(context, callableOwner) resolveSuite(context, callableOwner.body) + exportBatch.applyTo(context.analysisData()) runCallableEntryVarTypePost(context, callableOwner): for parameter in callableOwner.parameters(): @@ -251,14 +254,13 @@ runCallableEntryVarTypePost(context, callableOwner): resolveSuite(context, block): for statement in block.statements(): resolveStatement(context, statement) - context.typedEnvironment().flushPendingFacts() - resolvePendingBodiesIfAllowed(context) + context.exportBatch().accumulate(context.typedEnvironment().exportPatchTransaction()) ``` 参数是 callable-entry `VAR_TYPE_POST` facts。它们不是 statement root,因而必须在进入 statement 循环前写入 pending overlay 并 flush 到 current-suite committed overlay。该步骤与 statement-local var type post procedure 互补而非绕过:二者共享 `VAR_TYPE_POST` owner stage、 -冲突检查、compiler-only guard 与 suite-export transaction;此前不得写 stable `slotTypes()`。 +冲突检查、compiler-only guard 与 callable-scoped export batch;此前不得写 stable `slotTypes()`。 `resolveStatement(...)` 不是新的 owner。它只负责按 statement 结构调用 body-aware owner 子过程: @@ -282,7 +284,7 @@ statement-local var type post procedure 互补而非绕过:二者共享 `VAR_T `SuiteResolver` 必须把 interface surface 的 declaration index 交给 visible-value resolver,以检查 scope 选中的 ordinary local 的 declaration identity 已由 baseline inventory 发布;这不是重新扫描普通 `var`,也不改变 resolver 现有 filtered-hit 规则。它还必须把 typed lexical baseline 传给 root 与 child typed environment,使参数和 ordinary local 在没有更新 overlay / stable slot fact 时仍有 immutable source-facing 初始类型。 -Suite 收敛后,不能把 current-suite committed overlay 整体打包为单个多 owner `FrontendAnalysisPatch`。Stable export 必须构造按 owner 有序的 patch transaction,并按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 顺序依次 apply 每个 owner patch。这样既保留 source-order suite 的前缀可见性,又不破坏 `FrontendAnalysisPatch` 现有单 stage / 单 owner 约束。 +Suite 收敛后,不能把 current-suite committed overlay 整体打包为单个多 owner `FrontendAnalysisPatch`。它必须构造按 owner 有序的 patch transaction。嵌套 suite 只把该 transaction 追加到同一 callable-scoped export batch,不得在 child suite 边界直接 apply;根 callable suite 收敛后才由 batch 按追加顺序 apply 各 transaction。每个 transaction 内仍按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 顺序 apply 每个 owner patch。这样既保留 source-order suite 的前缀可见性,又不破坏 `FrontendAnalysisPatch` 现有单 stage / 单 owner 约束。 不得重排 1-5。尤其是 chain binding 读取 receiver local slot 时,必须先看到 local stabilization 对前序 statement 或当前 statement 前序子过程写入的 exact slot fact;否则会重新打开 receiver 被误读成 `Variant` 的历史回归。 @@ -320,6 +322,8 @@ Gate classifier 属于 statement 结构处理的一部分,只能在其 header Pending overlay 只对当前 statement 内后续 owner 子过程可见。`flushPendingFacts()` 后,它才变成 current-suite committed overlay,并对后续 statement / gate classifier 可见。Committed overlay 仍不是 stable publication。 +Parent environment 只能沿 parent 链读取上层 overlay,不能读取 child environment 的 pending 或 committed facts。Child suite 保持独立 overlay;其 facts 仅以 per-owner patch transaction 追加到 shared callable export batch,并在根 callable 完成前保持不在 stable side table 中。 + 写入规则: - 只有 `FrontendLocalTypeStabilizationAnalyzer` 可写 source-facing local slot overlay。 @@ -348,10 +352,11 @@ Overlay 的目标是模拟 Godot “当前 statement 已解析出的类型可被 - `flushPendingFacts(...)` 只把 pending overlay 合并到 current-suite committed overlay,并执行 owner、conflict、idempotent、exact-type 与 compiler-only guard。该 guard 必须与 suite export 使用同一 type-bearing field walker,避免 scratch 层接受 stable merge 层会拒绝的 compiler-only payload。 - `flushPendingFacts(...)` 不得通过 stable side table 的临时 whole-table publish 再“读回 overlay”。如果当前实现为了复用旧 analyzer helper 需要 `updateXxx(...)` / stable side table 作为中转,则该 helper 必须先被改写或隔离为 legacy path,不能把中转污染当作阶段性可接受状态。 - Suite 收敛时,current-suite committed overlay 只能导出为按 owner 有序的 patch transaction,不能导出为一个跨 owner `FrontendAnalysisPatch`。 -- Patch transaction 固定按 top binding -> local stabilization -> chain binding -> expr typing -> var type post apply;每个 step 只包含该 owner 的 facts。 -- Stable side table 与 `BlockScope.resetLocalType(...)` 只能在 per-owner patch apply / stable export helper 中更新。 +- 嵌套 suite 的 transaction 必须追加到 root callable 共享的 `FrontendCallableExportBatch`,不得在 child suite 收敛时独立 apply。根 callable suite 返回后,batch 才按追加顺序 apply 所有 transaction。 +- 每个 patch transaction 固定按 top binding -> local stabilization -> chain binding -> expr typing -> var type post apply;每个 step 只包含该 owner 的 facts。 +- Stable side table 与 `BlockScope.resetLocalType(...)` 只能在 callable export batch 的 per-owner patch apply / stable export helper 中更新。 - Diagnostics-only phase、compile gate 与 lowering 只能读取 suite export 后的 stable facts。 -- Nested supported suite 收敛后,可把其 per-owner patch transaction 追加到外层 export transaction;lexical visibility 仍由 scope graph 与 resolver filter 决定,不能因为 transaction 合并放宽 local 可见性。 +- Nested supported suite 收敛后,必须把其 per-owner patch transaction 追加到外层 callable export batch;lexical visibility 仍由 scope graph 与 resolver filter 决定,不能因为 transaction 合并放宽 local 可见性。 ### 4.5 Feature gate 与 body readiness @@ -524,6 +529,7 @@ Feature-specific header state 仍属于 gate header 语义,不得因为 body r - `gd.script.gdcc.frontend.sema.patch` 包。 - `FrontendOwnerPatch` 或等价 sealed interface。 - `FrontendPatchTransaction` 或等价有序 transaction 类型。 +- `FrontendCallableExportBatch`,累积单个 callable root 及其 nested suite 的 transaction,并只在 root suite 收敛后 apply。 - `FrontendTopBindingPatch`、`FrontendLocalTypeStabilizationPatch`、`FrontendChainBindingPatch`、`FrontendExprTypePatch`、`FrontendVarTypePostPatch` 等 per-owner patch carrier。 - patch package 内的 shared merge helper / type-bearing compiler-only guard / owner-field validator。 - `FrontendSuiteResolver` / `FrontendStatementResolver` 的 root-bounded statement dispatch 子框架。 diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java index 4ad78ada..3cbdb915 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java @@ -280,6 +280,9 @@ public void flushPendingFacts() { } /// 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. public @NotNull FrontendPatchTransaction exportPatchTransaction() { committedFacts.checkNoCompilerOnlyLeaks(); return new FrontendPatchTransaction(committedFacts.toOwnerPatches()); 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 index 4fad3795..62db2394 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteContext.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteContext.java @@ -10,6 +10,7 @@ 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; @@ -24,7 +25,9 @@ /// Statement-local context passed through the new body SuiteResolver skeleton. /// /// 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. +/// 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, @@ -38,7 +41,8 @@ public record FrontendSuiteContext( @NotNull FrontendTypedLexicalEnvironment typedEnvironment, @NotNull FrontendAnalysisData analysisData, @NotNull DiagnosticManager diagnosticManager, - @NotNull ClassRegistry classRegistry + @NotNull ClassRegistry classRegistry, + @Nullable FrontendCallableExportBatch exportBatch ) { public FrontendSuiteContext { Objects.requireNonNull(sourcePath, "sourcePath must not be null"); @@ -72,7 +76,8 @@ public record FrontendSuiteContext( childEnvironment, analysisData, diagnosticManager, - classRegistry + classRegistry, + exportBatch ); } 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 index 7c1bac00..27f60ad4 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolver.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolver.java @@ -16,6 +16,7 @@ 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; @@ -65,6 +66,10 @@ public void resolve( } } + /// 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"); @@ -75,7 +80,13 @@ public void resolveSuite(@NotNull FrontendSuiteContext context, @NotNull Block b for (var statement : block.statements()) { statementResolver.resolveStatement(context, statement, this::resolveChildSuite); } - context.typedEnvironment().exportPatchTransaction().applyTo(context.analysisData()); + 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()); } @@ -100,6 +111,7 @@ private void resolveCallableOwner( null, interfaceSurface.typedLexicalBaseline() ); + var exportBatch = new FrontendCallableExportBatch(); var context = new FrontendSuiteContext( sourcePathFor(interfaceSurface, callableOwner, analysisData), callableOwner, @@ -113,17 +125,19 @@ private void resolveCallableOwner( environment, analysisData, diagnosticManager, - classRegistry + classRegistry, + exportBatch ); runCallableEntryVarTypePost(context, callableOwner); resolveSuite(context, body); + 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 suite-export transaction. + /// owner stage, and callable-scoped export batch. private void runCallableEntryVarTypePost( @NotNull FrontendSuiteContext context, @NotNull Node callableOwner @@ -211,13 +225,16 @@ private void resolvePropertyInitializer( environment, analysisData, diagnosticManager, - classRegistry + 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 = blockScopeFor(parentContext, childBlock); if (blockScope == null || !isCallableLocalValueInventoryReady(parentContext, childBlock, blockScope)) { 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..00cdf55c --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendCallableExportBatch.java @@ -0,0 +1,27 @@ +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 complete batch only after every supported child suite has resolved. +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/FrontendPatchTransaction.java b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendPatchTransaction.java index 6d022cbb..5e15701d 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendPatchTransaction.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendPatchTransaction.java @@ -12,7 +12,8 @@ /// Ordered suite-export transaction made of single-owner patches. /// /// The constructor rejects duplicate or out-of-order owners so callers cannot accidentally recreate -/// the legacy multi-owner patch shape at suite export time. +/// 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")); diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java index 8e9ee4e1..59dbf74c 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java @@ -562,6 +562,43 @@ func read_path(point: Point, seed: int): 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", """ @@ -646,7 +683,7 @@ func ping(value: int): private static void resolveWith( @NotNull PhaseInput phaseInput, - @NotNull RecordingOwnerProcedures ownerProcedures + @NotNull FrontendStatementResolver.OwnerProcedures ownerProcedures ) { var surface = new FrontendInterfacePhase().analyze(phaseInput.registry(), phaseInput.analysisData()); new FrontendSuiteResolver(new FrontendStatementResolver(ownerProcedures)).resolve( @@ -748,7 +785,8 @@ private static boolean hasOwnerEvent(@NotNull List events, @NotNull new FrontendTypedLexicalEnvironment(blockScope, phaseInput.analysisData()), phaseInput.analysisData(), phaseInput.diagnostics(), - phaseInput.registry() + phaseInput.registry(), + null ); } @@ -1051,6 +1089,38 @@ private boolean 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; From 5efe5013d01a263bf3c848b1b48458667bed8032 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Fri, 10 Jul 2026 22:50:57 +0800 Subject: [PATCH 18/27] feat(frontend): retire legacy owner constructor injection in semantic analyzer - Drop all public constructor overloads that accepted discarded legacy owner analyzers and clean up the dead constructor chaining in the remaining active-phase entry points. - Reorient framework tests from "assert legacy analyzer not run" probes to verifying the default suite resolver publishes body facts while preserving phase-boundary diagnostics. - Refresh the segmented pipeline plan with the completed legacy-owner constructor removal and outline the follow-up phase for retiring the whole-phase comparison path and migrating test baselines. --- ...segmented_type_resolution_pipeline_plan.md | 67 +++ .../analyzer/FrontendSemanticAnalyzer.java | 318 +------------- ...FrontendSemanticAnalyzerFrameworkTest.java | 401 ++---------------- 3 files changed, 108 insertions(+), 678 deletions(-) diff --git a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md index faa610ad..53e45b1c 100644 --- a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md +++ b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md @@ -856,6 +856,73 @@ Compiler-only guard payload matrix: - `./gradlew classes --no-daemon --info --console=plain` 通过。 - 相关 targeted tests 使用 `script/run-gradle-targeted-tests.sh --tests ...` 通过。 +### 阶段 J:收口 `FrontendSemanticAnalyzer` 的 legacy-owner constructor 注入 + +阶段 I 已删除 shared pipeline 的 legacy whole-phase bypass,但不包含无效 constructor +注入的物理删除。本阶段负责删除 `FrontendSemanticAnalyzer` 中所有 legacy-owner +constructor overload,清理 constructor chaining 中被丢弃的 analyzer 实例,并将 +framework tests 从“断言 legacy analyzer 未运行”改为验证默认 +`FrontendSuiteResolver` 的 body fact publication。 + +- [x] J1 删除所有接收 `FrontendTopBindingAnalyzer`、 + `FrontendLocalTypeStabilizationAnalyzer`、`FrontendChainBindingAnalyzer`、 + `FrontendExprTypeAnalyzer` 或 `FrontendVarTypePostAnalyzer` 的 public constructor + overload。这五类参数此前只作 null-check,既不保存为 field,也不进入 production + pipeline。现有 public API 只保留 default、active phase、interface/body resolver 与 + skeleton/scope/variable 的注入边界。 +- [x] J2 清理其余 constructor chaining 中无意义的上述 analyzer `new` 操作;默认与 + active-dependency 注入入口不再构造被丢弃的 legacy owner analyzer。 +- [x] J3 将 framework tests 从“注入 probe 后断言 legacy analyzer 未运行”改为验证默认 + `FrontendSuiteResolver` 的 body fact publication;继续保留对 skeleton、scope、variable + inventory、diagnostics-only phase 与 compile-only gate 的 active dependency probes。 + `activeDependencyConstructorExcludesLegacyBodyOwnerParameters` 正向锚定 active-only + constructor,并反向检查全部 public constructor 均不含 legacy owner 参数; + `activeDependencyConstructorUsesDefaultSuiteResolverAndPreservesPhaseBoundaries` 保留 + diagnostics boundary probes 与 callable-entry slot publication,既有 default + SuiteResolver body-fact / unsupported-subtree tests 继续锚定 export 与 fail-closed 合同。 + +验收细则: + +- `FrontendSemanticAnalyzer` 不再暴露或构造被丢弃的 legacy owner analyzer;其 production + pipeline 仍只通过 interface/body + `FrontendSuiteResolver` 发布 body facts。 +- `./gradlew classes --no-daemon --info --console=plain` 通过。 +- 相关 targeted tests 使用 `script/run-gradle-targeted-tests.sh --tests ...` 通过。 + +### 阶段 K:删除 legacy whole-phase 比较路径并迁移测试基线 + +本阶段在前一阶段删除 legacy-owner constructor 注入的基础上,继续删除 legacy +whole-phase analyzer comparison path 及 window/patch compatibility shim。必须先完成测试 +基线迁移与常量收口再执行 K3-K4 的物理删除,不能因为 production 路径已切换就跳过这些 +前置条件。 + +- [ ] K1 迁移或删除五个 legacy owner analyzer 的 standalone/cross-analyzer tests,使新 + `FrontendBodyOwnerProcedures`、overlay 与 per-owner patch transaction 覆盖仍有效的 + semantic contracts;不得仅因 production 无调用者就删除这些测试。 +- [ ] K2 将 `FrontendVarTypePostAnalyzer.VARIABLE_SLOT_PUBLICATION_CATEGORY` 迁移到 + 非 legacy analyzer 的语义常量位置,并更新 `FrontendBodyOwnerProcedures` 与 + `FrontendCompileCheckAnalyzer` 的消费者。 +- [ ] K3 删除 `FrontendTopBindingAnalyzer`、`FrontendLocalTypeStabilizationAnalyzer`、 + `FrontendChainBindingAnalyzer`、`FrontendExprTypeAnalyzer` 与 + `FrontendVarTypePostAnalyzer` 的 whole-module `analyze(...)` / + `analyzeInWindow(...)` entrypoints、内部 `AstWalker...walk(sourceFile)` traversal + 与 legacy stable-table publication path。仍有效的 owner 语义边界必须由 + `FrontendBodyOwnerProcedures` 保持。 +- [ ] K4 在 K3 后删除 `FrontendWindowAnalysisContext`、 + `FrontendWindowPublicationSurface`、legacy `FrontendAnalysisPatch`、 + `FrontendAnalysisData.applyPatch(FrontendAnalysisPatch)` 与对应的 + `FrontendPublishedFactTypeGuard.checkAnalysisPatch(...)` compatibility path; + `FrontendLocalSlotTypeUpdate`、`FrontendOwnerPatch`、`FrontendPatchTransaction` 与 + `FrontendAnalysisPatchException` 仍是 active patch infrastructure,不得删除。 + +验收细则: + +- 不存在任何 production 或 focused test 对 legacy whole-module `analyze(...)` / + `analyzeInWindow(...)`、window shim 或 `FrontendAnalysisPatch` 的引用。 +- per-owner patches 与 `FrontendPatchTransaction` 是唯一的 body-fact export path,且 + `FrontendVarTypePostAnalyzer` 删除前完成 category 常量迁移。 +- `./gradlew classes --no-daemon --info --console=plain` 通过。 +- 相关 targeted tests 使用 `script/run-gradle-targeted-tests.sh --tests ...` 通过。 + ## 6. 必须新增或调整的测试 基础设施测试: 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 1aa89beb..686c96f7 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 @@ -41,11 +41,6 @@ public FrontendSemanticAnalyzer() { new FrontendClassSkeletonBuilder(), new FrontendScopeAnalyzer(), new FrontendVariableAnalyzer(), - new FrontendTopBindingAnalyzer(), - new FrontendLocalTypeStabilizationAnalyzer(), - new FrontendChainBindingAnalyzer(), - new FrontendExprTypeAnalyzer(), - new FrontendVarTypePostAnalyzer(), new FrontendAnnotationUsageAnalyzer(), new FrontendVirtualOverrideAnalyzer(), new FrontendTypeCheckAnalyzer(), @@ -62,11 +57,6 @@ public FrontendSemanticAnalyzer( new FrontendClassSkeletonBuilder(), new FrontendScopeAnalyzer(), new FrontendVariableAnalyzer(), - new FrontendTopBindingAnalyzer(), - new FrontendLocalTypeStabilizationAnalyzer(), - new FrontendChainBindingAnalyzer(), - new FrontendExprTypeAnalyzer(), - new FrontendVarTypePostAnalyzer(), new FrontendAnnotationUsageAnalyzer(), new FrontendVirtualOverrideAnalyzer(), new FrontendTypeCheckAnalyzer(), @@ -82,11 +72,6 @@ public FrontendSemanticAnalyzer(@NotNull FrontendClassSkeletonBuilder classSkele classSkeletonBuilder, new FrontendScopeAnalyzer(), new FrontendVariableAnalyzer(), - new FrontendTopBindingAnalyzer(), - new FrontendLocalTypeStabilizationAnalyzer(), - new FrontendChainBindingAnalyzer(), - new FrontendExprTypeAnalyzer(), - new FrontendVarTypePostAnalyzer(), new FrontendAnnotationUsageAnalyzer(), new FrontendVirtualOverrideAnalyzer(), new FrontendTypeCheckAnalyzer(), @@ -103,11 +88,6 @@ public FrontendSemanticAnalyzer( classSkeletonBuilder, scopeAnalyzer, new FrontendVariableAnalyzer(), - new FrontendTopBindingAnalyzer(), - new FrontendLocalTypeStabilizationAnalyzer(), - new FrontendChainBindingAnalyzer(), - new FrontendExprTypeAnalyzer(), - new FrontendVarTypePostAnalyzer(), new FrontendAnnotationUsageAnalyzer(), new FrontendVirtualOverrideAnalyzer(), new FrontendTypeCheckAnalyzer(), @@ -125,11 +105,6 @@ public FrontendSemanticAnalyzer( classSkeletonBuilder, scopeAnalyzer, variableAnalyzer, - new FrontendTopBindingAnalyzer(), - new FrontendLocalTypeStabilizationAnalyzer(), - new FrontendChainBindingAnalyzer(), - new FrontendExprTypeAnalyzer(), - new FrontendVarTypePostAnalyzer(), new FrontendAnnotationUsageAnalyzer(), new FrontendVirtualOverrideAnalyzer(), new FrontendTypeCheckAnalyzer(), @@ -138,286 +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 - ) { - this( - classSkeletonBuilder, - scopeAnalyzer, - variableAnalyzer, - topBindingAnalyzer, - 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, - @NotNull FrontendChainBindingAnalyzer chainBindingAnalyzer - ) { - this( - classSkeletonBuilder, - scopeAnalyzer, - variableAnalyzer, - topBindingAnalyzer, - new FrontendLocalTypeStabilizationAnalyzer(), - chainBindingAnalyzer, - 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, - @NotNull FrontendChainBindingAnalyzer chainBindingAnalyzer, - @NotNull FrontendExprTypeAnalyzer exprTypeAnalyzer - ) { - this( - classSkeletonBuilder, - scopeAnalyzer, - variableAnalyzer, - topBindingAnalyzer, - new FrontendLocalTypeStabilizationAnalyzer(), - chainBindingAnalyzer, - exprTypeAnalyzer, - 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, - @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, - @NotNull FrontendLoopControlFlowAnalyzer loopControlFlowAnalyzer, - @NotNull FrontendCompileCheckAnalyzer compileCheckAnalyzer - ) { - this( - classSkeletonBuilder, - scopeAnalyzer, - variableAnalyzer, - topBindingAnalyzer, - new FrontendLocalTypeStabilizationAnalyzer(), - chainBindingAnalyzer, - exprTypeAnalyzer, - varTypePostAnalyzer, - annotationUsageAnalyzer, - virtualOverrideAnalyzer, - 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 FrontendVarTypePostAnalyzer varTypePostAnalyzer, @NotNull FrontendAnnotationUsageAnalyzer annotationUsageAnalyzer, @NotNull FrontendVirtualOverrideAnalyzer virtualOverrideAnalyzer, @NotNull FrontendTypeCheckAnalyzer typeCheckAnalyzer, @@ -428,11 +129,6 @@ public FrontendSemanticAnalyzer( classSkeletonBuilder, scopeAnalyzer, variableAnalyzer, - topBindingAnalyzer, - localTypeStabilizationAnalyzer, - chainBindingAnalyzer, - exprTypeAnalyzer, - varTypePostAnalyzer, annotationUsageAnalyzer, virtualOverrideAnalyzer, typeCheckAnalyzer, @@ -447,11 +143,6 @@ 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, @@ -463,11 +154,6 @@ private FrontendSemanticAnalyzer( 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"); - Objects.requireNonNull(topBindingAnalyzer, "topBindingAnalyzer must not be null"); - Objects.requireNonNull(localTypeStabilizationAnalyzer, "localTypeStabilizationAnalyzer must not be null"); - Objects.requireNonNull(chainBindingAnalyzer, "chainBindingAnalyzer must not be null"); - Objects.requireNonNull(exprTypeAnalyzer, "exprTypeAnalyzer must not be null"); - Objects.requireNonNull(varTypePostAnalyzer, "varTypePostAnalyzer must not be null"); this.annotationUsageAnalyzer = Objects.requireNonNull( annotationUsageAnalyzer, "annotationUsageAnalyzer must not be null" 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 1362635e..0102dfba 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSemanticAnalyzerFrameworkTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSemanticAnalyzerFrameworkTest.java @@ -7,21 +7,13 @@ 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.FrontendBodyOwnerProcedures; -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.FrontendStatementResolver; -import gd.script.gdcc.frontend.sema.analyzer.FrontendSuiteContext; -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.scope.BlockScope; import gd.script.gdcc.frontend.scope.CallableScope; @@ -70,8 +62,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; @@ -99,13 +93,13 @@ void analyzeBootstrapsSideTablesAndCollectsSemanticallyRelevantAnnotations() thr @tool class_name AnnotatedPlayer extends Node - + @export var hp: int = 1 - + @rpc("authority") func ping(value): var local := value - + @warning_ignore_start("unused_variable") var tmp := 1 @@ -663,7 +657,7 @@ func _ready( } @Test - void analyzeUsesSuiteResolverBodyPublicationAndDoesNotCallLegacyWholePhaseAnalyzers() 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"), """ @@ -676,11 +670,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(); @@ -689,11 +678,6 @@ func ping(value): new FrontendClassSkeletonBuilder(), probeScopeAnalyzer, probeVariableAnalyzer, - probeTopBindingAnalyzer, - probeLocalTypeStabilizationAnalyzer, - probeChainBindingAnalyzer, - probeExprTypeAnalyzer, - probeVarTypePostAnalyzer, probeAnnotationUsageAnalyzer, probeVirtualOverrideAnalyzer, probeTypeCheckAnalyzer, @@ -715,11 +699,6 @@ func ping(value): assertTrue(probeVariableAnalyzer.invoked); assertTrue(probeVariableAnalyzer.scopeBoundaryPublished); assertTrue(probeVariableAnalyzer.preVariableDiagnosticsMatchedManager); - assertFalse(probeTopBindingAnalyzer.invoked); - assertFalse(probeLocalTypeStabilizationAnalyzer.invoked); - assertFalse(probeChainBindingAnalyzer.invoked); - assertFalse(probeExprTypeAnalyzer.invoked); - assertFalse(probeVarTypePostAnalyzer.invoked); assertTrue(probeAnnotationUsageAnalyzer.invoked); assertTrue(probeAnnotationUsageAnalyzer.varTypeBoundaryPublished); assertTrue(probeAnnotationUsageAnalyzer.preAnnotationUsageDiagnosticsMatchedManager); @@ -777,11 +756,6 @@ func ping(value): assertTrue(result.diagnostics().asList().stream().anyMatch(diagnostic -> diagnostic.category().equals("sema.annotation_usage_phase_probe") )); - assertTrue(diagnosticsByCategory(result.diagnostics(), "sema.top_binding_phase_probe").isEmpty()); - assertTrue(diagnosticsByCategory(result.diagnostics(), "sema.local_type_stabilization_phase_probe").isEmpty()); - assertTrue(diagnosticsByCategory(result.diagnostics(), "sema.chain_binding_phase_probe").isEmpty()); - assertTrue(diagnosticsByCategory(result.diagnostics(), "sema.expr_type_phase_probe").isEmpty()); - assertTrue(diagnosticsByCategory(result.diagnostics(), "sema.var_type_post_phase_probe").isEmpty()); assertTrue(result.diagnostics().asList().stream().anyMatch(diagnostic -> diagnostic.category().equals("sema.virtual_override_phase_probe") )); @@ -795,9 +769,40 @@ 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 Stage J API boundary: test doubles remain available for active phases, while + /// statement-local body ownership is exclusively selected by `FrontendSuiteResolver`. + @Test + void activeDependencyConstructorExcludesLegacyBodyOwnerParameters() 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()))); + } + @Test void analyzePublishesStableLocalTypeFactsAcrossBodyPhases() throws Exception { var parserService = new GdScriptParserService(); @@ -1135,10 +1140,8 @@ func ping(): new FrontendClassSkeletonBuilder(), new FrontendScopeAnalyzer(), new FrontendVariableAnalyzer(), - new FrontendTopBindingAnalyzer(), - new FrontendChainBindingAnalyzer(), - new FrontendExprTypeAnalyzer(), new FrontendAnnotationUsageAnalyzer(), + new FrontendVirtualOverrideAnalyzer(), new FrontendTypeCheckAnalyzer(), sharedLoopControlProbe, sharedProbe @@ -1159,10 +1162,8 @@ func ping(): new FrontendClassSkeletonBuilder(), new FrontendScopeAnalyzer(), new FrontendVariableAnalyzer(), - new FrontendTopBindingAnalyzer(), - new FrontendChainBindingAnalyzer(), - new FrontendExprTypeAnalyzer(), new FrontendAnnotationUsageAnalyzer(), + new FrontendVirtualOverrideAnalyzer(), new FrontendTypeCheckAnalyzer(), compileLoopControlProbe, compileProbe @@ -1667,106 +1668,6 @@ private ClassRegistry createRegistryWithSingleton(String singletonName) { )); } - private static boolean hasTypeNameSuffix(Object type, String suffix) { - return type instanceof GdType gdType && gdType.getTypeName().endsWith(suffix); - } - - private static final class FrameworkBodyOwnerProcedures implements FrontendStatementResolver.OwnerProcedures { - private final @NotNull FrontendBodyOwnerProcedures delegate = new FrontendBodyOwnerProcedures(); - private final @NotNull VariableDeclaration aliasDeclaration; - private final @NotNull VariableDeclaration numberDeclaration; - private final @NotNull AttributePropertyStep markerStep; - private final @NotNull Node numberInitializer; - private boolean aliasSlotWasPublishedBeforeLegacy; - private boolean memberFactWasPublishedBeforeLegacy; - private boolean expressionFactWasPublishedBeforeLegacy; - private boolean varPostFactWasPublishedBeforeLegacy; - - private FrameworkBodyOwnerProcedures( - @NotNull VariableDeclaration aliasDeclaration, - @NotNull VariableDeclaration numberDeclaration, - @NotNull AttributePropertyStep markerStep, - @NotNull Node numberInitializer - ) { - this.aliasDeclaration = aliasDeclaration; - this.numberDeclaration = numberDeclaration; - this.markerStep = markerStep; - this.numberInitializer = numberInitializer; - } - - @Override - public void runTopBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { - delegate.runTopBinding(context, root); - } - - @Override - public void runLocalTypeStabilization(@NotNull FrontendSuiteContext context, @NotNull Node root) { - delegate.runLocalTypeStabilization(context, root); - if (root == aliasDeclaration) { - var currentBlockScope = context.currentBlockScope(); - aliasSlotWasPublishedBeforeLegacy = context.analysisData().slotTypes().get(aliasDeclaration) == null - && currentBlockScope != null - && hasTypeNameSuffix(context.typedEnvironment().localSlotType( - currentBlockScope, - aliasDeclaration.name(), - aliasDeclaration - ), "Point"); - } - } - - @Override - public void runChainBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { - delegate.runChainBinding(context, root); - if (root == numberDeclaration) { - var markerMember = context.typedEnvironment().resolvedMember(markerStep); - memberFactWasPublishedBeforeLegacy = context.analysisData().resolvedMembers().get(markerStep) == null - && markerMember != null - && markerMember.status() == FrontendMemberResolutionStatus.RESOLVED - && hasTypeNameSuffix(markerMember.receiverType(), "Point") - && "int".equals(markerMember.resultType().getTypeName()); - } - } - - @Override - public void runExprType(@NotNull FrontendSuiteContext context, @NotNull Node root) { - delegate.runExprType(context, root); - if (root == numberDeclaration) { - var initializerType = context.typedEnvironment().expressionType(numberInitializer); - expressionFactWasPublishedBeforeLegacy = context.analysisData().expressionTypes().get(numberInitializer) == null - && initializerType != null - && initializerType.status() == FrontendExpressionTypeStatus.RESOLVED - && "int".equals(initializerType.publishedType().getTypeName()); - } - } - - @Override - public void runVarTypePost(@NotNull FrontendSuiteContext context, @NotNull Node root) { - delegate.runVarTypePost(context, root); - if (root == numberDeclaration) { - var slotType = context.typedEnvironment().slotType(numberDeclaration); - varPostFactWasPublishedBeforeLegacy = context.analysisData().slotTypes().get(numberDeclaration) == null - && slotType != null - && "int".equals(slotType.getTypeName()); - } - } - - private boolean aliasSlotWasPublishedBeforeLegacy() { - return aliasSlotWasPublishedBeforeLegacy; - } - - private boolean memberFactWasPublishedBeforeLegacy() { - return memberFactWasPublishedBeforeLegacy; - } - - private boolean expressionFactWasPublishedBeforeLegacy() { - return expressionFactWasPublishedBeforeLegacy; - } - - private boolean varPostFactWasPublishedBeforeLegacy() { - return varPostFactWasPublishedBeforeLegacy; - } - } - /// Test double that records the diagnostics snapshot and published skeleton boundary visible /// to scope analysis. private static final class RecordingScopeAnalyzer extends FrontendScopeAnalyzer { @@ -1823,230 +1724,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; From 6a18d860a461bff36b5484603d6eb6ca33922789 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Sat, 11 Jul 2026 11:37:20 +0800 Subject: [PATCH 19/27] feat(frontend): retire legacy analyzer chain and window publication surface - Drop the legacy window-scoped publication scaffolding and dedicated analyzers in favor of the body owner pipeline. - Relocate remaining patch carriers and test seams onto the new body owner lifecycle entry points. - Refresh the pipeline plan and execution summary to mark the legacy comparison path retired. - Add shared body owner test support and broaden regressions to cover the consolidated surface. --- ...e_resolution_pipeline_execution_summary.md | 8 +- ..._chain_binding_expr_type_implementation.md | 11 +- ...local_type_stabilization_implementation.md | 11 +- ...segmented_type_resolution_pipeline_plan.md | 28 +- ...end_top_binding_analyzer_implementation.md | 6 +- .../body/FrontendBodyLoweringSession.java | 5 +- ...ntendOpaqueExprInsnLoweringProcessors.java | 4 +- .../frontend/sema/FrontendAnalysisData.java | 22 +- .../sema/FrontendWindowAnalysisContext.java | 33 - .../FrontendWindowPublicationSurface.java | 240 -- .../analyzer/FrontendBodyOwnerProcedures.java | 80 +- .../FrontendChainBindingAnalyzer.java | 786 ------ .../FrontendCompileCheckAnalyzer.java | 20 +- .../analyzer/FrontendExprTypeAnalyzer.java | 988 ------- ...rontendLocalTypeStabilizationAnalyzer.java | 700 ----- .../analyzer/FrontendTopBindingAnalyzer.java | 1303 --------- .../analyzer/FrontendVarTypePostAnalyzer.java | 505 ---- .../FrontendChainHeadReceiverSupport.java | 6 +- .../sema/patch/FrontendAnalysisPatch.java | 46 - .../sema/patch/FrontendOwnerPatch.java | 2 +- .../patch/FrontendPublishedFactTypeGuard.java | 9 - .../FrontendLoweringAnalysisPassTest.java | 6 +- .../sema/FrontendAnalysisDataTest.java | 127 +- ...FrontendSemanticAnalyzerFrameworkTest.java | 44 +- .../FrontendTypedLexicalEnvironmentTest.java | 73 +- .../FrontendWindowPublicationSurfaceTest.java | 395 --- ...dBodyOwnerProceduresChainBindingTest.java} | 12 +- ...ntendBodyOwnerProceduresExprTypeTest.java} | 185 +- ...ndBodyOwnerProceduresVarTypePostTest.java} | 175 +- .../FrontendCompileCheckAnalyzerTest.java | 21 +- ...endLocalTypeStabilizationAnalyzerTest.java | 857 ------ .../FrontendSegmentedPipelineTestSupport.java | 95 + .../FrontendTopBindingAnalyzerTest.java | 2394 ----------------- .../FrontendTypeCheckAnalyzerTest.java | 7 +- .../FrontendVirtualOverrideAnalyzerTest.java | 9 +- 35 files changed, 514 insertions(+), 8699 deletions(-) delete mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendWindowAnalysisContext.java delete mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendWindowPublicationSurface.java delete mode 100644 src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendChainBindingAnalyzer.java delete mode 100644 src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzer.java delete mode 100644 src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendLocalTypeStabilizationAnalyzer.java delete mode 100644 src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTopBindingAnalyzer.java delete mode 100644 src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendVarTypePostAnalyzer.java delete mode 100644 src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendAnalysisPatch.java delete mode 100644 src/test/java/gd/script/gdcc/frontend/sema/FrontendWindowPublicationSurfaceTest.java rename src/test/java/gd/script/gdcc/frontend/sema/analyzer/{FrontendChainBindingAnalyzerTest.java => FrontendBodyOwnerProceduresChainBindingTest.java} (99%) rename src/test/java/gd/script/gdcc/frontend/sema/analyzer/{FrontendExprTypeAnalyzerTest.java => FrontendBodyOwnerProceduresExprTypeTest.java} (94%) rename src/test/java/gd/script/gdcc/frontend/sema/analyzer/{FrontendVarTypePostAnalyzerTest.java => FrontendBodyOwnerProceduresVarTypePostTest.java} (63%) delete mode 100644 src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendLocalTypeStabilizationAnalyzerTest.java create mode 100644 src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSegmentedPipelineTestSupport.java delete mode 100644 src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTopBindingAnalyzerTest.java diff --git a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md index 6f33709e..f8589680 100644 --- a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md +++ b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md @@ -2,8 +2,6 @@ 本文总结 `doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md` 执行完成后的前端分析流水线形态。内容只描述目标架构、执行顺序与不变量,不展开旧 whole-module 流水线或过渡实现资产。 -最近同步:2026-07-09(Phase I:shared analyzer legacy whole-phase bypass、test bridge 与 `FrontendSegmentedSemanticScheduler` 已删除)。 - ## 1. 总体形态 计划完成后,frontend shared semantic pipeline 分为四个层次: @@ -332,6 +330,12 @@ Top binding 先绑定 `receiver` use-site。Local stabilization 随后把 `recei - 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 可见。 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 f6f3e33d..0ba6015a 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()`、SuiteResolver statement-local owner procedures、typed overlay-aware 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-07-09(Phase I:production shared analyzer 不再提供 legacy whole-phase chain/expr bypass) +- 更新时间:2026-07-10 - 适用范围: - `src/main/java/gd/script/gdcc/frontend/sema/**` - `src/main/java/gd/script/gdcc/frontend/sema/analyzer/**` @@ -58,7 +61,9 @@ - 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 -- standalone `FrontendChainBindingAnalyzer.analyze(...)` / `FrontendExprTypeAnalyzer.analyze(...)` 保留为 focused analyzer / legacy shim 测试参考,不再由 shared analyzer 的 legacy whole-phase bypass 调用 +- 阶段 K 已删除 standalone chain/expr whole-module analyzer 与 window shim;focused tests + 直接覆盖 `FrontendBodyOwnerProcedures`、typed overlay 与 per-owner patch export,不再维护 + comparison publication path ### 1.2 当前 owner 边界 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 ab3d25ba..a2333a5a 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 边界合同、SuiteResolver body-owner overlay/export 路径已落地) -- 更新时间:2026-07-09(Phase I:production shared analyzer 不再调用 legacy whole-phase stabilization bypass) +- 更新时间:2026-07-10 - 适用范围: - `src/main/java/gd/script/gdcc/frontend/sema/**` - `src/main/java/gd/script/gdcc/frontend/sema/analyzer/**` @@ -52,7 +54,10 @@ 每个 shared phase 结束后,`FrontendSemanticAnalyzer` 都会调用 `analysisData.updateDiagnostics(...)` 刷新共享诊断快照。SuiteResolver body path 还会在 statement boundary 刷新快照,让后一 statement 读取 current-suite upstream diagnostics。 -生产 body path 中,local stabilization 作为 `FrontendBodyOwnerProcedures` 的 statement-local owner procedure 运行在 top binding 之后、chain binding 之前。Standalone `FrontendLocalTypeStabilizationAnalyzer.analyze(...)` / `analyzeInWindow(...)` 只保留为 focused analyzer / legacy shim 测试参考,不再由 shared analyzer 的 legacy whole-phase bypass 调用。 +生产 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 当前职责 diff --git a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md index 53e45b1c..eed9f712 100644 --- a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md +++ b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md @@ -895,24 +895,36 @@ whole-phase analyzer comparison path 及 window/patch compatibility shim。必 基线迁移与常量收口再执行 K3-K4 的物理删除,不能因为 production 路径已切换就跳过这些 前置条件。 -- [ ] K1 迁移或删除五个 legacy owner analyzer 的 standalone/cross-analyzer tests,使新 +- [x] K1 迁移或删除五个 legacy owner analyzer 的 standalone/cross-analyzer tests,使新 `FrontendBodyOwnerProcedures`、overlay 与 per-owner patch transaction 覆盖仍有效的 - semantic contracts;不得仅因 production 无调用者就删除这些测试。 -- [ ] K2 将 `FrontendVarTypePostAnalyzer.VARIABLE_SLOT_PUBLICATION_CATEGORY` 迁移到 + semantic contracts;不得仅因 production 无调用者就删除这些测试。chain、expression + 与 var-post 基线已迁入 `FrontendBodyOwnerProcedures*Test` 并通过默认 segmented pipeline + 或真实 root-bounded owner procedure 运行;纯 whole-module boundary/probe/整表清空断言已 + 删除。local stabilization 的 source-order/parent-prefix 合同继续由 suite/framework/expr + tests 锚定,overlay 另补错误 owner、stable conflict 与无副作用负向覆盖。 +- [x] K2 将 `FrontendVarTypePostAnalyzer.VARIABLE_SLOT_PUBLICATION_CATEGORY` 迁移到 非 legacy analyzer 的语义常量位置,并更新 `FrontendBodyOwnerProcedures` 与 - `FrontendCompileCheckAnalyzer` 的消费者。 -- [ ] K3 删除 `FrontendTopBindingAnalyzer`、`FrontendLocalTypeStabilizationAnalyzer`、 + `FrontendCompileCheckAnalyzer` 的消费者。常量现由实际发布该诊断的 + `FrontendBodyOwnerProcedures.VARIABLE_SLOT_PUBLICATION_CATEGORY` 持有;compile gate、 + lowering gate 与 focused tests 均不再依赖 legacy analyzer 类型。 +- [x] K3 删除 `FrontendTopBindingAnalyzer`、`FrontendLocalTypeStabilizationAnalyzer`、 `FrontendChainBindingAnalyzer`、`FrontendExprTypeAnalyzer` 与 `FrontendVarTypePostAnalyzer` 的 whole-module `analyze(...)` / `analyzeInWindow(...)` entrypoints、内部 `AstWalker...walk(sourceFile)` traversal 与 legacy stable-table publication path。仍有效的 owner 语义边界必须由 - `FrontendBodyOwnerProcedures` 保持。 -- [ ] K4 在 K3 后删除 `FrontendWindowAnalysisContext`、 + `FrontendBodyOwnerProcedures` 保持。五个 legacy 类已物理删除;active expression owner + 保留 discarded-result diagnostic 与 inferred-local consistency guard,但该 guard 只检查 + local stabilization 结果,不写 slot、不刷新 binding payload。framework negative test 同时 + 锚定这些类不再出现在 constructor API 或 runtime classpath。 +- [x] K4 在 K3 后删除 `FrontendWindowAnalysisContext`、 `FrontendWindowPublicationSurface`、legacy `FrontendAnalysisPatch`、 `FrontendAnalysisData.applyPatch(FrontendAnalysisPatch)` 与对应的 `FrontendPublishedFactTypeGuard.checkAnalysisPatch(...)` compatibility path; `FrontendLocalSlotTypeUpdate`、`FrontendOwnerPatch`、`FrontendPatchTransaction` 与 - `FrontendAnalysisPatchException` 仍是 active patch infrastructure,不得删除。 + `FrontendAnalysisPatchException` 仍是 active patch infrastructure,不得删除。window 与 + multi-owner shim 及其 direct API tests 已删除;`FrontendAnalysisDataTest` 现只构造 + owner-specific patches,并补充 binding/member/call conflict 无副作用测试。framework + negative regression 同时验证三个 shim 类和 legacy `applyPatch` overload 均不存在。 验收细则: 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..b5f20885 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 +- 更新时间:2026-07-10 - 适用范围: - `src/main/java/gd/script/gdcc/frontend/sema/**` - `src/main/java/gd/script/gdcc/frontend/sema/analyzer/**` 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/sema/FrontendAnalysisData.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendAnalysisData.java index a5016dd3..bcd65284 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendAnalysisData.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendAnalysisData.java @@ -2,7 +2,6 @@ import dev.superice.gdparser.frontend.ast.Node; import gd.script.gdcc.exception.FrontendAnalysisPatchException; -import gd.script.gdcc.frontend.sema.patch.FrontendAnalysisPatch; import gd.script.gdcc.frontend.sema.patch.FrontendLocalSlotTypeUpdate; import gd.script.gdcc.frontend.sema.patch.FrontendOwnerPatch; import gd.script.gdcc.frontend.sema.patch.FrontendPublishedFactTypeGuard; @@ -132,26 +131,7 @@ public void updateSlotTypes(@NotNull FrontendAstSideTable slotTypes) { ); } - /// Applies one segmented semantic patch without replacing any stable side-table reference. - /// - /// Whole-table `updateXxx(...)` remains the legacy publication path. This API is reserved for - /// segmented stages, so every merge validates conflicts and compiler-only leaks before mutating - /// the stable publication surface. - public void applyPatch(@NotNull FrontendAnalysisPatch patch) { - var checkedPatch = Objects.requireNonNull(patch, "patch must not be null"); - FrontendPublishedFactTypeGuard.checkAnalysisPatch(checkedPatch); - applyPatchFields( - checkedPatch.stage(), - checkedPatch.symbolBindings(), - checkedPatch.resolvedMembers(), - checkedPatch.resolvedCalls(), - checkedPatch.expressionTypes(), - checkedPatch.slotTypes(), - checkedPatch.localSlotTypeUpdates() - ); - } - - /// Applies one Phase C single-owner patch without replacing any stable side-table reference. + /// Applies one single-owner patch without replacing any stable side-table reference. public void applyPatch(@NotNull FrontendOwnerPatch patch) { var checkedPatch = Objects.requireNonNull(patch, "patch must not be null"); FrontendPublishedFactTypeGuard.checkOwnerPatch(checkedPatch); diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendWindowAnalysisContext.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendWindowAnalysisContext.java deleted file mode 100644 index e7df1b9f..00000000 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendWindowAnalysisContext.java +++ /dev/null @@ -1,33 +0,0 @@ -package gd.script.gdcc.frontend.sema; - -import gd.script.gdcc.frontend.sema.patch.FrontendAnalysisPatch; -import org.jetbrains.annotations.NotNull; - -import java.util.Objects; - -/// Stable analysis data plus one window-local scratch publication surface. -public record FrontendWindowAnalysisContext( - @NotNull FrontendAnalysisData stableData, - @NotNull FrontendWindowPublicationSurface publications -) { - public FrontendWindowAnalysisContext { - Objects.requireNonNull(stableData, "stableData must not be null"); - Objects.requireNonNull(publications, "publications must not be null"); - } - - public FrontendWindowAnalysisContext(@NotNull FrontendAnalysisData stableData) { - this(stableData, new FrontendWindowPublicationSurface(stableData)); - } - - public @NotNull FrontendAnalysisPatch toPatch(@NotNull FrontendSemanticStage stage) { - return publications.toPatch(stage); - } - - public @NotNull FrontendAnalysisPatch drainPatch(@NotNull FrontendSemanticStage stage) { - return publications.drainPatch(stage); - } - - public void discard() { - publications.discard(); - } -} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendWindowPublicationSurface.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendWindowPublicationSurface.java deleted file mode 100644 index 2ad2742f..00000000 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendWindowPublicationSurface.java +++ /dev/null @@ -1,240 +0,0 @@ -package gd.script.gdcc.frontend.sema; - -import dev.superice.gdparser.frontend.ast.Node; -import gd.script.gdcc.frontend.sema.patch.FrontendAnalysisPatch; -import gd.script.gdcc.frontend.sema.patch.FrontendLocalSlotTypeUpdate; -import gd.script.gdcc.frontend.sema.patch.FrontendPublishedFactTypeGuard; -import gd.script.gdcc.type.GdType; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -/// Window-local scratch publication surface layered over stable analysis data. -/// -/// Reads always prefer scratch facts and then fall back to the stable side tables captured from -/// `FrontendAnalysisData`. Writes never mutate the stable tables directly; callers must explicitly -/// convert the scratch state into a `FrontendAnalysisPatch` or discard it. -public final class FrontendWindowPublicationSurface { - private final @NotNull WindowSideTableView symbolBindings; - private final @NotNull WindowSideTableView resolvedMembers; - private final @NotNull WindowSideTableView resolvedCalls; - private final @NotNull WindowSideTableView expressionTypes; - private final @NotNull WindowSideTableView slotTypes; - private final @NotNull List localSlotTypeUpdates = new ArrayList<>(); - private boolean open = true; - - public FrontendWindowPublicationSurface(@NotNull FrontendAnalysisData stableData) { - var checkedStableData = Objects.requireNonNull(stableData, "stableData must not be null"); - symbolBindings = new WindowSideTableView<>( - checkedStableData.symbolBindings(), - FrontendAnalysisData::sameBinding, - "symbolBindings", - FrontendPublishedFactTypeGuard::checkBinding - ); - resolvedMembers = new WindowSideTableView<>( - checkedStableData.resolvedMembers(), - FrontendAnalysisData::sameResolvedMember, - "resolvedMembers", - FrontendPublishedFactTypeGuard::checkResolvedMember - ); - resolvedCalls = new WindowSideTableView<>( - checkedStableData.resolvedCalls(), - FrontendAnalysisData::sameResolvedCall, - "resolvedCalls", - FrontendPublishedFactTypeGuard::checkResolvedCall - ); - expressionTypes = new WindowSideTableView<>( - checkedStableData.expressionTypes(), - FrontendAnalysisData::sameExpressionType, - "expressionTypes", - FrontendPublishedFactTypeGuard::checkExpressionType - ); - slotTypes = new WindowSideTableView<>( - checkedStableData.slotTypes(), - FrontendAnalysisData::sameType, - "slotTypes", - value -> FrontendPublishedFactTypeGuard.checkNoCompilerOnlyLeak(value, "slotTypes() value") - ); - } - - public @NotNull WindowSideTableView symbolBindings() { - return symbolBindings; - } - - public @NotNull WindowSideTableView resolvedMembers() { - return resolvedMembers; - } - - public @NotNull WindowSideTableView resolvedCalls() { - return resolvedCalls; - } - - public @NotNull WindowSideTableView expressionTypes() { - return expressionTypes; - } - - public @NotNull WindowSideTableView slotTypes() { - return slotTypes; - } - - /// Collects one local-slot rewrite without mutating the underlying scope until commit time. - public void addLocalSlotTypeUpdate(@NotNull FrontendLocalSlotTypeUpdate update) { - requireOpen(); - var checkedUpdate = Objects.requireNonNull(update, "update must not be null"); - FrontendAnalysisData.checkNoVoidLocalSlotType(checkedUpdate.type(), checkedUpdate.name()); - FrontendPublishedFactTypeGuard.checkLocalSlotTypeUpdate(checkedUpdate); - localSlotTypeUpdates.add(checkedUpdate); - } - - public @NotNull List localSlotTypeUpdates() { - return List.copyOf(localSlotTypeUpdates); - } - - /// Snapshots only scratch-owned facts into a patch. Stable fallback entries are intentionally - /// excluded so commit remains an explicit delta over the pre-window publication surface. - public @NotNull FrontendAnalysisPatch toPatch(@NotNull FrontendSemanticStage stage) { - requireOpen(); - var checkedStage = Objects.requireNonNull(stage, "stage must not be null"); - checkLocalSlotUpdateStage(checkedStage); - return new FrontendAnalysisPatch( - checkedStage, - symbolBindings.copyScratch(), - resolvedMembers.copyScratch(), - resolvedCalls.copyScratch(), - expressionTypes.copyScratch(), - slotTypes.copyScratch(), - List.copyOf(localSlotTypeUpdates) - ); - } - - /// Snapshots the scratch facts into a patch and then closes the surface. - public @NotNull FrontendAnalysisPatch drainPatch(@NotNull FrontendSemanticStage stage) { - var patch = toPatch(stage); - clearScratch(); - open = false; - return patch; - } - - /// Drops every scratch fact so an unsupported or failed window cannot affect stable data. - public void discard() { - if (!open) { - return; - } - clearScratch(); - open = false; - } - - private void requireOpen() { - if (!open) { - throw new IllegalStateException("window publication surface is closed"); - } - } - - private void checkLocalSlotUpdateStage(@NotNull FrontendSemanticStage stage) { - if (!localSlotTypeUpdates.isEmpty() && stage != FrontendSemanticStage.LOCAL_TYPE_STABILIZATION) { - throw FrontendAnalysisData.patchFailure( - "Only LOCAL_TYPE_STABILIZATION patches may publish local slot type updates, but got " - + stage - ); - } - } - - private void clearScratch() { - symbolBindings.clearScratch(); - resolvedMembers.clearScratch(); - resolvedCalls.clearScratch(); - expressionTypes.clearScratch(); - slotTypes.clearScratch(); - localSlotTypeUpdates.clear(); - } - - /// Per-table effective view used by one semantic window. - public final class WindowSideTableView { - private final @NotNull FrontendAstSideTable stable; - private final @NotNull FrontendAstSideTable scratch = new FrontendAstSideTable<>(); - private final @NotNull SameValueChecker sameValueChecker; - private final @NotNull String fieldName; - private final @Nullable ValueGuard valueGuard; - - private WindowSideTableView( - @NotNull FrontendAstSideTable stable, - @NotNull SameValueChecker sameValueChecker, - @NotNull String fieldName, - @Nullable ValueGuard valueGuard - ) { - this.stable = Objects.requireNonNull(stable, "stable must not be null"); - this.sameValueChecker = Objects.requireNonNull(sameValueChecker, "sameValueChecker must not be null"); - this.fieldName = Objects.requireNonNull(fieldName, "fieldName must not be null"); - this.valueGuard = valueGuard; - } - - public @Nullable V get(@NotNull Node astNode) { - var checkedNode = Objects.requireNonNull(astNode, "astNode must not be null"); - var scratchValue = scratch.get(checkedNode); - return scratchValue != null ? scratchValue : stable.get(checkedNode); - } - - public @Nullable V getScratch(@NotNull Node astNode) { - return scratch.get(Objects.requireNonNull(astNode, "astNode must not be null")); - } - - public @Nullable V getStable(@NotNull Node astNode) { - return stable.get(Objects.requireNonNull(astNode, "astNode must not be null")); - } - - public boolean containsKey(@NotNull Node astNode) { - var checkedNode = Objects.requireNonNull(astNode, "astNode must not be null"); - return scratch.containsKey(checkedNode) || stable.containsKey(checkedNode); - } - - /// Records one scratch fact. Same-key writes are idempotent only when the logical value is - /// unchanged; otherwise the write fails immediately instead of silently shadowing stable data. - public void put(@NotNull Node astNode, @NotNull V value) { - requireOpen(); - var checkedNode = Objects.requireNonNull(astNode, "astNode must not be null"); - var checkedValue = Objects.requireNonNull(value, "value must not be null"); - if (valueGuard != null) { - valueGuard.check(checkedValue); - } - checkConflictingWrite(stable.get(checkedNode), checkedValue, checkedNode); - checkConflictingWrite(scratch.get(checkedNode), checkedValue, checkedNode); - scratch.put(checkedNode, checkedValue); - } - - private void checkConflictingWrite( - @Nullable V existingValue, - @NotNull V newValue, - @NotNull Node astNode - ) { - if (existingValue == null || sameValueChecker.sameValue(existingValue, newValue)) { - return; - } - throw FrontendAnalysisData.patchFailure( - fieldName + " scratch write conflicted on " + FrontendAnalysisData.describeNode(astNode) - ); - } - - private @NotNull FrontendAstSideTable copyScratch() { - var copy = new FrontendAstSideTable(); - copy.putAll(scratch); - return copy; - } - - private void clearScratch() { - scratch.clear(); - } - } - - @FunctionalInterface - private interface SameValueChecker { - boolean sameValue(@NotNull V first, @NotNull V second); - } - - @FunctionalInterface - private interface ValueGuard { - void check(@NotNull V value); - } -} 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 index 7d960c44..7a93ce6c 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProcedures.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProcedures.java @@ -78,10 +78,13 @@ /// 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 = @@ -217,6 +220,81 @@ public void runChainBinding(@NotNull FrontendSuiteContext context, @NotNull Node 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() 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 @@ -269,7 +347,7 @@ private void reportRejectedLocalSlotPublication( message.append("; this usually means earlier variable analysis rejected the declaration as duplicate or shadowing"); } context.diagnosticManager().warning( - FrontendVarTypePostAnalyzer.VARIABLE_SLOT_PUBLICATION_CATEGORY, + VARIABLE_SLOT_PUBLICATION_CATEGORY, message.toString(), context.sourcePath(), FrontendRange.fromAstRange(variableDeclaration.range()) 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 37df1ca7..00000000 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendChainBindingAnalyzer.java +++ /dev/null @@ -1,786 +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.FrontendSemanticStage; -import gd.script.gdcc.frontend.sema.FrontendWindowAnalysisContext; -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 - ) { - analysisData.updateResolvedMembers(new FrontendAstSideTable<>()); - analysisData.updateResolvedCalls(new FrontendAstSideTable<>()); - var window = new FrontendWindowAnalysisContext(analysisData); - analyzeInWindow(classRegistry, window, diagnosticManager); - - // Preserve whole-module replacement semantics for legacy callers; segmented callers commit - // the patch directly and therefore get conflict-checked incremental publication. - var patch = window.drainPatch(FrontendSemanticStage.CHAIN_BINDING); - analysisData.updateResolvedMembers(patch.resolvedMembers()); - analysisData.updateResolvedCalls(patch.resolvedCalls()); - } - - void analyzeInWindow( - @NotNull ClassRegistry classRegistry, - @NotNull FrontendWindowAnalysisContext window, - @NotNull DiagnosticManager diagnosticManager - ) { - Objects.requireNonNull(classRegistry, "classRegistry must not be null"); - Objects.requireNonNull(window, "window must not be null"); - Objects.requireNonNull(diagnosticManager, "diagnosticManager must not be null"); - - var analysisData = window.stableData(); - - 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()); - } - for (var entry : resolvedMembers.entrySet()) { - window.publications().resolvedMembers().put(entry.getKey(), entry.getValue()); - } - for (var entry : resolvedCalls.entrySet()) { - window.publications().resolvedCalls().put(entry.getKey(), entry.getValue()); - } - } - - 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 bd87b96e..6cb1f9f3 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( @@ -443,14 +439,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 { @@ -708,7 +704,7 @@ private boolean isCoveredByStaticSelfBindingDiagnostic(@Nullable String detailRe return publishedDiagnostics.asList().stream().anyMatch(diagnostic -> diagnostic.category().equals("sema.binding") && diagnostic.message().contains("Keyword 'self' is not available in static context") - && diagnostic.sourcePath().equals(FrontendDiagnostic.sourcePathText(sourcePath)) + && (diagnostic.sourcePath() != null && diagnostic.sourcePath().equals(FrontendDiagnostic.sourcePathText(sourcePath))) ); } @@ -837,7 +833,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 297a7a48..00000000 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendExprTypeAnalyzer.java +++ /dev/null @@ -1,988 +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.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()` through a window scratch table so nested chain reduction can -/// immediately consume freshly published inner expression facts without writing stable data early. -/// `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 is now a guard-only protocol check. -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 - ) { - analysisData.updateExpressionTypes(new FrontendAstSideTable<>()); - var window = new FrontendWindowAnalysisContext(analysisData); - analyzeInWindow(classRegistry, window, diagnosticManager); - - // Keep legacy whole-module expression typing as a complete snapshot replacement while bare - // call facts are merged incrementally into the resolvedCalls() table owned by this phase. - var patch = window.drainPatch(FrontendSemanticStage.EXPR_TYPE); - analysisData.updateExpressionTypes(patch.expressionTypes()); - analysisData.applyPatch(patch); - } - - void analyzeInWindow( - @NotNull ClassRegistry classRegistry, - @NotNull FrontendWindowAnalysisContext window, - @NotNull DiagnosticManager diagnosticManager - ) { - Objects.requireNonNull(classRegistry, "classRegistry must not be null"); - Objects.requireNonNull(window, "window must not be null"); - Objects.requireNonNull(diagnosticManager, "diagnosticManager must not be null"); - - var analysisData = window.stableData(); - - 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 = new FrontendAstSideTable(); - var bareResolvedCalls = new FrontendAstSideTable(); - for (var sourceClassRelation : moduleSkeleton.sourceClassRelations()) { - new AstWalkerExprTypePublisher( - sourceClassRelation.unit().path(), - classRegistry, - analysisData, - scopesByAst, - expressionTypes, - bareResolvedCalls, - diagnosticManager - ).walk(sourceClassRelation.unit().ast()); - } - for (var entry : expressionTypes.entrySet()) { - window.publications().expressionTypes().put(entry.getKey(), entry.getValue()); - } - for (var entry : bareResolvedCalls.entrySet()) { - window.publications().resolvedCalls().put(entry.getKey(), entry.getValue()); - } - } - - 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 FrontendAstSideTable bareResolvedCalls; - 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 FrontendAstSideTable bareResolvedCalls, - @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.bareResolvedCalls = Objects.requireNonNull(bareResolvedCalls, "bareResolvedCalls 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 must be stabilized before expression typing. This guard - /// only verifies that an already-stabilized exact slot still agrees with its initializer fact; - /// it never narrows an inventory-seeded `Variant` slot or refreshes binding payloads. - 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 -> publishedInitializerType.publishedType(); - case DYNAMIC -> null; - 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() - ); - } - } - } - - 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; - } - if (analysisData.resolvedCalls().containsKey(callExpression) || bareResolvedCalls.containsKey(callExpression)) { - throw new IllegalStateException( - "resolvedCalls() already contains a published fact for bare CallExpression at " - + callExpression.range() - ); - } - bareResolvedCalls.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/FrontendLocalTypeStabilizationAnalyzer.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendLocalTypeStabilizationAnalyzer.java deleted file mode 100644 index 75b81910..00000000 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendLocalTypeStabilizationAnalyzer.java +++ /dev/null @@ -1,700 +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.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.patch.FrontendLocalSlotTypeUpdate; -import gd.script.gdcc.frontend.sema.FrontendWindowAnalysisContext; -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, null); - } - - void analyzeInWindow( - @NotNull ClassRegistry classRegistry, - @NotNull FrontendWindowAnalysisContext window, - @NotNull DiagnosticManager diagnosticManager - ) { - Objects.requireNonNull(window, "window must not be null"); - run(classRegistry, window.stableData(), diagnosticManager, true, window); - } - - /// 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, null); - } - - 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, - @Nullable FrontendWindowAnalysisContext window - ) { - 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, - window - ).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); - var updatedValue = blockScope.resolveValueHere(localName); - return updatedValue != null && updatedValue.declaration() == variableDeclaration ? updatedValue : null; - } - - 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 FrontendAnalysisData analysisData; - private final @NotNull ASTWalker astWalker; - private final @NotNull SilentExpressionResolver silentExpressionResolver; - private final @NotNull List probes; - private final boolean writeBackStableSlots; - private final @Nullable FrontendWindowAnalysisContext window; - 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, - @Nullable FrontendWindowAnalysisContext window - ) { - var checkedAnalysisData = Objects.requireNonNull(analysisData, "analysisData must not be null"); - this.analysisData = checkedAnalysisData; - this.scopesByAst = Objects.requireNonNull(scopesByAst, "scopesByAst must not be null"); - this.probes = Objects.requireNonNull(probes, "probes must not be null"); - this.writeBackStableSlots = writeBackStableSlots; - this.window = window; - 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) { - if (window != null) { - var stableType = stableLocalTypeOrNull(initializerType); - if (stableType != null) { - // Window execution records the owner-approved rewrite and leaves the actual - // BlockScope mutation to patch commit, so discarded windows stay isolated. - window.publications().addLocalSlotTypeUpdate(new FrontendLocalSlotTypeUpdate( - blockScope, - variableDeclaration.name().trim(), - variableDeclaration, - stableType - )); - } - } else { - var updatedValue = stabilizeLocalSlot(blockScope, variableDeclaration, initializerType); - if (updatedValue == null) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - analysisData.refreshPublishedLocalBindingPayloads( - new FrontendLocalSlotTypeUpdate( - blockScope, - variableDeclaration.name().trim(), - variableDeclaration, - updatedValue.type() - ), - updatedValue - ); - } - } - 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 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 = analysisData.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/FrontendTopBindingAnalyzer.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTopBindingAnalyzer.java deleted file mode 100644 index ff4e19be..00000000 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTopBindingAnalyzer.java +++ /dev/null @@ -1,1303 +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.FrontendSemanticStage; -import gd.script.gdcc.frontend.sema.FrontendWindowAnalysisContext; -import gd.script.gdcc.frontend.sema.analyzer.support.FrontendDualRoleTypeMetaRouteSupport; -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.ExtensionUtilityFunction; -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 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.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.nio.file.Path; -import java.util.IdentityHashMap; -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 - ) { - analysisData.updateSymbolBindings(new FrontendAstSideTable<>()); - var window = new FrontendWindowAnalysisContext(analysisData); - analyzeInWindow(classRegistry, window, diagnosticManager); - - // The legacy whole-module API still means "replace the complete top-binding snapshot". - // Segmented callers commit the drained patch directly instead. - var patch = window.drainPatch(FrontendSemanticStage.TOP_BINDING); - analysisData.updateSymbolBindings(patch.symbolBindings()); - } - - /// Runs top-binding analysis into one window-local scratch publication surface. - void analyzeInWindow( - @NotNull ClassRegistry classRegistry, - @NotNull FrontendWindowAnalysisContext window, - @NotNull DiagnosticManager diagnosticManager - ) { - Objects.requireNonNull(classRegistry, "classRegistry must not be null"); - Objects.requireNonNull(window, "window must not be null"); - Objects.requireNonNull(diagnosticManager, "diagnosticManager must not be null"); - - var analysisData = window.stableData(); - - 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()); - } - for (var entry : symbolBindings.entrySet()) { - window.publications().symbolBindings().put(entry.getKey(), entry.getValue()); - } - } - - 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 FrontendDualRoleTypeMetaRouteSupport.supportsTopLevelTypeMeta(typeMeta); - } - - private boolean shouldPreferGlobalEnumTypeMeta( - @NotNull FrontendVisibleValueResolution valueResolution, - @NotNull ScopeLookupResult typeMetaResult - ) { - return FrontendDualRoleTypeMetaRouteSupport.shouldPreferGlobalEnumTypeMeta(valueResolution, typeMetaResult); - } - - /// 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 biasedTypeMeta = FrontendDualRoleTypeMetaRouteSupport.resolveBiasedTypeMeta( - attributeExpression, - resolveVisibleValue(identifierExpression), - currentScope, - currentRestriction, - moduleSkeleton, - classRegistry - ); - if (biasedTypeMeta == null) { - return false; - } - publishBinding( - identifierExpression, - name, - FrontendBindingKind.TYPE_META, - biasedTypeMeta.declaration() - ); - return true; - } - - 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 2b2da114..00000000 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendVarTypePostAnalyzer.java +++ /dev/null @@ -1,505 +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.frontend.sema.FrontendSemanticStage; -import gd.script.gdcc.frontend.sema.FrontendWindowAnalysisContext; -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 - ) { - analysisData.updateSlotTypes(new FrontendAstSideTable<>()); - var window = new FrontendWindowAnalysisContext(analysisData); - analyzeInWindow(window, diagnosticManager); - - // Slot types are a final whole-module snapshot for existing callers; incremental segmented - // execution should apply the drained patch instead of replacing the table. - var patch = window.drainPatch(FrontendSemanticStage.VAR_TYPE_POST); - analysisData.updateSlotTypes(patch.slotTypes()); - } - - void analyzeInWindow( - @NotNull FrontendWindowAnalysisContext window, - @NotNull DiagnosticManager diagnosticManager - ) { - Objects.requireNonNull(window, "window must not be null"); - Objects.requireNonNull(diagnosticManager, "diagnosticManager must not be null"); - - var analysisData = window.stableData(); - - 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()); - } - for (var entry : slotTypes.entrySet()) { - window.publications().slotTypes().put(entry.getKey(), entry.getValue()); - } - } - - /// 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/support/FrontendChainHeadReceiverSupport.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendChainHeadReceiverSupport.java index 14d6a4ff..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 @@ -32,12 +32,12 @@ /// 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 diff --git a/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendAnalysisPatch.java b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendAnalysisPatch.java deleted file mode 100644 index 6376b5f5..00000000 --- a/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendAnalysisPatch.java +++ /dev/null @@ -1,46 +0,0 @@ -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; -import java.util.Objects; - -/// Legacy incremental semantic facts published by one segmented frontend stage. -/// -/// Phase C production export uses `FrontendOwnerPatch` and `FrontendPatchTransaction`. This record -/// remains as the compatibility carrier for pre-existing window tests and legacy segmented shims. -public record FrontendAnalysisPatch( - @NotNull FrontendSemanticStage stage, - @NotNull FrontendAstSideTable symbolBindings, - @NotNull FrontendAstSideTable resolvedMembers, - @NotNull FrontendAstSideTable resolvedCalls, - @NotNull FrontendAstSideTable expressionTypes, - @NotNull FrontendAstSideTable slotTypes, - @NotNull List localSlotTypeUpdates -) { - public FrontendAnalysisPatch { - Objects.requireNonNull(stage, "stage must not be null"); - symbolBindings = FrontendPatchTables.copySideTable(symbolBindings, "symbolBindings"); - resolvedMembers = FrontendPatchTables.copySideTable(resolvedMembers, "resolvedMembers"); - resolvedCalls = FrontendPatchTables.copySideTable(resolvedCalls, "resolvedCalls"); - expressionTypes = FrontendPatchTables.copySideTable(expressionTypes, "expressionTypes"); - slotTypes = FrontendPatchTables.copySideTable(slotTypes, "slotTypes"); - localSlotTypeUpdates = List.copyOf(Objects.requireNonNull( - localSlotTypeUpdates, - "localSlotTypeUpdates must not be null" - )); - FrontendPublishedFactTypeGuard.checkSymbolBindings(symbolBindings); - FrontendPublishedFactTypeGuard.checkResolvedMembers(resolvedMembers); - FrontendPublishedFactTypeGuard.checkResolvedCalls(resolvedCalls); - FrontendPublishedFactTypeGuard.checkExpressionTypes(expressionTypes); - FrontendPublishedFactTypeGuard.checkSlotTypes(slotTypes); - FrontendPublishedFactTypeGuard.checkLocalSlotTypeUpdates(localSlotTypeUpdates); - } -} 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 index 74fdd449..e0f81c6e 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendOwnerPatch.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendOwnerPatch.java @@ -14,7 +14,7 @@ /// 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 legacy `FrontendAnalysisPatch`. +/// from multiple owners into one multi-owner payload. public sealed interface FrontendOwnerPatch permits FrontendTopBindingPatch, FrontendLocalTypeStabilizationPatch, 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 index b60389ac..42e5609c 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendPublishedFactTypeGuard.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendPublishedFactTypeGuard.java @@ -20,15 +20,6 @@ public final class FrontendPublishedFactTypeGuard { private FrontendPublishedFactTypeGuard() { } - public static void checkAnalysisPatch(@NotNull FrontendAnalysisPatch patch) { - checkSymbolBindings(patch.symbolBindings()); - checkResolvedMembers(patch.resolvedMembers()); - checkResolvedCalls(patch.resolvedCalls()); - checkExpressionTypes(patch.expressionTypes()); - checkSlotTypes(patch.slotTypes()); - checkLocalSlotTypeUpdates(patch.localSlotTypeUpdates()); - } - public static void checkOwnerPatch(@NotNull FrontendOwnerPatch patch) { checkSymbolBindings(patch.symbolBindings()); checkResolvedMembers(patch.resolvedMembers()); 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 b983d617..8e400c62 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendAnalysisDataTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendAnalysisDataTest.java @@ -5,11 +5,14 @@ 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.FrontendAnalysisPatch; +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; @@ -250,21 +253,13 @@ void updateSlotTypesClearsStaleEntriesWithoutReplacingStableSideTableReference() } @Test - void analysisPatchCopiesSourceTablesAtConstructionTime() { + 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 FrontendAnalysisPatch( - FrontendSemanticStage.TOP_BINDING, - symbolBindings, - new FrontendAstSideTable<>(), - new FrontendAstSideTable<>(), - new FrontendAstSideTable<>(), - new FrontendAstSideTable<>(), - List.of() - ); + var patch = new FrontendTopBindingPatch(symbolBindings); symbolBindings.clear(); assertSame(binding, patch.symbolBindings().get(bindingNode)); @@ -455,6 +450,84 @@ void applyPatchRejectsConflictingExpressionAndSlotFacts() { ); } + @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(); @@ -565,18 +638,6 @@ void applyPatchRejectsWrongStageLocalSlotUpdatesAndSourceFacingCompilerOnlyLeaks var localScope = newBodyScope(); var declaration = variable("local"); localScope.defineLocal("local", GdVariantType.VARIANT, declaration); - assertThrows( - FrontendAnalysisPatchException.class, - () -> analysisData.applyPatch(patch( - FrontendSemanticStage.EXPR_TYPE, - new FrontendAstSideTable<>(), - new FrontendAstSideTable<>(), - new FrontendAstSideTable<>(), - new FrontendAstSideTable<>(), - new FrontendAstSideTable<>(), - List.of(new FrontendLocalSlotTypeUpdate(localScope, "local", declaration, GdIntType.INT)) - )) - ); assertThrows( FrontendAnalysisPatchException.class, () -> analysisData.applyPatch(patch( @@ -832,7 +893,7 @@ private static PassStatement passNode() { return new BlockScope(callableScope, BlockScopeKind.FUNCTION_BODY); } - private static @NotNull FrontendAnalysisPatch patch( + private static @NotNull FrontendOwnerPatch patch( @NotNull FrontendSemanticStage stage, @NotNull FrontendAstSideTable symbolBindings, @NotNull FrontendAstSideTable resolvedMembers, @@ -841,18 +902,16 @@ private static PassStatement passNode() { @NotNull FrontendAstSideTable slotTypes, @NotNull List localSlotTypeUpdates ) { - return new FrontendAnalysisPatch( - stage, - symbolBindings, - resolvedMembers, - resolvedCalls, - expressionTypes, - slotTypes, - 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); + }; } - private static @NotNull FrontendAnalysisPatch patch( + private static @NotNull FrontendOwnerPatch patch( @NotNull FrontendSemanticStage stage, @NotNull FrontendAstSideTable symbolBindings ) { 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 0102dfba..9ad4cebe 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSemanticAnalyzerFrameworkTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSemanticAnalyzerFrameworkTest.java @@ -15,6 +15,7 @@ import gd.script.gdcc.frontend.sema.analyzer.FrontendTypeCheckAnalyzer; import gd.script.gdcc.frontend.sema.analyzer.FrontendVirtualOverrideAnalyzer; 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; @@ -75,6 +76,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 { @@ -93,13 +95,13 @@ void analyzeBootstrapsSideTablesAndCollectsSemanticallyRelevantAnnotations() thr @tool class_name AnnotatedPlayer extends Node - + @export var hp: int = 1 - + @rpc("authority") func ping(value): var local := value - + @warning_ignore_start("unused_variable") var tmp := 1 @@ -775,10 +777,10 @@ func ping(value): assertFalse(result.slotTypes().isEmpty()); } - /// Locks the Stage J API boundary: test doubles remain available for active phases, while - /// statement-local body ownership is exclusively selected by `FrontendSuiteResolver`. + /// Locks the Stage K boundary: active phase injection remains available while legacy body-owner + /// classes are absent from both constructor signatures and the runtime classpath. @Test - void activeDependencyConstructorExcludesLegacyBodyOwnerParameters() throws Exception { + void activeDependencyConstructorAndClasspathExcludeLegacyBodyOwners() throws Exception { var activeConstructor = FrontendSemanticAnalyzer.class.getConstructor( FrontendClassSkeletonBuilder.class, FrontendScopeAnalyzer.class, @@ -801,6 +803,30 @@ void activeDependencyConstructorExcludesLegacyBodyOwnerParameters() throws Excep 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 @@ -933,7 +959,7 @@ void defaultInterfaceBodyPipelinePublishesBodyFactsThroughSuiteExport() throws E extends RefCounted class Point: var marker: int = 1 - + func ping(value: Point) -> int: var alias := value var number := alias.marker @@ -1075,9 +1101,9 @@ void defaultInterfaceBodyPipelinePublishesAssignmentTargetLoweringFacts() throws 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 diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironmentTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironmentTest.java index 66df7251..7813a78a 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironmentTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironmentTest.java @@ -13,6 +13,7 @@ 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; @@ -71,7 +72,7 @@ void pendingFlushExportAndApplyPreserveStableDataUntilTransactionApply() throws var transaction = environment.exportPatchTransaction(); assertEquals(List.of(FrontendSemanticStage.LOCAL_TYPE_STABILIZATION), transaction.patches().stream() - .map(patch -> patch.stage()) + .map(FrontendOwnerPatch::stage) .toList()); transaction.applyTo(analysisData); @@ -216,6 +217,76 @@ void overlayRejectsExactLocalSlotRewriteAndExpressionFactNarrowing() throws Exce )); } + @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); } diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendWindowPublicationSurfaceTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendWindowPublicationSurfaceTest.java deleted file mode 100644 index f7ddadf2..00000000 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendWindowPublicationSurfaceTest.java +++ /dev/null @@ -1,395 +0,0 @@ -package gd.script.gdcc.frontend.sema; - -import dev.superice.gdparser.frontend.ast.AttributeCallStep; -import dev.superice.gdparser.frontend.ast.AttributePropertyStep; -import dev.superice.gdparser.frontend.ast.AttributeSubscriptStep; -import dev.superice.gdparser.frontend.ast.DeclarationKind; -import dev.superice.gdparser.frontend.ast.Expression; -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.diagnostic.DiagnosticSnapshot; -import gd.script.gdcc.frontend.diagnostic.FrontendDiagnostic; -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.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.ScopeLookupStatus; -import gd.script.gdcc.scope.ScopeOwnerKind; -import gd.script.gdcc.scope.ScopeValue; -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.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; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/// API-level coverage for the legacy window publication shim. -/// -/// These tests prove direct `FrontendWindowPublicationSurface` scratch writes are isolated and -/// guarded. They must not be read as proof that every legacy `analyzeInWindow(...)` implementation -/// is scratch-safe; `FrontendVarTypePostAnalyzer.analyzeInWindow(...)` is documented separately as a -/// stable `slotTypes()` contamination path until the SuiteResolver rewrite replaces it. -class FrontendWindowPublicationSurfaceTest { - private static final Range RANGE = new Range(0, 1, new Point(0, 0), new Point(0, 1)); - - @Test - void windowContextKeepsStableDataAndEffectiveReadsPreferScratch() { - var analysisData = FrontendAnalysisData.bootstrap(); - var stableNode = identifier("stable_value"); - var fallbackNode = identifier("fallback_value"); - var stableType = FrontendExpressionType.resolved(GdIntType.INT); - var fallbackType = FrontendExpressionType.failed("stable only"); - analysisData.expressionTypes().put(stableNode, stableType); - analysisData.expressionTypes().put(fallbackNode, fallbackType); - - var window = new FrontendWindowAnalysisContext(analysisData); - var scratchType = FrontendExpressionType.resolved(new GdIntType()); - window.publications().expressionTypes().put(stableNode, scratchType); - - assertSame(analysisData, window.stableData()); - assertSame(scratchType, window.publications().expressionTypes().get(stableNode)); - assertSame(scratchType, window.publications().expressionTypes().getScratch(stableNode)); - assertSame(stableType, window.publications().expressionTypes().getStable(stableNode)); - assertSame(fallbackType, window.publications().expressionTypes().get(fallbackNode)); - } - - @Test - void scratchWritesDoNotMutateStableSideTablesBeforeCommit() { - var analysisData = FrontendAnalysisData.bootstrap(); - var window = new FrontendWindowAnalysisContext(analysisData); - var bindingNode = identifier("local_use"); - var callNode = call("move", identifier("distance")); - var expressionNode = identifier("value"); - - window.publications().symbolBindings().put( - bindingNode, - new FrontendBinding("self", FrontendBindingKind.SELF, null) - ); - window.publications().resolvedCalls().put( - callNode, - FrontendResolvedCall.resolved( - "move", - FrontendCallResolutionKind.INSTANCE_METHOD, - FrontendReceiverKind.INSTANCE, - ScopeOwnerKind.GDCC, - new GdObjectType("Player"), - GdIntType.INT, - List.of(GdIntType.INT), - "Player.move" - ) - ); - window.publications().expressionTypes().put(expressionNode, FrontendExpressionType.resolved(GdIntType.INT)); - - assertNull(analysisData.symbolBindings().get(bindingNode)); - assertNull(analysisData.resolvedCalls().get(callNode)); - assertNull(analysisData.expressionTypes().get(expressionNode)); - assertTrue(window.publications().symbolBindings().containsKey(bindingNode)); - assertTrue(window.publications().resolvedCalls().containsKey(callNode)); - assertTrue(window.publications().expressionTypes().containsKey(expressionNode)); - } - - @Test - void toPatchCopiesOnlyScratchFactsWithoutStableFallbackEntries() { - var analysisData = FrontendAnalysisData.bootstrap(); - var stableNode = identifier("published"); - var scratchNode = identifier("pending"); - analysisData.expressionTypes().put(stableNode, FrontendExpressionType.resolved(GdIntType.INT)); - - var window = new FrontendWindowAnalysisContext(analysisData); - var scratchType = FrontendExpressionType.dynamic("retry result"); - window.publications().expressionTypes().put(scratchNode, scratchType); - - var patch = window.toPatch(FrontendSemanticStage.EXPR_TYPE); - - assertNull(patch.expressionTypes().get(stableNode)); - assertSame(scratchType, patch.expressionTypes().get(scratchNode)); - assertSame(analysisData.expressionTypes().get(stableNode), window.publications().expressionTypes().get(stableNode)); - } - - @Test - void toPatchRejectsNonLocalStabilizationSlotUpdateOwner() throws Exception { - var analysisData = FrontendAnalysisData.bootstrap(); - var window = new FrontendWindowAnalysisContext(analysisData); - var localScope = newBodyScope(); - var declaration = variable("local"); - localScope.defineLocal("local", GdVariantType.VARIANT, declaration); - window.publications().addLocalSlotTypeUpdate( - new FrontendLocalSlotTypeUpdate(localScope, "local", declaration, GdIntType.INT) - ); - - assertThrows( - FrontendAnalysisPatchException.class, - () -> window.toPatch(FrontendSemanticStage.EXPR_TYPE) - ); - assertSame(GdVariantType.VARIANT, requireLocal(localScope, "local").type()); - } - - @Test - void discardDropsScratchFactsWithoutTouchingStableDiagnosticsOrScope() throws Exception { - var analysisData = FrontendAnalysisData.bootstrap(); - var diagnostics = new DiagnosticSnapshot(List.of( - FrontendDiagnostic.warning("sema.window", "window warning", null, null) - )); - analysisData.updateDiagnostics(diagnostics); - var localScope = newBodyScope(); - var declaration = variable("local"); - localScope.defineLocal("local", GdVariantType.VARIANT, declaration); - var expressionNode = identifier("pending_value"); - - var window = new FrontendWindowAnalysisContext(analysisData); - window.publications().expressionTypes().put(expressionNode, FrontendExpressionType.resolved(GdIntType.INT)); - window.publications().addLocalSlotTypeUpdate( - new FrontendLocalSlotTypeUpdate(localScope, "local", declaration, GdIntType.INT) - ); - window.discard(); - - assertSame(diagnostics, analysisData.diagnostics()); - assertNull(analysisData.expressionTypes().get(expressionNode)); - assertTrue(analysisData.symbolBindings().isEmpty()); - assertSame(GdVariantType.VARIANT, requireLocal(localScope, "local").type()); - } - - @Test - void sameKeyIdempotentWriteCanShadowStableFactWithoutReplacingItOnCommit() { - var analysisData = FrontendAnalysisData.bootstrap(); - var expressionNode = identifier("value"); - var stableType = FrontendExpressionType.resolved(GdIntType.INT); - analysisData.expressionTypes().put(expressionNode, stableType); - - var window = new FrontendWindowAnalysisContext(analysisData); - var scratchType = FrontendExpressionType.resolved(new GdIntType()); - window.publications().expressionTypes().put(expressionNode, scratchType); - analysisData.applyPatch(window.drainPatch(FrontendSemanticStage.EXPR_TYPE)); - - assertSame(stableType, window.publications().expressionTypes().getStable(expressionNode)); - assertSame(stableType, analysisData.expressionTypes().get(expressionNode)); - } - - @Test - void sameKeyConflictsRejectStableShadowingAndNegativeToSuccessOverride() { - var analysisData = FrontendAnalysisData.bootstrap(); - var stableNode = identifier("value"); - var failedNode = identifier("failed"); - var slotNode = variable("slot"); - analysisData.expressionTypes().put(stableNode, FrontendExpressionType.resolved(GdIntType.INT)); - analysisData.expressionTypes().put(failedNode, FrontendExpressionType.failed("original failure")); - analysisData.slotTypes().put(slotNode, GdIntType.INT); - - var window = new FrontendWindowAnalysisContext(analysisData); - - assertThrows( - FrontendAnalysisPatchException.class, - () -> window.publications().expressionTypes().put(stableNode, FrontendExpressionType.resolved(GdFloatType.FLOAT)) - ); - assertThrows( - FrontendAnalysisPatchException.class, - () -> window.publications().expressionTypes().put(failedNode, FrontendExpressionType.resolved(GdIntType.INT)) - ); - assertThrows( - FrontendAnalysisPatchException.class, - () -> window.publications().slotTypes().put(slotNode, GdFloatType.FLOAT) - ); - } - - @Test - void finalRetryFactsStayScratchLocalUntilPatchCommit() { - var analysisData = FrontendAnalysisData.bootstrap(); - var expressionNode = identifier("retry_result"); - var window = new FrontendWindowAnalysisContext(analysisData); - var finalizedType = FrontendExpressionType.resolved(GdIntType.INT); - - window.publications().expressionTypes().put(expressionNode, finalizedType); - - assertSame(finalizedType, window.publications().expressionTypes().get(expressionNode)); - assertNull(analysisData.expressionTypes().get(expressionNode)); - - var patch = window.drainPatch(FrontendSemanticStage.EXPR_TYPE); - assertNull(analysisData.expressionTypes().get(expressionNode)); - analysisData.applyPatch(patch); - - assertSame(finalizedType, analysisData.expressionTypes().get(expressionNode)); - } - - @Test - void attributeStepKeysKeepIdentityLookupAndDuplicateGuards() { - var analysisData = FrontendAnalysisData.bootstrap(); - var window = new FrontendWindowAnalysisContext(analysisData); - var propertyStep = property("marker"); - var callStep = call("fetch", identifier("seed")); - var subscriptStep = subscript("items", identifier("index")); - var propertyMember = FrontendResolvedMember.resolved( - "marker", - FrontendBindingKind.PROPERTY, - FrontendReceiverKind.INSTANCE, - ScopeOwnerKind.GDCC, - new GdObjectType("Player"), - GdIntType.INT, - "Player.marker" - ); - var resolvedCall = FrontendResolvedCall.resolved( - "fetch", - FrontendCallResolutionKind.INSTANCE_METHOD, - FrontendReceiverKind.INSTANCE, - ScopeOwnerKind.GDCC, - new GdObjectType("Player"), - GdIntType.INT, - List.of(GdVariantType.VARIANT), - "Player.fetch" - ); - - window.publications().resolvedMembers().put(propertyStep, propertyMember); - window.publications().resolvedCalls().put(callStep, resolvedCall); - window.publications().expressionTypes().put(subscriptStep, FrontendExpressionType.resolved(GdIntType.INT)); - window.publications().expressionTypes().put(subscriptStep, FrontendExpressionType.resolved(new GdIntType())); - - assertSame(propertyMember, window.publications().resolvedMembers().get(propertyStep)); - assertSame(resolvedCall, window.publications().resolvedCalls().get(callStep)); - assertNull(window.publications().resolvedMembers().get(property("marker"))); - assertNull(window.publications().resolvedCalls().get(call("fetch", identifier("seed")))); - assertThrows( - FrontendAnalysisPatchException.class, - () -> window.publications().expressionTypes().put(subscriptStep, FrontendExpressionType.resolved(GdFloatType.FLOAT)) - ); - } - - @Test - void compilerOnlyTypesAreRejectedBeforeCommit() throws Exception { - var analysisData = FrontendAnalysisData.bootstrap(); - var window = new FrontendWindowAnalysisContext(analysisData); - - assertThrows( - FrontendAnalysisPatchException.class, - () -> window.publications().expressionTypes().put( - identifier("iter"), - FrontendExpressionType.resolved(GdccForRangeIterType.FOR_RANGE_ITER) - ) - ); - assertThrows( - FrontendAnalysisPatchException.class, - () -> window.publications().slotTypes().put(variable("iter_slot"), GdccForRangeIterType.FOR_RANGE_ITER) - ); - - var localScope = newBodyScope(); - var declaration = variable("local"); - localScope.defineLocal("local", GdVariantType.VARIANT, declaration); - assertThrows( - FrontendAnalysisPatchException.class, - () -> window.publications().addLocalSlotTypeUpdate( - new FrontendLocalSlotTypeUpdate( - localScope, - "local", - declaration, - GdccForRangeIterType.FOR_RANGE_ITER - ) - ) - ); - } - - @Test - void localSlotUpdatesRemainIsolatedUntilPatchApply() throws Exception { - var analysisData = FrontendAnalysisData.bootstrap(); - var localScope = newBodyScope(); - var declaration = variable("local"); - localScope.defineLocal("local", GdVariantType.VARIANT, declaration); - var bindingNode = identifier("local_use"); - var originalBinding = localBinding("local", declaration, requireLocal(localScope, "local")); - analysisData.symbolBindings().put(bindingNode, originalBinding); - - var window = new FrontendWindowAnalysisContext(analysisData); - window.publications().addLocalSlotTypeUpdate( - new FrontendLocalSlotTypeUpdate(localScope, "local", declaration, GdIntType.INT) - ); - - assertSame(GdVariantType.VARIANT, requireLocal(localScope, "local").type()); - assertSame(originalBinding, analysisData.symbolBindings().get(bindingNode)); - - var patch = window.toPatch(FrontendSemanticStage.LOCAL_TYPE_STABILIZATION); - - assertSame(GdVariantType.VARIANT, requireLocal(localScope, "local").type()); - assertSame(originalBinding, analysisData.symbolBindings().get(bindingNode)); - analysisData.applyPatch(patch); - - var refreshedBinding = analysisData.symbolBindings().get(bindingNode); - var refreshedValue = Objects.requireNonNull(refreshedBinding).resolvedValue(); - assertNotSame(originalBinding, refreshedBinding); - assertSame(GdIntType.INT, Objects.requireNonNull(refreshedValue).type()); - assertSame(GdIntType.INT, requireLocal(localScope, "local").type()); - } - - 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 AttributePropertyStep property(@NotNull String name) { - return new AttributePropertyStep(name, RANGE); - } - - private static @NotNull AttributeCallStep call(@NotNull String name, @NotNull Expression... arguments) { - return new AttributeCallStep(name, List.of(arguments), RANGE); - } - - private static @NotNull AttributeSubscriptStep subscript(@NotNull String name, @NotNull Expression... arguments) { - return new AttributeSubscriptStep(name, List.of(arguments), RANGE); - } - - 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/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 94% 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 c5f49cf9..171b07b6 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,15 +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.FrontendWindowAnalysisContext; 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; @@ -41,7 +40,6 @@ import gd.script.gdcc.type.GdVariantType; import gd.script.gdcc.frontend.diagnostic.FrontendDiagnostic; import gd.script.gdcc.frontend.sema.FrontendAnalysisData; -import gd.script.gdcc.frontend.sema.FrontendAstSideTable; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; @@ -49,10 +47,12 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +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; @@ -60,7 +60,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -class FrontendExprTypeAnalyzerTest { +class FrontendBodyOwnerProceduresExprTypeTest { @Test void analyzePublishesResolvedAtomicAndChainExpressionTypes() throws Exception { var analyzed = analyze( @@ -331,10 +331,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 @@ -394,7 +393,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 @@ -461,7 +459,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 @@ -526,7 +523,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 @@ -598,7 +594,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 @@ -868,9 +863,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 @@ -909,8 +903,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()); } @@ -1175,8 +1169,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()); } @@ -1464,7 +1456,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", """ @@ -1502,14 +1494,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()) )); @@ -2346,68 +2338,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 guardedBinding = input.analysisData().symbolBindings().get(valueUse); - var valueUseType = input.analysisData().expressionTypes().get(valueUse); assertAll( () -> 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()), - () -> assertNotNull(valueUseType), - () -> assertEquals(FrontendExpressionTypeStatus.RESOLVED, valueUseType.status()), - () -> assertEquals(GdVariantType.VARIANT, valueUseType.publishedType()) - ); - } - - @Test - void analyzeInWindowKeepsExpressionFactsScratchOnlyUntilPatchCommit() throws Exception { - var input = prepareInputBeforeExpressionTyping( - "expr_type_window_scratch_publication.gd", - """ - class_name ExprTypeWindowScratchPublication - extends RefCounted - - func make_value() -> int: - return 1 - - func ping(): - var value := make_value() - value - """ - ); - var pingFunction = findFunction(input.unit().ast(), "ping"); - var valueDeclaration = findVariable(pingFunction.body().statements(), "value"); - var valueUse = assertInstanceOf( - IdentifierExpression.class, - assertInstanceOf(ExpressionStatement.class, pingFunction.body().statements().get(1)).expression() - ); - input.analysisData().updateExpressionTypes(new FrontendAstSideTable<>()); - - var window = new FrontendWindowAnalysisContext(input.analysisData()); - new FrontendExprTypeAnalyzer().analyzeInWindow(input.classRegistry(), window, input.diagnosticManager()); - - assertNull(input.analysisData().expressionTypes().get(valueDeclaration.value())); - assertNull(input.analysisData().expressionTypes().get(valueUse)); - - input.analysisData().applyPatch(window.drainPatch(FrontendSemanticStage.EXPR_TYPE)); - - var initializerType = input.analysisData().expressionTypes().get(valueDeclaration.value()); - var valueUseType = input.analysisData().expressionTypes().get(valueUse); - assertAll( - () -> assertNotNull(initializerType), - () -> assertEquals(FrontendExpressionTypeStatus.RESOLVED, initializerType.status()), - () -> assertEquals("int", initializerType.publishedType().getTypeName()), - () -> assertNotNull(valueUseType), - () -> assertEquals(FrontendExpressionTypeStatus.RESOLVED, valueUseType.status()), - () -> assertEquals("int", valueUseType.publishedType().getTypeName()) + () -> assertEquals(GdVariantType.VARIANT, bodyScope.resolveValue("value").type()) ); } @@ -2471,10 +2415,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) ) ); @@ -2491,43 +2435,25 @@ void analyzeFailsFastWhenResolvedCompilerOnlyExpressionFactWouldBePublished() th 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 @@ -2753,43 +2679,21 @@ func ping(): var diagnostics = new DiagnosticManager(); var parserService = new GdScriptParserService(); var unit = parserService.parseUnit(Path.of("tmp", fileName), source, diagnostics); - var analysisData = analyzeWithOwnerAnalyzerBaseline(unit, registry, diagnostics, topLevelCanonicalNameMap); + var analysisData = analyzeWithSegmentedPipeline(unit, registry, diagnostics, topLevelCanonicalNameMap); return new AnalyzedScript(unit.ast(), analysisData); } - private static @NotNull FrontendAnalysisData analyzeWithOwnerAnalyzerBaseline( + private static @NotNull FrontendAnalysisData analyzeWithSegmentedPipeline( @NotNull FrontendSourceUnit unit, @NotNull ClassRegistry classRegistry, @NotNull DiagnosticManager diagnostics, @NotNull Map topLevelCanonicalNameMap ) { - var analysisData = FrontendAnalysisData.bootstrap(); - var moduleSkeleton = new FrontendClassSkeletonBuilder().build( + return new FrontendSemanticAnalyzer().analyze( new FrontendModule("test_module", List.of(unit), topLevelCanonicalNameMap), classRegistry, - diagnostics, - analysisData + diagnostics ); - analysisData.updateModuleSkeleton(moduleSkeleton); - analysisData.updateDiagnostics(diagnostics.snapshot()); - new FrontendScopeAnalyzer().analyze(classRegistry, analysisData, diagnostics); - analysisData.updateDiagnostics(diagnostics.snapshot()); - new FrontendVariableAnalyzer().analyze(analysisData, diagnostics); - analysisData.updateDiagnostics(diagnostics.snapshot()); - new FrontendTopBindingAnalyzer().analyze(classRegistry, analysisData, diagnostics); - analysisData.updateDiagnostics(diagnostics.snapshot()); - new FrontendLocalTypeStabilizationAnalyzer().analyze(classRegistry, analysisData, diagnostics); - analysisData.updateDiagnostics(diagnostics.snapshot()); - new FrontendChainBindingAnalyzer().analyze(classRegistry, analysisData, diagnostics); - analysisData.updateDiagnostics(diagnostics.snapshot()); - // These focused tests exercise the standalone owner analyzers after Phase I removed the - // shared FrontendSemanticAnalyzer legacy bypass. Production body publication remains covered - // by SuiteResolver/framework tests and must not call this helper. - new FrontendExprTypeAnalyzer().analyze(classRegistry, analysisData, diagnostics); - analysisData.updateDiagnostics(diagnostics.snapshot()); - new FrontendVarTypePostAnalyzer().analyze(analysisData, diagnostics); - analysisData.updateDiagnostics(diagnostics.snapshot()); - return analysisData; } private static @NotNull PreparedExpressionInput prepareInputBeforeExpressionTyping( @@ -2821,21 +2725,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); + FrontendSegmentedPipelineTestSupport.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..cd875abb 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,7 +149,7 @@ 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 ))); } @@ -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 762e4568..dcdec713 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 @@ -235,7 +235,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 +264,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"); @@ -896,10 +896,12 @@ static func ping_static(value: int): 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 @@ -1259,12 +1261,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()); + FrontendSegmentedPipelineTestSupport.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 ce1555f0..00000000 --- a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendLocalTypeStabilizationAnalyzerTest.java +++ /dev/null @@ -1,857 +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.frontend.sema.FrontendSemanticStage; -import gd.script.gdcc.frontend.sema.FrontendWindowAnalysisContext; -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.assertSame; -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 analyzeInWindowDefersLocalSlotRewriteUntilPatchCommit() throws Exception { - var prepared = prepareProbeInput( - "local_type_stabilization_window_commit.gd", - """ - class_name LocalTypeStabilizationWindowCommit - extends RefCounted - - func ping(): - var value := 1 - value - """ - ); - var pingFunction = findFunction(prepared.unit().ast().statements(), "ping"); - var bodyScope = assertInstanceOf(BlockScope.class, prepared.analysisData().scopesByAst().get(pingFunction.body())); - var valueUse = assertInstanceOf( - IdentifierExpression.class, - assertInstanceOf(ExpressionStatement.class, pingFunction.body().statements().get(1)).expression() - ); - var originalBinding = prepared.analysisData().symbolBindings().get(valueUse); - assertNotNull(originalBinding); - var originalResolvedValue = originalBinding.resolvedValue(); - assertNotNull(originalResolvedValue); - assertSame(GdVariantType.VARIANT, originalResolvedValue.type()); - - var window = new FrontendWindowAnalysisContext(prepared.analysisData()); - new FrontendLocalTypeStabilizationAnalyzer().analyzeInWindow( - prepared.classRegistry(), - window, - prepared.diagnosticManager() - ); - - assertSame(GdVariantType.VARIANT, bodyScope.resolveValue("value").type()); - assertSame(originalBinding, prepared.analysisData().symbolBindings().get(valueUse)); - - prepared.analysisData().applyPatch(window.drainPatch(FrontendSemanticStage.LOCAL_TYPE_STABILIZATION)); - - var refreshedBinding = prepared.analysisData().symbolBindings().get(valueUse); - assertNotNull(refreshedBinding); - assertNotNull(refreshedBinding.resolvedValue()); - assertEquals("int", bodyScope.resolveValue("value").type().getTypeName()); - assertEquals("int", refreshedBinding.resolvedValue().type().getTypeName()); - } - - @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/FrontendSegmentedPipelineTestSupport.java b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSegmentedPipelineTestSupport.java new file mode 100644 index 00000000..5ac98ba6 --- /dev/null +++ b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSegmentedPipelineTestSupport.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 without restoring whole-module analyzers. +final class FrontendSegmentedPipelineTestSupport { + private FrontendSegmentedPipelineTestSupport() { + } + + 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..30e4125d 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 @@ -1296,12 +1296,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()); + FrontendSegmentedPipelineTestSupport.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..04eefe74 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 @@ -282,14 +282,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()); + FrontendSegmentedPipelineTestSupport.resolveAllOwners(classRegistry, analysisData, diagnosticManager); new FrontendAnnotationUsageAnalyzer().analyze(classRegistry, analysisData, diagnosticManager); analysisData.updateDiagnostics(diagnosticManager.snapshot()); return new PreparedVirtualOverrideInput(units, analysisData, diagnosticManager, classRegistry); From a59d7cca79536a54f1aa961c2c23a96b943fc6a8 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Fri, 17 Jul 2026 20:33:32 +0800 Subject: [PATCH 20/27] docs(frontend): reframe for-in plan around iteration plan route classification - Re-scope from range-only to shared semantic for every for-in body, with iterable type driving iterator slot refinement and lowering route. - Decouple body support from route classification so unresolved iterables fall back to a Variant baseline handled by runtime helper. - Add phases and design sections for iterator declaration identity, iteration plan data structure, generic Variant route, and known iterable specialization. - Extend affected scope to LIR, backend, runtime helpers, and ABI contracts while keeping the range intrinsic as the first route and compiler-only iterator state reserved. - Refresh the test surface layout to match the new shared semantic inventory, suite resolver, and type-check checkpoints. --- ...tend_for_range_loop_implementation_plan.md | 701 ++++++++++++------ 1 file changed, 487 insertions(+), 214 deletions(-) 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 index 87df75ff..4ac86eda 100644 --- a/doc/module_impl/frontend/frontend_for_range_loop_implementation_plan.md +++ b/doc/module_impl/frontend/frontend_for_range_loop_implementation_plan.md @@ -1,306 +1,497 @@ -# Frontend for-range loop 实施计划 +# Frontend for-in loop 实施计划 -> 本文档记录 `for` loop 从当前 deferred 状态推进到“仅支持 range-like loop”的具体实施计划。本文中的 `for-range` 特指所有最终复用 `gdcc.for_range_iter.*` intrinsic contract 的 loop form:既包括 `for iterator[: Type] in range(...)`,也包括 Godot 兼容的 `for iterator[: Type] in ` 数值简写。目标是先让这两类 loop 进入 frontend shared semantic、compile gate、frontend CFG 与 body lowering 的正式支持面,同时要求第一版实现就预留未来任意 iterable / iterator `for` 的可扩展内部 iterator contract。 +> 本文档记录 `for iterator[: Type] in expr` 从当前 deferred 状态推进到 frontend shared semantic 正式支持面的实施计划。新目标不再把 body 是否可解析绑定到 range-like classifier:所有 `for-in` body 都发布 callable-local inventory,iterator 先以保守 source-facing type 进入 lexical inventory;`SuiteResolver` 在解析 iterable header 后发布 `FrontendForIterationPlan`,并在可以静态确定 element type 时把 iterator slot 从 `Variant` 精化为对应 element type。lowering 阶段根据 iteration plan 选择 `range(...)` / known iterable 的专用 helper,无法静态确定时走 generic Variant iterator helper。 ## 文档状态 - 状态:计划中 - 创建日期:2026-07-03 -- 最近校对:2026-07-09(Phase I:后续 for-range 解封必须基于最终 interface/body + SuiteResolver shared semantic pipeline) +- 最近校对:2026-07-17(改为 all for-in shared semantic supported,range / known iterable / generic Variant route 由 iteration plan 区分) - 适用范围: - `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/type/**` - `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_lowering_plan.md` - - `doc/module_impl/frontend/frontend_loop_control_flow_analyzer_implementation.md` - - `doc/module_impl/frontend/frontend_gdcompiler_type_implementation.md` + - `doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.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` - - Godot source `modules/gdscript/gdscript_utility_functions.cpp` - - Godot source `core/variant/variant_setget.cpp` + - Godot docs `tutorials/scripting/gdscript/gdscript_basics.rst` 的 `for` 语义说明 + - 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` ## 1. 范围与非目标 -本轮只实现 range-like loop 支持面: +本计划把 `for-in` 拆成两个互不混淆的支持面: -- 支持 `for i in range(stop):` -- 支持 `for i in range(start, end):` -- 支持 `for i in range(start, end, step):` -- 支持 `for i: int in range(...):` -- 支持 `for i in 3:`、`for i in limit:` 这类 `iterable` 已稳定发布为 `int` 的 Godot range 简写;其语义按 `range(stop)` 处理,但 frontend 不得通过伪造 `CallExpression("range")` AST 节点来实现。 -- range 参数必须在 frontend 表达式分析中拥有 lowering-ready typed fact,并能作为 `int` slot 下沉到 `gdcc.for_range_iter.init`。 -- `int` 简写的 `iterable` 表达式也必须在 frontend 表达式分析中拥有 lowering-ready typed fact,并能通过现有 ordinary boundary helper 进入 `int` stop slot。 -- loop iterator binding 的 source-facing element type 是 `int`,不是 `GdccForRangeIter`。 +- 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 values:` 这类任意 iterable / iterator loop。 -- `Array`、`Dictionary`、`String`、`Object`、typed array 等容器遍历。 -- Godot 的 `for i in 2.2` / `for i in some_float` 浮点数值简写。它们仍属于 range-like 后续扩面,届时必须明确 `ceil` 语义并复用本文定义的内部 iterator contract,而不是再开一条平行 lowering 管线。 -- 将 `range(...)` 当作普通 user-facing callable value。`range(...)` 在本计划中只作为 `for` iterable 位置的 loop-specific form 识别,不向普通 `resolvedCalls()` 发布 utility function route。 -- 让 `GdCompilerType` 进入 source-facing declared type、ordinary `expressionTypes()`、ordinary local/property/return `slotTypes()`、public ABI、`Variant` pack/unpack 或 Godot runtime 普通调用路径。 +- `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 发布。 ## 2. 当前基线 当前代码库已经具备的基础: - `gdparser` 的 `ForStatement` AST 已包含 `iterator`、`iteratorType`、`iterable`、`body`、`range` 字段。 -- `GdScriptParserService` 只是外部 parser adapter,本仓库没有本地 parser grammar 可改;若 parse smoke 发现 `range` loop AST 形态不足,应先升级或修复外部 parser,而不是在 frontend analyzer 中猜文本。 +- `GdScriptParserService` 只是外部 parser adapter,本仓库没有本地 parser grammar 可改;若 parse smoke 发现 `for` AST 形态不足,应先升级或修复外部 parser,而不是在 frontend analyzer 中猜文本。 - `FrontendScopeAnalyzer` 已为 `ForStatement` 建立 `FOR_BODY` scope,并保证 `iteratorType` 与 `iterable` 在外层 scope 下遍历,`body` 在独立 `FOR_BODY` scope 下遍历。 - `FrontendLoopControlFlowAnalyzer` 已把 `for` 与 `while` 一样视为 loop boundary,`break` / `continue` 在 `for` body 内不再报 `sema.loop_control_flow`。 -- 最终 shared semantic pipeline 已固定为 interface/body + SuiteResolver:`FrontendVisibleValueResolver`、`FrontendVariableAnalyzer`、body owner procedures 与 `FrontendCompileCheckAnalyzer` 当前仍分别把 `for` 作为 deferred / unsupported boundary;后续 for-range 解封必须在 SuiteResolver owner procedures 与 shared readiness policy 中闭环,不能恢复 legacy whole-phase analyzer bypass。 -- `FrontendCfgGraphBuilder.processStatement(...)` 当前没有 `ForStatement` 分支;`break` / `continue` lowering 依赖 active `LoopFrame`。 -- `FrontendCfgRegion` 当前只允许 `BlockRegion`、`FrontendIfRegion`、`FrontendElifRegion`、`FrontendWhileRegion`。 -- `GdccForRangeIterType.FOR_RANGE_ITER` 已作为唯一 `GdCompilerType` 子类型存在。 -- `doc/gdcc_lir_intrinsic.md` 已冻结四个 backend-owned intrinsic:`gdcc.for_range_iter.init`、`gdcc.for_range_iter.should_continue`、`gdcc.for_range_iter.next`、`gdcc.for_range_iter.get`。 +- `FrontendSemanticAnalyzer` 的 shared phase 顺序固定为 skeleton -> scope -> variable inventory -> interface surface -> `SuiteResolver` -> diagnostics-only analyzers。`SuiteResolver` 是逐语句运行的 typed fact owner,能在 `var limit := 3` flush 后让后续 `for i in limit:` 读取 `limit:int`。 +- `FrontendTypedLexicalEnvironment` 已支持 pending / committed overlay、per-owner patch transaction、`Variant -> exact` 的 local slot update 校验;但当前 API 只允许 `LOCAL_TYPE_STABILIZATION` owner 发布 local slot update。 +- `FrontendVariableAnalyzer`、`FrontendInterfacePhase`、`FrontendStatementResolver`、`FrontendVisibleValueResolver` 与 `FrontendCompileCheckAnalyzer` 当前仍把 `for` 作为 unsupported / deferred boundary。 +- `FrontendBodyLocalDeclaration` 与 `FrontendBodyDeclarationIndex` 当前只表达 ordinary `VariableDeclaration` local;iterator binding 没有现成 declaration entry。 +- `FrontendCfgGraphBuilder.processStatement(...)` 当前没有 `ForStatement` 分支;`FrontendCfgRegion` 当前只允许 `BlockRegion`、`FrontendIfRegion`、`FrontendElifRegion`、`FrontendWhileRegion`。 +- `GdccForRangeIterType.FOR_RANGE_ITER` 已作为 compiler-only `GdCompilerType` 子类型存在。 +- `doc/gdcc_lir_intrinsic.md` 已冻结四个 backend-owned range intrinsic:`gdcc.for_range_iter.init`、`gdcc.for_range_iter.should_continue`、`gdcc.for_range_iter.next`、`gdcc.for_range_iter.get`;C backend 与 runtime helper 已有对应实现。 +- generic Variant iterator helper、typed container iterator helper、Object `_iter_*` helper 在本仓库 runtime / intrinsic 层尚未实现。 当前实现张力: - 文档仍把 `for` 放在 post-MVP / deferred 范围。 - loop-control semantic 已把 `for` 当成合法 loop boundary。 - backend/LIR 已具备 range iterator state 与 intrinsic。 -- frontend shared semantic、compile gate、CFG 与 body lowering 尚未承认 `for-range` 为 compile-ready surface。 +- frontend shared semantic、compile gate、CFG 与 body lowering 尚未承认 `for-in` 为 supported surface。 -本计划的实施目标就是把这个张力收口为:“复用 range intrinsic contract 的 `for-range` 正式支持;非 range-like `for` 继续 fail-closed。” +本计划的实施目标就是把这个张力收口为:“所有 `for-in` 在 shared semantic 中都是 supported body;iteration plan 决定 iterator type refinement 与 lowering route;compile surface 按 route helper 准备度分阶段打开”。 ## 3. 核心设计 -### 3.1 range-like loop classifier +### 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`。 +- `ITERATOR` 的 `declaration` 必须是 owning `ForStatement`,`sourceOrder` 使用固定 sentinel,例如 `-1`,表示在 body 所有 ordinary statement 之前可见。 +- `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 需要显式扩展: -新增一个 frontend 内部 helper,用于识别且描述 supported `for` loop source form,并把它规范化为 lowering 可消费的内部 contract。建议命名为 `FrontendForLoopSupport`,放在 frontend sema/lowering 均可复用的位置;如果只被少量 analyzer 使用,保持为普通 final util class,不引入 public interface。第一版虽然只返回 range-like contract,但 helper 名称与 API 要保持面向未来对象迭代的可扩展形状,避免第二种 iterable 接入时再做大面积改名。 +- 新增 `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` -- 若 `ForStatement.iterable()` 是 `CallExpression`,则 `CallExpression.callee()` 必须是 bare `IdentifierExpression("range")`,且 `arguments().size()` 必须是 1、2 或 3。 -- 若 `ForStatement.iterable()` 不是 `range(...)` call,但其 root 已稳定发布 lowering-ready typed fact,且该 typed fact 可通过现有 ordinary typed-boundary helper 进入 `int` slot,则将其识别为 Godot 的 `range(stop)` 简写。 -- `float` 简写本轮仍不识别;它必须留给 follow-up,在明确 `ceil` 语义后复用同一 contract 扩面。 -- 不支持 named argument、attribute call、subscript call 或用户变量 `range` shadowing 的普通 callable 路由;本轮将 `range(...)` 视为 loop-specific syntax form。 -- `ForStatement.iterator()` 必须使用现有 AST 提供的 iterator name。 -- `ForStatement.iteratorType()` 可为空;为空时 iterator element type 为 `int`。 +新增 stable frontend fact,key 为 `ForStatement`。建议命名为 `FrontendForIterationPlan`,由 SuiteResolver header owner 发布,供 type-check、compile gate、CFG builder 与 lowering 消费。 -helper 输出建议使用一个简单 record,例如: +建议形状: ```java -enum ForRangeSourceKind { +enum FrontendForIterationRoute { RANGE_CALL, - INT_SHORTHAND + INT_SHORTHAND, + FLOAT_SHORTHAND, + STRING, + ARRAY, + DICTIONARY_KEYS, + PACKED_ARRAY, + OBJECT_CUSTOM, + GENERIC_VARIANT } -record ForRangeLoopSpec( +record FrontendForIterationPlan( @NotNull ForStatement statement, + @NotNull FrontendForIterationRoute route, @NotNull String iteratorName, @Nullable TypeRef declaredIteratorType, - @NotNull ForRangeSourceKind sourceKind, + @NotNull GdType rawElementType, + @NotNull GdType exposedIteratorType, + boolean requiresPerElementConversion, @NotNull List sourceOperands, - @NotNull GdType elementType, - @NotNull GdCompilerType iteratorStateType, - @NotNull String initIntrinsicName, - @NotNull String shouldContinueIntrinsicName, - @NotNull String nextIntrinsicName, - @NotNull String getIntrinsicName + @Nullable GdCompilerType iteratorStateType, + @NotNull List operationNames ) {} ``` -这里 `iteratorStateType` 第一版固定为 `GdccForRangeIterType.FOR_RANGE_ITER`,四个 intrinsic 名称第一版固定为 `gdcc.for_range_iter.*`。但 consumer 必须通过 `GdCompilerType` 与 contract 字段读取 storage / operation 协议,不能把 `GdccForRangeIterType` 或 `gdcc.for_range_iter` 字面量散落到多个 analyzer、CFG builder 与 lowering processor 中。`elementType` 第一版固定为 `GdIntType.INT`,用于 source-facing loop variable binding 与 ordinary typed boundary。 +约束: -`sourceOperands` 保留源码级 iterable 形态,不伪造 AST: +- `rawElementType` 是 runtime helper / intrinsic 产出的元素类型。 +- `exposedIteratorType` 是 body 中 iterator local 的 source-facing type。 +- `iteratorStateType` 只允许出现在该 dedicated iteration plan 与 lowering-internal state,不得进入 ordinary expression / slot / binding tables。 +- `operationNames` 由 route contract 提供,consumer 不得在 CFG builder / lowering processor 中散落硬编码 intrinsic 名称。 +- `sourceOperands` 保留源码 expression,不伪造 AST。 -- `RANGE_CALL`:按源码顺序保存 1/2/3 个 `range(...)` arguments。 -- `INT_SHORTHAND`:只保存一个 `stop` expression;后续 CFG/lowering 通过 contract 语义把它解释为 `(start=0, end=stop, step=1)`,不要构造假的 `0` / `1` AST 节点或假的 `range(stop)` call。 +`range(...)` route 第一版使用已有 contract: -不要为当前唯一实现新增 public `GdIteratorType`、`ForIterator` interface 或 builder。未来新增第二种 compiler-owned iterator state 时,再根据实际第二个 subtype 扩展 sealed `GdCompilerType` 层次与对应 contract;但第一版的 helper、CFG item 与 lowering API 必须已经采用通用命名,以便未来对象迭代直接挂到同一 contract 管线。 +- `iteratorStateType = GdccForRangeIterType.FOR_RANGE_ITER` +- `rawElementType = GdIntType.INT` +- `operationNames = gdcc.for_range_iter.init / should_continue / next / get` -### 3.2 iterator state 与 element type 分离 +generic Variant route 需要新增 contract: -range lowering 需要两个完全不同的类型事实: +- `iteratorStateType` 使用新的 compiler-only generic iterator state type,或由 LIR item 隐藏为 backend-owned storage。 +- `rawElementType = GdVariantType.VARIANT` +- `operationNames` 使用新 intrinsic,例如 `gdcc.for_variant_iter.init / should_continue / next / get`,具体名称在 intrinsic catalog 阶段冻结。 -- iterator state type:`GdccForRangeIterType.FOR_RANGE_ITER`,只用于 hidden LIR local/temp、range intrinsic operand/result、C storage lifecycle。 -- element type:`GdIntType.INT`,用于 `for` iterator binding、body 中 `i` 的 visible value、`slotTypes()`、assignment/type-check 与 return/boundary materialization。 +### 3.5 `range(...)` 与 ordinary call route 隔离 -禁止事项: +`range(...)` 在 `for` iterable 位置是 loop-specific syntax form: -- 不把 `GdccForRangeIterType` 发布到 `expressionTypes()`。 -- 不把 `GdccForRangeIterType` 发布为 loop variable 的 `slotTypes()`。 -- 不让 `GdccForRangeIterType` 进入 declared type parser、ordinary implicit conversion matrix、public ABI 或 `Variant` pack/unpack。 -- 不通过普通 `CallItem` 表达 `range(...)`,避免 body lowering 把它当成 user-facing call route。 +- bare `IdentifierExpression("range")` + 1/2/3 positional arguments 才进入 `RANGE_CALL` route。 +- named argument、attribute call、subscript call、`some_range(...)`、`obj.range(...)` 不进入 `RANGE_CALL` route。 +- `range(...)` root 不发布 ordinary `resolvedCalls()`。 +- `range(...)` arguments 必须按 source order 运行 top / chain / expr type,且能进入 `int` boundary。 +- literal `step == 0` 应在 frontend 发 diagnostic;非 literal step 交给 runtime helper 的防无限循环保护。 -### 3.3 未来 iterator 接口预留 +### 3.6 Godot iteration 语义落点 -未来任意 iterable 的接线应保持以下形状: +外部语义参考表明 Godot 对 `for-in` 使用统一 iteration 协议: -- semantic 阶段把 source iterable type 解析成一个内部 iterator contract。 -- iterator contract 持有: - - compiler-only `GdCompilerType` 子类型作为 iterator state storage。 - - source-facing `GdType` 作为 yielded element type。 - - init / should_continue / next / get 这组 operation 的 lowering 名称或 intrinsic 名称。 -- lowering 只消费该 contract,不重新推导 iterable 语义。 -- `range(...)`、`int` 简写、以及未来 `Object._iter_*` / container iterable 都必须复用这条 contract 管线;差异只能体现在 classifier 产出的 contract 内容,不得表现为多套互不相干的 `for` CFG / lowering 分支。 -- 第一版即使还不实现 `Array` / `Dictionary` / `Object` iterable,也必须把 `FrontendForRegion`、CFG item 与 lowering processor 命名设计为通用 `for-loop` 语义,而不是把 AST 级结构永久绑定到 `for-range` 专名。 +- `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。 -第一版不需要把这个 contract 抽成 public interface;可以先把 range-like 的 record 与 helper 留在 frontend 内部。文档与测试必须锁住“state type 与 yield type 分离”这个接口边界,保证未来新增 `GdCompilerType` 子类型时不会把 compiler-only state 泄漏到 ordinary type 系统。 +GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationRoute` 和 tests 保留扩展位。无法静态确定或尚未实现专用 helper 的类型必须落到 `GENERIC_VARIANT`,而不是重新把 body 关回 deferred boundary。 ## 4. 分阶段实施步骤 -### 阶段 A:parse / AST 基线测试 +### 阶段 A:parse / AST / scope 基线测试 目标: -- 确认外部 parser 对目标语法形态已经稳定产出 `ForStatement`。 -- 锁住 `iterator`、`iteratorType`、`iterable`、`body` 的 AST shape。 +- 锁住外部 parser 对 `for-in` 的 AST shape。 +- 确认 `FrontendScopeAnalyzer` 的 header/body scope 分层满足 all for-in 支持面。 实施内容: -- 增加或扩展 parse smoke / scope 测试,覆盖 `for i in range(3):`。 -- 增加或扩展 parse smoke / scope 测试,覆盖 `for i in 3:` 与 `for i in limit:`,确认 `iterable()` 保持原始 expression shape,而不是被 frontend 伪装成 `range(...)` call。 -- 覆盖 `for i: int in range(1, 3, 1):`,确认 `iteratorType()` 挂在 `ForStatement` 上,`iterable()` 是 `CallExpression`。 +- 增加或扩展 parse smoke / scope 测试,覆盖: + - `for i in range(3):` + - `for i in 3:` + - `for i in limit:` + - `for i in values:` + - `for i: int in values:` - 不修改 `GdScriptParserService`,除非 parser diagnostic mapping 本身有问题。 +- 确认 `iteratorType` 与 `iterable` 仍在外层 scope 解析,`body` 仍在独立 `FOR_BODY` scope 下解析。 验收细则: - `ForStatement.iterator()` 返回源码 iterator name。 - `ForStatement.iteratorType()` 对 typed iterator 非空,对 untyped iterator 为空。 -- `ForStatement.iterable()` 是 bare `range(...)` call。 -- `for i in 3:` / `for i in limit:` 的 `ForStatement.iterable()` 保持原始 literal / identifier / expression 形态,不被 rewrite。 +- `ForStatement.iterable()` 保持原始 expression shape,不被 rewrite。 - `ForStatement.body()` 已由 `FrontendScopeAnalyzer` 发布 `BlockScopeKind.FOR_BODY`。 +- nested `for` 的内层 body 也有独立 `FOR_BODY`,且 parent scope 链正确。 -### 阶段 B:range loop classifier 与诊断边界 +### 阶段 B:body inventory 与 declaration index 解封 目标: -- 建立单一 for-range 判定入口。 -- 非 range-like `for` 保持 fail-closed,不因 `FOR_BODY` scope 已存在而误进入普通 executable body。 +- 把 `for` 从 shared semantic 的 unsupported boundary 中移除。 +- 为所有 `for-in` 发布 iterator binding 与完整 body local inventory。 +- 扩展 body declaration index,使 iterator 不绕过 published inventory guard。 实施内容: -- 新增 `FrontendForLoopSupport` 或等价 helper。 -- 提供 `tryClassifyRangeLoop(ForStatement)` / `isSupportedRangeLoop(ForStatement)` 这类简洁 API;它们虽然先返回 range-like contract,但名字与返回值形状要允许未来对象迭代扩展。 -- 对 arity 非 1/2/3 的 `range(...)` 发 frontend diagnostic。 -- 对 `range(..., 0)` 的 literal zero step 发 frontend diagnostic;非 literal step 不能静态确定为零时允许进入 lowering,由 runtime helper 保留最终保护。 -- 对 `for i in ` 识别为 supported range-like loop,并通过 contract 记下 `INT_SHORTHAND` source form。 -- 对 `for i in ` 与其它非 bare `range(...)`、非 `int` 简写的 `for` 不发新的 range-specific error,继续沿用现有 deferred / unsupported boundary。 +- `FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(BlockScopeKind)` 增加 `FOR_BODY`。这是 all for-in semantic supported 的明确边界变化;`MATCH_SECTION_BODY`、`LAMBDA_BODY` 等仍不得一起打开。 +- `FrontendVariableAnalyzer.handleForStatement(...)` 改为 supported path: + - 找到 `forStatement.body()` 对应 `BlockScope`。 + - 用 `ForStatement` 作为 declaration identity 定义 iterator local。 + - 无显式 iterator type 时使用 `GdVariantType.VARIANT` baseline。 + - 有显式 iterator type 时通过既有 declared-type resolver 解析 source-facing type。 + - 使用现有 duplicate / same-callable shadow 规则检查 iterator 与参数、外层 local、同 body local 的冲突。 + - 遍历 body,发布 body 内 ordinary local `var`。 +- 移除 `UnsupportedVariableBoundaryReporter` 对 `ForStatement` 的 unsupported diagnostic。 +- `FrontendBodyLocalDeclaration` / `FrontendBodyDeclarationIndex` 扩展为支持 iterator declaration identity: + - declaration key 使用 `Node` 或 `Object` identity,而不是只接受 `VariableDeclaration`。 + - iterator entry kind 为 `ITERATOR`,source order 表示 body-start visible。 + - ordinary `var` entry kind 为 `ORDINARY_VAR`,继续使用 source-order visibility。 +- `FrontendInterfacePhase.handleForStatement(...)` 不再注册 `FOR_SUBTREE` pending gate;它应: + - walk `iteratorType` 与 `iterable`。 + - 记录 iterator baseline 到 `FrontendTypedLexicalBaseline`。 + - `enterSupportedBlock(forStatement.body())`,使 nested body local 与 nested for 都进入 interface surface。 +- `FrontendVisibleValueResolver` 删除 for-specific body/header deferred boundary: + - `ForStatement.body()` edge 不再返回 `FOR_SUBTREE`。 + - `ForStatement.iteratorType()` / `iterable()` header edge 不再返回 `VARIABLE_INVENTORY_NOT_PUBLISHED`。 + - `BlockScopeKind.FOR_BODY` current-scope gate 不再 fail-closed。 验收细则: -- `for i in range():` 与 `for i in range(1, 2, 3, 4):` 有清晰 diagnostic。 -- `for i in 3:`、`for i in limit:` 在 typed fact 为 lowering-ready `int` 时被识别为 supported range-like loop。 -- `for i in values:` 仍被归类为非 range-like `for`,保留 `FOR_SUBTREE` deferred / unsupported 行为。 -- `for i in 2.2:`、`for i in some_float:` 当前仍保留旧 deferred / unsupported 行为,不误走 `int` 简写捷径。 -- `for i in obj.range(3):`、`for i in some_range(3):` 不被误识别为 supported for-range。 -- classifier 不读取源码文本,不依赖 `range` identifier 的 source slice。 +- `for i in values: print(i)` 中 `i` 在 body 内解析为 `LOCAL`,baseline type 为 `Variant`。 +- `for i in range(3): print(i)` 在本阶段也至少解析为 `LOCAL`,即使还未精化为 `int`。 +- `i` 在 loop 后不可见。 +- body 内 `var local := i` 正常发布为 `FOR_BODY` ordinary local。 +- `for i in values: var item := i` 的 `item` 出现在 body declaration index 中。 +- iterator entry 出现在 body declaration index 中,且 resolver 的 published inventory guard 覆盖该 entry。 +- 同一 callable 内 iterator name 与已有 parameter/local 冲突时按现有 local conflict 规则报错。 +- body 内 `var i := ...` 与 iterator `i` 冲突,不能覆盖 iterator。 +- `match`、lambda、block-local `const` 的旧 unsupported / deferred 行为不因 `FOR_BODY` 解封而改变。 -### 阶段 C:variable inventory 与 visible value 解封 +### 阶段 C:iteration plan 数据结构与 publication surface 目标: -- 为 supported for-range 发布 loop iterator binding。 -- 只对 supported for-range body 发布 callable-local inventory。 -- 非 range `for` body 继续是 `FOR_SUBTREE` deferred domain。 +- 建立 lowering 与 type-check 共同消费的 `FrontendForIterationPlan`。 +- 增加 iteration planning owner,避免把 route 选择散落到 type-check / CFG / lowering。 实施内容: -- 不要直接把 `BlockScopeKind.FOR_BODY` 加进 `FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(BlockScopeKind)` 的无条件 true 列表。 -- 为 `FOR_BODY` 引入 AST-aware 支持判定:只有能从 use-site / body block 找到 owning `ForStatement`,且该 statement 是 supported for-range,才视为可发布 inventory。 -- `FrontendVariableAnalyzer.handleForStatement(...)` 对 supported for-range 执行: - - 在 `forStatement.body()` 的 `BlockScope` 中定义 iterator local。 - - iterator name 来自 `forStatement.iterator()`。 - - declared iterator type 为空时使用 `GdIntType.INT`。 - - declared iterator type 非空时通过既有 declared-type resolver 解析,再用 existing typed-boundary helper 检查 `int -> declaredType` 是否允许。 - - iterator binding 与 body local 按现有 duplicate / shadowing 规则处理。 - - 然后遍历 body,发布 body 内普通 local `var`。 -- `FrontendVisibleValueResolver` 对 supported for-range body 返回正常 executable lookup,对非 range `FOR_BODY` 继续返回 `DEFERRED_UNSUPPORTED + FOR_SUBTREE`。 +- 新增 `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。 验收细则: -- `for i in range(3): print(i)` 中 `i` 在 body 内可解析为 `LOCAL`,type 为 `int`。 -- `i` 在 loop 后不可见。 -- body 内 `var local := i` 正常发布为 `FOR_BODY` local。 -- `for i in values:` 的 body 内 lookup 仍返回 `DEFERRED_UNSUPPORTED + FOR_SUBTREE`。 -- 同一 callable 内 iterator name 与已有 parameter/local 冲突时按现有 local conflict 规则报错。 -- 非 range `for` 的旧 unsupported 测试继续存在,只把 supported range case 从旧断言中拆出。 +- `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 中重复硬编码。 -### 阶段 D:top-binding、chain-binding、expr-type、local stabilization、slot type 与 type-check +### 阶段 D:SuiteResolver header-only for path 与 iterator slot refinement 目标: -- 让 supported for-range 的 iterable arguments 与 body 进入正常 shared semantic facts。 -- 仍不把 `range(...)` root 当 ordinary call。 -- 让 loop iterator slot type 稳定为 element type。 +- 让 `SuiteResolver` 先解析 for header,再发布 iteration plan 与 iterator slot refinement,最后进入 child body。 +- body resolver 必须看到 header 已提交的 iterator effective type。 实施内容: -- `FrontendTopBindingAnalyzer.handleForStatement(...)`: - - supported for-range:分析 `range(...)` arguments 或 `int` 简写 iterable expression 与 body。 - - 非 range:继续 `reportDeferredSubtree(..., FOR_SUBTREE)`。 -- `FrontendChainBindingAnalyzer.handleForStatement(...)`: - - supported for-range:分析 range arguments 或 `int` 简写 iterable expression 与 body。 - - 不为 `range(...)` root 发布 ordinary `resolvedCalls()`。 -- `FrontendExprTypeAnalyzer.handleForStatement(...)`: - - supported for-range:发布 range arguments 或 `int` 简写 iterable expression 的 expression types,遍历 body。 - - 不为 iterator state 发布 ordinary expression type。 -- `FrontendLocalTypeStabilizationAnalyzer`: - - supported for-range:进入 body,使 body 内 `:=` local 能从 iterator / arguments / body expressions 稳定。 -- `FrontendVarTypePostAnalyzer`: - - supported for-range:为 iterator binding 与 body locals 发布 final `slotTypes()`。 - - iterator slot type 是 source-facing element type,不是 compiler-only iterator state type。 -- `FrontendTypeCheckAnalyzer`: -- 检查 range arguments 必须是 `int` 或通过现有 ordinary boundary 可安全进入 `int` slot。 -- 检查 `int` 简写 iterable expression 也必须是 `int` 或通过现有 ordinary boundary 可安全进入 `int` stop slot。 -- 检查 explicit iterator type 能接收 `int` element。 -- 不新增一套 parallel conversion matrix;必须复用现有 compatibility helper。 +- `FrontendStatementResolver` 增加 `resolveForStatement(...)`,替换当前 `resolveUnsupportedRoot(context, forStatement)`。 +- `resolveForStatement(...)` 必须是 header-only: + - 如果 `iteratorType` 非空,解析该 type ref 相关 expression / type use-site 所需 facts。 + - 解析 `iterable` 或 `range(...)` arguments。 + - 不在 header pass 中遍历 body statements。 +- 增加 owner hook,例如 `runForIterationPlanning(context, forStatement)`,执行位置固定在 expr typing 后、var type post 前。 +- `runForIterationPlanning(...)`: + - 读取 `iterable` 的 effective expression / slot typed fact。 + - 调用 `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。 +- `resolveForStatement(...)` 在 header facts flush 后调用 `childSuiteResolver.resolveChildSuite(context, forStatement.body())`。 +- child body 的 `FrontendSuiteContext` 应通过 parent environment 读取 iterator slot refinement。 验收细则: -- `range(start, end, step)` 的每个 argument 都有 lowering-ready expression type。 -- `for i in limit:` 的 `limit` expression 在进入 CFG 前也已有 lowering-ready expression type。 -- `range(...)` call root 不出现在 ordinary `resolvedCalls()` 成功路径中。 -- `for i: float in range(3):` 若现有 matrix 允许 `int -> float`,则 body 中 `i` 为 `float`;若不允许则发 `sema.type_check` / `sema.type_hint` 对应 owner 的诊断。 -- `for i in 2.2:` 当前仍不静默进入 supported path。 -- `for i: String in range(3):` 不能静默放行。 -- `GdccForRangeIterType` 不出现在 ordinary `expressionTypes()` 与 user-facing `slotTypes()` 中。 +- `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。 +- `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。 +- owner procedure 不从 `SourceFile` root 重新 walk,不恢复 legacy whole-module analyzer。 + +### 阶段 E:type-check 与 Godot iteration 语义 + +目标: + +- 在不关闭 body semantic 的前提下,对可静态验证的 route 进行 type-check。 +- 明确 generic route 的 runtime error / runtime dispatch 边界。 + +实施内容: -### 阶段 E:compile gate 分阶段解封 +- `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 分阶段解封 目标: -- 只在 shared semantic、CFG、body lowering 和 backend 消费都准备好之后,才让 supported for-range 通过 `analyzeForCompile(...)`。 +- shared semantic 支持与 compile-ready 支持分离。 +- 只有对应 lowering helper 已准备好的 route 才能通过 `analyzeForCompile(...)`。 实施内容: -- 在 CFG/body lowering 完成前,`FrontendCompileCheckAnalyzer.handleForStatement(...)` 继续跳过 `for`,沿用 upstream unsupported owner。 -- CFG/body lowering 完成后: - - supported for-range 加入 compile surface。 - - mark `ForStatement`、range arguments、body block、body statements 为 compile surface。 - - 非 range `for` 继续跳过,不把 deferred subtree 打平成 generic compile blocker。 +- shared semantic 阶段完成后,`FrontendCompileCheckAnalyzer.handleForStatement(...)` 仍可以按 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` 如 iterator contract 有新增事实 - - `diagnostic_manager.md` 如新增 category + - `frontend_gdcompiler_type_implementation.md` 如新增 compiler-only iterator state + - `gdcc_lir_intrinsic.md` 与 `gdcc_runtime_lib.md` 如新增 generic helper 验收细则: -- supported for-range 在无 error 时可通过 `analyzeForCompile(...)`。 -- 非 range `for` 在 compile mode 仍被阻断,且 diagnostic owner 仍是 upstream semantic boundary,不被 compile gate 重新包装。 -- compile gate 没有绕过 upstream `sema.loop_control_flow`、type-check 或 variable-binding error。 +- 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` 噪声。 -### 阶段 F:frontend CFG graph +### 阶段 G:frontend CFG graph 目标: -- 在 `FrontendCfgGraphBuilder` 中为 supported for-range 建立显式 CFG。 +- 在 `FrontendCfgGraphBuilder` 中为 `for-in` 建立显式 CFG。 - `break` 跳到 loop exit。 - `continue` 跳到 iterator update,再回 condition。 @@ -309,19 +500,18 @@ range lowering 需要两个完全不同的类型事实: - 新增 `FrontendForRegion`,不要复用 `FrontendWhileRegion`。 - `FrontendCfgRegion` sealed permits 增加 `FrontendForRegion`。 - `FrontendCfgGraphBuilder.processStatement(...)` 增加 `case ForStatement forStatement -> processForStatement(...)`。 -- `processForStatement(...)` 只接受 supported for-range;非 range `for` 到达这里必须 fail-fast。 +- `processForStatement(...)` 只消费已发布的 `FrontendForIterationPlan`,不得重新推导 iterable 语义。 - CFG shape 建议: - - `range(...)` arguments 或 `int` 简写 iterable expression 在进入 loop 前按 source order 计算。 - - init sequence 调用 contract 指定的 `init` operation,第一版仍是 `gdcc.for_range_iter.init`,并产出 hidden iterator state slot。 - - condition entry 调用 `gdcc.for_range_iter.should_continue`,产出 bool condition value。 - - body entry 先调用 `gdcc.for_range_iter.get` 写入 source-facing iterator slot,再执行 body statements。 - - update entry 调用 `gdcc.for_range_iter.next` 更新 iterator state,再跳回 condition entry。 + - iterable / source operands 在进入 loop 前按 source order 计算。 + - init entry 调用 plan 指定的 init operation,产出 hidden iterator state。 + - 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。 -- `INT_SHORTHAND` source form 不生成伪造 `range(stop)` AST;它只在 contract 层带一个 `stop` operand,init item/lowering processor 负责按 `(0, stop, 1)` 解释。 -- active loop frame 对 for-range 应使用: +- active loop frame 对 `for` 应使用: - `breakTargetId = exitId` - `continueTargetId = updateEntryId` -- hidden iterator state value id / slot id 必须稳定命名,建议沿用现有 `cfg_tmp_` 风格或用明确前缀,例如 `cfg_for_iter_`;一旦选择,测试锁定。 +- hidden iterator state value id / slot id 必须稳定命名,建议沿用现有 `cfg_tmp_` 风格或使用明确前缀,例如 `cfg_for_iter_`;一旦选择,测试锁定。 验收细则: @@ -329,20 +519,21 @@ range lowering 需要两个完全不同的类型事实: - `FrontendForRegion` 至少暴露 `initEntryId`、`conditionEntryId`、`bodyEntryId`、`updateEntryId`、`exitId`。 - `continue` 的 target 是 update entry,不是 condition entry。 - `break` 的 target 是 exit。 -- `range(...)` arguments 或 `int` 简写 stop operand 的 value ids 在 init item 前已经发布。 +- `range(...)` arguments、`INT_SHORTHAND` stop operand 或 generic iterable value ids 在 init item 前已经发布。 - condition branch 的 condition value type 是 `bool`,不会触发 compiler-only condition normalization。 +- nested `if` 中的 `break` / `continue` 能正确连边。 - unreachable body 后续 statement 处理仍遵守现有 reachability 规则。 -### 阶段 G:CFG item 与 body LIR lowering +### 阶段 H:range route LIR lowering 目标: -- 把 range iterator state 与四个 intrinsic materialize 为 LIR。 +- 先接通已有 backend/runtime 已支持的 `range(...)` 与 `int` shorthand route。 - 不重新扫描 AST,不重新推导 sema facts。 实施内容: -- 在 `frontend.lowering.cfg.item` 增加最小 dedicated item,建议保持 narrow 且使用面向未来的通用命名: +- 在 `frontend.lowering.cfg.item` 增加最小 dedicated item,保持通用命名: - `ForLoopInitItem` - `ForLoopShouldContinueItem` - `ForLoopGetItem` @@ -351,15 +542,16 @@ range lowering 需要两个完全不同的类型事实: - AST anchor - operand value ids - result value id - - iterator state type / element type / lowering operation 必要信息 + - route / iterator state type / element type / lowering operation 必要信息 - body lowering 在 `FrontendSequenceItemInsnLoweringProcessors` 中增加对应 processor。 -- processor 生成 `CallIntrinsicInsn`;第一版 range-like contract 对应: +- processor 消费 `FrontendForIterationPlan` 并生成 `CallIntrinsicInsn`;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 合同。 -- 不通过 `PackVariantInsn` / `UnpackVariantInsn` materialize iterator state。 +- `INT_SHORTHAND` source form 不生成伪造 `range(stop)` AST;init item/lowering processor 负责按 `(0, stop, 1)` 解释。 +- 不通过 `PackVariantInsn` / `UnpackVariantInsn` materialize range iterator state。 验收细则: @@ -370,52 +562,124 @@ range lowering 需要两个完全不同的类型事实: - generated C 使用 `gdcc_for_range_iter_*` helper,不出现 `godot_GdccForRangeIter`、`godot_new_GdccForRangeIter...`、`Variant` pack/unpack 相关路径。 - `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 iterator state type,或明确由 backend helper 持有 opaque state;无论哪种形态,都不得进入 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` 解析为 `ForStatement`,且 iterable 保持原始 expression 形态。 -- `for i: int in range(1, 3, 1): pass` 解析并保留 `iteratorType`。 +- `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 +### Shared semantic inventory -- `for i in range(3): var x := i` 成功发布 `i` 与 `x`。 -- `for i in 3: var x := i` 与 `for i in limit: var x := i` 在 `limit` 已稳定为 `int` 时也成功发布 `i` 与 `x`。 +- `for i in values: var x := i` 成功发布 iterator 与 body local。 - `i` 在 loop 后不可见。 -- `for i in values:` 继续是 `FOR_SUBTREE` deferred / unsupported。 -- `for i in 2.2:` 继续是 deferred / unsupported,直到 follow-up 明确 `ceil` 语义。 -- `break` / `continue` 在 for-range body 内合法。 -- `break` / `continue` 在 loop 外仍报 `sema.loop_control_flow`。 -- lambda / nested callable 内的 loop-control 仍按 callable boundary 重新判定。 +- 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 + +- `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` 不静默通过 type-check。 +- `for i: String in range(3): pass` 不静默通过。 +- `for i in 2.2:` 在未专用化前不报 frontend unsupported;若启用 float route,测试锁定 `ceil` 语义。 +- `Dictionary` route 测试锁定 iterator 是 key。 ### Compile gate -- supported for-range 在 `analyzeForCompile(...)` 无 error 时进入 lowering。 -- `for i in ` 这类 supported range-like loop 在无 error 时也进入 lowering。 -- 非 range `for` 仍不能进入 lowering。 +- 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` 覆盖 for-range region shape。 -- `FrontendCfgGraphBuilderTest` 覆盖 `INT_SHORTHAND` source form 与 `range(...)` source form 共用同一 `FrontendForRegion` shape。 +- `FrontendCfgGraphBuilderTest` 覆盖 `FrontendForRegion` shape。 - `continue` target 为 update entry。 - `break` target 为 exit。 - nested `if` 中的 `continue` / `break` 能正确连边。 -- 空 range body、只有 `pass` 的 body、body 内 `return` 的 reachable/fallthrough 行为均有断言。 +- 空 body、只有 `pass` 的 body、body 内 `return` 的 reachable/fallthrough 行为均有断言。 ### Body lowering / LIR / backend - `FrontendLoweringBuildCfgPassTest` 覆盖 function context 中发布 `FrontendForRegion`。 - `FrontendLoweringBodyInsnPassTest` 覆盖 range intrinsic instruction sequence,并锁住 `INT_SHORTHAND` 仍走相同 intrinsic 路线。 -- `DomLirParserTest` / `DomLirSerializerTest` 继续覆盖 `compiler::GdccForRangeIter` local round-trip。 +- 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)` 的 runtime output 锚点。 +- 如 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 命令 @@ -426,7 +690,11 @@ script/run-gradle-targeted-tests.sh --tests FrontendParseSmokeTest,FrontendScope ``` ```bash -script/run-gradle-targeted-tests.sh --tests FrontendVariableAnalyzerTest,FrontendVisibleValueResolverTest,FrontendTopBindingAnalyzerTest,FrontendTypeCheckAnalyzerTest +script/run-gradle-targeted-tests.sh --tests FrontendVariableAnalyzerTest,FrontendVisibleValueResolverTest,FrontendInterfacePhaseTest,FrontendSuiteResolverTest +``` + +```bash +script/run-gradle-targeted-tests.sh --tests FrontendTypeCheckAnalyzerTest,FrontendCompileCheckAnalyzerTest ``` ```bash @@ -445,22 +713,27 @@ script/run-gradle-targeted-tests.sh --tests GdCompilerTypeTest,GdccForRangeIterT ## 7. 风险与未定点 -- `FOR_BODY` 支持不能只靠 `BlockScopeKind` 判定,必须关联 owning `ForStatement` 是否为 supported for-range;否则会把未来 generic iterable `for` 误放行。 -- `range(...)` root 是否发布 expression type 必须统一。当前计划是不发布 ordinary root type,只发布 arguments 与 loop iterator binding,避免把 range 当作 user-facing `Array[int]` 或 builtin call。 +- `FOR_BODY` 一旦加入 unconditional supported block kind,必须同步删除 resolver 的三处 for-specific deferred boundary;否则会出现 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。 +- `range(...)` root 是否发布 expression type 必须统一。当前计划是不发布 ordinary root type,只发布 arguments 与 iteration plan,避免把 range 当作 user-facing builtin call。 - `int` 简写不允许通过 AST rewrite 伪装成 `range(stop)` call;否则 parser/scope/diagnostic anchor、future object-iterator classifier 与测试都会被污染。 -- explicit iterator type 的兼容规则必须复用 ordinary typed-boundary helper;不要为 `for` 私下硬编码 `int -> T` 特例。 -- `continue` 对 for-range 的目标不是 condition entry,而是 update entry。这一点和 `while` 不同,不能复用 `FrontendWhileRegion`。 -- literal `step == 0` 可以前端诊断;动态零 step 不能静态证明时仍需要 backend runtime helper 保护。 -- `for i in ` 与 `range(...)` 现在就应走同一 contract;未来若接入 `float` 简写或 `Object._iter_*` / container iterable,也必须在 classifier 层扩充 contract,而不是新增第二套 CFG / lowering 基础设施。 +- explicit iterator type 的兼容规则必须复用 ordinary typed-boundary helper;不要为 `for` 私下硬编码 `int -> T` 或 `Variant -> T` 特例。 +- `continue` 对 `for` 的目标不是 condition entry,而是 update entry。这一点和 `while` 不同,不能复用 `FrontendWhileRegion`。 +- 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 i in range(...)` 与 `for i in ` 在 shared semantic 中都不再触发 `FOR_SUBTREE` unsupported boundary。 -2. 非 range-like `for` 仍保持 deferred / unsupported,不进入 compile-ready surface。 -3. loop iterator 在 body 内是 source-facing local,类型为 `int` 或显式兼容类型。 -4. compiler-only `GdccForRangeIterType` 只出现在 hidden LIR local / intrinsic operand-result / backend C storage 路径。 -5. CFG 中存在独立 `FrontendForRegion`,且 `continue` / `break` 连边正确;同一 region/item/processor 基础设施能承载未来对象迭代扩展。 -6. Body lowering 生成 contract 指定的 intrinsic;第一版 range-like loop 仍生成 `gdcc.for_range_iter.*`,且不重扫 AST、不重做 semantic 推导。 -7. 文档、正反测试、targeted tests 与 compile check 全部同步。 +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、hidden LIR local / intrinsic operand-result / backend C storage 路径。 +7. CFG 中存在独立 `FrontendForRegion`,且 `continue` / `break` 连边正确;同一 region/item/processor 基础设施能承载 range、generic Variant 与 known iterable route。 +8. 文档、正反测试、targeted tests、compile check、lowering plan、runtime / intrinsic catalog 全部同步。 From 4bd72108a11591cac3b0cb8f8e0e9f438d82834e Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Mon, 20 Jul 2026 14:19:50 +0800 Subject: [PATCH 21/27] docs(frontend): define structural body support and export failure semantics - Separate lexical inventory and body entry from type refinement, route selection, and compile readiness. - Split iteration rollout into baseline semantic entry, later refinement, and an interim compile safety boundary. - Retire typed-dependent lifecycle gates while preserving fail-closed boundaries for unsupported syntax. - Document ordered export as non-atomic without cross-transaction preflight or rollback. --- ...e_resolution_pipeline_execution_summary.md | 11 +- ...tend_for_range_loop_implementation_plan.md | 65 ++- ...segmented_type_resolution_pipeline_plan.md | 386 +++++++++++++++--- .../frontend/sema/FrontendAnalysisData.java | 3 + .../sema/FrontendTypedLexicalEnvironment.java | 3 +- .../sema/analyzer/FrontendSuiteResolver.java | 2 + .../patch/FrontendCallableExportBatch.java | 5 +- .../sema/patch/FrontendPatchTransaction.java | 4 + 8 files changed, 404 insertions(+), 75 deletions(-) diff --git a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md index f8589680..d3bed892 100644 --- a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md +++ b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md @@ -81,6 +81,8 @@ statement-local var type post procedure 互补的正规步骤,而不是绕过 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、gate classifier 与 var type post。 Body procedure 必须是 root-bounded、statement-local 的实现。每个 owner 子过程只处理 `SuiteResolver` 传入的 statement、header 或 expression root 及其允许的子表达式。实现可以复用纯语义 helper,但 owner 子过程不能依赖 whole-module traversal 建立隐式上下文。 @@ -171,7 +173,7 @@ Statement owner runner 只能写当前 statement pending overlay;callable-entr `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 中更新。 +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。 @@ -197,9 +199,9 @@ Diagnostics-only phase、compile gate 与 lowering 只能读取 suite export 后 - `FrontendExprTypePatch`:`expressionTypes()` + bare-call `resolvedCalls()` delta。 - `FrontendVarTypePostPatch`:`slotTypes()` delta。 -`FrontendPatchTransaction` 按固定 owner 顺序 apply:top binding -> local stabilization -> chain binding -> expr typing -> var type post。 +`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。Property initializer 是独立 root,不加入 callable-scoped batch。 +`FrontendCallableExportBatch` 按 suite 收敛顺序保存 root callable 与 nested suite 的 transaction,且仅在 root callable suite 返回后依次 apply。它不预检 queued transaction 之间的冲突,也不提供跨 transaction 原子性或回滚。Property initializer 是独立 root,不加入 callable-scoped batch。 Merge 规则如下: @@ -213,6 +215,8 @@ Merge 规则如下: - `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。 @@ -341,6 +345,7 @@ Top binding 先绑定 `receiver` use-site。Local stabilization 随后把 `recei - 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 可以产生。 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 index 4ac86eda..8674b651 100644 --- a/doc/module_impl/frontend/frontend_for_range_loop_implementation_plan.md +++ b/doc/module_impl/frontend/frontend_for_range_loop_implementation_plan.md @@ -324,6 +324,13 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR - 有显式 iterator type 时通过既有 declared-type resolver 解析 source-facing type。 - 使用现有 duplicate / same-callable shadow 规则检查 iterator 与参数、外层 local、同 body local 的冲突。 - 遍历 body,发布 body 内 ordinary local `var`。 +- 在移除 shared semantic 的 `ForStatement` unsupported diagnostic 前,先让 + `FrontendCompileCheckAnalyzer.handleForStatement(...)` 对所有 `for-in` 发布临时、无条件的 + statement-root `sema.compile_check` blocker: + - blocker 只锚定 `ForStatement`,不得进入 body 重扫 semantic facts。 + - blocker 不读取 iteration plan、iterable type 或 route readiness。 + - 阶段 F 以 route-aware compile policy 替换该临时 blocker;在此之前任何 `for-in` 都不能进入 + `FrontendCfgGraphBuilder`。 - 移除 `UnsupportedVariableBoundaryReporter` 对 `ForStatement` 的 unsupported diagnostic。 - `FrontendBodyLocalDeclaration` / `FrontendBodyDeclarationIndex` 扩展为支持 iterator declaration identity: - declaration key 使用 `Node` 或 `Object` identity,而不是只接受 `VariableDeclaration`。 @@ -349,13 +356,16 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR - 同一 callable 内 iterator name 与已有 parameter/local 冲突时按现有 local conflict 规则报错。 - body 内 `var i := ...` 与 iterator `i` 冲突,不能覆盖 iterator。 - `match`、lambda、block-local `const` 的旧 unsupported / deferred 行为不因 `FOR_BODY` 解封而改变。 +- 默认 shared `analyze(...)` 不再为 `for-in` 产生 `FOR_SUBTREE` / variable-inventory unsupported + diagnostic;同一 source 通过 `analyzeForCompile(...)` 时由临时 `sema.compile_check` blocker + 明确阻断,且 lowering pipeline 不进入 CFG build。 ### 阶段 C:iteration plan 数据结构与 publication surface 目标: - 建立 lowering 与 type-check 共同消费的 `FrontendForIterationPlan`。 -- 增加 iteration planning owner,避免把 route 选择散落到 type-check / CFG / lowering。 +- 建立 iteration planning owner 后续需要的数据与 publication surface,避免把 route 选择散落到 type-check / CFG / lowering。 实施内容: @@ -387,12 +397,13 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR - `GENERIC_VARIANT` plan 不携带 range iterator state type。 - route 与 operation name 不在 CFG builder / lowering processor 中重复硬编码。 -### 阶段 D:SuiteResolver header-only for path 与 iterator slot refinement +### 阶段 D0:SuiteResolver header-only for path 与 baseline body entry 目标: -- 让 `SuiteResolver` 先解析 for header,再发布 iteration plan 与 iterator slot refinement,最后进入 child body。 -- body resolver 必须看到 header 已提交的 iterator effective type。 +- 让 `SuiteResolver` 先解析 for header,再以阶段 B 已发布的 iterator baseline 进入 child body。 +- 本阶段只依赖阶段 B,不依赖阶段 C 的 iteration plan 数据结构,也不做 iterator slot refinement。 +- 为 segmented pipeline 阶段 L 提供“没有 typed gate 也能进入 for body”的 production path。 实施内容: @@ -401,7 +412,41 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR - 如果 `iteratorType` 非空,解析该 type ref 相关 expression / type use-site 所需 facts。 - 解析 `iterable` 或 `range(...)` arguments。 - 不在 header pass 中遍历 body statements。 -- 增加 owner hook,例如 `runForIterationPlanning(context, forStatement)`,执行位置固定在 expr typing 后、var type post 前。 +- 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。 + +验收细则: + +- `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。 + +目标: + +- 在 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)`,执行位置 + 固定在 header expr typing 后、iterator var type post 前。该 hook 不复用或恢复 + `runGateClassifier(...)`。 - `runForIterationPlanning(...)`: - 读取 `iterable` 的 effective expression / slot typed fact。 - 调用 `FrontendForLoopSupport` 构造 plan。 @@ -411,8 +456,8 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR - `runVarTypePost(...)` 扩展为支持 `ForStatement` iterator declaration: - 为 iterator declaration key 发布 final source-facing `slotTypes()` fact。 - `slotTypes()` value 必须是 exposed iterator type,不能是 compiler-only state type。 -- `resolveForStatement(...)` 在 header facts flush 后调用 `childSuiteResolver.resolveChildSuite(context, forStatement.body())`。 -- child body 的 `FrontendSuiteContext` 应通过 parent environment 读取 iterator slot refinement。 +- D0 的 header flush 必须发生在 planning 与 iterator var type post 之后;child body 的 + `FrontendSuiteContext` 通过 parent environment 读取 iterator slot refinement。 验收细则: @@ -422,7 +467,8 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR - `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。 -- owner procedure 不从 `SourceFile` root 重新 walk,不恢复 legacy whole-module analyzer。 +- 改变 iterable 的 resolved type 只能改变 plan/refinement/type diagnostic,不能改变 D0 已建立的 + inventory、declaration index、suite entry 或 child-body dispatch。 ### 阶段 E:type-check 与 Godot iteration 语义 @@ -461,7 +507,8 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR 实施内容: -- shared semantic 阶段完成后,`FrontendCompileCheckAnalyzer.handleForStatement(...)` 仍可以按 route 分阶段阻断 compile: +- 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。 diff --git a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md index eed9f712..258942c2 100644 --- a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md +++ b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md @@ -24,6 +24,33 @@ - `doc/module_impl/frontend/frontend_for_range_loop_implementation_plan.md` - `doc/analysis/frontend_semantic_analyzer_research_report.md` +### 1.1 架构决定:无条件结构性 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。 + +第 4.5 节、第 4.8 节和阶段 F 中的 `FrontendInventoryGate` / readiness 设计是当前遗留实现 +的记录与清理参照,不能作为后续 feature 的实施方案。现有 typed-dependent gate registry、 +resolver readiness fallback、pending-gate 注册与 synthetic classifier 必须在其不再承担当前 +unsupported compatibility 行为后删除或重写为纯结构性支持判断;不得为它们补充新的 feature +consumer 或 publication protocol。 + ## 2. 背景与问题 当前 `FrontendSemanticAnalyzer.analyze(...)` 是 whole-module phase pipeline: @@ -65,10 +92,10 @@ 因此新计划改为 interface/body 双层结构: -- interface 层基于基础结构层已发布的 lexical inventory 建立 declaration index、signature/interface facts、typed-dependent gate registry。 +- interface 层基于基础结构层已发布的 lexical inventory 建立 declaration index、signature/interface facts 与 typed baseline。 - body 层用 `SuiteResolver` 按源码顺序解析 supported body statements。 - `TypedLexicalEnvironment` overlay 在 body 层提供 Godot 风格的“当前语句已知 typed fact 对后续语义立即可见”能力,同时保留 GDCC 的 side-table owner、patch conflict 与 compiler-only type 隔离。 -- Suite 收敛后的 stable export 采用按 owner 有序的 patch transaction:每个 owner 子过程的最终 facts 按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 顺序分别提交为独立 owner patch,不能合并成一个多 owner `FrontendAnalysisPatch`。 +- Suite 收敛后的 stable export 采用按 owner 有序的 patch transaction:base owner facts 按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 顺序提交;已实现的 feature-specific semantic route owner 可以在 expr typing 与对应 var type post 之间插入独立 patch,例如后续 `FOR_ITERATION_PLANNING`。不能把这些 owner 合并成一个多 owner `FrontendAnalysisPatch`。 - Patch 相关类型统一迁入 `gd.script.gdcc.frontend.sema.patch` 包,包括旧 `FrontendAnalysisPatch`、`FrontendLocalSlotTypeUpdate` 与新建 per-owner patch / transaction 类型;window publication 类型若保留,只能作为 legacy shim。 ## 3. 不变量 @@ -106,8 +133,8 @@ GDCC 的完整 lexical inventory 要求不是因为 Godot 缺少前向 local 检 - 对已支持的 block,基础结构层沿用现有 `FrontendVariableAnalyzer` + scope graph 发布完整 ordinary local inventory;interface 层建立 body declaration index / typed baseline view,而不是另起一套重复发布通道。生产 resolver 仍由 scope 完成按名称查找,并以 index 的 declaration identity 验证命中的 ordinary local 属于已发布 inventory;现有 source byte range filter 继续负责 declaration-order 与 initializer self-reference 语义。 - local `:=` 初始类型仍是 `Variant`。 -- body `SuiteResolver` 只负责按源码顺序稳定类型、发布 use-site facts、推进 gate readiness。 -- 若某个 child feature gate 后续转正,必须先发布该 child body 的 gate-owned bindings 与完整 local inventory,再解析 body suite。 +- body `SuiteResolver` 只负责按源码顺序稳定类型并发布 use-site facts;它不得通过 typed fact 决定 child body 的 inventory 或 entry readiness。 +- 若某个 child feature 后续转正,必须在 interface/inventory 阶段无条件发布该 child body 的结构性 binding 与完整 local inventory,再解析 body suite;header typed fact 只能影响后续 type refinement、semantic route 或 lowering。 这保证 `var x := y; var y := 1` 仍能产生 declaration-after-use filtered hit,而不是把 `y` 误判成普通 miss 或外层 fallback。 @@ -188,8 +215,7 @@ Interface 层: 1. class / callable / property signature interface 2. per-callable body declaration index -3. typed-dependent gate registry -4. source-order local typed baseline +3. source-order local typed baseline Interface 层借鉴 Godot `resolve_interface()` 与 `resolve_body()` 之间的边界:它不直接 lowering body,也不发布 compile-ready body facts,但必须准备 body `SuiteResolver` 所需的 typed lexical baseline。 @@ -223,7 +249,6 @@ Interface phase 输入: Interface phase 输出: - `FrontendBodyDeclarationIndex`:每个 supported block 的完整 declaration 列表与 source order;production resolver 用它验证 scope local 的 published-inventory identity。 -- `FrontendInventoryGateRegistry`:typed-dependent subtree 的 gate owner、header root、body root、deferred domain、readiness。 - `FrontendTypedLexicalBaseline`:参数、显式 typed local、已可静态确定的 interface-level source-facing slot baseline;production `TypedLexicalEnvironment` 将其作为冻结 fallback。 - `FrontendSuiteEntryRoots`:body layer 可进入的 callable/property initializer/supported block 根列表。 @@ -231,7 +256,7 @@ Interface phase 不得: - 发布 `expressionTypes()`。 - 发布 `resolvedMembers()` / `resolvedCalls()`。 -- 打开 `FOR_BODY` / `MATCH_SECTION_BODY` / `LAMBDA_BODY`,除非对应 gate readiness 已明确 published。 +- 将已转正 AST 节点的 body inventory 延迟到 typed fact 可用后;每个转正节点必须在本阶段无条件发布其 body scope、完整 declaration index 与 baseline。 - 将 `GdCompilerType` 写入 source-facing lexical baseline。 ### 4.3 Body `SuiteResolver` @@ -279,20 +304,23 @@ statement-local var type post procedure 互补而非绕过:二者共享 `VAR_T 2. Local stabilization runner:在 top binding overlay、当前 suite 已提交 typed fact、stable lexical inventory 之上解析 eligible `:=` initializer,并写入当前 statement 的 local slot pending overlay。 3. Chain binding runner:消费 top binding overlay 与 local stabilization pending / committed slot fact,发布 `resolvedMembers()` 与 chain-owned `resolvedCalls()` overlay。 4. Expr typing runner:消费 binding、member、call 与 local slot overlay,发布 `expressionTypes()` 与 bare-call `resolvedCalls()` overlay;`backfillInferredLocalType(...)` 仍只做 guard-only 检查。 -5. Var type post procedure:消费 expression type 与 source-facing local slot overlay,发布 final `slotTypes()` overlay。 -6. Pending fact flush:把当前 statement pending overlay 转入 current-suite committed overlay,供后续 statement / gate classifier 读取;不得在这一步写 stable side table。 +5. Feature-specific semantic route planning:仅在该 statement kind 已有 concrete owner 时运行,例如后续 `FOR_ITERATION_PLANNING`;消费 header expression facts,发布 dedicated route fact / source-facing refinement,但不能控制 body entry。普通 statement 省略本步。 +6. Var type post procedure:消费 expression type、feature-specific refinement 与 source-facing local slot overlay,发布 final `slotTypes()` overlay。 +7. Pending fact flush:把当前 statement pending overlay 转入 current-suite committed overlay,供后续 statement 读取;不得在这一步写 stable side table。 `SuiteResolver` 必须把 interface surface 的 declaration index 交给 visible-value resolver,以检查 scope 选中的 ordinary local 的 declaration identity 已由 baseline inventory 发布;这不是重新扫描普通 `var`,也不改变 resolver 现有 filtered-hit 规则。它还必须把 typed lexical baseline 传给 root 与 child typed environment,使参数和 ordinary local 在没有更新 overlay / stable slot fact 时仍有 immutable source-facing 初始类型。 -Suite 收敛后,不能把 current-suite committed overlay 整体打包为单个多 owner `FrontendAnalysisPatch`。它必须构造按 owner 有序的 patch transaction。嵌套 suite 只把该 transaction 追加到同一 callable-scoped export batch,不得在 child suite 边界直接 apply;根 callable suite 收敛后才由 batch 按追加顺序 apply 各 transaction。每个 transaction 内仍按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 顺序 apply 每个 owner patch。这样既保留 source-order suite 的前缀可见性,又不破坏 `FrontendAnalysisPatch` 现有单 stage / 单 owner 约束。 +Suite 收敛后,不能把 current-suite committed overlay 整体打包为单个多 owner `FrontendAnalysisPatch`。它必须构造按 owner 有序的 patch transaction。嵌套 suite 只把该 transaction 追加到同一 callable-scoped export batch,不得在 child suite 边界直接 apply;根 callable suite 收敛后才由 batch 按追加顺序 apply 各 transaction。每个 transaction 内按 top binding -> local stabilization -> chain binding -> expr typing -> optional feature-specific semantic route planning -> var type post 顺序 apply owner patch;缺少 feature-specific owner 的 statement 直接跳过该 stage。这样既保留 source-order suite 的前缀可见性,又不破坏 `FrontendAnalysisPatch` 现有单 stage / 单 owner 约束。 + +当前阶段的 transaction / batch 只表达分组、owner 顺序与推迟 stable publication,不表达原子提交。`FrontendPatchTransaction.applyTo(...)` 逐 patch 立即提交;后续 patch 失败时,先前 patch 已写入的 stable side table、`BlockScope` slot 与 binding payload 不回滚。`FrontendCallableExportBatch.applyTo(...)` 同样逐 transaction 提交,且不在 apply 前预检 queued transaction 之间的冲突。由于 child / sibling suite overlay 彼此隔离,已加入 batch 但尚未 apply 的 fact 不参与另一个 transaction 的 overlay conflict check;若错误 traversal、重复 publication 或未来 feature 使两个 transaction 对同一 key 发布不兼容 fact,冲突可能延迟到后一个 transaction apply 时才暴露。该 corner case 本阶段明确保留、不做 prepare/commit 修复;任何 apply 异常后,当前 `FrontendAnalysisData` 及其 stable scope 状态必须整体丢弃,不能继续进入 diagnostics-only phase、compile gate、lowering 或增量复用。 -不得重排 1-5。尤其是 chain binding 读取 receiver local slot 时,必须先看到 local stabilization 对前序 statement 或当前 statement 前序子过程写入的 exact slot fact;否则会重新打开 receiver 被误读成 `Variant` 的历史回归。 +不得重排 1-6,且 feature-specific stage 只能插在 expr typing 与对应 var type post 之间。尤其是 chain binding 读取 receiver local slot 时,必须先看到 local stabilization 对前序 statement 或当前 statement 前序子过程写入的 exact slot fact;否则会重新打开 receiver 被误读成 `Variant` 的历史回归。 Nested chain / argument retry 只能发生在当前 owner 子过程内部的非导出 transient cache 中。当前实现由 `FrontendBodyOwnerProcedures.BodyExpressionResolver` 的 `expressionTypes` / `finalizedExpressionTypes` / `resolvedCalls` 缓存、`FrontendChainReductionFacade.reducedChains` 以及 `FrontendChainReductionHelper` 的 bounded `finalizeWindow` retry 共同承担。Retry 可读取本次 reduction 已经推导出的 receiver / argument / step 临时事实,也可读取当前 statement 之前 owner 子过程已发布到 pending / committed overlay 的事实;但 retry 产生的中间 facts 不写入 `expressionTypes()` overlay、statement pending overlay、current-suite committed overlay 或 stable side table,也不能被 `TypedLexicalEnvironment` 的普通 lookup 读取。它们在当前 owner 子过程结束时丢弃。 `FrontendExprTypeAnalyzer` 对同一 expression / step key 只能在完成 retry、选定最终 `FrontendExpressionType` 后写入一次 expression type overlay。若同一 key 需要先得到 `DEFERRED`、暂定 `Variant` 或其他非最终状态,再得到 exact result,必须把中间值保存在 owner-local transient cache 或专用非导出状态里,而不是发布为 `expressionTypes()` 后再 narrowing / status upgrade。 -Gate classifier 属于 statement 结构处理的一部分,只能在其 header 所需的 top binding、local stabilization、chain binding 与 expr typing 子过程完成后运行。Classifier 可读取当前 statement pending overlay 与 current-suite committed overlay,但不能读取后续 statement facts。 +Feature-specific semantic route planning 若需要插入 statement owner 顺序,只能位于其 header 所需的 top binding、local stabilization、chain binding 与 expr typing 之后,并位于对应 var type post / statement flush 之前。它可以读取当前 statement pending overlay 与 current-suite committed overlay,但不能读取后续 statement facts,也不能控制 body inventory 或 child-suite entry。 第一版 body statement 支持面: @@ -302,7 +330,7 @@ Gate classifier 属于 statement 结构处理的一部分,只能在其 header - `AssertStatement`,保持现有 compile gate blocker。 - `IfStatement` / `ElifClause` / `else`,header 先解析,body 后递归。 - `WhileStatement`,condition 先解析,body 后递归。 -- `ForStatement` 第一版继续 fail-closed;后续 for-range plan 可作为 generic typed-dependent gate infra 的真实消费者。 +- `ForStatement` 由 for-range 阶段 B/D0 转为 structural supported:header-first,body 通过普通 child-suite path 进入;iteration planning 与 compile readiness 仍由后续 for-range 阶段独立处理。 - `MatchStatement`、`LambdaExpression`、block-local `const` 继续 deferred / unsupported,除非后续阶段显式转正。 ### 4.4 `TypedLexicalEnvironment` overlay @@ -320,7 +348,7 @@ Gate classifier 属于 statement 结构处理的一部分,只能在其 header 5. interface typed baseline 提供的冻结 source-facing fallback;`BlockScope` / `CallableScope` 继续提供 lexical inventory 名称查找。 6. class/global/singleton/type-meta lookup。 -Pending overlay 只对当前 statement 内后续 owner 子过程可见。`flushPendingFacts()` 后,它才变成 current-suite committed overlay,并对后续 statement / gate classifier 可见。Committed overlay 仍不是 stable publication。 +Pending overlay 只对当前 statement 内后续 owner 子过程可见。`flushPendingFacts()` 后,它才变成 current-suite committed overlay,并对后续 statement / feature-specific semantic route planning 可见。Committed overlay 仍不是 stable publication。 Parent environment 只能沿 parent 链读取上层 overlay,不能读取 child environment 的 pending 或 committed facts。Child suite 保持独立 overlay;其 facts 仅以 per-owner patch transaction 追加到 shared callable export batch,并在根 callable 完成前保持不在 stable side table 中。 @@ -341,7 +369,7 @@ Parent environment 只能沿 parent 链读取上层 overlay,不能读取 child - Owner procedure transient cache:owner 子过程私有,只给当前 chain / expr reduction 的 retry 回调读取;不属于 `TypedLexicalEnvironment`,不参与 flush / export,owner 子过程结束即丢弃。 - 当前 statement pending overlay:只给当前 statement 后续 owner 子过程读取;只接受每个 AST key 的最终 publication fact。 -- current-suite committed overlay:由 pending fact flush 合并而来,给后续 statement 与 gate classifier 读取;仍不是 stable publication。 +- current-suite committed overlay:由 pending fact flush 合并而来,给后续 statement 与 feature-specific semantic route planning 读取;仍不是 stable publication。 - `FrontendAnalysisData` stable side tables / `BlockScope` stable slot:只在 suite export 的 per-owner patch apply / stable export helper 后更新,供 diagnostics-only phase、compile gate 与 lowering 使用。 Overlay 的目标是模拟 Godot “当前 statement 已解析出的类型可被后续 statement 使用”的效果,同时避免提前污染 `FrontendAnalysisData` stable tables。 @@ -352,13 +380,17 @@ Overlay 的目标是模拟 Godot “当前 statement 已解析出的类型可被 - `flushPendingFacts(...)` 只把 pending overlay 合并到 current-suite committed overlay,并执行 owner、conflict、idempotent、exact-type 与 compiler-only guard。该 guard 必须与 suite export 使用同一 type-bearing field walker,避免 scratch 层接受 stable merge 层会拒绝的 compiler-only payload。 - `flushPendingFacts(...)` 不得通过 stable side table 的临时 whole-table publish 再“读回 overlay”。如果当前实现为了复用旧 analyzer helper 需要 `updateXxx(...)` / stable side table 作为中转,则该 helper 必须先被改写或隔离为 legacy path,不能把中转污染当作阶段性可接受状态。 - Suite 收敛时,current-suite committed overlay 只能导出为按 owner 有序的 patch transaction,不能导出为一个跨 owner `FrontendAnalysisPatch`。 -- 嵌套 suite 的 transaction 必须追加到 root callable 共享的 `FrontendCallableExportBatch`,不得在 child suite 收敛时独立 apply。根 callable suite 返回后,batch 才按追加顺序 apply 所有 transaction。 -- 每个 patch transaction 固定按 top binding -> local stabilization -> chain binding -> expr typing -> var type post apply;每个 step 只包含该 owner 的 facts。 +- 嵌套 suite 的 transaction 必须追加到 root callable 共享的 `FrontendCallableExportBatch`,不得在 child suite 收敛时独立 apply。根 callable suite 返回后,batch 才按追加顺序逐个 apply 已累积的 transaction;该顺序提交不构成原子批次,也不提供跨 queued transaction 预检或失败回滚。 +- 每个 patch transaction 固定按 top binding -> local stabilization -> chain binding -> expr typing -> optional feature-specific semantic route planning -> var type post apply;每个 step 只包含该 owner 的 facts。 - Stable side table 与 `BlockScope.resetLocalType(...)` 只能在 callable export batch 的 per-owner patch apply / stable export helper 中更新。 - Diagnostics-only phase、compile gate 与 lowering 只能读取 suite export 后的 stable facts。 - Nested supported suite 收敛后,必须把其 per-owner patch transaction 追加到外层 callable export batch;lexical visibility 仍由 scope graph 与 resolver filter 决定,不能因为 transaction 合并放宽 local 可见性。 -### 4.5 Feature gate 与 body readiness +### 4.5 已废弃:Feature gate 与 body readiness + +本节描述当前代码中仍存在的 typed-dependent gate scaffolding,仅用于界定遗留清理范围。 +它已被第 1.1 节的无条件结构性 inventory 决定取代,不得用于新增 feature;后续实现应删除 +本节所述的 registry/readiness body-entry 路径,而不是补全其 publication protocol。 新增 `FrontendInventoryGate` 记录 typed-dependent subtree 的待决状态。第一版至少表达: @@ -417,7 +449,7 @@ record FrontendInventoryGate( - `FrontendChainBindingPatch`:只包含 `resolvedMembers()` 与 chain-owned `resolvedCalls()` delta。 - `FrontendExprTypePatch`:只包含 `expressionTypes()` 与 bare-call `resolvedCalls()` delta。 - `FrontendVarTypePostPatch`:只包含 `slotTypes()` delta。 -- `FrontendPatchTransaction` 或等价 batch container,保存上述 patch 的有序列表并负责按固定 owner 顺序 apply。 +- `FrontendPatchTransaction` 或等价 ordered patch carrier,保存上述 patch 的有序列表并负责按固定 owner 顺序 apply;`Transaction` 在这里不表示原子提交,类型不提供跨 patch 预检或失败回滚。 - 旧 `FrontendAnalysisPatch`、`FrontendLocalSlotTypeUpdate` 迁入同一 package。旧 `FrontendAnalysisPatch` 只能作为 legacy single-stage patch / 测试兼容层或被拆解;suite export 生产路径不得再用它承载多 owner facts。`FrontendWindowPublicationSurface`、`FrontendWindowAnalysisContext` 若迁入该包,必须单独标记为 legacy shim,不属于 production overlay/export 参考资产。 保留 `FrontendLocalSlotTypeUpdate` / `FrontendAnalysisData.applyPatch(...)` 的核心规则,但 `applyPatch` 的输入必须是单一 owner patch。以下规则适用于每个 per-owner patch 的 merge: @@ -433,6 +465,8 @@ record FrontendInventoryGate( - `slotTypes()` 不允许同一 source slot 被不同类型覆盖;同类型 idempotent 允许。 - merge 前统一检查 compiler-only type 不泄漏。当前 `FrontendAnalysisData.checkPatchDoesNotLeakCompilerOnlyTypes(...)` 只覆盖 `expressionTypes()` 与 `slotTypes()`,阶段 C 必须把该检查扩展到 `symbolBindings().resolvedValue.type()`、`resolvedMembers()` 的 `receiverType` / `resultType`、`resolvedCalls()` 的 `receiverType` / `returnType` / `argumentTypes` / callable boundary parameter types。否则不能宣称 overlay export 已复用完整 compiler-only guard。 +上述 conflict、idempotent、local-slot 与 compiler-only 校验只约束当前 per-owner patch。`FrontendAnalysisData.applyPatch(...)` 在当前 patch 的 merge 前完成这些检查,但连续调用多个 patch 不会形成新的原子边界;fail-fast 只中止当前 apply 链路,不撤销同一 transaction 内已提交的 patch,也不撤销同一 callable batch 内已提交的 transaction。 + 新增或扩展统一 helper,例如 `checkNoCompilerOnlyLeakInPublishedFact(...)` / `FrontendPublishedFactTypeGuard`,用于 patch commit 与 typed overlay 写入两处。它必须遍历 `FrontendBinding`、`FrontendResolvedMember`、`FrontendResolvedCall`、`FrontendExpressionType`、`slotTypes()` value 与 `FrontendLocalSlotTypeUpdate` 中所有 user-visible `GdType`,并复用同一错误策略。Legacy window publication surface 不能作为该 helper 的正确性基线。 阶段 C 实施前,至少要明确并锁定下面的 guard 扩展 checklist: @@ -485,6 +519,10 @@ Overlay 可在导出前提供 effective type,但最终 stable `BlockScope.rese ### 4.8 Resolver 复用规则 +第 1.1 节已废弃本节后文的 typed-dependent readiness 规则。保留这些文字仅为识别当前 +`FrontendVisibleValueResolver` 的遗留 gate 分支;新增或转正 AST 节点必须以完整结构性 +inventory 和纯结构性 supported-body 判断进入 resolver,不得查询 typed readiness。 + `FrontendVisibleValueResolver` 继续一次性索引完整 source AST,并继续读取已经发布的完整 lexical inventory。完整 inventory 是 `DECLARATION_AFTER_USE_SITE` filtered hit 的前提条件;重构时不得让 resolver 自己扫描 AST 补找普通 `var`,也不得把 supported block inventory 缩成只包含当前 source-order 前缀的 incremental view。 新增 resolver 能力: @@ -529,7 +567,7 @@ Feature-specific header state 仍属于 gate header 语义,不得因为 body r - `gd.script.gdcc.frontend.sema.patch` 包。 - `FrontendOwnerPatch` 或等价 sealed interface。 - `FrontendPatchTransaction` 或等价有序 transaction 类型。 -- `FrontendCallableExportBatch`,累积单个 callable root 及其 nested suite 的 transaction,并只在 root suite 收敛后 apply。 +- `FrontendCallableExportBatch`,累积单个 callable root 及其 nested suite 的 transaction,并只在 root suite 收敛后按追加顺序逐个 apply;当前不做跨 queued transaction 预检,也不提供 batch 原子性或回滚。 - `FrontendTopBindingPatch`、`FrontendLocalTypeStabilizationPatch`、`FrontendChainBindingPatch`、`FrontendExprTypePatch`、`FrontendVarTypePostPatch` 等 per-owner patch carrier。 - patch package 内的 shared merge helper / type-bearing compiler-only guard / owner-field validator。 - `FrontendSuiteResolver` / `FrontendStatementResolver` 的 root-bounded statement dispatch 子框架。 @@ -935,6 +973,202 @@ whole-phase analyzer comparison path 及 window/patch compatibility shim。必 - `./gradlew classes --no-daemon --info --console=plain` 通过。 - 相关 targeted tests 使用 `script/run-gradle-targeted-tests.sh --tests ...` 通过。 +### 阶段 L 系列:删除 typed-dependent body gate 历史架构债务 + +第 1.1 节已确定:body 是否进入 shared semantic 只能由结构性支持面和完整 lexical +inventory 决定,typed fact 只能影响后续 refinement、semantic route 与 lowering。本阶段系列 +删除阶段 F 为 synthetic fixture 建立、但从未成为 production feature consumer 的 +typed-dependent gate scaffolding;不得将其扩展为 publication protocol、failure state 或新的 +feature integration API。 + +阶段 L 系列只依赖 `frontend_for_range_loop_implementation_plan.md` 的阶段 B 与阶段 D0: + +- 阶段 B 已为 `FOR_BODY` 发布 iterator、完整 ordinary local inventory、declaration index 与 + source-facing baseline。 +- 阶段 D0 已建立 header-only dispatch,并在 header facts flush 后通过普通 + `resolveChildSuite(...)` 进入 body;iterator 此时允许保持 `Variant` 或显式 declared baseline。 +- 阶段 B 在移除 shared semantic unsupported diagnostic 前,已经安装临时、无条件的 + `ForStatement` compile blocker,确保 for-range 阶段 F/G 尚未完成时不会进入 CFG/lowering。 + +阶段 L 系列**不依赖** for-range 阶段 C、D1、type-check、route-aware compile 解封、CFG 或 +lowering。iteration planning 与 iterator slot refinement 可以晚于 gate 删除;它们只能改变 typed +fact 和 lowering route,不能改变阶段 D0 已建立的 body entry。 + +对于阶段 L 时仍未转正的 lambda、match、block-local `const`、parameter default 等节点,必须 +保持纯结构性 unsupported / deferred boundary:不创建 `PENDING` gate、不查询 typed readiness、 +不因前缀或 header expression 的类型变化而改变 body entry。这里的“跳过 inventory”必须按 +feature-owned surface 理解:lambda 包括 parameter/capture/body,match 包括 pattern binding、guard +与 section body,block-local `const` 包括 declaration/initializer boundary;不能把它们错误归一化 +成只有 `BlockScopeKind` 的普通 block body。 + +实施规则: + +- 每个子阶段结束时保持 main/test source 可编译;不得先物理删除类型,再等待后续阶段清理消费者。 +- 每个 patch batch 最多修改三个文件;一个子阶段可以拆为多个 batch,并在 behavior batch 后运行 + focused tests、在子阶段结束时运行 compile check。 +- “structural support”只表示某种 AST/scope kind 的 inventory path 已实现;“structural + completeness”表示本次 interface surface 确实发布了完整事实。二者不得合并为一个 boolean。 +- compile readiness 不得进入 structural semantic support matrix。 + +#### 阶段 L0:冻结 for 前置与 compile safety bridge + +- [ ] 确认 for-range 阶段 B 与 D0 已完成,且 `FOR_BODY` 不再查询 gate registry。 +- [ ] 确认临时 `FrontendCompileCheckAnalyzer.handleForStatement(...)` blocker 只锚定 + `ForStatement`、不进入 body、不读取 iterable type / iteration plan / route。 +- [ ] 增加 for-only characterization:shared `analyze(...)` 无 `FOR_SUBTREE` unsupported; + `analyzeForCompile(...)` 有明确 `sema.compile_check` error,lowering pipeline 在 CFG 前停止。 + +验收细则: + +- `for item in values: var copy := item` 已进入 shared semantic,且即使 `values` 仍是 `Variant` + 也不改变 body entry。 +- `FrontendCfgGraphBuilder` 尚无 `ForStatement` 分支时,任何 `for-in` 都不能从 compile-only + boundary 泄漏到 CFG。 + +#### 阶段 L1:建立不可变 structural semantic support matrix + +- [ ] 新增 `FrontendBodySemanticSupportPolicy` 或等价不可变 helper,统一表达当前结构支持面; + `FrontendExecutableInventorySupport` 必须委托该事实源或被其替代,不能保留平行 allowlist。 +- [ ] policy 只能由 AST/scope kind 决定,不能读取 `GdType`、expression type、typed overlay、 + iteration plan、diagnostic state、compile surface 或 gate lifecycle。 +- [ ] policy 必须覆盖下列结构位置,且每一行都有 focused test。 + +结构支持矩阵最低合同: + +- function/constructor/if/elif/else/while/ordinary block body:发布 lexical inventory,进入 + `SuiteResolver`,不使用 deferred domain。 +- `for` header:使用外层 scope,只运行 header-only dispatch,不使用 deferred domain。 +- `FOR_BODY`:发布 lexical inventory,进入 `SuiteResolver`,不使用 deferred domain。 +- lambda parameter/capture/body:不发布 feature-owned inventory,不进入 `SuiteResolver`,domain 为 + `LAMBDA_SUBTREE`。 +- match pattern/guard/section body:不发布 feature-owned inventory,不进入 `SuiteResolver`,domain 为 + `MATCH_SUBTREE`。 +- block-local `const` declaration/initializer:不发布 local-const inventory,不进入 + `SuiteResolver`,domain 为 `BLOCK_LOCAL_CONST_SUBTREE`。 +- parameter default:不进入 body pipeline,domain 为 `PARAMETER_DEFAULT`。 +- 未知或 skipped structure:不发布 inventory,不进入 `SuiteResolver`,domain 为 + `UNKNOWN_OR_SKIPPED_SUBTREE`。 + +- [ ] enum switch 必须 exhaustive;新增 `BlockScopeKind` / `CallableScopeKind` 时必须显式选择 + policy,不能通过 `default -> EXECUTABLE_BODY` 自动放行。 + +验收细则: + +- `FOR_BODY` 的 policy 是 structural supported;lambda/match 不因 for 解封而改变。 +- policy 类型和测试中不存在 `PENDING`、`PUBLISHED`、readiness、route 或 compile-ready 状态。 + +#### 阶段 L2:增加 structural completeness fail-fast certificate + +- [ ] 在 `FrontendSuiteResolver` 进入 root/child body 前,通过 + `requireStructurallyCompleteBody(...)` 或等价 helper 验证: + - `scopesByAst()` 中 body root 映射到预期 identity / kind 的 `BlockScope`。 + - `FrontendSuiteEntryRoots` 明确包含该 supported body。 + - `FrontendBodyDeclarationIndex.containsBodyRoot(body)` 为 true,即使 body 没有 ordinary local。 + - 每个 index entry 的 declaration/binding/scope identity 一致,且 typed baseline 包含该 + source-facing declaration;`FOR_BODY` 的 iterator `Node` identity 也必须覆盖。 +- [ ] certificate 只读取 scope graph、suite entry roots、declaration index 与 baseline;不得读取 + pending/committed overlay、expression type、slot refinement 或 iteration plan。 +- [ ] 结构事实缺洞属于 phase protocol / programmer error,必须 fail-fast,不得伪装成源码 + diagnostic,也不得静默跳过 body。 + +验收细则: + +- scope 存在但 suite-entry、body-index、iterator entry 或 baseline 任一缺失时 focused test + fail-fast。 +- header resolved type 变化不会改变 certificate 结果。 + +#### 阶段 L3:迁移 SuiteResolver 与 body-local consumer + +- [ ] `FrontendSuiteResolver`、`FrontendSuiteContext` 与 + `FrontendBodyOwnerProcedures.eligibleInferredLocalScope(...)` 改为消费 structural policy 与 + completeness certificate,不再用 gate readiness 决定 suite entry / ordinary local + stabilization。 +- [ ] `FrontendSuiteContext.visibleValueDomainForCurrentBody()` 对 supported body 直接生成 + `EXECUTABLE_BODY`;unsupported body 根据 policy 返回精确 deferred domain,未知 kind 不得 + fallback 为 `EXECUTABLE_BODY`。 +- [ ] 本阶段允许 `FrontendInterfaceSurface` 暂时继续携带 registry 供尚未迁移的 resolver 使用; + 不得为了提前删除字段形成不可编译的中间状态。 + +#### 阶段 L4:迁移 FrontendVisibleValueResolver 的结构封口 + +- [ ] 删除 resolver 的 gate registry constructor dependency、request-domain readiness + normalization、`gateBodyBoundary(...)`、`isNearestOwningGateReady(...)`、 + `nearestOwningGate(...)` 及所有 `SUPPORTED + PUBLISHED` 分支。 +- [ ] 保留 request-domain hard boundary、AST boundary 与 current-scope fail-closed。删除的是 typed + readiness 条件,不是这三类结构检查本身;AST boundary 与 current-scope 两层结构封口都不得 + 删除。 +- [ ] `ForStatement.body()`、for header edge 与 `FOR_BODY` current scope 直接允许 normal lookup; + lambda/match/const/parameter-default 根据 structural policy 直接返回 deferred boundary。 +- [ ] 保留 declaration-order、initializer self-reference、skipped subtree 以及 visible block-local + `const` 不得 fallback 到 outer same-name binding 的合同。 +- [ ] 同批更新 `FrontendBodyOwnerProcedures` 的 resolver 构造点和 focused resolver tests。 + +#### 阶段 L5:删除 synthetic statement classifier + +- [ ] 删除 `FrontendStatementResolver.OwnerProcedures.runGateClassifier(...)`、 + `resolveSupportedRoot(...)` 中的调用及 synthetic classifier fixtures。 +- [ ] 该删除只针对 body-entry classifier。for-range 阶段 D1 后续可以在 header expr typing 后、 + iterator var type post 前新增 concrete `runForIterationPlanning(...)`;不得复用 classifier 名称、 + lifecycle 或 readiness 语义。 +- [ ] 真实 feature typed result 只能驱动 refinement、semantic route、type diagnostic 或 lowering + route,不能决定是否调用 `resolveChildSuite(...)`。 + +#### 阶段 L6:删除 interface gate producer 与 surface dependency + +- [ ] 移除 `FrontendInterfacePhase` 的 `FrontendInventoryGateRegistry.Builder`、 + `addPendingGate(...)`、match registration 及 lambda/match/block-local `const` 调用点。 +- [ ] 已转正 `for` 继续使用阶段 B/D0 的 structural path;未转正节点只跳过其 feature-owned + inventory,并由 structural policy / AST boundary 表达限制。 +- [ ] `FrontendInterfaceSurface`、`FrontendSuiteEntryRoots` 的字段、构造器与文档不再产出、持有 + 或描述 gate registry。 + +#### 阶段 L7:迁移测试合同 + +- [ ] 重写 `FrontendInterfacePhaseTest`、`FrontendSuiteResolverTest`、 + `FrontendVisibleValueResolverTest` 中 pending/published gate、registry 与 classifier fixture。 +- [ ] 新增/保留以下结构合同: + - `FOR_BODY` 在 typed resolution 前已有 iterator、ordinary local、index、baseline 与 suite entry。 + - supported kind 但 certificate 任一事实缺失时 fail-fast。 + - iterable 分别为 exact、`Variant`、error fact 时,body inventory 与 entry 保持不变。 + - nested for 中 lambda/match/const 仍保持 structural deferred。 + - visible block-local `const` 不 fallback;future declaration 与 initializer self-reference filtered + hit 不退化。 + - for-only compile path 在 CFG 未支持时命中临时 blocker。 +- [ ] 删除手工推进 gate lifecycle 的 fixture 与 `FrontendInventoryGateRegistryTest`。 + +#### 阶段 L8:物理删除 gate lifecycle 类型 + +- [ ] 仅在 L3-L7 的生产/测试消费者全部清除后,物理删除 + `FrontendInventoryGate`、`FrontendInventoryGateRegistry`、 + `FrontendInventoryGateStatus`、`FrontendBodyInventoryReadiness`。 +- [ ] 不保留 compatibility shim、empty registry、反射检查或公开 + `markSupported(...)` / `markBodyInventoryPublished(...)` setter。 +- [ ] `src/main/java` 与 `src/test/java` 中不存在上述类型、`runGateClassifier`、 + `markBodyInventoryPublished` 或 `isBodyInventoryReady` 引用。 + +#### 阶段 L9:同步文档、风险与完成定义 + +- [ ] 删除或改写本文第 4.5、4.8、阶段 F 中把 `SUPPORTED + PUBLISHED` 作为 body entry 条件的 + 历史设计;复核第 6/7/8 节不再保留 readiness registry 不变量。 +- [ ] 更新 `frontend_rules.md`:`for` 已进入 shared semantic,但在 route-aware compile gate 落地前 + 由临时 blocker 阻断;compile gate 不得把该 blocker 反向变成 semantic body gate。 +- [ ] 更新 `frontend_segmented_type_resolution_pipeline_execution_summary.md`、visible-value + resolver、interface phase 与 for-range 文档,明确 structural policy、completeness certificate、 + semantic body entry 与 compile route readiness 四者分离。 + +阶段 L 系列完成验收: + +- `FrontendInterfaceSurface`、`FrontendSuiteContext`、`FrontendSuiteResolver`、 + `FrontendBodyOwnerProcedures` 与 `FrontendVisibleValueResolver` 均不接收、持有或查询 gate + registry;不存在带 body root / registry 参数的 readiness API。 +- 已转正 body 的 resolver/suite entry 不依赖 typed fact;改变 header expression 的 resolved type + 不会改变 scope inventory、declaration index、completeness certificate 或 child-suite dispatch。 +- `for i in values: var local := i` 在 iterable typing 前已有 `i` baseline 与 `local` 完整 inventory; + 两者都由 declaration index 覆盖,body 解析后 `local` 通过既有 owner/export 路径发布 slot type。 +- 未转正节点保持 fail-closed,但没有 `PENDING` / `PUBLISHING` / `PUBLISHED` lifecycle;其 + deferred diagnostic/domain 只由 AST/scope 的结构性 unsupported boundary 决定。 +- 相关 focused tests 与 `./gradlew classes --no-daemon --info --console=plain` 通过。 + ## 6. 必须新增或调整的测试 基础设施测试: @@ -942,7 +1176,7 @@ whole-phase analyzer comparison path 及 window/patch compatibility shim。必 - `FrontendAnalysisDataTest`:patch merge 新 key / idempotent / conflict / stable reference。 - `FrontendAnalysisDataTest`:`FrontendOwnerPatch` / per-owner patch merge 时只能携带该 owner 允许的 side table 或 local slot update;跨 owner payload fail-fast。 - `FrontendAnalysisDataTest`:`FrontendLocalTypeStabilizationPatch` 是唯一允许携带 `FrontendLocalSlotTypeUpdate` 的 patch,且不能同时携带独立 `symbolBindings()` delta。 -- `FrontendAnalysisDataTest`:`FrontendPatchTransaction` 按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 顺序 apply;乱序或重复 owner patch fail-fast。 +- `FrontendAnalysisDataTest`:`FrontendPatchTransaction` 按 top binding -> local stabilization -> chain binding -> expr typing -> optional feature-specific semantic route planning -> var type post 顺序 apply;乱序或重复 owner patch fail-fast。 - `FrontendAnalysisDataTest`:`expressionTypes()` patch 对同一 stable key 的 same-status different publishedType、status change、Variant-published fact -> exact fact 仍 fail-fast,证明没有 slot-style narrowing 例外。 - `FrontendAnalysisDataTest`:compiler-only type 泄漏 guard 覆盖 `expressionTypes()`、`slotTypes()`、`localSlotTypeUpdates()`。 - `FrontendAnalysisDataTest`:`applyPatch` 拒绝 `symbolBindings()` 中 `resolvedValue.type()` 为 `GdCompilerType` 的 patch entry。 @@ -969,7 +1203,9 @@ Interface phase 测试: - `FrontendInterfacePhaseTest.buildsSupportedBodyDeclarationIndexTypedBaselineAndSuiteEntryRoots`:构建 `FrontendBodyDeclarationIndex`,完整记录 supported block local declaration source order,同时锚定 typed baseline 与 suite entry roots 不写 stable typed side table。 - `FrontendInterfacePhaseTest.keepsFutureDeclarationVisibleToResolverThroughCompleteBodyIndex`:`var first := second; var second := 1` 仍通过完整 inventory 被 resolver 过滤为 `DECLARATION_AFTER_USE_SITE`。 -- `FrontendInterfacePhaseTest.recordsPendingTypedDependentGatesWithoutOpeningTheirBodies`:记录 `FrontendInventoryGate(PENDING, NOT_PUBLISHED)`,但不发布 gate body inventory,且 `for` / `match` / lambda / block-local `const` 未转正时仍 fail-closed。 +- `FrontendInterfacePhaseTest.recordsStructuralSupportWithoutTypedBodyGates`:`FOR_BODY` 已进入 + suite entry / declaration index / baseline;lambda、match、block-local `const` 仍不发布其 + feature-owned inventory,且 interface surface 不产出 gate registry。 - `FrontendInterfacePhaseTest.typedLexicalBaselineRejectsCompilerOnlySourceFacingTypes`:source-facing typed baseline 写入 `GdCompilerType` 时 fail-fast。 - `FrontendSemanticAnalyzerFrameworkTest`:skeleton / scope / variable diagnostics snapshot boundary 不漂移。 @@ -977,7 +1213,7 @@ Suite/body pipeline 测试: - `FrontendSuiteResolverTest`:source-order statement traversal 与 AST order 一致。 - `FrontendSuiteResolverTest`:fake owner procedure 只收到当前 statement/header/expression root,不能拿到 module `SourceFile` root 重新 whole-module walk。 -- `FrontendSuiteResolverTest`:单个 statement 内 owner procedure 顺序固定为 top binding -> local stabilization -> chain binding -> expr typing -> var type post。 +- `FrontendSuiteResolverTest`:普通 statement 的 owner procedure 顺序固定为 top binding -> local stabilization -> chain binding -> expr typing -> var type post;for-range D1 完成后,`FOR_ITERATION_PLANNING` 只能插在 header expr typing 与 iterator var type post 之间。 - `FrontendSuiteResolverTest`:chain binding 消费 receiver local 时看到 local stabilization 已写入的 exact slot fact。 - `FrontendSuiteResolverTest`:nested chain / argument retry 可读取 owner-local transient facts,但这些 facts 不对其他 owner procedure 或 `TypedLexicalEnvironment` 普通 lookup 可见。 - `FrontendSuiteResolverTest`:retry 后 final result 只写入同一 key 的最终 fact,不会把 earlier intermediate fact 写入 stable 或 committed overlay。 @@ -985,20 +1221,26 @@ Suite/body pipeline 测试: - `FrontendSuiteResolverTest`:local stabilization patch apply 后由 commit helper 派生刷新同 declaration 的 `symbolBindings()` payload,而不是通过同一个 patch 携带 binding delta。 - `FrontendSuiteResolverTest`:`if` / `elif` / `else` / `while` header 先解析,body 后递归。 - `FrontendSuiteResolverTest`:unsupported body 不进入 resolver。 -- `FrontendSemanticAnalyzerFrameworkTest`:默认 interface/body pipeline 发布 body facts、nested source-order facts,并保持 unsupported `for` / `match` / block-local `const` fail-closed;测试不再通过 shared analyzer legacy whole-phase baseline。 +- `FrontendSemanticAnalyzerFrameworkTest`:默认 interface/body pipeline 发布 body facts、nested source-order facts,`for` 进入 shared semantic,match / lambda / block-local `const` 继续 structural fail-closed;测试不再通过 shared analyzer legacy whole-phase baseline。 - `FrontendExprTypeAnalyzerTest`:owner-specific baseline 只能在测试内显式串联 focused owner analyzers;不得恢复 shared analyzer legacy bridge 或旧 expr-phase slot mutation。 Resolver 测试: - 同 block future local:`var x := y; var y := 1`,必须得到 `DECLARATION_AFTER_USE_SITE` filtered hit。 - initializer self-reference:`var x := x`,必须得到 `SELF_REFERENCE_IN_INITIALIZER` filtered hit。 -- pending gate body lookup 必须是 `DEFERRED_UNSUPPORTED`,不能 fallback。 -- typed-dependent body scope 已存在但 gate 缺失、`PENDING`、`SUPPORTED + NOT_PUBLISHED`、`SUPPORTED + PUBLISHING`、`UNSUPPORTED` 时,body lookup 都必须是 `DEFERRED_UNSUPPORTED` 或对应 deferred domain。 -- `SUPPORTED + PUBLISHED` 后,body 内 gate-owned binding 和 body local lookup 返回 `FOUND_ALLOWED`,并继续保留 declaration-after-use filtered hit。 -- Request-domain gate 测试:PUBLISHED gate body lookup 必须由统一 request factory 进入 `EXECUTABLE_BODY`,或由同一 readiness policy normalization 后进入 lookup;未发布 gate、unsupported gate、无 owning gate 的 deferred-domain request 仍在 domain gate fail-closed。 -- AST boundary gate 测试:gate body edge 在非 `PUBLISHED` 状态继续返回 deferred boundary,`SUPPORTED + PUBLISHED` 后才放行;header edge 只在 header/classifier 上下文放行,不能提前打开 body edge。 -- Current-scope gate 测试:合成 `FOR_BODY` / typed-dependent body scope 即使没有 AST boundary,也必须在非 `PUBLISHED` 状态 fail-closed;`PUBLISHED` 后才允许 normal lookup。 -- 三闸同步测试:同一 synthetic gate 的 body lookup 必须覆盖 domain、AST boundary、current-scope 三处,防止只改其中一处导致“看似转正但实际仍被另一处封口”。 +- Structural request-domain 测试:supported body request 直接使用 `EXECUTABLE_BODY`;lambda、match、 + block-local `const`、parameter default 与 unknown/skipped request 使用精确 deferred domain,不存在 + readiness normalization。 +- AST boundary 测试:`ForStatement.body()` 与 for header edge 不再封口;lambda body、match + pattern/guard/body、block-local `const` initializer 与 parameter default 继续 structural + fail-closed。 +- Current-scope 测试:真实 `FOR_BODY` current scope 允许 normal lookup;synthetic + `LAMBDA_BODY`、`MATCH_SECTION_BODY` 与 lambda callable scope 即使缺少 AST edge 也继续 + fail-closed。 +- 双重结构封口测试:unsupported feature 同时覆盖 AST boundary 与 current-scope backstop,防止 + 只改一处导致 outer binding fallback。 +- Structural completeness 测试:supported body 的 scope、suite entry、body index、baseline 或 + iterator identity 任一缺失时 fail-fast,不能返回普通 `NOT_FOUND` 或静默跳过 suite。 Local stabilization 测试: @@ -1009,17 +1251,24 @@ Local stabilization 测试: - assignment initializer / bare `TYPE_META` / dynamic fallback 保持 `Variant`。 - `FrontendExprTypeAnalyzerTest` 继续覆盖 backfill guard:inventory-seeded `Variant` 不被 expr phase narrowing,已稳定同类型 no-op,已稳定异类型 fail-fast,compiler-only initializer fact fail-fast。 -Typed-dependent gate 测试: +Structural body support 测试: -- 前缀 `:=` local 稳定后,后续合成 gate classifier 能读取 typed fact。 -- gate 转正只产生 `SUPPORTED + NOT_PUBLISHED`,不得使 body resolver / binder 放行。 -- body inventory publication 成功后,readiness 原子推进为 `PUBLISHED`。 -- 所有 callable-local inventory 消费者对 typed-dependent body 使用同一 readiness 查询,不允许各自直接判断 `BlockScopeKind`。 -- feature-specific `GdCompilerType` 不出现在 `expressionTypes()` / source-facing `slotTypes()` / ordinary `ScopeValue.type()`。 +- structural semantic support matrix 不读取 typed fact、overlay、iteration plan、diagnostic state 或 + compile readiness。 +- `FOR_BODY` 是 structural supported;lambda/match/const/parameter default 的 policy 保持精确 + unsupported/deferred domain,新增 scope kind 没有 allow-by-default。 +- 前缀 `:=` local 稳定后,后续 for header planning 可以读取 typed fact,但该事实不会改变 body + inventory、completeness certificate 或 suite entry。 +- for iterator 与 ordinary local 都有 declaration-index entry 和 source-facing baseline;缺失任一 + entry 时 fail-fast。 +- feature-specific `GdCompilerType` 不出现在 `expressionTypes()` / source-facing `slotTypes()` / + ordinary `ScopeValue.type()`,也不进入 structural policy/certificate。 Compile gate 测试: - interface/body facts 中残留 `DEFERRED` / `FAILED` / `UNSUPPORTED` 时仍被 compile gate 阻断。 +- for-range route-aware compile policy 尚未落地时,for-only source 由临时 statement-root + `sema.compile_check` blocker 阻断,不能进入 `FrontendCfgGraphBuilder`。 - upstream diagnostic 去重跨 interface/body phase 生效。 - `analyze(...)` 与 `analyzeForCompile(...)` split 不变。 @@ -1027,11 +1276,11 @@ Compile gate 测试: ### R1:side-table 冲突被静默覆盖 -缓解:overlay export 必须通过 per-owner patch merge API;默认不同 value fail-fast。需要覆盖 overlay shadow stable、idempotent、conflict tests 与 patch transaction 顺序 tests。 +缓解:overlay export 必须通过 per-owner patch merge API;默认不同 value fail-fast。需要覆盖 overlay shadow stable、idempotent、conflict tests 与 patch transaction 顺序 tests。这里的 fail-fast 只阻止当前冲突 patch 覆盖既有 value,不意味着 transaction 或 callable batch 失败后无部分提交;非原子失败语义见第 4.3、4.6 节与 R18。 ### R2:resolver 看不到未来声明 -缓解:interface phase 必须基于基础结构层已发布的 inventory 为 supported suite 建立完整 local declaration index。禁止 resolver 自己扫描普通 `var` 弥补缺口;resolver 通过 scope 的已发布 inventory 完成名称查找,并读取 index 与 readiness 验证该命中属于可解析 body。现有 source byte range filter 继续产出 declaration-after-use 与 initializer self-reference provenance;仅有 local declaration `sourceOrder` 不能替代任意 use-site 的位置模型。参见第 3.2 节:完整 inventory 是 resolver filtered-hit 模型的前提,不是为了与 Godot source-order analysis 对立。 +缓解:interface phase 必须基于基础结构层已发布的 inventory 为 supported suite 建立完整 local declaration index。禁止 resolver 自己扫描普通 `var` 弥补缺口;resolver 通过 scope 的已发布 inventory 完成名称查找,并读取 index 与 structural completeness certificate 验证该命中属于可解析 body。现有 source byte range filter 继续产出 declaration-after-use 与 initializer self-reference provenance;仅有 local declaration `sourceOrder` 不能替代任意 use-site 的位置模型。参见第 3.2 节:完整 inventory 是 resolver filtered-hit 模型的前提,不是为了与 Godot source-order analysis 对立。 ### R3:scope slot mutation 与 published binding payload 脱节 @@ -1041,9 +1290,13 @@ Compile gate 测试: 缓解:`FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 必须是 strict no-op / guard-only;测试同时锁住“不调用 `BlockScope.resetLocalType(...)`”、“不刷新 `symbolBindings()` payload”、“不生成 `FrontendLocalSlotTypeUpdate`”。需要新的 narrowing 能力时只能扩展 local stabilization。 -### R5:gate supported 与 body inventory readiness 漂移 +### R5:structural capability 与实际 inventory completeness 被混为一谈 -缓解:`bodyInventoryReadiness` 是唯一可查询事实;`SUPPORTED` 只是 classifier 结果。resolver、variable analyzer、local stabilization、var type post、compile check 都必须通过共享 readiness 查询,测试覆盖 `SUPPORTED + NOT_PUBLISHED` 与 `SUPPORTED + PUBLISHING` 继续 fail-closed。 +缓解:immutable structural semantic support matrix 只回答某种 AST/scope kind 的 inventory path +是否已实现;`FrontendSuiteEntryRoots`、`FrontendBodyDeclarationIndex`、typed baseline 与 scope +identity 共同证明本次 interface surface 的 structural completeness。`canPublish...` 或等价 +allowlist 不能单独充当 body-entry certificate。缺失结构事实必须 fail-fast,不能静默跳过 body, +也不能重新引入 `PENDING` / `PUBLISHED` lifecycle。 ### R6:`SuiteResolver` 绕过 phase owner 边界 @@ -1053,13 +1306,16 @@ Compile gate 测试: 缓解:interface phase、body statement、suite export、diagnostics-only phase 都有明确 diagnostics snapshot 边界;新增跨 phase duplicate suppression tests。若顺序需要调整,必须更新 framework probe tests 与文档。 -### R8:unsupported subtree 被过早打开 +### R8:unsupported subtree 被 structural support matrix 过早打开 -缓解:pending gate 默认 fail-closed;只有 classifier 明确返回 supported 且 `bodyInventoryReadiness == PUBLISHED`,resolver 才能把对应 body 当普通 executable body。 +缓解:support matrix 对 lambda parameter/capture/body、match pattern/guard/body、block-local +`const` initializer、parameter default 和 unknown/skipped structure 显式返回各自 deferred domain; +新增 scope/AST kind 必须通过 exhaustive mapping 显式选择 policy。AST boundary 与 current-scope +backstop 同时保留,不能依赖 `default -> EXECUTABLE_BODY`。 ### R9:compiler-only type 泄漏 -缓解:typed overlay、per-owner patch merge 与 local slot update 都做 `GdCompilerType` guard;feature-specific compiler state 只能通过专用 contract 给 CFG/lowering。当前 `checkPatchDoesNotLeakCompilerOnlyTypes(...)` 只完整覆盖 expression / slot / local update 路径,必须扩展到 `symbolBindings()`、`resolvedMembers()`、`resolvedCalls()` 的 type-bearing payload 后,本风险才算关闭。`FrontendWindowPublicationSurface` 不能作为 guard 完整性的基线,因为 VarTypePost window caller 在 surface guard 生效前已经直接写入 stable `slotTypes()`。 +缓解:阶段 C3 已通过 `FrontendPublishedFactTypeGuard` shared walker 覆盖 binding/member/call/expression/slot/update 六类 source-facing typed payload,并接入 patch、overlay 与保留的 whole-table publication API。feature-specific compiler state 仍只能通过专用 contract 给 CFG/lowering;新增 type-bearing payload 时必须同步扩展 shared walker 与 regression tests,否则本风险重新打开。`FrontendWindowPublicationSurface` 不能作为 guard 完整性的基线,因为历史 VarTypePost window caller 曾在 surface guard 生效前直接写入 stable `slotTypes()`。 补充:如果保留 `updateXxx(...)` whole-table publish 或可变 stable side-table 引用,它们也必须被纳入同一风险面。只要 production body path 还能通过 `analysisData.xxx().put()/clear()/putAll()`、`updateXxx(...)` whole-table replace 或 window caller 中转直接触达 stable typed table,就不能宣称 overlay export 安全性已经被证明。 @@ -1069,15 +1325,19 @@ Compile gate 测试: ### R11:实现一次性改动过大 -缓解:先冻结资产边界,再落地 interface 数据结构、overlay、suite skeleton、owner 子过程、typed-dependent gate、diagnostics、默认切换。每阶段都应有 targeted tests。 +缓解:先冻结资产边界,再落地 interface 数据结构、overlay、suite skeleton、owner 子过程、diagnostics 与默认切换;历史 typed-dependent gate 按 L0-L9 的 compile-safe 小阶段迁移并最后删除类型。每个子阶段都应有 targeted tests,单个 patch batch 不超过三个文件。 ### R12:statement 内 owner 顺序或 overlay 导出时机漂移 -缓解:第 4.3 节的 owner procedure 顺序不可重排;第 4.4 节的 pending -> committed -> stable export 时机不可折叠。Statement flush 不写 stable side table,suite export 只能通过按 owner 有序的 patch transaction 更新 stable facts,suite export 后 diagnostics-only phase 才能运行。测试必须覆盖 chain binding 读取 receiver local 时已经看到 local stabilization 的 exact slot fact,以及 suite export 前后 stable side table / `BlockScope` 状态。 +缓解:第 4.3 节的 base owner procedure 顺序不可重排;feature-specific semantic route owner 只能在 expr typing 与对应 var type post 之间插入。第 4.4 节的 pending -> committed -> stable export 时机不可折叠。Statement flush 不写 stable side table,suite export 只能通过按 owner 有序的 patch transaction 更新 stable facts,suite export 后 diagnostics-only phase 才能运行。测试必须覆盖 chain binding 读取 receiver local 时已经看到 local stabilization 的 exact slot fact、for planning 读取 header expr fact,以及 suite export 前后 stable side table / `BlockScope` 状态。 -### R13:resolver 三道封口只打开了一道 +### R13:resolver structural domain、AST edge 与 current scope 漂移 -缓解:request-domain gate、AST boundary gate、current-scope gate 都必须由同一个 owner/body/domain readiness policy 条件化。PUBLISHED gate body 的 resolver request 不能继续裸传 `FOR_SUBTREE` 并被 domain gate 提前拒止;AST boundary 不能继续按 `ForStatement.body()` 恒定封口;current-scope 也不能只看 `BlockScopeKind.FOR_BODY`。Stage F synthetic gate tests 必须逐一覆盖三道封口。 +缓解:supported `FOR_BODY` 的 request domain、`ForStatement.body()`/header AST edge 与 current +scope 必须同时允许 ordinary executable lookup;unsupported lambda/match/const/parameter-default +则由同一 structural policy 返回精确 deferred domain。request-domain hard boundary、AST boundary +与 current-scope backstop 仍是三类独立检查,但不再读取 readiness registry。测试必须同时覆盖 +supported for 的三处放行与 unsupported feature 的 AST/current-scope 双重封口。 ### R14:retry 中间 expression type 被导出导致 patch 冲突 @@ -1085,7 +1345,7 @@ Compile gate 测试: ### R15:single-stage patch 被误用为 multi-owner suite export -缓解:suite export 生产路径不得构造跨 owner `FrontendAnalysisPatch`。旧 `FrontendAnalysisPatch` 迁入 patch package 后只能作为 legacy single-stage patch / 测试兼容层或被拆解;`FrontendPatchTransaction` 必须按固定 owner 顺序 apply per-owner patches,并拒绝乱序、重复 owner 或跨 owner payload。 +缓解:suite export 生产路径不得构造跨 owner `FrontendAnalysisPatch`。旧 `FrontendAnalysisPatch` 迁入 patch package 后只能作为 legacy single-stage patch / 测试兼容层或被拆解;`FrontendPatchTransaction` 必须按固定 owner 顺序 apply per-owner patches,并拒绝乱序、重复 owner 或单一 patch 内跨 owner 的 payload。该 per-patch owner 校验不构成 callable batch 的跨 transaction 预检。 ### R16:把 whole-module analyzer 包装成 body runner @@ -1095,34 +1355,38 @@ Compile gate 测试: 缓解:`FrontendWindowPublicationSurface` 的 direct API 可以表达 scratch-over-stable,但现有 analyzer caller 已经违反该模型:`FrontendVarTypePostAnalyzer.analyzeInWindow(...)` 直接 clear/write stable `slotTypes()`,再复制到 window scratch。阶段 A 必须把该类型降级为 legacy shim;阶段 C overlay 必须独立实现和验证;阶段 E var type post 重写不得复用旧 window path。任何以 `FrontendWindowPublicationSurfaceTest` 代替 overlay isolation test 的验收都无效。 +### R18:ordered transaction / callable batch 被误认为原子提交 + +当前限制:`FrontendPatchTransaction` 只保证 owner 顺序,`FrontendCallableExportBatch` 只保证延迟到 root callable 返回后按追加顺序 apply。二者都不做整体 prepare、不提供失败回滚;batch 也看不到 queued transaction 之间尚未进入 stable state 的冲突。后续 patch / transaction 失败时,之前已经 apply 的 stable side table、local slot 与 binding payload 会保留。本阶段不修复该 corner case;代码与文档必须持续声明 non-atomic 合同,production path 必须传播 apply 异常并丢弃整个 `FrontendAnalysisData`。未来若引入增量复用、可恢复 diagnostics 或继续分析语义,必须先落地覆盖整个 callable batch 的两阶段 prepare/commit,而不能只给单个 patch 增加预检。 + ## 8. 完成定义 本计划完成时应满足: - frontend shared semantic 默认使用 interface/body pipeline,且现有 supported surface 行为等价。 - 过渡用 `FrontendSegmentedSemanticScheduler` 与 `segmentedSemanticRunner` 已删除,或只作为明确隔离的测试辅助存在。 -- interface phase 建立完整 local declaration index 与 pending gate registry,但不做 body typed resolution。 -- `SuiteResolver` 按 source order 解析 supported body,并在 child body 前完成必要 readiness / inventory publication。 -- `SuiteResolver` 在每个 statement 内固定按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 调用 statement-local owner procedure。 +- interface phase 建立完整 local declaration index、typed baseline 与 suite entry roots,但不做 body typed resolution,也不产出 gate registry。 +- `SuiteResolver` 按 source order 解析 supported body,并在 child body 前验证 structural completeness certificate;typed refinement 不参与 body-entry 决策。 +- `SuiteResolver` 的 base statement owner 顺序固定为 top binding -> local stabilization -> chain binding -> expr typing -> var type post;concrete feature-specific semantic route owner 只能插在 expr typing 与对应 var type post 之间。 - Production SuiteResolver path 不调用 `analyzeInWindow(...)`、不从 module `SourceFile` root 启动内部 `AstWalker...walk(...)`,也不通过整表 `updateXxx(...)` 表达 body typed result。 - 五个 owner analyzer 的 whole-module walker state inventory 已关闭,并已用 statement-local rewrite 替代 production body path;现有 walker 只可作为 legacy comparison path 或删除对象存在。 - typed overlay 能区分 current statement pending facts 与 current suite committed facts,export 前不污染 stable side table 或 `BlockScope`;该标准明确排除旧 `FrontendVarTypePostAnalyzer.analyzeInWindow(...)` 的 stable `slotTypes()` clear/write 模式。 -- `FrontendAnalysisData` 支持安全 per-owner patch merge,并保持 stable reference 合同。 -- Suite export 使用按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 顺序 apply 的 `FrontendPatchTransaction` 或等价机制;生产路径不使用 single-stage `FrontendAnalysisPatch` 承载多 owner facts。 +- `FrontendAnalysisData` 支持带 conflict / guard 校验的 per-owner patch merge,并保持 stable reference 合同;这里的安全边界只覆盖当前 patch,不包含跨 patch 原子性。 +- Suite export 使用按 top binding -> local stabilization -> chain binding -> expr typing -> optional feature-specific semantic route planning -> var type post 顺序 apply 的 `FrontendPatchTransaction` 或等价机制;生产路径不使用 single-stage `FrontendAnalysisPatch` 承载多 owner facts。该 transaction 与 callable batch 明确为 non-atomic ordered apply,失败后必须丢弃当前 analysis state。 - 所有 production patch 相关类型位于 `gd.script.gdcc.frontend.sema.patch` 包,包括旧 `FrontendAnalysisPatch`、`FrontendLocalSlotTypeUpdate`、新建 per-owner patch 类型、transaction 与 shared merge / guard helper。`FrontendWindowPublicationSurface` / `FrontendWindowAnalysisContext` 若保留或迁入该包,也必须标记为 legacy shim,不能作为 production overlay/export 参考。 - patch commit 与 typed overlay 写入的 compiler-only guard 覆盖所有 user-visible type-bearing publication surfaces:`symbolBindings()`、`resolvedMembers()`、`resolvedCalls()`、`expressionTypes()`、`slotTypes()`、`localSlotTypeUpdates()`;guard 完整性不得以 `FrontendWindowPublicationSurface` 行为作为证明。 - shared compiler-only walker 同时用于 patch commit、overlay pending write、overlay flush,以及任何保留的 source-facing whole-table publication API;新增 type-bearing payload 时必须同步更新该 walker 与对应 regression tests。 - production body path 不通过 `FrontendAnalysisData.symbolBindings()/resolvedMembers()/resolvedCalls()/expressionTypes()/slotTypes()` 返回的可变 stable 引用直接 `put()` / `clear()` / `putAll()`,也不通过 `updateXxx(...)` whole-table replace 把 body typed facts 中转到 stable side table 后再校验。 - `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 不改写 `BlockScope`、不刷新 `symbolBindings()` payload、不产生 slot update,只保留 guard-only 协议检查。 - supported suite 的完整 local inventory 先于 body typed resolution,production resolver 使用 declaration index 验证 scope local identity,且 declaration-after-use filtered hit 行为不退化。 -- local `:=` 的 source-order type stabilization 可被后续 statement / gate classifier 消费。 +- local `:=` 的 source-order type stabilization 可被后续 statement 与 feature-specific semantic route planning 消费,但不能改变 structural body entry。 - chain binding 消费 receiver local slot 时,必须看到 local stabilization 已发布到 overlay 的 exact type,而不是 interface baseline `Variant`。 -- pending feature gate 能在 typed fact 就绪后安全转正,但 child body 只有在 `bodyInventoryReadiness == PUBLISHED` 后才可解析。 -- typed-dependent body inventory readiness 由单一 registry/query 表达;scope 存在或 `SUPPORTED` 状态都不能被当作 readiness 替代品。 -- resolver 的 request-domain gate、AST boundary gate、current-scope gate 都已接入同一 readiness policy;PUBLISHED gate body 能作为 executable body normal lookup,非 PUBLISHED / unsupported / 缺失 owner 的 body 继续 fail-closed。 -- gate header edge 与 body edge 在 resolver 中可区分:header classifier 可读取前缀 facts,但不会提前打开 body-local visibility。 +- immutable structural semantic support matrix 是 AST/scope kind 支持面的单一事实源;它不读取 typed fact、compile readiness 或 lifecycle state。 +- supported body 只有在 scope identity、suite entry root、body declaration index 与 typed baseline 全部满足 structural completeness certificate 后才进入 `SuiteResolver`;缺洞 fail-fast。 +- resolver 的 request-domain hard boundary、AST boundary 与 current-scope backstop 不读取 registry;supported `FOR_BODY` 三处均放行,unsupported lambda/match/const/parameter-default 继续 structural fail-closed。 +- for header 与 body edge 可区分:header typed resolution 可读取前缀 facts并驱动后续 iteration planning,但 body entry 已由无条件 structural inventory 与 D0 child-suite path 决定。 - nested chain / argument retry 不会产生 stable `expressionTypes()` narrowing rewrite 或 status upgrade;每个 expression / step key 在 overlay export 与 stable table 中最多有一个最终 fact。 - `applyPatch` 对 `FrontendExprTypePatch.expressionTypes()` 的 conflict 检测保持 status + publishedType + detailReason 严格相等,只有 local slot update 保留 `Variant -> exact` 例外;如未来需要表达受控 expression narrowing,必须新增显式 merge/upgrade 机制,而不是复用当前 republish 路径。 -- unsupported gate 仍 fail-closed,不能 fallback 或误发布 body facts。 +- unsupported subtree 仍通过 structural policy、AST boundary 与 current-scope backstop fail-closed,不能 fallback 或误发布 body facts。 - compile gate、lowering-ready fact 边界和 compiler-only type 隔离不变。 - `doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md` 已根据最终实现同步更新,用作目标架构摘要,并与本完成定义中的 pipeline 层级、owner 顺序、overlay 生命周期、patch/export 合同、compiler-only guard 与 diagnostics / compile gate 边界保持一致。 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 bcd65284..bb48d59c 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendAnalysisData.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendAnalysisData.java @@ -132,6 +132,9 @@ 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); diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java index 3cbdb915..0f4f6f9f 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendTypedLexicalEnvironment.java @@ -282,7 +282,8 @@ public void flushPendingFacts() { /// 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. + /// 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()); 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 index 27f60ad4..26c0c3f5 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolver.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolver.java @@ -130,6 +130,8 @@ private void resolveCallableOwner( ); 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); } 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 index 00cdf55c..45a43e69 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendCallableExportBatch.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendCallableExportBatch.java @@ -10,7 +10,10 @@ /// Callable-scoped batch of suite export transactions. /// /// Nested suites contribute their transactions here without mutating stable side tables. The root -/// callable applies the complete batch only after every supported child suite has resolved. +/// 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<>(); 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 index 5e15701d..377257ed 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendPatchTransaction.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/patch/FrontendPatchTransaction.java @@ -11,6 +11,10 @@ /// 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. From 512018ebd98f96fe16455deb00028402dfbed5e3 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Tue, 21 Jul 2026 19:49:18 +0800 Subject: [PATCH 22/27] feat(frontend): open for-in shared semantic support and retire inventory gates - Replace typed-dependent body readiness gates with structural completeness checks and a static support policy. - Publish loop iterator inventory and resolve for bodies through the ordinary suite entry path. - Keep header typing independent of body entry so unresolved iterables still admit a baseline Variant path. - Block compile mode at the for root until CFG and lowering land, while refreshing docs and regressions. --- ...e_resolution_pipeline_execution_summary.md | 49 ++-- ...ntend_semantic_analyzer_research_report.md | 2 +- .../frontend/diagnostic_manager.md | 7 +- ..._chain_binding_expr_type_implementation.md | 10 +- ...d_compile_check_analyzer_implementation.md | 21 +- ...tend_for_range_loop_implementation_plan.md | 19 +- ...frontend_gdcompiler_type_implementation.md | 4 +- ...local_type_stabilization_implementation.md | 15 +- ...op_control_flow_analyzer_implementation.md | 6 +- doc/module_impl/frontend/frontend_rules.md | 4 +- ...segmented_type_resolution_pipeline_plan.md | 207 +++++--------- ...end_top_binding_analyzer_implementation.md | 16 +- ...tend_type_check_analyzer_implementation.md | 10 +- ...ontend_variable_analyzer_implementation.md | 21 +- ...d_visible_value_resolver_implementation.md | 23 +- .../frontend/scope_analyzer_implementation.md | 8 +- .../scope_architecture_refactor_plan.md | 8 +- .../scope_type_resolver_implementation.md | 14 +- .../sema/FrontendBodyDeclarationIndex.java | 9 +- .../sema/FrontendBodyInventoryReadiness.java | 8 - .../sema/FrontendBodyLocalDeclaration.java | 11 +- .../FrontendBodySemanticSupportPolicy.java | 130 +++++++++ .../FrontendBodyStructuralCompleteness.java | 168 +++++++++++ .../FrontendExecutableInventorySupport.java | 28 +- .../sema/FrontendInterfaceSurface.java | 2 - .../frontend/sema/FrontendInventoryGate.java | 77 ----- .../sema/FrontendInventoryGateRegistry.java | 161 ----------- .../sema/FrontendInventoryGateStatus.java | 8 - .../sema/FrontendSuiteEntryRoots.java | 3 +- .../analyzer/FrontendBodyOwnerProcedures.java | 11 +- .../FrontendCompileCheckAnalyzer.java | 10 +- .../sema/analyzer/FrontendInterfacePhase.java | 83 ++---- .../analyzer/FrontendStatementResolver.java | 45 +-- .../sema/analyzer/FrontendSuiteContext.java | 23 +- .../sema/analyzer/FrontendSuiteResolver.java | 43 ++- .../analyzer/FrontendVariableAnalyzer.java | 59 +++- .../resolver/FrontendVisibleValueDomain.java | 1 - .../FrontendVisibleValueResolver.java | 162 +++-------- ...FrontendBodySemanticSupportPolicyTest.java | 76 +++++ .../sema/FrontendInterfacePhaseTest.java | 158 +++++++++-- .../FrontendInventoryGateRegistryTest.java | 185 ------------ .../sema/FrontendScopeAnalyzerTest.java | 2 +- ...FrontendSemanticAnalyzerFrameworkTest.java | 14 +- .../sema/FrontendSuiteResolverTest.java | 266 +++++++++--------- .../sema/FrontendVariableAnalyzerTest.java | 95 ++++++- ...endBodyOwnerProceduresVarTypePostTest.java | 4 +- .../FrontendCompileCheckAnalyzerTest.java | 34 ++- .../FrontendLoopControlFlowAnalyzerTest.java | 4 +- .../FrontendVisibleValueResolverTest.java | 198 +++---------- 49 files changed, 1198 insertions(+), 1324 deletions(-) delete mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyInventoryReadiness.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendBodySemanticSupportPolicy.java create mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyStructuralCompleteness.java delete mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGate.java delete mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateRegistry.java delete mode 100644 src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateStatus.java create mode 100644 src/test/java/gd/script/gdcc/frontend/sema/FrontendBodySemanticSupportPolicyTest.java delete mode 100644 src/test/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateRegistryTest.java diff --git a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md index d3bed892..3a73c829 100644 --- a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md +++ b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md @@ -7,7 +7,7 @@ 计划完成后,frontend shared semantic pipeline 分为四个层次: 1. 基础结构层:建立 module skeleton、scope graph 与 baseline inventory。 -2. Interface 层:建立 body 解析所需的 declaration index、gate registry、typed lexical baseline 与 suite entry roots。 +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。 @@ -20,7 +20,7 @@ - `FrontendModuleSkeleton`。 - `scopesByAst()`。 - callable parameter inventory。 -- supported ordinary local inventory。 +- supported parameter、iterator 与 ordinary local inventory。 - skipped / deferred subtree 的硬边界。 这一层不做 body expression typing,也不发布 `resolvedMembers()`、`resolvedCalls()`、`expressionTypes()` 或最终 `slotTypes()`。它只保证后续 resolver 能基于完整 lexical inventory 做 declaration-order 与 self-reference 过滤。 @@ -41,11 +41,12 @@ Interface 层在基础结构层之后运行,负责准备 body `SuiteResolver` 输出包括: -- `FrontendBodyDeclarationIndex`:记录每个 supported block 的完整 declaration 列表与 source order;生产 resolver 用 declaration identity 验证 scope 命中的 ordinary local 属于已发布 inventory,但不替代 declaration-order / self-reference filtered-hit。 -- `FrontendInventoryGateRegistry`:记录 typed-dependent subtree 的 gate owner、header root、body root、deferred domain 与 readiness。 +- `FrontendBodyDeclarationIndex`:记录每个 supported block 的完整 body-local declaration 列表与 source order;ordinary `var` 使用 `VariableDeclaration` identity,for iterator 使用 owning `ForStatement` identity。生产 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 完整,且 `FOR_BODY` 必须包含 iterator entry。这两个合同分别表达“该结构受支持”和“本次 interface surface 确实完整”,不能合并为 typed-dependent readiness。 + Interface 层不得发布 body typed facts。特别是不得发布 `expressionTypes()`、`resolvedMembers()` 或 `resolvedCalls()`,也不得把 `GdCompilerType` 写入 source-facing lexical baseline。 ## 4. Body `SuiteResolver` @@ -56,6 +57,7 @@ Body 层由 `FrontendSuiteResolver` 或等价 coordinator 驱动。它按源码 resolveCallableOwner(context, callableOwner): exportBatch = new FrontendCallableExportBatch() context = newSuiteContext(callableOwner, exportBatch) + requireStructurallyCompleteBody(callableOwner.body) runCallableEntryVarTypePost(context, callableOwner) resolveSuite(context, callableOwner.body) exportBatch.applyTo(context.analysisData()) @@ -83,7 +85,7 @@ batch 才按追加顺序 apply;child pending/committed overlay 不合并到 pa 这里的 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、gate classifier 与 var type post。 +`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 建立隐式上下文。 @@ -98,8 +100,9 @@ Body procedure 必须是 root-bounded、statement-local 的实现。每个 owner - `AssertStatement`。 - `IfStatement` / `ElifClause` / `else`。 - `WhileStatement`。 +- `ForStatement`:header-only statement boundary 完成后,通过普通 `resolveChildSuite(...)` 进入结构完整的 `FOR_BODY`。 -`ForStatement`、`MatchStatement`、`LambdaExpression` 与 block-local `const` 默认保持 deferred / unsupported,除非后续 feature gate 明确转正。 +`MatchStatement`、`LambdaExpression` 与 block-local `const` 仍保持结构性 deferred / unsupported;它们不创建 lifecycle state,也不能因 typed result 改变 body entry。 ## 5. Statement 内 Owner 顺序 @@ -109,12 +112,11 @@ Body procedure 必须是 root-bounded、statement-local 的实现。每个 owner 2. Local stabilization runner。 3. Chain binding runner。 4. Expr typing runner。 -5. Gate classifier hook。 -6. Var type post procedure。 -7. Pending fact flush。 +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-7 步的 statement +`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。 @@ -125,11 +127,9 @@ Chain binding runner 消费 top binding overlay 与 local stabilization pending Expr typing runner 消费 binding、member、call 与 local slot overlay,发布 `expressionTypes()` 与 bare-call `resolvedCalls()` overlay。`backfillInferredLocalType(...)` 在目标架构中仍只保留 guard-only 协议检查。 -Gate classifier hook 在 header / synthetic fixture 已完成 top binding、local stabilization、chain binding 与 expr typing 后运行。当前 Phase F 只使用该 hook 推进 generic gate lifecycle,不实现任何 for-range feature rule,也不发布 iterator binding。 - 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 与 gate classifier 读取。Flush 不得写入 stable side table 或 stable `BlockScope` slot。 +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。 @@ -146,7 +146,7 @@ Pending fact flush 把当前 statement pending overlay 转入 current-suite comm 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 与 gate classifier 可见。Committed overlay 仍不是 stable publication。 +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。 @@ -160,7 +160,7 @@ stable side tables。 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 与 gate classifier 可读,但仍不是 stable publication。 +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。 @@ -258,7 +258,7 @@ Shared walker 至少覆盖: Guard 必须在 pending overlay write 时 fail-fast,不能等 suite export 时再补救。只要一个 fact 命中 compiler-only payload,write API 就必须拒绝该 fact,不能先写入 pending / committed overlay 再回滚。 -## 12. Resolver 与 Gate Readiness +## 12. Resolver 与结构边界 `FrontendVisibleValueResolver` 继续依赖完整 source inventory 与 declaration-order filter。重构后的 resolver 能读取 `TypedLexicalEnvironment` effective view,但不能让 overlay 绕过 resolver filter。 @@ -271,15 +271,13 @@ Guard 必须在 pending overlay write 时 fail-fast,不能等 suite export 时 - committed fact 可被后续 statement 消费。 - future declaration 仍必须报告 `DECLARATION_AFTER_USE_SITE` filtered hit。 -Typed-dependent gate 使用统一 readiness policy。三道封口必须同步条件化: +Resolver 保留三类彼此独立的结构检查: -1. Request-domain gate。 -2. AST boundary gate。 -3. Current-scope gate。 +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。 -只有 owning gate 达到 `SUPPORTED + PUBLISHED` 后,body lookup 才能作为普通 executable body lookup 进入 resolver。`PENDING`、`SUPPORTED + NOT_PUBLISHED`、`PUBLISHING`、`UNSUPPORTED`、缺失 owning gate 或找不到 owner 的情况都必须 fail-closed。 - -Phase F 当前实现以 `FrontendInventoryGateRegistry` 作为 gate lifecycle 事实源。`FrontendVisibleValueResolver` 的 request-domain gate、AST boundary gate 与 current-scope gate 均读取同一个 registry readiness;`FrontendSuiteContext.visibleValueResolveRequest(...)` 统一创建 body owner lookup request;`FrontendSuiteResolver` 与 body-local stabilization 通过 shared readiness helper 只接受 unconditional supported block 或 `SUPPORTED + PUBLISHED` gate body。 +这些检查不读取 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 @@ -303,6 +301,8 @@ Diagnostics snapshot 在 interface/body path 中有明确层级:同 statement `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. 典型行为 @@ -352,5 +352,6 @@ Top binding 先绑定 `receiver` use-site。Local stabilization 随后把 `recei - `backfillInferredLocalType(...)` 保持 guard-only。 - shared compiler-only walker 覆盖所有 user-visible type-bearing publication surfaces。 - resolver 的 declaration-order 与 self-reference filter 不能被 overlay 绕过。 -- typed-dependent gate 的 request-domain、AST boundary 与 current-scope 三道封口由同一 readiness policy 控制。 +- structural policy 与 completeness certificate 分别控制支持面和 interface 完整性;二者都不读取 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 0264201a..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 未实现状态 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 0ba6015a..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 @@ -7,8 +7,8 @@ ## 文档状态 -- 状态:事实源维护中(`resolvedMembers()` / `resolvedCalls()` / `expressionTypes()`、SuiteResolver statement-local owner procedures、typed overlay-aware 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-07-10 +- 状态:事实源维护中(`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/**` @@ -34,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 独立管理 --- @@ -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 9c754945..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 分流边界、interface/body SuiteResolver facts、unary/binary 非 blocker 合同已落地) -- 更新时间:2026-07-09(Phase I:compile gate 消费最终 shared pipeline 导出的 stable facts,不依赖 legacy whole-phase publication) +- 状态:事实源维护中(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/**` @@ -101,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。 --- @@ -114,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`。 这里需要同时保持两条事实: @@ -123,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 显式拦截,并直接发出 @@ -374,6 +381,7 @@ compile gate 当前统一使用: 这条规则同样适用于: - `assert` +- `ForStatement` - `ConditionalExpression` - `ArrayExpression` - `DictionaryExpression` @@ -400,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 之后执行 @@ -418,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 index 8674b651..6ad2ca2a 100644 --- a/doc/module_impl/frontend/frontend_for_range_loop_implementation_plan.md +++ b/doc/module_impl/frontend/frontend_for_range_loop_implementation_plan.md @@ -1,12 +1,12 @@ # Frontend for-in loop 实施计划 -> 本文档记录 `for iterator[: Type] in expr` 从当前 deferred 状态推进到 frontend shared semantic 正式支持面的实施计划。新目标不再把 body 是否可解析绑定到 range-like classifier:所有 `for-in` body 都发布 callable-local inventory,iterator 先以保守 source-facing type 进入 lexical inventory;`SuiteResolver` 在解析 iterable header 后发布 `FrontendForIterationPlan`,并在可以静态确定 element type 时把 iterator slot 从 `Variant` 精化为对应 element type。lowering 阶段根据 iteration plan 选择 `range(...)` / known iterable 的专用 helper,无法静态确定时走 generic Variant iterator helper。 +> 本文档记录 `for iterator[: Type] in expr` 的分阶段实施。阶段 B/D0 已把所有 `for-in` body 转为 frontend shared semantic 正式支持面:body entry 不依赖 range-like classifier,iterator 先以保守 source-facing type 进入 lexical inventory;后续 `SuiteResolver` iteration-planning 阶段再发布 `FrontendForIterationPlan`,并在可静态确定 element type 时精化 iterator slot。lowering 最终根据 iteration plan 选择 `range(...)` / known iterable 专用 helper或 generic Variant iterator helper。 ## 文档状态 -- 状态:计划中 +- 状态:实施中(阶段 B 与 D0 已完成;阶段 C/D1 及后续 route、CFG、lowering 尚未实施) - 创建日期:2026-07-03 -- 最近校对:2026-07-17(改为 all for-in shared semantic supported,range / known iterable / generic Variant route 由 iteration plan 区分) +- 最近校对:2026-07-20(阶段 B/D0 已落地;all for-in shared semantic 已解封,compile-only 仍由临时 root blocker 阻断) - 适用范围: - `src/main/java/gd/script/gdcc/frontend/sema/**` - `src/main/java/gd/script/gdcc/frontend/lowering/**` @@ -82,8 +82,9 @@ compile / lowering 最终目标必须覆盖: - `FrontendLoopControlFlowAnalyzer` 已把 `for` 与 `while` 一样视为 loop boundary,`break` / `continue` 在 `for` body 内不再报 `sema.loop_control_flow`。 - `FrontendSemanticAnalyzer` 的 shared phase 顺序固定为 skeleton -> scope -> variable inventory -> interface surface -> `SuiteResolver` -> diagnostics-only analyzers。`SuiteResolver` 是逐语句运行的 typed fact owner,能在 `var limit := 3` flush 后让后续 `for i in limit:` 读取 `limit:int`。 - `FrontendTypedLexicalEnvironment` 已支持 pending / committed overlay、per-owner patch transaction、`Variant -> exact` 的 local slot update 校验;但当前 API 只允许 `LOCAL_TYPE_STABILIZATION` owner 发布 local slot update。 -- `FrontendVariableAnalyzer`、`FrontendInterfacePhase`、`FrontendStatementResolver`、`FrontendVisibleValueResolver` 与 `FrontendCompileCheckAnalyzer` 当前仍把 `for` 作为 unsupported / deferred boundary。 -- `FrontendBodyLocalDeclaration` 与 `FrontendBodyDeclarationIndex` 当前只表达 ordinary `VariableDeclaration` local;iterator binding 没有现成 declaration entry。 +- `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;ordinary local 继续使用 `VariableDeclaration` identity。 +- `FrontendCompileCheckAnalyzer` 当前对每个 `ForStatement` root 发布一个临时、无条件的 `sema.compile_check` blocker,不读取 iterable typed fact、iteration plan 或 route,也不进入 body。 - `FrontendCfgGraphBuilder.processStatement(...)` 当前没有 `ForStatement` 分支;`FrontendCfgRegion` 当前只允许 `BlockRegion`、`FrontendIfRegion`、`FrontendElifRegion`、`FrontendWhileRegion`。 - `GdccForRangeIterType.FOR_RANGE_ITER` 已作为 compiler-only `GdCompilerType` 子类型存在。 - `doc/gdcc_lir_intrinsic.md` 已冻结四个 backend-owned range intrinsic:`gdcc.for_range_iter.init`、`gdcc.for_range_iter.should_continue`、`gdcc.for_range_iter.next`、`gdcc.for_range_iter.get`;C backend 与 runtime helper 已有对应实现。 @@ -91,7 +92,7 @@ compile / lowering 最终目标必须覆盖: 当前实现张力: -- 文档仍把 `for` 放在 post-MVP / deferred 范围。 +- 早期文档曾把 `for` 放在 post-MVP / deferred 范围;阶段 B/D0/L 完成后,shared semantic 文档已改为结构支持,compile/lowering 仍分阶段开放。 - loop-control semantic 已把 `for` 当成合法 loop boundary。 - backend/LIR 已具备 range iterator state 与 intrinsic。 - frontend shared semantic、compile gate、CFG 与 body lowering 尚未承认 `for-in` 为 supported surface。 @@ -308,6 +309,8 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR ### 阶段 B:body inventory 与 declaration index 解封 +状态:已完成(2026-07-20)。 + 目标: - 把 `for` 从 shared semantic 的 unsupported boundary 中移除。 @@ -399,6 +402,8 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR ### 阶段 D0:SuiteResolver header-only for path 与 baseline body entry +状态:已完成(2026-07-20)。 + 目标: - 让 `SuiteResolver` 先解析 for header,再以阶段 B 已发布的 iterator baseline 进入 child body。 @@ -760,7 +765,7 @@ script/run-gradle-targeted-tests.sh --tests GdCompilerTypeTest,GdccForRangeIterT ## 7. 风险与未定点 -- `FOR_BODY` 一旦加入 unconditional supported block kind,必须同步删除 resolver 的三处 for-specific deferred boundary;否则会出现 inventory 已发布但 lookup 仍 fail-closed 的矛盾。 +- `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。 - `range(...)` root 是否发布 expression type 必须统一。当前计划是不发布 ordinary root type,只发布 arguments 与 iteration plan,避免把 range 当作 user-facing builtin call。 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 a2333a5a..63bad8ab 100644 --- a/doc/module_impl/frontend/frontend_local_type_stabilization_implementation.md +++ b/doc/module_impl/frontend/frontend_local_type_stabilization_implementation.md @@ -6,8 +6,8 @@ ## 文档状态 -- 状态:事实源维护中(source-order local `:=` slot stabilization、parameter/local alias 传播、复杂 initializer 求型、assignment initializer 与 bare `TYPE_META` fail-closed、parent/child block 边界合同、SuiteResolver body-owner overlay/export 路径已落地) -- 更新时间:2026-07-10 +- 状态:事实源维护中(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/**` @@ -30,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 --- @@ -152,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 单遍 @@ -278,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。 --- @@ -334,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_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_segmented_type_resolution_pipeline_plan.md b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md index 258942c2..90b7b6bd 100644 --- a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md +++ b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md @@ -45,11 +45,10 @@ frontend feature 实施文档。 baseline -> `SuiteResolver` body entry -> typed resolution -> type refinement / lowering route。 compile gate 的 route readiness 属于 lowering 边界,不是 semantic body entry gate。 -第 4.5 节、第 4.8 节和阶段 F 中的 `FrontendInventoryGate` / readiness 设计是当前遗留实现 -的记录与清理参照,不能作为后续 feature 的实施方案。现有 typed-dependent gate registry、 -resolver readiness fallback、pending-gate 注册与 synthetic classifier 必须在其不再承担当前 -unsupported compatibility 行为后删除或重写为纯结构性支持判断;不得为它们补充新的 feature -consumer 或 publication protocol。 +阶段 L 已删除早期 synthetic typed-dependent gate 试验。第 4.5 节现定义 structural policy 与 +completeness certificate,第 4.8 节现定义 resolver 的纯结构边界,阶段 F 只保留迁移历史结论。 +生产代码不存在 registry、readiness fallback、pending-gate 注册或 synthetic classifier;后续 feature +不得恢复等价 lifecycle 或 publication protocol。 ## 2. 背景与问题 @@ -386,56 +385,22 @@ Overlay 的目标是模拟 Godot “当前 statement 已解析出的类型可被 - Diagnostics-only phase、compile gate 与 lowering 只能读取 suite export 后的 stable facts。 - Nested supported suite 收敛后,必须把其 per-owner patch transaction 追加到外层 callable export batch;lexical visibility 仍由 scope graph 与 resolver filter 决定,不能因为 transaction 合并放宽 local 可见性。 -### 4.5 已废弃:Feature gate 与 body readiness +### 4.5 Structural support 与 completeness certificate -本节描述当前代码中仍存在的 typed-dependent gate scaffolding,仅用于界定遗留清理范围。 -它已被第 1.1 节的无条件结构性 inventory 决定取代,不得用于新增 feature;后续实现应删除 -本节所述的 registry/readiness body-entry 路径,而不是补全其 publication protocol。 +Body entry 已冻结为两个互不替代的结构事实: -新增 `FrontendInventoryGate` 记录 typed-dependent subtree 的待决状态。第一版至少表达: +- `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 必须 + 同时完整;`FOR_BODY` 还必须包含以 `ForStatement` 为 identity 的 iterator entry。 -```java -enum FrontendInventoryGateStatus { - PENDING, - SUPPORTED, - UNSUPPORTED -} - -enum FrontendBodyInventoryReadiness { - NOT_PUBLISHED, - PUBLISHING, - PUBLISHED -} - -record FrontendInventoryGate( - @NotNull Node owner, - @NotNull Node headerRoot, - @NotNull Node bodyRoot, - @NotNull FrontendVisibleValueDomain deferredDomain, - @NotNull FrontendInventoryGateStatus status, - @NotNull FrontendBodyInventoryReadiness bodyInventoryReadiness -) {} -``` - -生命周期固定为: +Policy 不读取 expression type、typed overlay、iteration plan、diagnostic state 或 compile surface; +certificate 不读取 pending/committed overlay、slot refinement 或 semantic/lowering route。Typed fact 只能 +驱动 refinement、type diagnostic 与 route planning,不能改变 body inventory 或 child-suite dispatch。 -1. Interface phase 发现 typed-dependent gate:`PENDING + NOT_PUBLISHED`。 -2. Body suite 解析 gate header 所需 expression facts。 -3. classifier 判定 unsupported:`UNSUPPORTED + NOT_PUBLISHED`,保留 deferred / unsupported boundary。 -4. classifier 判定 supported:`SUPPORTED + NOT_PUBLISHED`,此时 body 仍不可解析。 -5. body inventory publication 开始:临时 `PUBLISHING`,resolver / binder 仍 fail-closed。 -6. gate-owned body binding 与 body 完整 local inventory 成功发布后:`SUPPORTED + PUBLISHED`。 -7. 只有 `SUPPORTED + PUBLISHED` 的 body 可以进入 `SuiteResolver`。 - -需要提供共享 readiness policy 作为唯一入口,例如 `FrontendExecutableInventorySupport.isCallableLocalValueInventoryReady(BlockScope scope, Node useSite, FrontendAnalysisData data)`、`FrontendInventoryGateRegistry.isResolverGateReady(...)` 或等价命名。它不能只回答 `BlockScopeKind`,还必须能回答 owner/body/domain 级别的问题。它必须: - -- 对无条件支持的 block kind 继续返回 true。 -- 对 gate body,只在能找到 owning gate,且 `status == SUPPORTED && bodyInventoryReadiness == PUBLISHED` 时返回 true。 -- 对缺失 gate、`PENDING`、`SUPPORTED + NOT_PUBLISHED`、`SUPPORTED + PUBLISHING`、`UNSUPPORTED`、合成但无 owning gate 的 body 返回 false。 -- 为 resolver request-domain、AST boundary edge、current-scope fail-closed 三处使用同一 readiness 事实,避免三处各自判断 `BlockScopeKind.FOR_BODY` 或 deferred domain。 -- 被 `FrontendVariableAnalyzer`、`FrontendVisibleValueResolver`、`FrontendLocalTypeStabilizationAnalyzer`、`FrontendVarTypePostAnalyzer`、`FrontendCompileCheckAnalyzer` 等所有 callable-local inventory 消费者共同使用。 - -现有 `FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(BlockScopeKind)` 只能继续表达无条件支持的 block kind,不得成为 typed-dependent body readiness 的事实源。 +未转正的 lambda、match、block-local `const`、parameter default 与 unknown/skipped structure 由 policy、 +AST boundary 和 current-scope backstop 直接 fail-closed,不存在 body lifecycle 或 publication setter。 ### 4.6 Stable export 与 per-owner patch transaction @@ -519,35 +484,33 @@ Overlay 可在导出前提供 effective type,但最终 stable `BlockScope.rese ### 4.8 Resolver 复用规则 -第 1.1 节已废弃本节后文的 typed-dependent readiness 规则。保留这些文字仅为识别当前 -`FrontendVisibleValueResolver` 的遗留 gate 分支;新增或转正 AST 节点必须以完整结构性 -inventory 和纯结构性 supported-body 判断进入 resolver,不得查询 typed readiness。 - `FrontendVisibleValueResolver` 继续一次性索引完整 source AST,并继续读取已经发布的完整 lexical inventory。完整 inventory 是 `DECLARATION_AFTER_USE_SITE` filtered hit 的前提条件;重构时不得让 resolver 自己扫描 AST 补找普通 `var`,也不得把 supported block inventory 缩成只包含当前 source-order 前缀的 incremental view。 新增 resolver 能力: - 接受 `TypedLexicalEnvironment` 或等价 effective lexical view,用于读取当前 statement / current suite 的 typed overlay。 - 继续通过 declaration-order filter 处理 future local 与 initializer self-reference。 -- 对 gate header / gate body 使用共享 readiness policy,不允许只靠 `BlockScopeKind.FOR_BODY`、`FrontendVisibleValueDomain.FOR_SUBTREE` 或 scope 存在放行。 -- domain gate、AST boundary 检测与 current-scope fail-closed 三道封口都必须保留,并作为同一个 gate readiness contract 同步条件化。 +- request-domain hard boundary、AST boundary 与 current-scope backstop 三道结构检查必须保留。 +- `FOR_BODY` 与 for header edge 直接允许 ordinary lookup;lambda、match、block-local `const`、parameter + default 与 unknown/skipped structure 使用 structural policy 的精确 deferred domain。 -三道封口的 gate 化规则: +三道结构检查的规则: -1. Request-domain gate:gate body use-site 的 `FrontendVisibleValueResolveRequest` 不能由各 analyzer 手写 domain。`SuiteResolver` / `FrontendSuiteContext` / resolver request factory 必须根据 owning gate readiness 统一决定 domain。`SUPPORTED + PUBLISHED` 的 body lookup 才能作为 `EXECUTABLE_BODY` 进入 resolver;未发布、unsupported、缺失 owning gate 的 body lookup 继续返回 deferred domain 或不进入 resolver。若过渡实现仍允许传入 `FOR_SUBTREE` 等 deferred domain,domain gate 必须先通过同一个 readiness policy 做 owner/body 归属校验与 normalization,不能在裸 `domain != EXECUTABLE_BODY` 判断处提前拒止一个已经 `PUBLISHED` 的 owning body。 -2. AST boundary gate:`classifyBoundaryEdge(...)` 不能只按 `ForStatement.body() == childNode` 恒定返回 `FOR_SUBTREE`。对 gate owner 的 body edge,只有 owning gate `SUPPORTED + PUBLISHED` 时才返回 `null` 并允许继续 normal lookup;所有非 published 状态继续返回原 deferred boundary。对 gate header edge,只有当前正在解析该 owner 的 header/classifier 时才允许读取前缀 executable facts;header edge 放行不代表 body edge 已发布。非 gate 或 unsupported edge 保持 fail-closed。 -3. Current-scope gate:`BlockScopeKind.FOR_BODY`、`MATCH_SECTION_BODY`、`LAMBDA_BODY` 等 current-scope 兜底仍保留。对 typed-dependent gate body,resolver 必须从 use-site scope 找到 owning body/gate,并且只有 `SUPPORTED + PUBLISHED` 返回 `null`;找不到 owner、gate 缺失、`PENDING`、`SUPPORTED + NOT_PUBLISHED`、`PUBLISHING`、`UNSUPPORTED` 都继续返回对应 deferred boundary。 - -Header/body edge 需要显式区分。`FrontendInventoryGate.headerRoot` 表示 gate-owned header region;如果某个 feature 有多个 header child root,实施时可以把它扩展为 immutable roots 列表或使用等价的 edge classifier,但必须能判断“当前 use-site 是 header 解析”还是“当前 use-site 是 body 解析”。Body readiness 为 `PUBLISHED` 只打开 body lookup,不自动赋予 header feature-specific state 或 body-local visibility。 +1. Request-domain hard boundary:`EXECUTABLE_BODY` 才进入 ordinary lookup;其它 domain 直接返回 + `DEFERRED_UNSUPPORTED`,不做 readiness normalization。 +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 即使缺少 AST + edge 也继续 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。 - 当前 statement pending slot fact 可被同一 statement 后续 owner 子过程消费,但不能让 `var x := x` 的右侧 `x` 通过 self-reference 过滤。 - 跨 statement committed fact 可被后续 statement 消费,但 future declaration 仍必须报告 `DECLARATION_AFTER_USE_SITE` filtered hit。 -- Gate classifier 使用 resolver 时也只能读取 header use-site 当前可见的 overlay fact,不能扫描后续 suite statement 弥补 typed fact。 - -Feature-specific header state 仍属于 gate header 语义,不得因为 body readiness 为 `PUBLISHED` 自动获得 body-local visibility。非 supported 或 readiness 未发布的 gate body 继续返回 `DEFERRED_UNSUPPORTED` 或对应 deferred domain。 +- Feature-specific header planning 只能读取 header use-site 当前可见的 overlay fact,不能扫描后续 + suite statement,也不能以 typed result 改写 body entry。 ### 4.9 已实施资产分类 @@ -668,16 +631,16 @@ Compiler-only guard payload matrix: - [x] B1 新增 `FrontendInterfacePhase` coordinator;它在 skeleton / scope / variable inventory 之后构建独立 `FrontendInterfaceSurface`,不写入 stable typed side table,也不改变 legacy analyzer 顺序。 - [x] B2 新增 `FrontendBodyDeclarationIndex`,按 supported body root 记录已发布 ordinary local declaration 与 body-local source order;该 index 是 baseline inventory 的只读 view,不重新发布 local。 -- [x] B3 新增 `FrontendInventoryGateRegistry`,记录 typed-dependent subtree 与 body readiness;Phase B gate 固定从 `PENDING + NOT_PUBLISHED` 起步,非 `SUPPORTED + PUBLISHED` 查询必须 fail-closed。 +- [x] B3 早期 synthetic typed-dependent registry 试验已完成并在阶段 L 物理删除;最终 `FrontendInterfaceSurface` 不持有 lifecycle registry。 - [x] B4 新增 `FrontendTypedLexicalBaseline`,记录 parameter / ordinary local source-facing slot baseline,并在写入时 fail-fast 拒绝 `GdCompilerType`。 -- [x] B5 新增 `FrontendSuiteEntryRoots`,列出 body layer 可进入的 callable / property initializer / supported block roots,并明确 typed-dependent body 在 gate 发布前不进入 entry roots。 +- [x] B5 新增 `FrontendSuiteEntryRoots`,列出 body layer 可进入的 callable / property initializer / structural supported block roots;entry 不依赖 typed fact。 - [x] B6 保持 skeleton / scope / variable analyzer public contract 不变;新增 `FrontendInterfacePhaseTest` 只调用已发布基础结构层并断言 Interface phase 不写 typed stable side table,现有 phase-boundary / variable inventory focused tests 继续作为回归锚点。 实施内容: - 新增 `FrontendInterfacePhase` 或等价 coordinator。 - 新增 `FrontendBodyDeclarationIndex`,按 callable/block 记录完整 ordinary local declaration 与 source order。 -- 新增 `FrontendInventoryGateRegistry`,记录 typed-dependent subtree 与 body readiness。 +- 使用 `FrontendBodySemanticSupportPolicy` 表达结构支持面,并由 `FrontendBodyStructuralCompleteness` 验证本次 interface surface 的 scope、suite entry、declaration index 与 baseline 完整性。 - 新增 `FrontendTypedLexicalBaseline`,记录 interface 层可确定的 source-facing typed baseline。 - 新增 `FrontendSuiteEntryRoots`,列出 body layer 可进入的 callable/property initializer/supported block roots。 - 保持 skeleton/scope/variable analyzer 的 public contract 不变。 @@ -688,7 +651,7 @@ Compiler-only guard payload matrix: - `FrontendSemanticAnalyzerFrameworkTest` 继续证明 skeleton/scope/variable phase boundary 未漂移。 - `FrontendVariableAnalyzerTest` 继续证明 supported block 的完整 local inventory 先发布。 - `var x := y; var y := 1` 仍能通过 resolver 看到 future declaration 并过滤为 `DECLARATION_AFTER_USE_SITE`。 -- `for` / `match` / lambda / block-local `const` 仍默认 deferred / unsupported。 +- `for` 已通过后续阶段 B/D0 与 L 系列转正;`match` / lambda / block-local `const` 继续 structural deferred / unsupported。 ### 阶段 C:引入 `TypedLexicalEnvironment` overlay @@ -732,7 +695,7 @@ Compiler-only guard payload matrix: - [x] D2 `FrontendSemanticAnalyzer.analyze()` 已在 skeleton / scope / variable inventory 之后构建真实 `FrontendInterfaceSurface`,并在 legacy body analyzers 之前传入 `FrontendSuiteResolver`;legacy analyzer 顺序保留,SuiteResolver 目前是 shadow no-op body path。 - [x] D3 新增 `FrontendSuiteContext`,显式携带 source path、callable owner、current block scope / scope、restriction、static context、property initializer context、interface surface、typed lexical environment、analysis data、diagnostic manager 与 class registry。 - [x] D4 新增 `FrontendStatementResolver` dispatcher;supported roots 按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 固定顺序调用 injected owner hooks,并在每个 statement/header boundary flush pending overlay。 -- [x] D5 `if` / `elif` / `else` / `while` 的 Phase D traversal 已建立 header-first / child-suite-after-header 形状;`for` / `match` / block-local `const` 只触发 fail-closed unsupported hook,不进入 body。 +- [x] D5 `if` / `elif` / `else` / `while` 的 Phase D traversal 已建立 header-first / child-suite-after-header 形状;Phase D 当时让 `for` / `match` / block-local `const` 走 fail-closed hook。最终状态由 for-range B/D0 与阶段 L 覆盖:`for` 已进入普通 child-suite path,match/const 仍 structural fail-closed。 - [x] D6 新增并运行 `FrontendSuiteResolverTest` 及相关 targeted regressions,覆盖 source-order、owner order、header-before-body、unsupported body not entered、overlay export boundary、main pipeline surface hand-off;`FrontendInterfacePhaseTest`、`FrontendTypedLexicalEnvironmentTest`、`FrontendVisibleValueResolverTest`、`FrontendAnalysisDataTest`、`FrontendSemanticAnalyzerFrameworkTest`、`FrontendVariableAnalyzerTest`、`FrontendLocalTypeStabilizationAnalyzerTest`、`FrontendVarTypePostAnalyzerTest`、`FrontendChainBindingAnalyzerTest`、`FrontendExprTypeAnalyzerTest` 均通过。 实施内容: @@ -809,35 +772,17 @@ Compiler-only guard payload matrix: - 每个 owner 子过程的 suite export 以独立 per-owner patch 出现在 transaction 中;transaction coordinator 按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 顺序 apply。 - Var type post procedure 在 statement flush 前不得改变 stable `slotTypes()`;targeted test 必须在 procedure 运行、flush、suite export 三个点分别断言 stable table 只在 export/apply patch 后变化。 -### 阶段 F:接入 generic typed-dependent gate readiness +### 阶段 F:历史 typed-dependent gate 试验(已由阶段 L 删除) -本阶段只验收 gate registry、readiness 查询与 fail-closed 生命周期,不使用 for-range 作为验收目标。`frontend_for_range_loop_implementation_plan.md` 是该基础设施的后续真实消费者,不是本阶段的前置或完成条件。 - -实施内容: - -- [x] F1 建立 typed-dependent inventory gate 的注册、lookup、status update 与 readiness update API。当前实现以 `FrontendInventoryGate` 的 immutable state transition helper、`FrontendInventoryGateRegistry` 的 mutable lifecycle API,以及 `FrontendExecutableInventorySupport.isCallableLocalValueInventoryReady(...)` 作为 shared readiness 入口;非 `SUPPORTED + PUBLISHED` 的 gate 仍统一 fail-closed。 -- [x] F2 使用合成 fixture 或最小测试 gate 验证 `PENDING + NOT_PUBLISHED`、`SUPPORTED + NOT_PUBLISHED`、`PUBLISHING`、`SUPPORTED + PUBLISHED`、`UNSUPPORTED` 的转换。`FrontendInventoryGateRegistryTest.registryTransitionsOnlySupportedPublishedGateToReady` 锚定所有有效生命周期状态,invalid-state tests 锚定 pending / unsupported 不得携带 published readiness。 -- [x] F3 支持由前缀 statement typed fact 驱动的测试 classifier,但 classifier 只服务于 gate lifecycle,不实现任何 for-range 规则。`FrontendStatementResolver.OwnerProcedures.runGateClassifier(...)` 接在 top/local/chain/expr 后与 flush 前,`FrontendSuiteResolverTest.classifierReadsPrefixOverlayAndDoesNotOpenUnpublishedBody` 证明 classifier 可读取前缀 `:=` local exact overlay 与 expr typing 已最终发布的 expression overlay;同测例证明 local stabilization 的 transient expression cache 不会提前进入 overlay,并且 classifier 只推进指定 synthetic gate。 -- [x] F4 `SUPPORTED` 只表示 header/classifier 通过,不能使 body resolver / binder 放行。`FrontendVisibleValueResolverTest.resolveKeepsSupportedButUnpublishedGateBodyFailClosed` 覆盖 `SUPPORTED + NOT_PUBLISHED` / `SUPPORTED + PUBLISHING`,`FrontendSuiteResolverTest.classifierReadsPrefixOverlayAndDoesNotOpenUnpublishedBody` 覆盖 binder 不进入未发布 body。 -- [x] F5 body inventory publication 成功后才原子推进为 `PUBLISHED`。`FrontendInventoryGateRegistry.markBodyInventoryPublished(...)` 是唯一 readiness published transition,resolver / suite entry 只接受 `SUPPORTED + PUBLISHED`;`FrontendSuiteResolverTest.publishedGateBodyIsResolvedBySuiteResolver` 证明 published 后 body facts 可发布。 -- [x] F6 建立 resolver gate readiness policy,并接入 request-domain gate、AST boundary gate、current-scope gate 三处判断。`FrontendVisibleValueResolver` 三处封口、`FrontendSuiteResolver` body entry 与 body-local stabilization 均复用 `FrontendExecutableInventorySupport.isCallableLocalValueInventoryReady(...)` / `FrontendInventoryGateRegistry` readiness;`resolvePublishedGateBodyPassesRequestAstAndCurrentScopeGates` 使用 deferred request domain 证明任一旧常量封口未接入都会失败。 -- [x] F7 统一 resolver request 创建路径:gate header / gate body lookup 都必须由 `SuiteResolver` / `FrontendSuiteContext` 构造,不能由 analyzer 直接决定 deferred domain。body owner path 已改为通过 `FrontendSuiteContext.visibleValueResolveRequest(...)` 创建请求,并随 current body readiness 选择 `EXECUTABLE_BODY` 或 gate deferred domain;`missingOwningGateBodyContextUsesDeferredDomainAndStaysFailClosed` 锚定缺失 gate 的 typed-dependent body 不回退为 executable lookup。 -- [x] F8 unsupported gate 继续保留对应 deferred / unsupported boundary。`FrontendVisibleValueResolverTest.resolveUnsupportedGateBodyDoesNotFallBackToOuterBinding` 证明 `UNSUPPORTED` body lookup 不回退到外层同名 binding。 -- 明确非目标:不实现 `range(...)` AST shape 识别、不实现 `INT_SHORTHAND`、不发布 iterator binding、不解封 supported for-range body、不调整 for-range compile gate。 - -验收细则: +阶段 F 曾用于验证 synthetic body-entry lifecycle,但没有 production feature consumer。阶段 L 已完成清理: -- 合成 gate classifier 可读取前缀 `:=` local 的 typed fact,并只推进该合成 gate 的 lifecycle。 -- classifier 已返回 supported 但 body readiness 仍为 `NOT_PUBLISHED` / `PUBLISHING` 时,body lookup 仍必须是 `DEFERRED_UNSUPPORTED` 或对应 deferred domain。 -- Classifier 只能在 header statement 的 top binding、local stabilization、chain binding 与 expr typing 子过程完成后读取 overlay。 -- Classifier 可读取前序 statement committed typed fact,但不能读取后续 statement 或未运行子过程的 fact。 -- Classifier 只能读取 expr typing 已最终发布到 overlay 的 expression facts,不能读取 owner-local transient cache 中未导出的中间 expression type。 -- body inventory publication 成功后,readiness 原子推进为 `PUBLISHED`,resolver 才允许进入该合成 body。 -- 合成 gate 的 body use-site 在 `PUBLISHED` 后必须同时通过 request-domain gate、AST boundary gate 与 current-scope gate;任一 gate 仍按旧常量逻辑封口都应有测试失败。 -- 合成 gate 的 header use-site 可在 header/classifier 上下文读取前缀 overlay fact,但 header 放行不能让 body lookup 提前通过。 -- `UNSUPPORTED` gate body lookup 不能 fallback 到外层并制造误导 binding。 -- 缺失 owning gate 的合成 body 即使已有 scope,也必须返回 readiness false。 -- source-facing facts 仍拒绝所有 feature-specific `GdCompilerType`。 +- [x] registry、status/readiness 类型、publication setter 与 synthetic classifier 已物理删除。 +- [x] Interface surface 不产出 body gate;SuiteResolver、body-local stabilization 与 visible-value resolver + 不接收或查询 registry。 +- [x] F 中仍有价值的 overlay 顺序测试保留在普通 statement/header 测试中,但 typed result 只能驱动 + refinement 或 route planning,不能决定 child body entry。 +- [x] 未转正 feature 通过 structural policy、AST boundary 与 current-scope backstop fail-closed。 +- [x] source-facing facts 继续拒绝 feature-specific `GdCompilerType`。 ### 阶段 G:收敛 diagnostics 与 compile gate @@ -1012,10 +957,10 @@ feature-owned surface 理解:lambda 包括 parameter/capture/body,match 包 #### 阶段 L0:冻结 for 前置与 compile safety bridge -- [ ] 确认 for-range 阶段 B 与 D0 已完成,且 `FOR_BODY` 不再查询 gate registry。 -- [ ] 确认临时 `FrontendCompileCheckAnalyzer.handleForStatement(...)` blocker 只锚定 +- [x] 确认 for-range 阶段 B 与 D0 已完成,且 `FOR_BODY` 不再查询 gate registry。 +- [x] 确认临时 `FrontendCompileCheckAnalyzer.handleForStatement(...)` blocker 只锚定 `ForStatement`、不进入 body、不读取 iterable type / iteration plan / route。 -- [ ] 增加 for-only characterization:shared `analyze(...)` 无 `FOR_SUBTREE` unsupported; +- [x] 增加 for-only characterization:shared `analyze(...)` 无 `FOR_SUBTREE` unsupported; `analyzeForCompile(...)` 有明确 `sema.compile_check` error,lowering pipeline 在 CFG 前停止。 验收细则: @@ -1027,11 +972,11 @@ feature-owned surface 理解:lambda 包括 parameter/capture/body,match 包 #### 阶段 L1:建立不可变 structural semantic support matrix -- [ ] 新增 `FrontendBodySemanticSupportPolicy` 或等价不可变 helper,统一表达当前结构支持面; +- [x] 新增 `FrontendBodySemanticSupportPolicy` 或等价不可变 helper,统一表达当前结构支持面; `FrontendExecutableInventorySupport` 必须委托该事实源或被其替代,不能保留平行 allowlist。 -- [ ] policy 只能由 AST/scope kind 决定,不能读取 `GdType`、expression type、typed overlay、 +- [x] policy 只能由 AST/scope kind 决定,不能读取 `GdType`、expression type、typed overlay、 iteration plan、diagnostic state、compile surface 或 gate lifecycle。 -- [ ] policy 必须覆盖下列结构位置,且每一行都有 focused test。 +- [x] policy 必须覆盖下列结构位置,且每一行都有 focused test。 结构支持矩阵最低合同: @@ -1049,7 +994,7 @@ feature-owned surface 理解:lambda 包括 parameter/capture/body,match 包 - 未知或 skipped structure:不发布 inventory,不进入 `SuiteResolver`,domain 为 `UNKNOWN_OR_SKIPPED_SUBTREE`。 -- [ ] enum switch 必须 exhaustive;新增 `BlockScopeKind` / `CallableScopeKind` 时必须显式选择 +- [x] enum switch 必须 exhaustive;新增 `BlockScopeKind` / `CallableScopeKind` 时必须显式选择 policy,不能通过 `default -> EXECUTABLE_BODY` 自动放行。 验收细则: @@ -1059,16 +1004,16 @@ feature-owned surface 理解:lambda 包括 parameter/capture/body,match 包 #### 阶段 L2:增加 structural completeness fail-fast certificate -- [ ] 在 `FrontendSuiteResolver` 进入 root/child body 前,通过 +- [x] 在 `FrontendSuiteResolver` 进入 root/child body 前,通过 `requireStructurallyCompleteBody(...)` 或等价 helper 验证: - `scopesByAst()` 中 body root 映射到预期 identity / kind 的 `BlockScope`。 - `FrontendSuiteEntryRoots` 明确包含该 supported body。 - `FrontendBodyDeclarationIndex.containsBodyRoot(body)` 为 true,即使 body 没有 ordinary local。 - 每个 index entry 的 declaration/binding/scope identity 一致,且 typed baseline 包含该 source-facing declaration;`FOR_BODY` 的 iterator `Node` identity 也必须覆盖。 -- [ ] certificate 只读取 scope graph、suite entry roots、declaration index 与 baseline;不得读取 +- [x] certificate 只读取 scope graph、suite entry roots、declaration index 与 baseline;不得读取 pending/committed overlay、expression type、slot refinement 或 iteration plan。 -- [ ] 结构事实缺洞属于 phase protocol / programmer error,必须 fail-fast,不得伪装成源码 +- [x] 结构事实缺洞属于 phase protocol / programmer error,必须 fail-fast,不得伪装成源码 diagnostic,也不得静默跳过 body。 验收细则: @@ -1079,54 +1024,54 @@ feature-owned surface 理解:lambda 包括 parameter/capture/body,match 包 #### 阶段 L3:迁移 SuiteResolver 与 body-local consumer -- [ ] `FrontendSuiteResolver`、`FrontendSuiteContext` 与 +- [x] `FrontendSuiteResolver`、`FrontendSuiteContext` 与 `FrontendBodyOwnerProcedures.eligibleInferredLocalScope(...)` 改为消费 structural policy 与 completeness certificate,不再用 gate readiness 决定 suite entry / ordinary local stabilization。 -- [ ] `FrontendSuiteContext.visibleValueDomainForCurrentBody()` 对 supported body 直接生成 +- [x] `FrontendSuiteContext.visibleValueDomainForCurrentBody()` 对 supported body 直接生成 `EXECUTABLE_BODY`;unsupported body 根据 policy 返回精确 deferred domain,未知 kind 不得 fallback 为 `EXECUTABLE_BODY`。 -- [ ] 本阶段允许 `FrontendInterfaceSurface` 暂时继续携带 registry 供尚未迁移的 resolver 使用; +- [x] 本阶段允许 `FrontendInterfaceSurface` 暂时继续携带 registry 供尚未迁移的 resolver 使用; 不得为了提前删除字段形成不可编译的中间状态。 #### 阶段 L4:迁移 FrontendVisibleValueResolver 的结构封口 -- [ ] 删除 resolver 的 gate registry constructor dependency、request-domain readiness +- [x] 删除 resolver 的 gate registry constructor dependency、request-domain readiness normalization、`gateBodyBoundary(...)`、`isNearestOwningGateReady(...)`、 `nearestOwningGate(...)` 及所有 `SUPPORTED + PUBLISHED` 分支。 -- [ ] 保留 request-domain hard boundary、AST boundary 与 current-scope fail-closed。删除的是 typed +- [x] 保留 request-domain hard boundary、AST boundary 与 current-scope fail-closed。删除的是 typed readiness 条件,不是这三类结构检查本身;AST boundary 与 current-scope 两层结构封口都不得 删除。 -- [ ] `ForStatement.body()`、for header edge 与 `FOR_BODY` current scope 直接允许 normal lookup; +- [x] `ForStatement.body()`、for header edge 与 `FOR_BODY` current scope 直接允许 normal lookup; lambda/match/const/parameter-default 根据 structural policy 直接返回 deferred boundary。 -- [ ] 保留 declaration-order、initializer self-reference、skipped subtree 以及 visible block-local +- [x] 保留 declaration-order、initializer self-reference、skipped subtree 以及 visible block-local `const` 不得 fallback 到 outer same-name binding 的合同。 -- [ ] 同批更新 `FrontendBodyOwnerProcedures` 的 resolver 构造点和 focused resolver tests。 +- [x] 同批更新 `FrontendBodyOwnerProcedures` 的 resolver 构造点和 focused resolver tests。 #### 阶段 L5:删除 synthetic statement classifier -- [ ] 删除 `FrontendStatementResolver.OwnerProcedures.runGateClassifier(...)`、 +- [x] 删除 `FrontendStatementResolver.OwnerProcedures.runGateClassifier(...)`、 `resolveSupportedRoot(...)` 中的调用及 synthetic classifier fixtures。 -- [ ] 该删除只针对 body-entry classifier。for-range 阶段 D1 后续可以在 header expr typing 后、 +- [x] 该删除只针对 body-entry classifier。for-range 阶段 D1 后续可以在 header expr typing 后、 iterator var type post 前新增 concrete `runForIterationPlanning(...)`;不得复用 classifier 名称、 lifecycle 或 readiness 语义。 -- [ ] 真实 feature typed result 只能驱动 refinement、semantic route、type diagnostic 或 lowering +- [x] 真实 feature typed result 只能驱动 refinement、semantic route、type diagnostic 或 lowering route,不能决定是否调用 `resolveChildSuite(...)`。 #### 阶段 L6:删除 interface gate producer 与 surface dependency -- [ ] 移除 `FrontendInterfacePhase` 的 `FrontendInventoryGateRegistry.Builder`、 +- [x] 移除 `FrontendInterfacePhase` 的 `FrontendInventoryGateRegistry.Builder`、 `addPendingGate(...)`、match registration 及 lambda/match/block-local `const` 调用点。 -- [ ] 已转正 `for` 继续使用阶段 B/D0 的 structural path;未转正节点只跳过其 feature-owned +- [x] 已转正 `for` 继续使用阶段 B/D0 的 structural path;未转正节点只跳过其 feature-owned inventory,并由 structural policy / AST boundary 表达限制。 -- [ ] `FrontendInterfaceSurface`、`FrontendSuiteEntryRoots` 的字段、构造器与文档不再产出、持有 +- [x] `FrontendInterfaceSurface`、`FrontendSuiteEntryRoots` 的字段、构造器与文档不再产出、持有 或描述 gate registry。 #### 阶段 L7:迁移测试合同 -- [ ] 重写 `FrontendInterfacePhaseTest`、`FrontendSuiteResolverTest`、 +- [x] 重写 `FrontendInterfacePhaseTest`、`FrontendSuiteResolverTest`、 `FrontendVisibleValueResolverTest` 中 pending/published gate、registry 与 classifier fixture。 -- [ ] 新增/保留以下结构合同: +- [x] 新增/保留以下结构合同: - `FOR_BODY` 在 typed resolution 前已有 iterator、ordinary local、index、baseline 与 suite entry。 - supported kind 但 certificate 任一事实缺失时 fail-fast。 - iterable 分别为 exact、`Variant`、error fact 时,body inventory 与 entry 保持不变。 @@ -1134,25 +1079,25 @@ feature-owned surface 理解:lambda 包括 parameter/capture/body,match 包 - visible block-local `const` 不 fallback;future declaration 与 initializer self-reference filtered hit 不退化。 - for-only compile path 在 CFG 未支持时命中临时 blocker。 -- [ ] 删除手工推进 gate lifecycle 的 fixture 与 `FrontendInventoryGateRegistryTest`。 +- [x] 删除手工推进 gate lifecycle 的 fixture 与 `FrontendInventoryGateRegistryTest`。 #### 阶段 L8:物理删除 gate lifecycle 类型 -- [ ] 仅在 L3-L7 的生产/测试消费者全部清除后,物理删除 +- [x] 仅在 L3-L7 的生产/测试消费者全部清除后,物理删除 `FrontendInventoryGate`、`FrontendInventoryGateRegistry`、 `FrontendInventoryGateStatus`、`FrontendBodyInventoryReadiness`。 -- [ ] 不保留 compatibility shim、empty registry、反射检查或公开 +- [x] 不保留 compatibility shim、empty registry、反射检查或公开 `markSupported(...)` / `markBodyInventoryPublished(...)` setter。 -- [ ] `src/main/java` 与 `src/test/java` 中不存在上述类型、`runGateClassifier`、 +- [x] `src/main/java` 与 `src/test/java` 中不存在上述类型、`runGateClassifier`、 `markBodyInventoryPublished` 或 `isBodyInventoryReady` 引用。 #### 阶段 L9:同步文档、风险与完成定义 -- [ ] 删除或改写本文第 4.5、4.8、阶段 F 中把 `SUPPORTED + PUBLISHED` 作为 body entry 条件的 +- [x] 删除或改写本文第 4.5、4.8、阶段 F 中把 `SUPPORTED + PUBLISHED` 作为 body entry 条件的 历史设计;复核第 6/7/8 节不再保留 readiness registry 不变量。 -- [ ] 更新 `frontend_rules.md`:`for` 已进入 shared semantic,但在 route-aware compile gate 落地前 +- [x] 更新 `frontend_rules.md`:`for` 已进入 shared semantic,但在 route-aware compile gate 落地前 由临时 blocker 阻断;compile gate 不得把该 blocker 反向变成 semantic body gate。 -- [ ] 更新 `frontend_segmented_type_resolution_pipeline_execution_summary.md`、visible-value +- [x] 更新 `frontend_segmented_type_resolution_pipeline_execution_summary.md`、visible-value resolver、interface phase 与 for-range 文档,明确 structural policy、completeness certificate、 semantic body entry 与 compile route readiness 四者分离。 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 b5f20885..156dd223 100644 --- a/doc/module_impl/frontend/frontend_top_binding_analyzer_implementation.md +++ b/doc/module_impl/frontend/frontend_top_binding_analyzer_implementation.md @@ -6,8 +6,8 @@ ## 文档状态 -- 状态:事实源维护中(`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-07-10 +- 状态:事实源维护中(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/**` @@ -28,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 之后 @@ -364,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 @@ -381,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 @@ -466,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 @@ -649,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): @@ -696,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 d0669497..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-07-09(Phase I:默认 shared analyzer 只走 interface/body + SuiteResolver body publication) +- 最后校对: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 协议 --- @@ -58,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 - 参数默认值表达式的类型/绑定分析 @@ -83,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` 为: @@ -93,6 +95,7 @@ - `ELIF_BODY` - `ELSE_BODY` - `WHILE_BODY` +- `FOR_BODY` ### 2.2 当前不写入的绑定 @@ -101,8 +104,6 @@ - lambda parameter - lambda local - lambda capture -- `for` iterator binding -- `for` body local - `match` pattern binding - `match` section local - block-local `const` @@ -191,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 判定 @@ -228,7 +229,6 @@ - 参数默认值当前被忽略 - `sema.unsupported_variable_inventory_subtree` - lambda subtree 当前不支持 - - `for` subtree 当前不支持 - `match` subtree 当前不支持 - block-local `const` 当前不支持 @@ -253,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 记录的语义 @@ -317,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` @@ -332,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 39a4efc3..478a7cd5 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、typed overlay-aware resolve overload、shared support matrix 与核心单元测试已落地) -- 更新时间:2026-07-09(Phase I:resolver 服务最终 interface/body + SuiteResolver pipeline,不依赖 legacy whole-phase bypass) +- 状态:事实源维护中(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` 的分工 @@ -84,7 +85,7 @@ shared `Scope` 继续负责: ### 2.3 与 `FrontendTypedLexicalEnvironment` 的分工 -SuiteResolver body path 中的 caller 必须优先通过 typed overlay-aware `resolve(request, environment)` 入口读取 current statement / current suite 已发布的 local slot 与 binding payload。resolver 仍保留 request-domain、AST boundary 与 current-scope 三道 fail-closed gate;overlay 只改变可见事实来源,不允许绕过 declaration-order、initializer self-reference 或 typed-dependent body readiness 判定。 +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。 --- @@ -107,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` 不是封口域 @@ -185,7 +184,6 @@ resolver 当前只对 `EXECUTABLE_BODY` 域提供正常 lookup,并要求同时 - `PARAMETER_DEFAULT` - `LAMBDA_SUBTREE` - `BLOCK_LOCAL_CONST_SUBTREE` -- `FOR_SUBTREE` - `MATCH_SUBTREE` - `UNKNOWN_OR_SKIPPED_SUBTREE` @@ -202,7 +200,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` @@ -242,7 +240,7 @@ resolver 当前只对 `EXECUTABLE_BODY` 域提供正常 lookup,并要求同时 ### 6.2 support matrix 必须共享 -`FrontendVariableAnalyzer` 发布 inventory 的范围,与 resolver 允许正常 lookup 的范围,本质上是同一份事实。该事实现在由 `FrontendExecutableInventorySupport` 统一承载;后续若扩展支持域,必须同时评估: +`FrontendVariableAnalyzer` 发布 inventory 的范围,与 resolver 允许正常 lookup 的范围,本质上是同一份事实。该事实现在由 `FrontendBodySemanticSupportPolicy` 统一承载;后续若扩展支持域,必须同时评估: - variable inventory 是否真的发布 - resolver 是否真的能在该域内给出不误导的绑定结果 @@ -268,7 +266,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/frontend/sema/FrontendBodyDeclarationIndex.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyDeclarationIndex.java index c9fd6980..fdd8a0df 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyDeclarationIndex.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyDeclarationIndex.java @@ -1,7 +1,6 @@ package gd.script.gdcc.frontend.sema; import dev.superice.gdparser.frontend.ast.Node; -import dev.superice.gdparser.frontend.ast.VariableDeclaration; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -20,14 +19,14 @@ /// explicitly published by a later body phase. public final class FrontendBodyDeclarationIndex { private final @NotNull Map> declarationsByBodyRoot; - private final @NotNull Map declarationsByDeclaration; + private final @NotNull Map declarationsByDeclaration; public FrontendBodyDeclarationIndex( @NotNull Map> declarationsByBodyRoot ) { Objects.requireNonNull(declarationsByBodyRoot, "declarationsByBodyRoot"); var copiedDeclarations = new IdentityHashMap>(); - var copiedDeclarationsByDeclaration = 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")); @@ -57,8 +56,8 @@ public boolean containsBodyRoot(@NotNull Node bodyRoot) { return declarationsByBodyRoot.containsKey(Objects.requireNonNull(bodyRoot, "bodyRoot")); } - /// Returns the published inventory entry for one ordinary local declaration, if any. - public @Nullable FrontendBodyLocalDeclaration declarationFor(@NotNull VariableDeclaration declaration) { + /// 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")); } diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyInventoryReadiness.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyInventoryReadiness.java deleted file mode 100644 index f18fc02f..00000000 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyInventoryReadiness.java +++ /dev/null @@ -1,8 +0,0 @@ -package gd.script.gdcc.frontend.sema; - -/// Publication readiness of the full local inventory owned by a typed-dependent body gate. -public enum FrontendBodyInventoryReadiness { - NOT_PUBLISHED, - PUBLISHING, - PUBLISHED -} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyLocalDeclaration.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyLocalDeclaration.java index 73ba2e89..cbbe1bf5 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyLocalDeclaration.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyLocalDeclaration.java @@ -1,6 +1,6 @@ package gd.script.gdcc.frontend.sema; -import dev.superice.gdparser.frontend.ast.VariableDeclaration; +import dev.superice.gdparser.frontend.ast.Node; import gd.script.gdcc.scope.ScopeValue; import org.jetbrains.annotations.NotNull; @@ -12,13 +12,15 @@ /// source-order model required by `FrontendVisibleValueResolver`: the resolver can see future locals /// in the scope graph, then decide whether a use-site is before or after this declaration. public record FrontendBodyLocalDeclaration( - @NotNull VariableDeclaration declaration, + @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"); } @@ -26,4 +28,9 @@ public record FrontendBodyLocalDeclaration( throw new IllegalArgumentException("sourceOrder must not be negative"); } } + + public enum Kind { + ITERATOR, + 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..3abaafe8 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodySemanticSupportPolicy.java @@ -0,0 +1,130 @@ +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; + } + + /// 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..84e9efa8 --- /dev/null +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyStructuralCompleteness.java @@ -0,0 +1,168 @@ +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.VariableDeclaration; +import gd.script.gdcc.frontend.scope.BlockScope; +import gd.script.gdcc.frontend.scope.BlockScopeKind; +import org.jetbrains.annotations.NotNull; + +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. +/// +/// 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 enters the suite resolver. + /// 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 source order, matching binding/declaration/scope identities, + /// and a source-facing typed baseline equal to the published lexical binding type. + /// 6. A `FOR_BODY` must additionally contain the iterator entry identified by its owning `ForStatement`. + /// + /// 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.entersSuiteResolver()) { + 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"); + } + requireCompleteDeclaration( + analysisData, + interfaceSurface, + body, + expectedScope, + declaration + ); + } + + if (expectedScope.kind() == BlockScopeKind.FOR_BODY + && declarations.stream().noneMatch( + declaration -> declaration.kind() == FrontendBodyLocalDeclaration.Kind.ITERATOR + )) { + throw incomplete(body, "`for` body declaration index does not contain its iterator"); + } + } + + /// 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"); + } + } + } + } + + /// 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 3265a255..627defcf 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendExecutableInventorySupport.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendExecutableInventorySupport.java @@ -1,12 +1,7 @@ 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.scope.BlockScopeKind; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Objects; /// Shared semantic contract for executable block kinds whose callable-local value inventory is supported being /// published by `FrontendVariableAnalyzer`. @@ -15,28 +10,7 @@ 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(); } - public static boolean isCallableLocalValueInventoryReady( - @NotNull BlockScope blockScope, - @Nullable Node bodyRoot, - @NotNull FrontendInventoryGateRegistry gateRegistry - ) { - Objects.requireNonNull(blockScope, "blockScope"); - Objects.requireNonNull(gateRegistry, "gateRegistry"); - if (canPublishCallableLocalValueInventory(blockScope.kind())) { - return true; - } - return bodyRoot != null && gateRegistry.isBodyInventoryReady(bodyRoot); - } } diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendInterfaceSurface.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendInterfaceSurface.java index a8ebcd94..3113a598 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendInterfaceSurface.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendInterfaceSurface.java @@ -7,13 +7,11 @@ /// Complete Interface-phase surface consumed by future body-suite orchestration. public record FrontendInterfaceSurface( @NotNull FrontendBodyDeclarationIndex bodyDeclarationIndex, - @NotNull FrontendInventoryGateRegistry inventoryGateRegistry, @NotNull FrontendTypedLexicalBaseline typedLexicalBaseline, @NotNull FrontendSuiteEntryRoots suiteEntryRoots ) { public FrontendInterfaceSurface { Objects.requireNonNull(bodyDeclarationIndex, "bodyDeclarationIndex"); - Objects.requireNonNull(inventoryGateRegistry, "inventoryGateRegistry"); Objects.requireNonNull(typedLexicalBaseline, "typedLexicalBaseline"); Objects.requireNonNull(suiteEntryRoots, "suiteEntryRoots"); } diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGate.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGate.java deleted file mode 100644 index 4842088b..00000000 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGate.java +++ /dev/null @@ -1,77 +0,0 @@ -package gd.script.gdcc.frontend.sema; - -import dev.superice.gdparser.frontend.ast.Node; -import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueDomain; -import org.jetbrains.annotations.NotNull; - -import java.util.Objects; - -/// Typed-dependent subtree gate discovered by the Interface phase. -/// -/// Interface discovery creates `PENDING + NOT_PUBLISHED` gates. Later body phases can classify the -/// header and publish the body inventory, but consumers must keep body lookup fail-closed until the -/// combined state reaches `SUPPORTED + PUBLISHED`. -public record FrontendInventoryGate( - @NotNull Node owner, - @NotNull Node headerRoot, - @NotNull Node bodyRoot, - @NotNull FrontendVisibleValueDomain deferredDomain, - @NotNull FrontendInventoryGateStatus status, - @NotNull FrontendBodyInventoryReadiness bodyInventoryReadiness -) { - public FrontendInventoryGate { - Objects.requireNonNull(owner, "owner"); - Objects.requireNonNull(headerRoot, "headerRoot"); - Objects.requireNonNull(bodyRoot, "bodyRoot"); - Objects.requireNonNull(deferredDomain, "deferredDomain"); - Objects.requireNonNull(status, "status"); - Objects.requireNonNull(bodyInventoryReadiness, "bodyInventoryReadiness"); - if (status != FrontendInventoryGateStatus.SUPPORTED - && bodyInventoryReadiness != FrontendBodyInventoryReadiness.NOT_PUBLISHED) { - throw new IllegalArgumentException("only supported inventory gates can publish body inventory"); - } - } - - public static @NotNull FrontendInventoryGate pending( - @NotNull Node owner, - @NotNull Node headerRoot, - @NotNull Node bodyRoot, - @NotNull FrontendVisibleValueDomain deferredDomain - ) { - return new FrontendInventoryGate( - owner, - headerRoot, - bodyRoot, - deferredDomain, - FrontendInventoryGateStatus.PENDING, - FrontendBodyInventoryReadiness.NOT_PUBLISHED - ); - } - - public @NotNull FrontendInventoryGate withStatus(@NotNull FrontendInventoryGateStatus newStatus) { - Objects.requireNonNull(newStatus, "newStatus"); - // Unsupported and pending gates never own a partially published body inventory. - var nextReadiness = newStatus == FrontendInventoryGateStatus.SUPPORTED - ? bodyInventoryReadiness - : FrontendBodyInventoryReadiness.NOT_PUBLISHED; - return new FrontendInventoryGate(owner, headerRoot, bodyRoot, deferredDomain, newStatus, nextReadiness); - } - - public @NotNull FrontendInventoryGate withBodyInventoryReadiness( - @NotNull FrontendBodyInventoryReadiness newReadiness - ) { - return new FrontendInventoryGate( - owner, - headerRoot, - bodyRoot, - deferredDomain, - status, - Objects.requireNonNull(newReadiness, "newReadiness") - ); - } - - public boolean isBodyInventoryReady() { - return status == FrontendInventoryGateStatus.SUPPORTED - && bodyInventoryReadiness == FrontendBodyInventoryReadiness.PUBLISHED; - } -} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateRegistry.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateRegistry.java deleted file mode 100644 index 3b64c01a..00000000 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateRegistry.java +++ /dev/null @@ -1,161 +0,0 @@ -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.ArrayList; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -/// Interface-layer registry for typed-dependent body gates. -/// -/// The registry is the single body-readiness fact source for future resolver/binder consumers. A -/// missing gate or any state other than `SUPPORTED + PUBLISHED` must be interpreted as fail-closed. -public final class FrontendInventoryGateRegistry { - private final @NotNull List gates = new ArrayList<>(); - private final @NotNull Map gatesByBodyRoot; - private final @NotNull Map> gatesByOwner; - private final @NotNull Map> gatesByHeaderRoot; - - public FrontendInventoryGateRegistry(@NotNull List gates) { - Objects.requireNonNull(gates, "gates"); - gatesByBodyRoot = new IdentityHashMap<>(); - gatesByOwner = new IdentityHashMap<>(); - gatesByHeaderRoot = new IdentityHashMap<>(); - for (var gate : gates) { - register(gate); - } - } - - public static @NotNull FrontendInventoryGateRegistry empty() { - return new FrontendInventoryGateRegistry(List.of()); - } - - public static @NotNull Builder builder() { - return new Builder(); - } - - public @NotNull List gates() { - return List.copyOf(gates); - } - - public @Nullable FrontendInventoryGate gateForBodyRoot(@NotNull Node bodyRoot) { - return gatesByBodyRoot.get(Objects.requireNonNull(bodyRoot, "bodyRoot")); - } - - public @NotNull List gatesForOwner(@NotNull Node owner) { - return copyIndexedGates(gatesByOwner, Objects.requireNonNull(owner, "owner")); - } - - public @NotNull List gatesForHeaderRoot(@NotNull Node headerRoot) { - return copyIndexedGates(gatesByHeaderRoot, Objects.requireNonNull(headerRoot, "headerRoot")); - } - - public boolean isBodyInventoryReady(@NotNull Node bodyRoot) { - var gate = gateForBodyRoot(bodyRoot); - return gate != null && gate.isBodyInventoryReady(); - } - - public void register(@NotNull FrontendInventoryGate gate) { - Objects.requireNonNull(gate, "gate"); - if (gatesByBodyRoot.containsKey(gate.bodyRoot())) { - throw new IllegalArgumentException("duplicate inventory gate body root"); - } - gates.add(gate); - gatesByBodyRoot.put(gate.bodyRoot(), gate); - gatesByOwner.computeIfAbsent(gate.owner(), _ -> new ArrayList<>()).add(gate); - gatesByHeaderRoot.computeIfAbsent(gate.headerRoot(), _ -> new ArrayList<>()).add(gate); - } - - public @NotNull FrontendInventoryGate updateStatus( - @NotNull Node bodyRoot, - @NotNull FrontendInventoryGateStatus status - ) { - return replaceGate(requireGateForBodyRoot(bodyRoot).withStatus(status)); - } - - public @NotNull FrontendInventoryGate updateBodyInventoryReadiness( - @NotNull Node bodyRoot, - @NotNull FrontendBodyInventoryReadiness readiness - ) { - return replaceGate(requireGateForBodyRoot(bodyRoot).withBodyInventoryReadiness(readiness)); - } - - public @NotNull FrontendInventoryGate markSupported(@NotNull Node bodyRoot) { - return updateStatus(bodyRoot, FrontendInventoryGateStatus.SUPPORTED); - } - - public @NotNull FrontendInventoryGate markUnsupported(@NotNull Node bodyRoot) { - return updateStatus(bodyRoot, FrontendInventoryGateStatus.UNSUPPORTED); - } - - public @NotNull FrontendInventoryGate markBodyInventoryPublishing(@NotNull Node bodyRoot) { - return updateBodyInventoryReadiness(bodyRoot, FrontendBodyInventoryReadiness.PUBLISHING); - } - - public @NotNull FrontendInventoryGate markBodyInventoryPublished(@NotNull Node bodyRoot) { - return updateBodyInventoryReadiness(bodyRoot, FrontendBodyInventoryReadiness.PUBLISHED); - } - - private @NotNull FrontendInventoryGate requireGateForBodyRoot(@NotNull Node bodyRoot) { - var gate = gateForBodyRoot(bodyRoot); - if (gate == null) { - throw new IllegalArgumentException("missing inventory gate body root"); - } - return gate; - } - - private @NotNull FrontendInventoryGate replaceGate(@NotNull FrontendInventoryGate nextGate) { - var previousGate = requireGateForBodyRoot(nextGate.bodyRoot()); - var gateIndex = gates.indexOf(previousGate); - gates.set(gateIndex, nextGate); - removeIndexedGate(gatesByOwner, previousGate.owner(), previousGate); - removeIndexedGate(gatesByHeaderRoot, previousGate.headerRoot(), previousGate); - gatesByBodyRoot.put(nextGate.bodyRoot(), nextGate); - gatesByOwner.computeIfAbsent(nextGate.owner(), _ -> new ArrayList<>()).add(nextGate); - gatesByHeaderRoot.computeIfAbsent(nextGate.headerRoot(), _ -> new ArrayList<>()).add(nextGate); - return nextGate; - } - - private static @NotNull List copyIndexedGates( - @NotNull Map> index, - @NotNull Node key - ) { - var indexedGates = index.get(key); - return indexedGates == null ? List.of() : List.copyOf(indexedGates); - } - - private static void removeIndexedGate( - @NotNull Map> index, - @NotNull Node key, - @NotNull FrontendInventoryGate gate - ) { - var indexedGates = index.get(key); - if (indexedGates == null) { - return; - } - indexedGates.remove(gate); - if (indexedGates.isEmpty()) { - index.remove(key); - } - } - - public static final class Builder { - private final @NotNull List gates = new ArrayList<>(); - - private Builder() { - } - - public @NotNull Builder add(@NotNull FrontendInventoryGate gate) { - gates.add(Objects.requireNonNull(gate, "gate")); - return this; - } - - public @NotNull FrontendInventoryGateRegistry build() { - return new FrontendInventoryGateRegistry(gates); - } - } -} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateStatus.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateStatus.java deleted file mode 100644 index 0bb604b1..00000000 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateStatus.java +++ /dev/null @@ -1,8 +0,0 @@ -package gd.script.gdcc.frontend.sema; - -/// Classification state for a typed-dependent body gate. -public enum FrontendInventoryGateStatus { - PENDING, - SUPPORTED, - UNSUPPORTED -} diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendSuiteEntryRoots.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendSuiteEntryRoots.java index 43b3c9c4..7b928d69 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendSuiteEntryRoots.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendSuiteEntryRoots.java @@ -16,8 +16,7 @@ /// Body-entry roots produced by the Interface phase. /// -/// Only roots listed here are legal for the future `SuiteResolver` to enter. Typed-dependent bodies -/// are deliberately excluded while their `FrontendInventoryGate` remains unpublished. +/// Only structurally supported roots listed here are legal for `FrontendSuiteResolver` to enter. public record FrontendSuiteEntryRoots( @NotNull List callableOwners, @NotNull List propertyInitializers, 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 index 7a93ce6c..90d0b5d0 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProcedures.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProcedures.java @@ -29,11 +29,11 @@ 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.FrontendExecutableInventorySupport; import gd.script.gdcc.frontend.sema.FrontendExpressionType; import gd.script.gdcc.frontend.sema.FrontendExpressionTypeStatus; import gd.script.gdcc.frontend.sema.FrontendReceiverKind; @@ -639,11 +639,9 @@ private boolean tryPublishTypeMetaBinding( } var declarationScope = context.analysisData().scopesByAst().get(variableDeclaration); if (!(declarationScope instanceof BlockScope blockScope) - || !FrontendExecutableInventorySupport.isCallableLocalValueInventoryReady( - blockScope, - context.currentBlockRoot(), - context.interfaceSurface().inventoryGateRegistry() - )) { + || !FrontendBodySemanticSupportPolicy.forBlockScopeKind( + blockScope.kind() + ).publishesLexicalInventory()) { return null; } var survivingLocal = blockScope.resolveValueHere(variableDeclaration.name().trim()); @@ -1038,7 +1036,6 @@ private static void publishAttributeStepExpressionTypes( cachedBodyDeclarationIndex = bodyDeclarationIndex; cachedVisibleValueResolver = new FrontendVisibleValueResolver( context.analysisData(), - context.interfaceSurface().inventoryGateRegistry(), bodyDeclarationIndex ); } 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 6cb1f9f3..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 @@ -418,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; } 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 index b86497a1..8399eadd 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendInterfacePhase.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendInterfacePhase.java @@ -12,7 +12,6 @@ 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.MatchSection; import dev.superice.gdparser.frontend.ast.MatchStatement; import dev.superice.gdparser.frontend.ast.Node; import dev.superice.gdparser.frontend.ast.Parameter; @@ -28,14 +27,12 @@ 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.FrontendInventoryGate; -import gd.script.gdcc.frontend.sema.FrontendInventoryGateRegistry; import gd.script.gdcc.frontend.sema.FrontendSuiteEntryRoots; import gd.script.gdcc.frontend.sema.FrontendTypedLexicalBaseline; -import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueDomain; 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; @@ -48,8 +45,7 @@ /// /// 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, typed baseline slots, and pending -/// typed-dependent gates. +/// describing supported body entries, declaration source order, and typed baseline slots. public class FrontendInterfacePhase { public @NotNull FrontendInterfaceSurface analyze( @NotNull ClassRegistry classRegistry, @@ -73,8 +69,6 @@ private static final class InterfaceSurfaceBuilder implements ASTNodeHandler { private final @NotNull Map> declarationsByBodyRoot = new IdentityHashMap<>(); private final @NotNull Map sourcePathsByEntryRoot = new IdentityHashMap<>(); - private final @NotNull FrontendInventoryGateRegistry.Builder gateRegistryBuilder = - FrontendInventoryGateRegistry.builder(); private final @NotNull FrontendTypedLexicalBaseline.Builder typedBaselineBuilder = FrontendTypedLexicalBaseline.builder(); private final @NotNull List callableOwners = new ArrayList<>(); @@ -97,7 +91,6 @@ private void walk(@NotNull Path sourcePath, @NotNull SourceFile sourceFile) { private @NotNull FrontendInterfaceSurface build() { return new FrontendInterfaceSurface( new FrontendBodyDeclarationIndex(declarationsByBodyRoot), - gateRegistryBuilder.build(), typedBaselineBuilder.build(), new FrontendSuiteEntryRoots( callableOwners, @@ -146,14 +139,6 @@ private void walk(@NotNull Path sourcePath, @NotNull SourceFile sourceFile) { @Override public @NotNull FrontendASTTraversalDirective handleLambdaExpression(@NotNull LambdaExpression lambdaExpression) { - if (!isNotPublished(lambdaExpression.body())) { - addPendingGate( - lambdaExpression, - lambdaExpression, - lambdaExpression.body(), - FrontendVisibleValueDomain.LAMBDA_SUBTREE - ); - } return FrontendASTTraversalDirective.SKIP_CHILDREN; } @@ -211,24 +196,12 @@ private void walk(@NotNull Path sourcePath, @NotNull SourceFile sourceFile) { astWalker.walk(forStatement.iteratorType()); } astWalker.walk(forStatement.iterable()); - addPendingGate( - forStatement, - forStatement, - forStatement.body(), - FrontendVisibleValueDomain.FOR_SUBTREE - ); + enterSupportedBlock(forStatement.body(), forStatement); return FrontendASTTraversalDirective.SKIP_CHILDREN; } @Override public @NotNull FrontendASTTraversalDirective handleMatchStatement(@NotNull MatchStatement matchStatement) { - if (supportedBodyDepth <= 0 || isNotPublished(matchStatement)) { - return FrontendASTTraversalDirective.SKIP_CHILDREN; - } - astWalker.walk(matchStatement.value()); - for (var section : matchStatement.sections()) { - registerMatchSectionGate(matchStatement, section); - } return FrontendASTTraversalDirective.SKIP_CHILDREN; } @@ -241,12 +214,6 @@ private void walk(@NotNull Path sourcePath, @NotNull SourceFile sourceFile) { return FrontendASTTraversalDirective.SKIP_CHILDREN; } if (variableDeclaration.kind() == DeclarationKind.CONST) { - addPendingGate( - variableDeclaration, - variableDeclaration, - variableDeclaration, - FrontendVisibleValueDomain.BLOCK_LOCAL_CONST_SUBTREE - ); return FrontendASTTraversalDirective.SKIP_CHILDREN; } if (variableDeclaration.kind() == DeclarationKind.VAR) { @@ -295,6 +262,10 @@ private void recordPropertyInitializer(@NotNull VariableDeclaration variableDecl } private void enterSupportedBlock(@NotNull Block block) { + enterSupportedBlock(block, null); + } + + private void enterSupportedBlock(@NotNull Block block, @Nullable ForStatement ownerFor) { var scope = scopesByAst.get(block); if (!(scope instanceof BlockScope blockScope) || !FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(blockScope.kind())) { @@ -307,6 +278,9 @@ private void enterSupportedBlock(@NotNull Block block) { currentBodyDeclarations = bodyDeclarations; supportedBodyDepth++; try { + if (ownerFor != null) { + recordIteratorDeclaration(ownerFor, blockScope); + } walkStatements(block.statements()); } finally { supportedBodyDepth--; @@ -328,36 +302,27 @@ private void recordLocalDeclaration(@NotNull VariableDeclaration variableDeclara currentBodyDeclarations.add(new FrontendBodyLocalDeclaration( variableDeclaration, binding, + FrontendBodyLocalDeclaration.Kind.ORDINARY_VAR, currentBodyDeclarations.size() )); typedBaselineBuilder.put(variableDeclaration, binding.type()); } - private void registerMatchSectionGate( - @NotNull MatchStatement matchStatement, - @NotNull MatchSection section + private void recordIteratorDeclaration( + @NotNull ForStatement forStatement, + @NotNull BlockScope blockScope ) { - for (var pattern : section.patterns()) { - astWalker.walk(pattern); - } - if (section.guard() != null) { - astWalker.walk(section.guard()); + var binding = blockScope.resolveValueHere(forStatement.iterator().trim()); + if (binding == null || binding.declaration() != forStatement) { + return; } - addPendingGate( - matchStatement, - matchStatement, - section.body(), - FrontendVisibleValueDomain.MATCH_SUBTREE - ); - } - - private void addPendingGate( - @NotNull Node owner, - @NotNull Node headerRoot, - @NotNull Node bodyRoot, - @NotNull FrontendVisibleValueDomain deferredDomain - ) { - gateRegistryBuilder.add(FrontendInventoryGate.pending(owner, headerRoot, bodyRoot, deferredDomain)); + currentBodyDeclarations.add(new FrontendBodyLocalDeclaration( + forStatement, + binding, + FrontendBodyLocalDeclaration.Kind.ITERATOR, + 0 + )); + typedBaselineBuilder.put(forStatement, binding.type()); } private void walkStatements(@NotNull List statements) { 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 index 790434c0..899b4367 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendStatementResolver.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendStatementResolver.java @@ -22,16 +22,12 @@ /// Root-bounded statement dispatcher for the new body SuiteResolver skeleton. /// -/// The no-op constructor is retained for traversal tests and explicit legacy shims. Production -/// wiring reaches this dispatcher through `FrontendSuiteResolver`, which supplies real owner -/// procedures and publishes facts through the typed lexical environment. +/// 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() { - this(OwnerProcedures.noop()); - } - public FrontendStatementResolver(@NotNull OwnerProcedures ownerProcedures) { this.ownerProcedures = Objects.requireNonNull(ownerProcedures, "ownerProcedures must not be null"); } @@ -59,7 +55,7 @@ public void resolveStatement( case AssertStatement assertStatement -> resolveSupportedRoot(context, assertStatement); case IfStatement ifStatement -> resolveIfStatement(context, ifStatement, childSuiteResolver); case WhileStatement whileStatement -> resolveWhileStatement(context, whileStatement, childSuiteResolver); - case ForStatement forStatement -> resolveUnsupportedRoot(context, forStatement); + case ForStatement forStatement -> resolveForStatement(context, forStatement, childSuiteResolver); case MatchStatement matchStatement -> resolveUnsupportedRoot(context, matchStatement); case PassStatement _, BreakStatement _, ContinueStatement _ -> flushStatementBoundary(context); default -> resolveUnsupportedRoot(context, statement); @@ -110,14 +106,32 @@ private void resolveWhileStatement( 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 gate. + 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.runGateClassifier(context, root); ownerProcedures.runVarTypePost(context, root); - flushStatementBoundary(context); } private void resolveUnsupportedRoot(@NotNull FrontendSuiteContext context, @NotNull Node root) { @@ -138,11 +152,6 @@ public interface ChildSuiteResolver { } public interface OwnerProcedures { - static @NotNull OwnerProcedures noop() { - return new OwnerProcedures() { - }; - } - default void runTopBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { } @@ -155,12 +164,6 @@ default void runChainBinding(@NotNull FrontendSuiteContext context, @NotNull Nod default void runExprType(@NotNull FrontendSuiteContext context, @NotNull Node root) { } - /// Runs after header expression facts are finalized into the current overlay but before the - /// statement boundary is flushed. Phase F test classifiers use this hook to advance synthetic - /// gate lifecycle without implementing any feature-specific rules. - default void runGateClassifier(@NotNull FrontendSuiteContext context, @NotNull Node root) { - } - default void runVarTypePost(@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 index 62db2394..f1076757 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteContext.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteContext.java @@ -4,9 +4,8 @@ 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.scope.BlockScopeKind; 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.FrontendInterfaceSurface; import gd.script.gdcc.frontend.sema.FrontendTypedLexicalEnvironment; import gd.script.gdcc.frontend.sema.analyzer.support.FrontendPropertyInitializerSupport; @@ -92,24 +91,6 @@ public record FrontendSuiteContext( if (currentBlockRoot == null || currentBlockScope == null) { return FrontendVisibleValueDomain.EXECUTABLE_BODY; } - var gateRegistry = interfaceSurface.inventoryGateRegistry(); - if (FrontendExecutableInventorySupport.isCallableLocalValueInventoryReady( - currentBlockScope, - currentBlockRoot, - gateRegistry - )) { - return FrontendVisibleValueDomain.EXECUTABLE_BODY; - } - var gate = gateRegistry.gateForBodyRoot(currentBlockRoot); - return gate != null ? gate.deferredDomain() : deferredDomainForBlockKind(currentBlockScope.kind()); - } - - private static @NotNull FrontendVisibleValueDomain deferredDomainForBlockKind(@NotNull BlockScopeKind kind) { - return switch (kind) { - case FOR_BODY -> FrontendVisibleValueDomain.FOR_SUBTREE; - case MATCH_SECTION_BODY -> FrontendVisibleValueDomain.MATCH_SUBTREE; - case LAMBDA_BODY -> FrontendVisibleValueDomain.LAMBDA_SUBTREE; - default -> 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 index 26c0c3f5..29fbb62d 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolver.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolver.java @@ -11,7 +11,7 @@ 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.FrontendExecutableInventorySupport; +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; @@ -73,10 +73,13 @@ public void resolve( 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 = blockScopeFor(context, block); - if (blockScope == null || !isCallableLocalValueInventoryReady(context, block, blockScope)) { - return; - } + var blockScope = requireBlockScope(context, block); + FrontendBodyStructuralCompleteness.requireStructurallyCompleteBody( + context.analysisData(), + context.interfaceSurface(), + block, + blockScope + ); for (var statement : block.statements()) { statementResolver.resolveStatement(context, statement, this::resolveChildSuite); } @@ -98,12 +101,12 @@ private void resolveCallableOwner( @NotNull DiagnosticManager diagnosticManager ) { var body = callableBody(callableOwner); - if (body == null || !interfaceSurface.suiteEntryRoots().containsSupportedBlock(body)) { - return; + 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)) { - return; + throw new IllegalStateException("Suite entry callable body has no published BlockScope"); } var environment = new FrontendTypedLexicalEnvironment( blockScope, @@ -238,31 +241,19 @@ private void resolvePropertyInitializer( /// 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 = blockScopeFor(parentContext, childBlock); - if (blockScope == null || !isCallableLocalValueInventoryReady(parentContext, childBlock, blockScope)) { - return; - } + var blockScope = requireBlockScope(parentContext, childBlock); resolveSuite(parentContext.withChildBlock(childBlock, blockScope), childBlock); } - private static boolean isCallableLocalValueInventoryReady( - @NotNull FrontendSuiteContext context, - @NotNull Block block, - @NotNull BlockScope blockScope - ) { - return FrontendExecutableInventorySupport.isCallableLocalValueInventoryReady( - blockScope, - block, - context.interfaceSurface().inventoryGateRegistry() - ); - } - - private static @Nullable BlockScope blockScopeFor( + private static @NotNull BlockScope requireBlockScope( @NotNull FrontendSuiteContext context, @NotNull Block block ) { var scope = context.analysisData().scopesByAst().get(block); - return scope instanceof BlockScope blockScope ? blockScope : null; + if (scope instanceof BlockScope blockScope) { + return blockScope; + } + throw new IllegalStateException("Suite body has no published BlockScope"); } private static @Nullable Block callableBody(@NotNull Node callableOwner) { 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..7dadd257 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 @@ -625,16 +664,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/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 dd8e019b..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 @@ -2,20 +2,17 @@ import gd.script.gdcc.frontend.scope.BlockScope; 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.FrontendBodySemanticSupportPolicy; import gd.script.gdcc.frontend.sema.FrontendBodyDeclarationIndex; -import gd.script.gdcc.frontend.sema.FrontendExecutableInventorySupport; -import gd.script.gdcc.frontend.sema.FrontendInventoryGate; -import gd.script.gdcc.frontend.sema.FrontendInventoryGateRegistry; import gd.script.gdcc.frontend.sema.FrontendModuleSkeleton; import gd.script.gdcc.frontend.sema.FrontendTypedLexicalEnvironment; -import gd.script.gdcc.frontend.scope.CallableScopeKind; 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; @@ -38,31 +35,19 @@ /// explicit deferred-boundary sealing for domains whose variable inventory is not published yet public final class FrontendVisibleValueResolver { private final @NotNull FrontendAnalysisData analysisData; - private final @NotNull FrontendInventoryGateRegistry gateRegistry; 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, FrontendInventoryGateRegistry.empty(), null); + this(analysisData, null); } public FrontendVisibleValueResolver( @NotNull FrontendAnalysisData analysisData, - @NotNull FrontendInventoryGateRegistry gateRegistry - ) { - this(analysisData, gateRegistry, null); - } - - /// `bodyDeclarationIndex` is supplied by the production SuiteResolver path to validate that a - /// selected local belongs to Interface-phase inventory. Legacy callers retain range-only filtering. - public FrontendVisibleValueResolver( - @NotNull FrontendAnalysisData analysisData, - @NotNull FrontendInventoryGateRegistry gateRegistry, @Nullable FrontendBodyDeclarationIndex bodyDeclarationIndex ) { this.analysisData = Objects.requireNonNull(analysisData, "analysisData must not be null"); - this.gateRegistry = Objects.requireNonNull(gateRegistry, "gateRegistry must not be null"); this.bodyDeclarationIndex = bodyDeclarationIndex; indexSourceAstParents(analysisData.moduleSkeleton()); } @@ -92,14 +77,13 @@ public FrontendVisibleValueResolver( 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, useSite); + var currentScopeBoundary = classifyUnsupportedCurrentScopeBoundary(currentScope); if (currentScopeBoundary != null) { return FrontendVisibleValueResolution.deferredUnsupported(currentScopeBoundary); } @@ -188,39 +172,22 @@ 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 -> gateBodyBoundary( - lambdaExpression.body(), - FrontendVisibleValueDomain.LAMBDA_SUBTREE + 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) -> gateBodyBoundary( - variableDeclaration, - FrontendVisibleValueDomain.BLOCK_LOCAL_CONST_SUBTREE - ); - case ForStatement forStatement when forStatement.body() == childNode -> gateBodyBoundary( - forStatement.body(), - FrontendVisibleValueDomain.FOR_SUBTREE + && isDeferredBlockLocalConst(variableDeclaration) -> structuralBoundary( + FrontendBodySemanticSupportPolicy.BLOCK_LOCAL_CONST_SUBTREE ); - case ForStatement forStatement when (forStatement.iteratorType() == childNode - || forStatement.iterable() == childNode) -> new FrontendVisibleValueDeferredBoundary( - FrontendVisibleValueDomain.FOR_SUBTREE, - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED - ); - case MatchSection matchSection when matchSection.body() == childNode -> gateBodyBoundary( - matchSection.body(), - FrontendVisibleValueDomain.MATCH_SUBTREE + case MatchSection matchSection when matchSection.body() == childNode -> structuralBoundary( + FrontendBodySemanticSupportPolicy.MATCH_SUBTREE ); case MatchSection matchSection when (matchSection.guard() == 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; }; } @@ -231,28 +198,18 @@ case MatchSection matchSection when (matchSection.guard() == childNode if (request.domain() == FrontendVisibleValueDomain.EXECUTABLE_BODY) { return null; } - // Transitional callers may still pass a deferred domain for a gate body. A published owning - // gate normalizes that request into ordinary executable-body lookup; every other case stays - // fail-closed at the request-domain boundary. - if (isNearestOwningGateReady(request.useSite(), request.domain())) { - return null; - } return new FrontendVisibleValueDeferredBoundary( request.domain(), FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN ); } - private @Nullable FrontendVisibleValueDeferredBoundary gateBodyBoundary( - @NotNull Node bodyRoot, - @NotNull FrontendVisibleValueDomain deferredDomain + private @NotNull FrontendVisibleValueDeferredBoundary structuralBoundary( + @NotNull FrontendBodySemanticSupportPolicy policy ) { - if (gateRegistry.isBodyInventoryReady(bodyRoot)) { - return null; - } return new FrontendVisibleValueDeferredBoundary( - deferredDomain, - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED + policy.visibleValueDomain(), + FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN ); } @@ -261,7 +218,9 @@ case MatchSection matchSection when (matchSection.guard() == childNode 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 @@ -313,13 +272,12 @@ private boolean isDeferredBlockLocalConst(@NotNull VariableDeclaration variableD if (!(statement instanceof VariableDeclaration variableDeclaration) || variableDeclaration.kind() != DeclarationKind.CONST || !variableDeclaration.name().trim().equals(name) - || !isVisibleLocal(variableDeclaration, useSite) - || gateRegistry.isBodyInventoryReady(variableDeclaration)) { + || !isVisibleLocal(variableDeclaration, useSite)) { return null; } return new FrontendVisibleValueDeferredBoundary( FrontendVisibleValueDomain.BLOCK_LOCAL_CONST_SUBTREE, - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED + FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN ); } @@ -327,13 +285,12 @@ private boolean isDeferredBlockLocalConst(@NotNull VariableDeclaration variableD /// that still reuse one outer supported scope. This current-scope gate exists as the fail-closed /// backstop for unsupported bodies whose own scope is already published in `scopesByAst()`. private @Nullable FrontendVisibleValueDeferredBoundary classifyUnsupportedCurrentScopeBoundary( - @NotNull Scope currentScope, - @NotNull Node useSite + @NotNull Scope currentScope ) { return switch (currentScope) { - case BlockScope blockScope -> classifyUnsupportedCurrentBlockScopeBoundary(blockScope, useSite); + case BlockScope blockScope -> classifyUnsupportedCurrentBlockScopeBoundary(blockScope); case CallableScope callableScope when callableScope.kind() == CallableScopeKind.LAMBDA_EXPRESSION -> - unsupportedScopeBoundary(useSite, FrontendVisibleValueDomain.LAMBDA_SUBTREE); + unsupportedScopeBoundary(FrontendBodySemanticSupportPolicy.LAMBDA_SUBTREE); default -> new FrontendVisibleValueDeferredBoundary( FrontendVisibleValueDomain.EXECUTABLE_BODY, FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN @@ -353,60 +310,27 @@ private boolean containsNodeIdentity(@NotNull List nodes, @NotNu } private @Nullable FrontendVisibleValueDeferredBoundary classifyUnsupportedCurrentBlockScopeBoundary( - @NotNull BlockScope blockScope, - @NotNull Node useSite + @NotNull BlockScope blockScope ) { - if (FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(blockScope.kind())) { + var policy = FrontendBodySemanticSupportPolicy.forBlockScopeKind(blockScope.kind()); + if (policy.publishesLexicalInventory()) { return null; } - return switch (blockScope.kind()) { - case LAMBDA_BODY -> unsupportedScopeBoundary(useSite, FrontendVisibleValueDomain.LAMBDA_SUBTREE); - case FOR_BODY -> unsupportedScopeBoundary(useSite, FrontendVisibleValueDomain.FOR_SUBTREE); - case MATCH_SECTION_BODY -> unsupportedScopeBoundary(useSite, FrontendVisibleValueDomain.MATCH_SUBTREE); - default -> new FrontendVisibleValueDeferredBoundary( - FrontendVisibleValueDomain.EXECUTABLE_BODY, - FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN - ); - }; - } - - private @Nullable FrontendVisibleValueDeferredBoundary unsupportedScopeBoundary( - @NotNull Node useSite, - @NotNull FrontendVisibleValueDomain deferredDomain - ) { - if (isNearestOwningGateReady(useSite, deferredDomain)) { - return null; - } - return new FrontendVisibleValueDeferredBoundary( - deferredDomain, - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED - ); + return unsupportedScopeBoundary(policy); } - private boolean isNearestOwningGateReady( - @NotNull Node useSite, - @NotNull FrontendVisibleValueDomain deferredDomain + private @NotNull FrontendVisibleValueDeferredBoundary unsupportedScopeBoundary( + @NotNull FrontendBodySemanticSupportPolicy policy ) { - var gate = nearestOwningGate(useSite); - return gate != null && gate.deferredDomain() == deferredDomain && gate.isBodyInventoryReady(); - } - - private @Nullable FrontendInventoryGate nearestOwningGate(@NotNull Node useSite) { - Node currentNode = useSite; - while (currentNode != null) { - var gate = gateRegistry.gateForBodyRoot(currentNode); - if (gate != null) { - return gate; - } - currentNode = parentByNode.get(currentNode); - } - return null; + 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( @@ -495,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/sema/FrontendBodySemanticSupportPolicyTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendBodySemanticSupportPolicyTest.java new file mode 100644 index 00000000..d8f80d7a --- /dev/null +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendBodySemanticSupportPolicyTest.java @@ -0,0 +1,76 @@ +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()); + } +} diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendInterfacePhaseTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendInterfacePhaseTest.java index d9f14171..1ffb3b08 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendInterfacePhaseTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendInterfacePhaseTest.java @@ -98,7 +98,10 @@ func ping(value: int, alias): var bodyDeclarations = surface.bodyDeclarationIndex().declarationsFor(pingFunction.body()); assertEquals(List.of("first", "second"), bodyDeclarations.stream() - .map(localDeclaration -> localDeclaration.declaration().name()) + .map(localDeclaration -> assertInstanceOf( + VariableDeclaration.class, + localDeclaration.declaration() + ).name()) .toList()); assertEquals(0, bodyDeclarations.getFirst().sourceOrder()); assertEquals(1, bodyDeclarations.getLast().sourceOrder()); @@ -152,7 +155,10 @@ func ping(): 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()); + var resolver = new FrontendVisibleValueResolver( + phaseInput.analysisData(), + surface.bodyDeclarationIndex() + ); var resolution = resolver.resolve(new FrontendVisibleValueResolveRequest( "second", secondUseSite, @@ -162,7 +168,10 @@ func ping(): assertEquals(List.of("first", "second"), surface.bodyDeclarationIndex() .declarationsFor(pingFunction.body()) .stream() - .map(localDeclaration -> localDeclaration.declaration().name()) + .map(localDeclaration -> assertInstanceOf( + VariableDeclaration.class, + localDeclaration.declaration() + ).name()) .toList()); assertEquals(FrontendVisibleValueStatus.NOT_FOUND, resolution.status()); assertNull(resolution.visibleValue()); @@ -173,7 +182,7 @@ func ping(): } @Test - void recordsPendingTypedDependentGatesWithoutOpeningTheirBodies() throws Exception { + void publishesForInventoryWhileUnsupportedFeatureOwnedBodiesStayExcluded() throws Exception { var phaseInput = phaseInput("interface_pending_gates.gd", """ class_name InterfacePendingGates extends Node @@ -209,26 +218,126 @@ func ping(items, choice): 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 registry = surface.inventoryGateRegistry(); - assertPendingGate(registry.gateForBodyRoot(forStatement.body()), FrontendVisibleValueDomain.FOR_SUBTREE); - assertPendingGate( - registry.gateForBodyRoot(matchStatement.sections().getFirst().body()), - FrontendVisibleValueDomain.MATCH_SUBTREE - ); - assertPendingGate(registry.gateForBodyRoot(lambda.body()), FrontendVisibleValueDomain.LAMBDA_SUBTREE); - assertPendingGate(registry.gateForBodyRoot(answer), FrontendVisibleValueDomain.BLOCK_LOCAL_CONST_SUBTREE); - assertFalse(registry.isBodyInventoryReady(forStatement.body())); - assertFalse(surface.suiteEntryRoots().containsSupportedBlock(forStatement.body())); + 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(forStatement.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", """ @@ -251,18 +360,6 @@ func ping(value: int): assertTrue(error.getMessage().contains("compiler-only type leaked")); } - private static void assertPendingGate( - FrontendInventoryGate maybeGate, - @NotNull FrontendVisibleValueDomain domain - ) { - if (maybeGate == null) { - fail("Expected pending gate for domain: " + domain); - } - assertEquals(domain, maybeGate.deferredDomain()); - assertEquals(FrontendInventoryGateStatus.PENDING, maybeGate.status()); - assertEquals(FrontendBodyInventoryReadiness.NOT_PUBLISHED, maybeGate.bodyInventoryReadiness()); - } - private static @NotNull FrontendFilteredValueHitReason primaryFilteredHitReason( gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueResolution resolution ) { @@ -353,4 +450,11 @@ private record PhaseInput( @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/FrontendInventoryGateRegistryTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateRegistryTest.java deleted file mode 100644 index 18f0a002..00000000 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendInventoryGateRegistryTest.java +++ /dev/null @@ -1,185 +0,0 @@ -package gd.script.gdcc.frontend.sema; - -import dev.superice.gdparser.frontend.ast.ForStatement; -import dev.superice.gdparser.frontend.ast.Node; -import gd.script.gdcc.frontend.diagnostic.DiagnosticManager; -import gd.script.gdcc.frontend.parse.GdScriptParserService; -import gd.script.gdcc.frontend.sema.resolver.FrontendVisibleValueDomain; -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.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class FrontendInventoryGateRegistryTest { - @Test - void pendingGateFactoryStartsFailClosed() throws Exception { - var forStatement = parseForStatement(); - - var gate = FrontendInventoryGate.pending( - forStatement, - forStatement, - forStatement.body(), - FrontendVisibleValueDomain.FOR_SUBTREE - ); - - assertEquals(FrontendInventoryGateStatus.PENDING, gate.status()); - assertEquals(FrontendBodyInventoryReadiness.NOT_PUBLISHED, gate.bodyInventoryReadiness()); - assertFalse(gate.isBodyInventoryReady()); - } - - @Test - void registryTransitionsOnlySupportedPublishedGateToReady() throws Exception { - var forStatement = parseForStatement(); - var registry = FrontendInventoryGateRegistry.builder() - .add(FrontendInventoryGate.pending( - forStatement, - forStatement, - forStatement.body(), - FrontendVisibleValueDomain.FOR_SUBTREE - )) - .build(); - - assertFalse(registry.isBodyInventoryReady(forStatement.body())); - - var supported = registry.markSupported(forStatement.body()); - assertEquals(FrontendInventoryGateStatus.SUPPORTED, supported.status()); - assertEquals(FrontendBodyInventoryReadiness.NOT_PUBLISHED, supported.bodyInventoryReadiness()); - assertFalse(registry.isBodyInventoryReady(forStatement.body())); - - var publishing = registry.markBodyInventoryPublishing(forStatement.body()); - assertEquals(FrontendBodyInventoryReadiness.PUBLISHING, publishing.bodyInventoryReadiness()); - assertFalse(registry.isBodyInventoryReady(forStatement.body())); - - var published = registry.markBodyInventoryPublished(forStatement.body()); - assertEquals(FrontendInventoryGateStatus.SUPPORTED, published.status()); - assertEquals(FrontendBodyInventoryReadiness.PUBLISHED, published.bodyInventoryReadiness()); - assertTrue(registry.isBodyInventoryReady(forStatement.body())); - - var unsupported = registry.markUnsupported(forStatement.body()); - assertEquals(FrontendInventoryGateStatus.UNSUPPORTED, unsupported.status()); - assertEquals(FrontendBodyInventoryReadiness.NOT_PUBLISHED, unsupported.bodyInventoryReadiness()); - assertFalse(registry.isBodyInventoryReady(forStatement.body())); - } - - @Test - void invalidReadinessStatesAreRejected() throws Exception { - var forStatement = parseForStatement(); - var pending = FrontendInventoryGate.pending( - forStatement, - forStatement, - forStatement.body(), - FrontendVisibleValueDomain.FOR_SUBTREE - ); - - assertThrows( - IllegalArgumentException.class, - () -> pending.withBodyInventoryReadiness(FrontendBodyInventoryReadiness.PUBLISHING) - ); - assertThrows( - IllegalArgumentException.class, - () -> new FrontendInventoryGate( - forStatement, - forStatement, - forStatement.body(), - FrontendVisibleValueDomain.FOR_SUBTREE, - FrontendInventoryGateStatus.UNSUPPORTED, - FrontendBodyInventoryReadiness.PUBLISHED - ) - ); - } - - @Test - void registryRejectsDuplicateBodyRootAndReturnsImmutableSnapshot() throws Exception { - var forStatement = parseForStatement(); - var gate = FrontendInventoryGate.pending( - forStatement, - forStatement, - forStatement.body(), - FrontendVisibleValueDomain.FOR_SUBTREE - ); - - assertThrows( - IllegalArgumentException.class, - () -> FrontendInventoryGateRegistry.builder().add(gate).add(gate).build() - ); - var registry = FrontendInventoryGateRegistry.builder().add(gate).build(); - - assertSame(gate, registry.gateForBodyRoot(forStatement.body())); - assertEquals(List.of(gate), registry.gatesForOwner(forStatement)); - assertEquals(List.of(gate), registry.gatesForHeaderRoot(forStatement)); - assertThrows(UnsupportedOperationException.class, () -> registry.gates().add(gate)); - } - - @Test - void missingGateStaysFailClosed() throws Exception { - var forStatement = parseForStatement(); - - assertFalse(FrontendInventoryGateRegistry.empty().isBodyInventoryReady(forStatement.body())); - assertThrows( - IllegalArgumentException.class, - () -> FrontendInventoryGateRegistry.empty().markSupported(forStatement.body()) - ); - } - - private static @NotNull ForStatement parseForStatement() throws Exception { - var diagnostics = new DiagnosticManager(); - var unit = new GdScriptParserService().parseUnit(Path.of("tmp", "gate_registry.gd"), """ - class_name GateRegistry - extends Node - - func ping(values): - for value in values: - print(value) - """, diagnostics); - assertTrue(diagnostics.isEmpty(), () -> "Unexpected parse diagnostics: " + diagnostics.snapshot()); - return findNode(unit.ast(), ForStatement.class, _ -> true); - } - - private static @NotNull 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; - } -} 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 9ad4cebe..f66daa43 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSemanticAnalyzerFrameworkTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSemanticAnalyzerFrameworkTest.java @@ -3,6 +3,7 @@ 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; @@ -33,6 +34,7 @@ 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; @@ -1055,7 +1057,7 @@ func ping(value: Point) -> int: } @Test - void defaultInterfaceBodyPipelineKeepsUnsupportedSubtreesFailClosed() throws Exception { + void defaultInterfaceBodyPipelineSupportsForWhileOtherUnsupportedSubtreesStayFailClosed() throws Exception { var parserService = new GdScriptParserService(); var unit = parserService.parseUnit(Path.of("tmp", "segmented_unsupported_equivalence.gd"), """ class_name SegmentedUnsupportedEquivalence @@ -1088,9 +1090,15 @@ func ping(values): var hiddenUseSite = assertInstanceOf(IdentifierExpression.class, hiddenLocal.value()); assertNull(interfaceBody.slotTypes().get(blockedConst)); - assertNull(interfaceBody.slotTypes().get(hiddenLocal)); - assertNull(interfaceBody.symbolBindings().get(hiddenUseSite)); + 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()); } diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java index 59dbf74c..9addb0ac 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java @@ -32,19 +32,21 @@ 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.frontend.sema.resolver.FrontendVisibleValueDomain; 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; @@ -54,6 +56,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 FrontendSuiteResolverTest { @@ -203,7 +206,7 @@ func ping(value, other): } @Test - void unsupportedTypedDependentBodiesRemainFailClosed() throws Exception { + void forBodyResolvesWhileUnsupportedFeatureOwnedBodiesRemainFailClosed() throws Exception { var phaseInput = phaseInput("suite_fail_closed.gd", """ class_name SuiteFailClosed extends Node @@ -268,13 +271,13 @@ func ping(items, choice): phaseInput.diagnostics() ); - assertFalse(surface.suiteEntryRoots().containsSupportedBlock(forStatement.body())); + assertTrue(surface.suiteEntryRoots().containsSupportedBlock(forStatement.body())); assertFalse(surface.suiteEntryRoots().containsSupportedBlock(matchStatement.sections().getFirst().body())); assertFalse(surface.suiteEntryRoots().containsSupportedBlock(lambdaExpression.body())); - assertTrue(ownerProcedures.unsupportedRoots().contains(forStatement)); + assertFalse(ownerProcedures.unsupportedRoots().contains(forStatement)); assertTrue(ownerProcedures.unsupportedRoots().contains(matchStatement)); assertTrue(ownerProcedures.unsupportedRoots().contains(answer)); - assertFalse(hasOwnerEvent(ownerProcedures.events(), fromFor)); + assertTrue(hasOwnerEvent(ownerProcedures.events(), fromFor)); assertFalse(hasOwnerEvent(ownerProcedures.events(), fromMatch)); assertFalse(hasOwnerEvent(ownerProcedures.events(), fromLambda)); assertFalse(hasOwnerEvent(ownerProcedures.events(), answer)); @@ -283,18 +286,15 @@ func ping(items, choice): } @Test - void classifierReadsPrefixOverlayAndDoesNotOpenUnpublishedBody() throws Exception { - var phaseInput = phaseInput("suite_gate_classifier_prefix.gd", """ - class_name SuiteGateClassifierPrefix + void forHeaderReadsPrefixOverlayAndBodyEntryDoesNotDependOnTypedClassification() throws Exception { + var phaseInput = phaseInput("suite_for_header_prefix.gd", """ + class_name SuiteForHeaderPrefix extends Node - func ping(values, choice): + func ping(): var limit := 1 - for item in values: + for item in limit: var from_for := item - match choice: - 0: - var from_match := choice """); var pingFunction = findStatement( phaseInput.unit().ast().statements(), @@ -312,34 +312,34 @@ func ping(values, choice): VariableDeclaration.class, variableDeclaration -> variableDeclaration.name().equals("from_for") ); - var matchStatement = findStatement(pingFunction.body().statements(), MatchStatement.class, _ -> true); var surface = new FrontendInterfacePhase().analyze(phaseInput.registry(), phaseInput.analysisData()); - var ownerProcedures = new PrefixGateClassifierOwnerProcedures(limit, forStatement.body()); + var itemUseSite = findNode( + fromFor, + IdentifierExpression.class, + identifierExpression -> identifierExpression.name().equals("item") + ); - new FrontendSuiteResolver(new FrontendStatementResolver(ownerProcedures)).resolve( + new FrontendSuiteResolver().resolve( surface, phaseInput.registry(), phaseInput.analysisData(), phaseInput.diagnostics() ); - var forGate = surface.inventoryGateRegistry().gateForBodyRoot(forStatement.body()); - assertNotNull(forGate); - assertEquals(FrontendInventoryGateStatus.SUPPORTED, forGate.status()); - assertEquals(FrontendBodyInventoryReadiness.NOT_PUBLISHED, forGate.bodyInventoryReadiness()); - assertFalse(surface.inventoryGateRegistry().isBodyInventoryReady(forStatement.body())); - assertTrue(ownerProcedures.classifierSawPrefixExactType()); - assertTrue(ownerProcedures.localStageCouldNotSeeTransientExpressionFact()); - assertTrue(ownerProcedures.classifierSawFinalExpressionFact()); - assertFalse(ownerProcedures.events().stream().anyMatch(event -> event.root() == fromFor)); - - var matchGate = surface.inventoryGateRegistry().gateForBodyRoot(matchStatement.sections().getFirst().body()); - assertNotNull(matchGate); - assertEquals(FrontendInventoryGateStatus.PENDING, matchGate.status()); + 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 publishedGateBodyIsResolvedBySuiteResolver() throws Exception { + void structurallySupportedForBodyIsResolvedBySuiteResolver() throws Exception { var phaseInput = phaseInput("suite_gate_body_published.gd", """ class_name SuiteGateBodyPublished extends Node @@ -361,9 +361,6 @@ func ping(values, seed: int): ); var seedUseSite = findNode(fromFor, IdentifierExpression.class, identifier -> identifier.name().equals("seed")); var surface = new FrontendInterfacePhase().analyze(phaseInput.registry(), phaseInput.analysisData()); - surface.inventoryGateRegistry().markSupported(forStatement.body()); - surface.inventoryGateRegistry().markBodyInventoryPublishing(forStatement.body()); - surface.inventoryGateRegistry().markBodyInventoryPublished(forStatement.body()); var forBodyContext = contextForBlock(phaseInput, surface, pingFunction, forStatement.body()); new FrontendSuiteResolver().resolveSuite(forBodyContext, forStatement.body()); @@ -377,14 +374,14 @@ func ping(values, seed: int): } @Test - void missingOwningGateBodyContextUsesDeferredDomainAndStaysFailClosed() throws Exception { - var phaseInput = phaseInput("suite_gate_body_missing_owner.gd", """ - class_name SuiteGateBodyMissingOwner + void supportedBodyCertificateFailsFastForEveryMissingStructuralFact() 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: - print(seed) + var copy := seed """); var pingFunction = findStatement( phaseInput.unit().ast().statements(), @@ -392,21 +389,79 @@ func ping(values, seed: int): functionDeclaration -> functionDeclaration.name().equals("ping") ); var forStatement = findStatement(pingFunction.body().statements(), ForStatement.class, _ -> true); - var seedUseSite = findNode(forStatement.body(), IdentifierExpression.class, identifier -> identifier.name().equals("seed")); var originalSurface = new FrontendInterfacePhase().analyze(phaseInput.registry(), phaseInput.analysisData()); - var surfaceWithoutGate = new FrontendInterfaceSurface( + var originalRoots = originalSurface.suiteEntryRoots(); + + var missingSuiteEntry = new FrontendInterfaceSurface( originalSurface.bodyDeclarationIndex(), - FrontendInventoryGateRegistry.empty(), originalSurface.typedLexicalBaseline(), - originalSurface.suiteEntryRoots() + new FrontendSuiteEntryRoots( + originalRoots.callableOwners(), + originalRoots.propertyInitializers(), + originalRoots.supportedBlocks().stream() + .filter(block -> block != forStatement.body()) + .toList() + ) ); - var forBodyContext = contextForBlock(phaseInput, surfaceWithoutGate, pingFunction, forStatement.body()); + assertCertificateFailure(phaseInput, missingSuiteEntry, pingFunction, forStatement.body(), "suite entry roots"); - var request = forBodyContext.visibleValueResolveRequest(seedUseSite.name(), seedUseSite); - new FrontendSuiteResolver().resolveSuite(forBodyContext, forStatement.body()); + 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 + ); + assertCertificateFailure(phaseInput, missingIterator, pingFunction, forStatement.body(), "iterator"); + + 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"); - assertEquals(FrontendVisibleValueDomain.FOR_SUBTREE, request.domain()); - assertNull(phaseInput.analysisData().symbolBindings().get(seedUseSite)); + 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 @@ -567,7 +622,7 @@ void nestedSuiteFactsStayUnpublishedUntilCallableRootCompletes() throws Exceptio var phaseInput = phaseInput("suite_nested_export_batch.gd", """ class_name SuiteNestedExportBatch extends RefCounted - + func ping(seed): if seed: var child := seed @@ -649,7 +704,7 @@ void callableEntryVarTypePostFactsStayOverlayLocalUntilSuiteExport() throws Exce var phaseInput = phaseInput("suite_callable_entry_var_post.gd", """ class_name SuiteCallableEntryVarPost extends RefCounted - + func ping(value: int): var first := value """); @@ -756,6 +811,21 @@ private static boolean hasOwnerEvent(@NotNull List events, @NotNull 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 @@ -771,7 +841,8 @@ private static boolean hasOwnerEvent(@NotNull List events, @NotNull @NotNull Node callableOwner, @NotNull Block block ) { - var blockScope = assertInstanceOf(BlockScope.class, phaseInput.analysisData().scopesByAst().get(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, @@ -782,7 +853,12 @@ private static boolean hasOwnerEvent(@NotNull List events, @NotNull false, null, surface, - new FrontendTypedLexicalEnvironment(blockScope, phaseInput.analysisData()), + new FrontendTypedLexicalEnvironment( + blockScope, + phaseInput.analysisData(), + null, + surface.typedLexicalBaseline() + ), phaseInput.analysisData(), phaseInput.diagnostics(), phaseInput.registry(), @@ -927,96 +1003,6 @@ private boolean nextStatementSawCurrentSuiteSnapshot() { } } - private static final class PrefixGateClassifierOwnerProcedures implements FrontendStatementResolver.OwnerProcedures { - private final @NotNull FrontendBodyOwnerProcedures delegate = new FrontendBodyOwnerProcedures(); - private final @NotNull VariableDeclaration prefixLocal; - private final @NotNull Block targetBody; - private final @NotNull List events = new ArrayList<>(); - private boolean classifierSawPrefixExactType; - private boolean localStageCouldNotSeeTransientExpressionFact; - private boolean classifierSawFinalExpressionFact; - - private PrefixGateClassifierOwnerProcedures( - @NotNull VariableDeclaration prefixLocal, - @NotNull Block targetBody - ) { - this.prefixLocal = prefixLocal; - this.targetBody = targetBody; - } - - @Override - public void runTopBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { - events.add(new OwnerEvent("top", root)); - delegate.runTopBinding(context, root); - } - - @Override - public void runLocalTypeStabilization(@NotNull FrontendSuiteContext context, @NotNull Node root) { - events.add(new OwnerEvent("local", root)); - delegate.runLocalTypeStabilization(context, root); - if (root == prefixLocal && prefixLocal.value() != null) { - localStageCouldNotSeeTransientExpressionFact = context.typedEnvironment() - .expressionType(prefixLocal.value()) == null; - } - } - - @Override - public void runChainBinding(@NotNull FrontendSuiteContext context, @NotNull Node root) { - events.add(new OwnerEvent("chain", root)); - delegate.runChainBinding(context, root); - } - - @Override - public void runExprType(@NotNull FrontendSuiteContext context, @NotNull Node root) { - events.add(new OwnerEvent("expr", root)); - delegate.runExprType(context, root); - } - - @Override - public void runGateClassifier(@NotNull FrontendSuiteContext context, @NotNull Node root) { - if (root != prefixLocal || context.currentBlockScope() == null) { - return; - } - var prefixType = context.typedEnvironment().localSlotType( - context.currentBlockScope(), - prefixLocal.name(), - prefixLocal - ); - var prefixExpressionType = prefixLocal.value() != null - ? context.typedEnvironment().expressionType(prefixLocal.value()) - : null; - classifierSawPrefixExactType = prefixType == GdIntType.INT; - classifierSawFinalExpressionFact = prefixExpressionType != null - && prefixExpressionType.status() == FrontendExpressionTypeStatus.RESOLVED - && prefixExpressionType.publishedType() == GdIntType.INT; - if (classifierSawPrefixExactType) { - context.interfaceSurface().inventoryGateRegistry().markSupported(targetBody); - } - } - - @Override - public void runVarTypePost(@NotNull FrontendSuiteContext context, @NotNull Node root) { - events.add(new OwnerEvent("var_post", root)); - delegate.runVarTypePost(context, root); - } - - private @NotNull List events() { - return events; - } - - private boolean classifierSawPrefixExactType() { - return classifierSawPrefixExactType; - } - - private boolean localStageCouldNotSeeTransientExpressionFact() { - return localStageCouldNotSeeTransientExpressionFact; - } - - private boolean classifierSawFinalExpressionFact() { - return classifierSawFinalExpressionFact; - } - } - private static final class RecordingOwnerProcedures implements FrontendStatementResolver.OwnerProcedures { private final boolean publishTopBinding; private final @NotNull List events = new ArrayList<>(); 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/FrontendBodyOwnerProceduresVarTypePostTest.java b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProceduresVarTypePostTest.java index cd875abb..63125e0d 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProceduresVarTypePostTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProceduresVarTypePostTest.java @@ -154,7 +154,7 @@ func ping(dynamic_host): } @Test - void analyzeKeepsUnsupportedForBodyLocalsOutOfPublishedSlotTypeTable() throws Exception { + void analyzePublishesSupportedForBodyLocalSlotType() throws Exception { var analyzed = analyzeShared( "var_type_post_unsupported_for_local.gd", """ @@ -175,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()) 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 dcdec713..edbc1727 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 """); @@ -674,12 +674,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 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/resolver/FrontendVisibleValueResolverTest.java b/src/test/java/gd/script/gdcc/frontend/sema/resolver/FrontendVisibleValueResolverTest.java index 8b98e46f..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 @@ -7,11 +7,7 @@ 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.FrontendBodyInventoryReadiness; import gd.script.gdcc.frontend.sema.FrontendBodyLocalDeclaration; -import gd.script.gdcc.frontend.sema.FrontendInventoryGate; -import gd.script.gdcc.frontend.sema.FrontendInventoryGateRegistry; -import gd.script.gdcc.frontend.sema.FrontendInventoryGateStatus; import gd.script.gdcc.frontend.sema.analyzer.FrontendSemanticAnalyzer; import gd.script.gdcc.frontend.sema.FrontendSemanticStage; import gd.script.gdcc.frontend.sema.FrontendTypedLexicalEnvironment; @@ -54,7 +50,7 @@ void resolveFindsVisibleParameterInsideExecutableBody() throws Exception { var analyzedInput = analyzedInput("visible_parameter.gd", """ class_name VisibleParameter extends Node - + func ping(value: int): print(value) """); @@ -81,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 @@ -127,11 +123,15 @@ func ping(): 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, 0)) + List.of(new FrontendBodyLocalDeclaration( + declaration, + binding, + FrontendBodyLocalDeclaration.Kind.ORDINARY_VAR, + 0 + )) )); var resolver = new FrontendVisibleValueResolver( analyzedInput.analysisData(), - FrontendInventoryGateRegistry.empty(), publishedIndex ); @@ -150,7 +150,6 @@ func ping(): var incompleteIndex = new FrontendBodyDeclarationIndex(Map.of()); var incompleteResolver = new FrontendVisibleValueResolver( analyzedInput.analysisData(), - FrontendInventoryGateRegistry.empty(), incompleteIndex ); assertThrows(IllegalStateException.class, () -> incompleteResolver.resolve( @@ -361,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() ); } @@ -389,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() ); } @@ -418,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() ); } @@ -450,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() ); } @@ -482,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 @@ -503,64 +502,18 @@ 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() - ); - } - - @Test - void resolveKeepsSupportedButUnpublishedGateBodyFailClosed() throws Exception { - var analyzedInput = analyzedInput("for_body_supported_not_published.gd", """ - class_name ForBodySupportedNotPublished - 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 notPublished = resolveWithGate( - analyzedInput, - forStatement, - useSite, - FrontendInventoryGateStatus.SUPPORTED, - FrontendBodyInventoryReadiness.NOT_PUBLISHED, - FrontendVisibleValueDomain.FOR_SUBTREE - ); - var publishing = resolveWithGate( - analyzedInput, - forStatement, - useSite, - FrontendInventoryGateStatus.SUPPORTED, - FrontendBodyInventoryReadiness.PUBLISHING, - FrontendVisibleValueDomain.FOR_SUBTREE - ); - - assertEquals(FrontendVisibleValueStatus.DEFERRED_UNSUPPORTED, notPublished.status()); - assertEquals(FrontendVisibleValueDomain.FOR_SUBTREE, notPublished.deferredBoundary().domain()); - assertEquals( - FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN, - notPublished.deferredBoundary().reason() - ); - assertEquals(FrontendVisibleValueStatus.DEFERRED_UNSUPPORTED, publishing.status()); - assertEquals(FrontendVisibleValueDomain.FOR_SUBTREE, publishing.deferredBoundary().domain()); - assertEquals( - FrontendVisibleValueDeferredReason.UNSUPPORTED_DOMAIN, - publishing.deferredBoundary().reason() - ); + assertNull(result.deferredBoundary()); } @Test - void resolvePublishedGateBodyPassesRequestAstAndCurrentScopeGates() throws Exception { - var analyzedInput = analyzedInput("for_body_published.gd", """ - class_name ForBodyPublished + void resolveForBodyCanReadOuterParameterThroughNormalLookup() throws Exception { + var analyzedInput = analyzedInput("for_body_outer_parameter.gd", """ + class_name ForBodyOuterParameter extends Node func ping(values, seed): @@ -569,15 +522,13 @@ func ping(values, seed): """); var forStatement = findNode(analyzedInput.unit().ast(), ForStatement.class, _ -> true); var useSite = findIdentifierExpression(forStatement.body(), "seed"); + var resolver = new FrontendVisibleValueResolver(analyzedInput.analysisData()); - var result = resolveWithGate( - analyzedInput, - forStatement, + var result = resolver.resolve(new FrontendVisibleValueResolveRequest( + "seed", useSite, - FrontendInventoryGateStatus.SUPPORTED, - FrontendBodyInventoryReadiness.PUBLISHED, - FrontendVisibleValueDomain.FOR_SUBTREE - ); + FrontendVisibleValueDomain.EXECUTABLE_BODY + )); assertEquals(FrontendVisibleValueStatus.FOUND_ALLOWED, result.status()); assertNotNull(result.visibleValue()); @@ -587,43 +538,9 @@ func ping(values, seed): } @Test - void resolveUnsupportedGateBodyDoesNotFallBackToOuterBinding() throws Exception { - var analyzedInput = analyzedInput("for_body_unsupported_no_fallback.gd", """ - class_name ForBodyUnsupportedNoFallback - extends Node - - var item = 100 - - func ping(values): - for item in values: - print(item) - """); - var forStatement = findNode(analyzedInput.unit().ast(), ForStatement.class, _ -> true); - var useSite = findIdentifierExpression(forStatement.body(), "item"); - - var result = resolveWithGate( - analyzedInput, - forStatement, - useSite, - FrontendInventoryGateStatus.UNSUPPORTED, - FrontendBodyInventoryReadiness.NOT_PUBLISHED, - FrontendVisibleValueDomain.EXECUTABLE_BODY - ); - - assertEquals(FrontendVisibleValueStatus.DEFERRED_UNSUPPORTED, result.status()); - assertNull(result.visibleValue()); - assertTrue(result.filteredHits().isEmpty()); - assertEquals(FrontendVisibleValueDomain.FOR_SUBTREE, result.deferredBoundary().domain()); - assertEquals( - FrontendVisibleValueDeferredReason.VARIABLE_INVENTORY_NOT_PUBLISHED, - result.deferredBoundary().reason() - ); - } - - @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 @@ -641,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 @@ -669,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 @@ -703,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() ); } @@ -731,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() ); } @@ -760,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() ); } @@ -878,31 +791,6 @@ func ping(): ); } - private static @NotNull FrontendVisibleValueResolution resolveWithGate( - @NotNull AnalyzedInput analyzedInput, - @NotNull ForStatement forStatement, - @NotNull IdentifierExpression useSite, - @NotNull FrontendInventoryGateStatus status, - @NotNull FrontendBodyInventoryReadiness readiness, - @NotNull FrontendVisibleValueDomain requestDomain - ) { - var gate = FrontendInventoryGate.pending( - forStatement, - forStatement, - forStatement.body(), - FrontendVisibleValueDomain.FOR_SUBTREE - ) - .withStatus(status) - .withBodyInventoryReadiness(readiness); - var registry = FrontendInventoryGateRegistry.builder().add(gate).build(); - var resolver = new FrontendVisibleValueResolver(analyzedInput.analysisData(), registry); - return resolver.resolve(new FrontendVisibleValueResolveRequest( - useSite.name(), - useSite, - requestDomain - )); - } - private static @NotNull T findNode( @NotNull Node root, @NotNull Class nodeType, From 5686df96a939a9dba917a38141bb7658bd2a3ab1 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Tue, 21 Jul 2026 21:22:23 +0800 Subject: [PATCH 23/27] feat(frontend): tighten body inventory completeness at suite entry - Require bidirectional agreement between published locals and the body declaration inventory before suite resolution begins - Enforce contiguous source-order facts that follow AST range order across ordinary locals and loop iterators - Pin loop-body inventory shape so the iterator leads the list while ordinary locals follow contiguously - Fail fast on missing reverse inventory, reordered source order, or malformed iterator placement - Refresh pipeline docs and regressions for the strengthened structural certificate --- ...e_resolution_pipeline_execution_summary.md | 6 +- ...segmented_type_resolution_pipeline_plan.md | 37 +++-- .../gdcc/frontend/scope/BlockScope.java | 12 ++ .../sema/FrontendBodyDeclarationIndex.java | 14 +- .../sema/FrontendBodyLocalDeclaration.java | 17 +- .../FrontendBodyStructuralCompleteness.java | 90 +++++++++- .../sema/analyzer/FrontendInterfacePhase.java | 7 + .../sema/FrontendSuiteResolverTest.java | 156 +++++++++++++++++- 8 files changed, 306 insertions(+), 33 deletions(-) diff --git a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md index 3a73c829..5716041c 100644 --- a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md +++ b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md @@ -41,11 +41,11 @@ Interface 层在基础结构层之后运行,负责准备 body `SuiteResolver` 输出包括: -- `FrontendBodyDeclarationIndex`:记录每个 supported block 的完整 body-local declaration 列表与 source order;ordinary `var` 使用 `VariableDeclaration` identity,for iterator 使用 owning `ForStatement` identity。生产 resolver 用 declaration identity 验证 scope 命中的 local 属于已发布 inventory,但不替代 declaration-order / self-reference filtered-hit。 +- `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 完整,且 `FOR_BODY` 必须包含 iterator entry。这两个合同分别表达“该结构受支持”和“本次 interface surface 确实完整”,不能合并为 typed-dependent readiness。 +`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。 @@ -352,6 +352,6 @@ Top binding 先绑定 `receiver` use-site。Local stabilization 随后把 `recei - `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 完整性;二者都不读取 typed fact 或 compile readiness。 +- 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/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md index 90b7b6bd..96e01492 100644 --- a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md +++ b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md @@ -393,7 +393,10 @@ Body entry 已冻结为两个互不替代的结构事实: 是否进入 `FrontendSuiteResolver`,以及 unsupported 位置使用哪个精确 deferred domain。 - `FrontendBodyStructuralCompleteness` 对本次 Interface surface 做 fail-fast 验证:scope identity、suite entry、body declaration index、declaration/binding/scope identity 与 source-facing typed baseline 必须 - 同时完整;`FOR_BODY` 还必须包含以 `ForStatement` 为 identity 的 iterator entry。 + 同时完整;完整性对 published body inventory 是双向的——每个 index entry 必须能回到 scope/baseline, + 且 body `BlockScope` 中每条已发布 `LOCAL` binding 必须出现在 declaration index 中;index 的 + `sourceOrder` 必须从 0 连续,并与 AST start-byte 非降序一致;`FOR_BODY` 还必须恰好包含一个以 + `ForStatement` 为 identity 的 iterator entry,且该 entry 位于 inventory 列表头部、`sourceOrder == 0`。 Policy 不读取 expression type、typed overlay、iteration plan、diagnostic state 或 compile surface; certificate 不读取 pending/committed overlay、slot refinement 或 semantic/lowering route。Typed fact 只能 @@ -1009,17 +1012,28 @@ feature-owned surface 理解:lambda 包括 parameter/capture/body,match 包 - `scopesByAst()` 中 body root 映射到预期 identity / kind 的 `BlockScope`。 - `FrontendSuiteEntryRoots` 明确包含该 supported body。 - `FrontendBodyDeclarationIndex.containsBodyRoot(body)` 为 true,即使 body 没有 ordinary local。 - - 每个 index entry 的 declaration/binding/scope identity 一致,且 typed baseline 包含该 - source-facing declaration;`FOR_BODY` 的 iterator `Node` identity 也必须覆盖。 -- [x] certificate 只读取 scope graph、suite entry roots、declaration index 与 baseline;不得读取 - pending/committed overlay、expression type、slot refinement 或 iteration plan。 + - **正向**(index → scope/baseline):每个 index entry 的 declaration/binding/scope identity 一致, + 且 typed baseline 包含该 source-facing declaration;`sourceOrder` 从 0 连续,并与 AST start-byte + 非降序一致。 + - **反向**(scope → index):`expectedScope` 中每条已发布 `LOCAL` binding(ordinary `var` 与 + for iterator)都必须有对应 declaration-index entry,且 declaration identity / binding kind 与 + scope inventory 一致。反向检查只覆盖该 body-root scope,不遍历 child scope;不包含尚未发布的 + block-local `const`。 + - `FOR_BODY` inventory 契约:恰好一个 iterator entry,且必须是 inventory 列表首项、 + `sourceOrder == 0`;ordinary body local 仅能跟在其后并占用连续 `sourceOrder >= 1`。Interface + 层 `FrontendInterfacePhase` 通过“先 record iterator、再 walk body statements”发布该形状, + certificate 在 suite entry 再次钉死。 +- [x] certificate 只读取 scope graph、suite entry roots、declaration index、baseline 与 body scope + inventory 枚举;不得读取 pending/committed overlay、expression type、slot refinement 或 iteration + plan。 - [x] 结构事实缺洞属于 phase protocol / programmer error,必须 fail-fast,不得伪装成源码 diagnostic,也不得静默跳过 body。 验收细则: -- scope 存在但 suite-entry、body-index、iterator entry 或 baseline 任一缺失时 focused test - fail-fast。 +- scope 存在但 suite-entry、body-index、iterator entry、ordinary inventory entry 或 baseline 任一 + 缺失时 focused test fail-fast。 +- 仅 index 列表重编号连续但 AST 顺序错乱、或 for iterator 不在 `sourceOrder` 0 时 fail-fast。 - header resolved type 变化不会改变 certificate 结果。 #### 阶段 L3:迁移 SuiteResolver 与 body-local consumer @@ -1239,9 +1253,10 @@ Compile gate 测试: 缓解:immutable structural semantic support matrix 只回答某种 AST/scope kind 的 inventory path 是否已实现;`FrontendSuiteEntryRoots`、`FrontendBodyDeclarationIndex`、typed baseline 与 scope -identity 共同证明本次 interface surface 的 structural completeness。`canPublish...` 或等价 -allowlist 不能单独充当 body-entry certificate。缺失结构事实必须 fail-fast,不能静默跳过 body, -也不能重新引入 `PENDING` / `PUBLISHED` lifecycle。 +identity 共同证明本次 interface surface 的 structural completeness,且 declaration index 对 body +scope inventory 必须双向一致(index entry 回到 scope/baseline,scope 中每条 published `LOCAL` +都能在 index 找到)。`canPublish...` 或等价 allowlist 不能单独充当 body-entry certificate。缺失 +结构事实必须 fail-fast,不能静默跳过 body,也不能重新引入 `PENDING` / `PUBLISHED` lifecycle。 ### R6:`SuiteResolver` 绕过 phase owner 边界 @@ -1327,7 +1342,7 @@ supported for 的三处放行与 unsupported feature 的 AST/current-scope 双 - local `:=` 的 source-order type stabilization 可被后续 statement 与 feature-specific semantic route planning 消费,但不能改变 structural body entry。 - chain binding 消费 receiver local slot 时,必须看到 local stabilization 已发布到 overlay 的 exact type,而不是 interface baseline `Variant`。 - immutable structural semantic support matrix 是 AST/scope kind 支持面的单一事实源;它不读取 typed fact、compile readiness 或 lifecycle state。 -- supported body 只有在 scope identity、suite entry root、body declaration index 与 typed baseline 全部满足 structural completeness certificate 后才进入 `SuiteResolver`;缺洞 fail-fast。 +- supported body 只有在 scope identity、suite entry root、body declaration index 与 typed baseline 全部满足 structural completeness certificate 后才进入 `SuiteResolver`;certificate 对 published body inventory 做 index↔scope 双向校验与 AST 源序 `sourceOrder` 校验,缺洞 fail-fast。 - resolver 的 request-domain hard boundary、AST boundary 与 current-scope backstop 不读取 registry;supported `FOR_BODY` 三处均放行,unsupported lambda/match/const/parameter-default 继续 structural fail-closed。 - for header 与 body edge 可区分:header typed resolution 可读取前缀 facts并驱动后续 iteration planning,但 body entry 已由无条件 structural inventory 与 D0 child-suite path 决定。 - nested chain / argument retry 不会产生 stable `expressionTypes()` narrowing rewrite 或 status upgrade;每个 expression / step key 在 overlay export 与 stable table 中最多有一个最终 fact。 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/FrontendBodyDeclarationIndex.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyDeclarationIndex.java index fdd8a0df..7fe0e97d 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyDeclarationIndex.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyDeclarationIndex.java @@ -10,13 +10,17 @@ import java.util.Map; import java.util.Objects; -/// Interface-layer index of ordinary locals that already exist in baseline `BlockScope` inventory. +/// 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. -/// 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. -/// Unsupported typed-dependent bodies therefore have no entries until their gate-owned inventory is -/// explicitly published by a later body phase. +/// `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; diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyLocalDeclaration.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyLocalDeclaration.java index cbbe1bf5..3de873db 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyLocalDeclaration.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyLocalDeclaration.java @@ -6,11 +6,17 @@ import java.util.Objects; -/// One ordinary local declaration published by the baseline inventory layer for a supported body. +/// 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. It preserves the complete-inventory plus -/// source-order model required by `FrontendVisibleValueResolver`: the resolver can see future locals -/// in the scope graph, then decide whether a use-site is before or after this declaration. +/// `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, +/// it must occupy list position 0, and its `sourceOrder` must be `0`. 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, @@ -30,7 +36,10 @@ public record FrontendBodyLocalDeclaration( } 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/FrontendBodyStructuralCompleteness.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyStructuralCompleteness.java index 84e9efa8..8c9dc6ae 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyStructuralCompleteness.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyStructuralCompleteness.java @@ -2,11 +2,14 @@ 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 @@ -18,6 +21,11 @@ /// 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] @@ -39,9 +47,12 @@ private FrontendBodyStructuralCompleteness() { /// 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 source order, matching binding/declaration/scope identities, - /// and a source-facing typed baseline equal to the published lexical binding type. - /// 6. A `FOR_BODY` must additionally contain the iterator entry identified by its owning `ForStatement`. + /// 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 occupy `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. @@ -86,6 +97,10 @@ public static void requireStructurallyCompleteBody( 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, @@ -95,11 +110,10 @@ public static void requireStructurallyCompleteBody( ); } - if (expectedScope.kind() == BlockScopeKind.FOR_BODY - && declarations.stream().noneMatch( - declaration -> declaration.kind() == FrontendBodyLocalDeclaration.Kind.ITERATOR - )) { - throw incomplete(body, "`for` body declaration index does not contain its iterator"); + requireScopeInventoryPublished(declarationIndex, body, expectedScope); + + if (expectedScope.kind() == BlockScopeKind.FOR_BODY) { + requireForBodyIteratorInventory(body, declarations); } } @@ -152,6 +166,66 @@ private static void requireCompleteDeclaration( } } + /// 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, 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 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 index 8399eadd..a0bc5784 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendInterfacePhase.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendInterfacePhase.java @@ -265,6 +265,11 @@ private void enterSupportedBlock(@NotNull Block block) { enterSupportedBlock(block, null); } + /// Opens one supported body inventory list. + /// + /// For a `for` body, the iterator is recorded first 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) @@ -299,6 +304,7 @@ private void recordLocalDeclaration(@NotNull VariableDeclaration variableDeclara 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, @@ -308,6 +314,7 @@ private void recordLocalDeclaration(@NotNull VariableDeclaration variableDeclara typedBaselineBuilder.put(variableDeclaration, binding.type()); } + /// Publishes the sole for-body iterator inventory entry at list head with `sourceOrder == 0`. private void recordIteratorDeclaration( @NotNull ForStatement forStatement, @NotNull BlockScope blockScope diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java index 9addb0ac..d9d5fc82 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSuiteResolverTest.java @@ -374,7 +374,7 @@ func ping(values, seed: int): } @Test - void supportedBodyCertificateFailsFastForEveryMissingStructuralFact() throws Exception { + void supportedBodyCertificateFailsFastForMissingSuiteSurfaceFacts() throws Exception { var phaseInput = phaseInput("suite_body_certificate_missing_fact.gd", """ class_name SuiteBodyCertificateMissingFact extends Node @@ -432,7 +432,15 @@ func ping(values, seed: int): originalSurface.typedLexicalBaseline(), originalRoots ); - assertCertificateFailure(phaseInput, missingIterator, pingFunction, forStatement.body(), "iterator"); + // 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()) { @@ -464,6 +472,150 @@ func ping(values, seed: int): 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", """ From e78e7f23f2cee0f2cbb1ec10df7493cae4a39c20 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Thu, 23 Jul 2026 00:21:40 +0800 Subject: [PATCH 24/27] feat(frontend): unify body-entry semantic entry and retract D0 completion claim - Introduce isSupportedSuiteBodyRoot() as the single semantic entry shared by the Interface-phase suite-entry decision and the structural completeness first certificate gate - Remove the dead VARIABLE_INVENTORY_NOT_PUBLISHED deferred reason and the obsolete ForStatement unsupported binding/chain reporting from owner procedures - Retract the D0 completion claim in the for-range plan: bare range(...) header pre-route is specified but not yet landed - Add focused regressions covering the new body-entry semantic entry across all block and callable scope kinds --- ...tend_for_range_loop_implementation_plan.md | 75 +++++++++++++------ ...segmented_type_resolution_pipeline_plan.md | 10 +-- ...d_visible_value_resolver_implementation.md | 1 - .../sema/FrontendBodyLocalDeclaration.java | 8 +- .../FrontendBodySemanticSupportPolicy.java | 19 ++++- .../FrontendBodyStructuralCompleteness.java | 12 +-- .../FrontendExecutableInventorySupport.java | 14 ++++ .../analyzer/FrontendBodyOwnerProcedures.java | 20 +++-- .../sema/analyzer/FrontendInterfacePhase.java | 11 +-- .../analyzer/FrontendStatementResolver.java | 2 +- .../analyzer/FrontendVariableAnalyzer.java | 18 +---- .../FrontendVisibleValueDeferredReason.java | 3 +- ...FrontendBodySemanticSupportPolicyTest.java | 73 ++++++++++++++++++ 13 files changed, 195 insertions(+), 71 deletions(-) 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 index 6ad2ca2a..bd2cb838 100644 --- a/doc/module_impl/frontend/frontend_for_range_loop_implementation_plan.md +++ b/doc/module_impl/frontend/frontend_for_range_loop_implementation_plan.md @@ -1,12 +1,12 @@ # Frontend for-in loop 实施计划 -> 本文档记录 `for iterator[: Type] in expr` 的分阶段实施。阶段 B/D0 已把所有 `for-in` body 转为 frontend shared semantic 正式支持面:body entry 不依赖 range-like classifier,iterator 先以保守 source-facing type 进入 lexical inventory;后续 `SuiteResolver` iteration-planning 阶段再发布 `FrontendForIterationPlan`,并在可静态确定 element type 时精化 iterator slot。lowering 最终根据 iteration plan 选择 `range(...)` / known iterable 专用 helper或 generic Variant iterator helper。 +> 本文档记录 `for iterator[: Type] in expr` 的分阶段实施。阶段 B 已把所有 `for-in` body 转为 frontend shared semantic 结构支持面;D0 已落地 ordinary iterable 的 header/body 调度骨架,但 bare `range(...)` 仍缺少进入 ordinary owner pipeline 前的专用预路由,因此 D0 尚未完成。iterator 先以保守 source-facing type 进入 lexical inventory;后续 `SuiteResolver` iteration-planning 阶段再发布 `FrontendForIterationPlan`,并在可静态确定 element type 时精化 iterator slot。lowering 最终根据 iteration plan 选择 `range(...)` / known iterable 专用 helper 或 generic Variant iterator helper。 ## 文档状态 -- 状态:实施中(阶段 B 与 D0 已完成;阶段 C/D1 及后续 route、CFG、lowering 尚未实施) +- 状态:实施中(阶段 B 已完成;阶段 D0 仅完成结构性 header/body path,bare `range(...)` header 预路由与 canonical regression tests 尚未完成;阶段 C/D1 及后续 route、CFG、lowering 尚未实施) - 创建日期:2026-07-03 -- 最近校对:2026-07-20(阶段 B/D0 已落地;all for-in shared semantic 已解封,compile-only 仍由临时 root blocker 阻断) +- 最近校对:2026-07-22(撤销 D0 完成声明:canonical `for i in range(3)` 当前仍产生 `sema.binding` / `sema.expression_resolution`;body structural entry 已落地,compile-only 仍由临时 root blocker 阻断) - 适用范围: - `src/main/java/gd/script/gdcc/frontend/sema/**` - `src/main/java/gd/script/gdcc/frontend/lowering/**` @@ -36,6 +36,8 @@ - `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` @@ -76,13 +78,15 @@ compile / lowering 最终目标必须覆盖: 当前代码库已经具备的基础: -- `gdparser` 的 `ForStatement` AST 已包含 `iterator`、`iteratorType`、`iterable`、`body`、`range` 字段。 +- `gdparser` 的 `ForStatement` AST 已包含 `iterator`、`iteratorType`、`iterable`、`body`、`range` 字段;其中 `range()` 是 AST source-location anchor,不是 `range(...)` builtin classifier,D0 pre-route 必须检查 `iterable` expression shape。 - `GdScriptParserService` 只是外部 parser adapter,本仓库没有本地 parser grammar 可改;若 parse smoke 发现 `for` AST 形态不足,应先升级或修复外部 parser,而不是在 frontend analyzer 中猜文本。 - `FrontendScopeAnalyzer` 已为 `ForStatement` 建立 `FOR_BODY` scope,并保证 `iteratorType` 与 `iterable` 在外层 scope 下遍历,`body` 在独立 `FOR_BODY` scope 下遍历。 - `FrontendLoopControlFlowAnalyzer` 已把 `for` 与 `while` 一样视为 loop boundary,`break` / `continue` 在 `for` body 内不再报 `sema.loop_control_flow`。 - `FrontendSemanticAnalyzer` 的 shared phase 顺序固定为 skeleton -> scope -> variable inventory -> interface surface -> `SuiteResolver` -> diagnostics-only analyzers。`SuiteResolver` 是逐语句运行的 typed fact owner,能在 `var limit := 3` flush 后让后续 `for i in limit:` 读取 `limit:int`。 - `FrontendTypedLexicalEnvironment` 已支持 pending / committed overlay、per-owner patch transaction、`Variant -> exact` 的 local slot update 校验;但当前 API 只允许 `LOCAL_TYPE_STABILIZATION` owner 发布 local slot update。 - `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。 +- 当前 `FrontendStatementResolver.resolveForStatement(...)` 仍无条件对整个 `forStatement.iterable()` 调用 ordinary `runSupportedRoot(...)`。由于 `range` 不是当前 registry 中可绑定的 ordinary value / utility function,canonical `for i in range(3)` 会先为 callee 发布 unknown binding,再让 identifier 与 call root expression typing 失败;argument `3` 也不会获得可供 planning 消费的 stable expression type。 +- 上述 header 错误不会关闭 body:iterator 与 `var x := i` 仍以 `Variant` baseline 进入 body。这只证明 structural child-suite entry 已落地,不证明 `range(...)` header 已受 shared semantic 支持。 - `FrontendBodyLocalDeclaration` 与 `FrontendBodyDeclarationIndex` 已支持 `Node` declaration identity,并以 `ForStatement` 作为 `ITERATOR` entry identity;ordinary local 继续使用 `VariableDeclaration` identity。 - `FrontendCompileCheckAnalyzer` 当前对每个 `ForStatement` root 发布一个临时、无条件的 `sema.compile_check` blocker,不读取 iterable typed fact、iteration plan 或 route,也不进入 body。 - `FrontendCfgGraphBuilder.processStatement(...)` 当前没有 `ForStatement` 分支;`FrontendCfgRegion` 当前只允许 `BlockRegion`、`FrontendIfRegion`、`FrontendElifRegion`、`FrontendWhileRegion`。 @@ -92,10 +96,10 @@ compile / lowering 最终目标必须覆盖: 当前实现张力: -- 早期文档曾把 `for` 放在 post-MVP / deferred 范围;阶段 B/D0/L 完成后,shared semantic 文档已改为结构支持,compile/lowering 仍分阶段开放。 +- 早期文档曾把 `for` 放在 post-MVP / deferred 范围;阶段 B 与 D0 的结构性子集落地后,shared semantic 文档已改为 body 结构支持,但 D0 range header 预路由、compile/lowering 仍分阶段开放。 - loop-control semantic 已把 `for` 当成合法 loop boundary。 - backend/LIR 已具备 range iterator state 与 intrinsic。 -- frontend shared semantic、compile gate、CFG 与 body lowering 尚未承认 `for-in` 为 supported surface。 +- frontend shared semantic 已承认 `for-in` body 为 structural supported surface,但 bare `range(...)` header 仍未接通;compile gate、CFG 与 body lowering 也尚未承认任何 `for-in` route 为 compile-ready surface。 本计划的实施目标就是把这个张力收口为:“所有 `for-in` 在 shared semantic 中都是 supported body;iteration plan 决定 iterator type refinement 与 lowering route;compile surface 按 route helper 准备度分阶段打开”。 @@ -170,8 +174,8 @@ record FrontendBodyLocalDeclaration( 约束: -- `ORDINARY_VAR` 的 `declaration` 仍必须是 `VariableDeclaration`,`sourceOrder >= 0`。 -- `ITERATOR` 的 `declaration` 必须是 owning `ForStatement`,`sourceOrder` 使用固定 sentinel,例如 `-1`,表示在 body 所有 ordinary statement 之前可见。 +- `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。 @@ -255,16 +259,20 @@ generic Variant route 需要新增 contract: - `rawElementType = GdVariantType.VARIANT` - `operationNames` 使用新 intrinsic,例如 `gdcc.for_variant_iter.init / should_continue / next / get`,具体名称在 intrinsic catalog 阶段冻结。 -### 3.5 `range(...)` 与 ordinary call route 隔离 +### 3.5 `range(...)` header 预路由与 ordinary call route 隔离 `range(...)` 在 `for` iterable 位置是 loop-specific syntax form: -- bare `IdentifierExpression("range")` + 1/2/3 positional arguments 才进入 `RANGE_CALL` route。 -- named argument、attribute call、subscript call、`some_range(...)`、`obj.range(...)` 不进入 `RANGE_CALL` route。 -- `range(...)` root 不发布 ordinary `resolvedCalls()`。 -- `range(...)` arguments 必须按 source order 运行 top / chain / expr type,且能进入 `int` boundary。 +- `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.6 Godot iteration 语义落点 外部语义参考表明 Godot 对 `for-in` 使用统一 iteration 协议: @@ -337,7 +345,7 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR - 移除 `UnsupportedVariableBoundaryReporter` 对 `ForStatement` 的 unsupported diagnostic。 - `FrontendBodyLocalDeclaration` / `FrontendBodyDeclarationIndex` 扩展为支持 iterator declaration identity: - declaration key 使用 `Node` 或 `Object` identity,而不是只接受 `VariableDeclaration`。 - - iterator entry kind 为 `ITERATOR`,source order 表示 body-start visible。 + - iterator entry kind 为 `ITERATOR`,作为 synthetic 第 0 项固定 `sourceOrder == 0`,表示 body-start visible。 - ordinary `var` entry kind 为 `ORDINARY_VAR`,继续使用 source-order visibility。 - `FrontendInterfacePhase.handleForStatement(...)` 不再注册 `FOR_SUBTREE` pending gate;它应: - walk `iteratorType` 与 `iterable`。 @@ -345,7 +353,7 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR - `enterSupportedBlock(forStatement.body())`,使 nested body local 与 nested for 都进入 interface surface。 - `FrontendVisibleValueResolver` 删除 for-specific body/header deferred boundary: - `ForStatement.body()` edge 不再返回 `FOR_SUBTREE`。 - - `ForStatement.iteratorType()` / `iterable()` header edge 不再返回 `VARIABLE_INVENTORY_NOT_PUBLISHED`。 + - `ForStatement.iteratorType()` / `iterable()` header edge 不再被 deferred。 - `BlockScopeKind.FOR_BODY` current-scope gate 不再 fail-closed。 验收细则: @@ -402,21 +410,28 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR ### 阶段 D0:SuiteResolver header-only for path 与 baseline body entry -状态:已完成(2026-07-20)。 +状态:未完成。2026-07-20 已落地 ordinary iterable 的结构性 header/body path;2026-07-22 复核确认 bare `range(...)` 仍被错误送入 ordinary owner pipeline,canonical range header 不可用,因此不得保留“D0 已完成”声明。 目标: - 让 `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。 - 为 segmented pipeline 阶段 L 提供“没有 typed gate 也能进入 for body”的 production path。 实施内容: -- `FrontendStatementResolver` 增加 `resolveForStatement(...)`,替换当前 `resolveUnsupportedRoot(context, forStatement)`。 +- 已落地:`FrontendStatementResolver` 增加 `resolveForStatement(...)`,替换原 `resolveUnsupportedRoot(context, forStatement)`。 - `resolveForStatement(...)` 必须是 header-only: - 如果 `iteratorType` 非空,解析该 type ref 相关 expression / type use-site 所需 facts。 - - 解析 `iterable` 或 `range(...)` arguments。 + - 若 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` 已存在。 @@ -427,6 +442,11 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR 验收细则: +- 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,不产生 @@ -439,7 +459,7 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR ### 阶段 D1:iteration planning 与 iterator slot refinement -依赖:阶段 C 与阶段 D0。 +依赖:阶段 C 与完整完成的阶段 D0,包括 bare `range(...)` header 预路由及其 canonical regression tests。 目标: @@ -450,10 +470,11 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR 实施内容: - 增加 feature-specific owner hook,例如 `runForIterationPlanning(context, forStatement)`,执行位置 - 固定在 header expr typing 后、iterator var type post 前。该 hook 不复用或恢复 + 固定在 ordinary iterable root expr typing 或 bare range arguments expr typing 完成后、iterator var type post 前。该 hook 不复用或恢复 `runGateClassifier(...)`。 - `runForIterationPlanning(...)`: - - 读取 `iterable` 的 effective expression / slot typed fact。 + - 对 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。 @@ -461,14 +482,15 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR - `runVarTypePost(...)` 扩展为支持 `ForStatement` iterator declaration: - 为 iterator declaration key 发布 final source-facing `slotTypes()` fact。 - `slotTypes()` value 必须是 exposed iterator type,不能是 compiler-only state type。 -- D0 的 header flush 必须发生在 planning 与 iterator var type post 之后;child body 的 - `FrontendSuiteContext` 通过 parent environment 读取 iterator slot refinement。 +- 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。 @@ -695,6 +717,9 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR ### 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`。 @@ -768,7 +793,9 @@ script/run-gradle-targeted-tests.sh --tests GdCompilerTypeTest,GdccForRangeIterT - `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`。 @@ -789,3 +816,5 @@ script/run-gradle-targeted-tests.sh --tests GdCompilerTypeTest,GdccForRangeIterT 6. compiler-only iterator state type 只出现在 dedicated iteration plan、hidden LIR local / intrinsic operand-result / backend C storage 路径。 7. CFG 中存在独立 `FrontendForRegion`,且 `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 完成定义。 diff --git a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md index 96e01492..e146e59e 100644 --- a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md +++ b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md @@ -705,7 +705,7 @@ Compiler-only guard payload matrix: - 新增 `FrontendSuiteResolver`。 - 在 `FrontendSemanticAnalyzer.analyze()` 主 pipeline 中,于 skeleton / scope / variable inventory 之后构建 `FrontendInterfaceSurface`,并把它作为 `FrontendSuiteResolver` 的输入;不能继续只依赖测试或手动 fixture 构造 interface surface。 -- 新增 `FrontendSuiteContext`,携带 source path、callable owner、current block scope、restriction、static context、property initializer context、gate registry、typed lexical environment。 +- 新增 `FrontendSuiteContext`,携带 source path、callable owner、current block scope / scope、restriction、static context、property initializer context、interface surface、typed lexical environment、analysis data、diagnostic manager 与 class registry。 - 新增 `FrontendStatementResolver` 或等价 statement dispatcher。 - 新增 owner procedure registry / dispatch contract,但第一版只接线 no-op 或 fail-closed hook,不复用 whole-module `analyzeInWindow(...)`。 - `FrontendSuiteContext` / owner procedure registry 必须把 `FrontendTypedLexicalEnvironment` 作为显式依赖暴露给后续 owner procedure;阶段 C8 的 expression semantic support 与 chain reduction facade 接入点由阶段 E 真正替换,阶段 D 只建立可传递该依赖的骨架。 @@ -837,7 +837,7 @@ Compiler-only guard payload matrix: 验收细则: - 所有 frontend semantic focused tests 通过。 -- 新 pipeline 测试覆盖 per-owner patch merge、patch transaction 顺序、typed overlay、backfill guard-only、source-order typed fact、pending gate、resolver filtered hit、diagnostic dedup、compile gate。 +- 新 pipeline 测试覆盖 per-owner patch merge、patch transaction 顺序、typed overlay、backfill guard-only、source-order typed fact、pending overlay flush、resolver filtered hit、diagnostic dedup、compile gate。 - `doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md` 已与最终代码行为和本计划完成定义同步,且未把 legacy whole-phase 或 window / scheduler 过渡资产写成目标流水线的一部分。 - `./gradlew classes --no-daemon --info --console=plain` 通过。 - 相关 targeted tests 使用 `script/run-gradle-targeted-tests.sh --tests ...` 通过。 @@ -929,11 +929,11 @@ inventory 决定,typed fact 只能影响后续 refinement、semantic route 与 typed-dependent gate scaffolding;不得将其扩展为 publication protocol、failure state 或新的 feature integration API。 -阶段 L 系列只依赖 `frontend_for_range_loop_implementation_plan.md` 的阶段 B 与阶段 D0: +阶段 L 系列只依赖 `frontend_for_range_loop_implementation_plan.md` 的阶段 B 与阶段 D0 结构性 header/body 子集,不依赖 bare `range(...)` header 预路由: - 阶段 B 已为 `FOR_BODY` 发布 iterator、完整 ordinary local inventory、declaration index 与 source-facing baseline。 -- 阶段 D0 已建立 header-only dispatch,并在 header facts flush 后通过普通 +- 阶段 D0 的结构性子集已建立 header-only dispatch,并在 header facts flush 后通过普通 `resolveChildSuite(...)` 进入 body;iterator 此时允许保持 `Variant` 或显式 declared baseline。 - 阶段 B 在移除 shared semantic unsupported diagnostic 前,已经安装临时、无条件的 `ForStatement` compile blocker,确保 for-range 阶段 F/G 尚未完成时不会进入 CFG/lowering。 @@ -960,7 +960,7 @@ feature-owned surface 理解:lambda 包括 parameter/capture/body,match 包 #### 阶段 L0:冻结 for 前置与 compile safety bridge -- [x] 确认 for-range 阶段 B 与 D0 已完成,且 `FOR_BODY` 不再查询 gate registry。 +- [x] 确认 for-range 阶段 B 与 D0 结构性 header/body 子集已完成(bare `range(...)` pre-route 不属于阶段 L 依赖),且 `FOR_BODY` 不再查询 gate registry。 - [x] 确认临时 `FrontendCompileCheckAnalyzer.handleForStatement(...)` blocker 只锚定 `ForStatement`、不进入 body、不读取 iterable type / iteration plan / route。 - [x] 增加 for-only characterization:shared `analyze(...)` 无 `FOR_SUBTREE` unsupported; 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 478a7cd5..26f36cb4 100644 --- a/doc/module_impl/frontend/frontend_visible_value_resolver_implementation.md +++ b/doc/module_impl/frontend/frontend_visible_value_resolver_implementation.md @@ -191,7 +191,6 @@ resolver 当前只对 `EXECUTABLE_BODY` 域提供正常 lookup,并要求同时 - `UNSUPPORTED_DOMAIN` - `MISSING_SCOPE_OR_SKIPPED_SUBTREE` -- `VARIABLE_INVENTORY_NOT_PUBLISHED` --- diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyLocalDeclaration.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyLocalDeclaration.java index 3de873db..56a8cfc2 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyLocalDeclaration.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyLocalDeclaration.java @@ -13,10 +13,10 @@ /// 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, -/// it must occupy list position 0, and its `sourceOrder` must be `0`. Ordinary body locals follow at -/// `sourceOrder >= 1`. [FrontendBodyStructuralCompleteness] and the Interface-phase publisher both -/// preserve this shape. +/// 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, diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodySemanticSupportPolicy.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodySemanticSupportPolicy.java index 3abaafe8..fc4348c1 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodySemanticSupportPolicy.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodySemanticSupportPolicy.java @@ -43,8 +43,8 @@ public enum FrontendBodySemanticSupportPolicy { /// 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 + /// @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, @@ -75,6 +75,21 @@ 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 diff --git a/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyStructuralCompleteness.java b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyStructuralCompleteness.java index 8c9dc6ae..f2bfe9c1 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyStructuralCompleteness.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendBodyStructuralCompleteness.java @@ -43,7 +43,7 @@ private FrontendBodyStructuralCompleteness() { /// /// Validation proceeds from the coarsest body-entry fact to declaration-level identities: /// - /// 1. `expectedScope.kind()` must map to a policy that enters the suite resolver. + /// 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. @@ -52,7 +52,8 @@ private FrontendBodyStructuralCompleteness() { /// 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 occupy `sourceOrder` 0 at the head of the body inventory list. + /// 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. @@ -76,7 +77,7 @@ public static void requireStructurallyCompleteBody( Objects.requireNonNull(expectedScope, "expectedScope"); var policy = FrontendBodySemanticSupportPolicy.forBlockScopeKind(expectedScope.kind()); - if (!policy.entersSuiteResolver()) { + if (!policy.isSupportedSuiteBodyRoot()) { throw incomplete(body, "scope kind " + expectedScope.kind() + " is not a supported suite body"); } if (analysisData.scopesByAst().get(body) != expectedScope) { @@ -200,8 +201,9 @@ private static void requireScopeInventoryPublished( } } - /// Requires the Interface-phase `FOR_BODY` inventory contract: exactly one iterator entry, list head, - /// `sourceOrder == 0`. Ordinary body locals may follow only at contiguous `sourceOrder >= 1`. + /// 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 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 627defcf..d0110380 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/FrontendExecutableInventorySupport.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/FrontendExecutableInventorySupport.java @@ -13,4 +13,18 @@ public static boolean canPublishCallableLocalValueInventory(@NotNull BlockScopeK 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/analyzer/FrontendBodyOwnerProcedures.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProcedures.java index 90d0b5d0..1bc31784 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProcedures.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProcedures.java @@ -11,7 +11,6 @@ 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.ForStatement; import dev.superice.gdparser.frontend.ast.FunctionDeclaration; import dev.superice.gdparser.frontend.ast.IdentifierExpression; import dev.superice.gdparser.frontend.ast.LambdaExpression; @@ -244,6 +243,7 @@ private static void reportDiscardedExpressionWarning( ) { if (expressionType == null || expressionType.status() != FrontendExpressionTypeStatus.RESOLVED + || expressionType.publishedType() == null || expressionType.publishedType() instanceof GdVoidType) { return; } @@ -362,7 +362,17 @@ private void reportRejectedLocalSlotPublication( if (currentLayerSlot != null) { return currentLayerSlot; } - Scope currentScope = declarationScope.getParentScope(); + 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); @@ -435,14 +445,10 @@ public void runUnsupported(@NotNull FrontendSuiteContext context, @NotNull Node reportUnsupportedBinding(context, variableDeclaration.value(), "block-local const initializer"); reportUnsupportedChain(context, variableDeclaration.value(), "block-local const initializer"); } - case ForStatement forStatement -> { - reportUnsupportedBinding(context, forStatement, "for subtree"); - reportUnsupportedChain(context, forStatement, "for subtree"); - } case MatchStatement matchStatement -> { reportUnsupportedBinding(context, matchStatement, "match subtree"); reportUnsupportedChain(context, matchStatement, "match subtree"); - // `match` sections remain sealed until their gate is supported, but the subject + // `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()); 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 index a0bc5784..e51a32fc 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendInterfacePhase.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendInterfacePhase.java @@ -267,13 +267,13 @@ private void enterSupportedBlock(@NotNull Block block) { /// Opens one supported body inventory list. /// - /// For a `for` body, the iterator is recorded first 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]. + /// 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.canPublishCallableLocalValueInventory(blockScope.kind())) { + || !FrontendExecutableInventorySupport.isSupportedSuiteBodyRoot(blockScope.kind())) { return; } supportedBlocks.add(block); @@ -314,7 +314,8 @@ private void recordLocalDeclaration(@NotNull VariableDeclaration variableDeclara typedBaselineBuilder.put(variableDeclaration, binding.type()); } - /// Publishes the sole for-body iterator inventory entry at list head with `sourceOrder == 0`. + /// 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 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 index 899b4367..303ed084 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendStatementResolver.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendStatementResolver.java @@ -112,7 +112,7 @@ private void resolveForStatement( @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 gate. + // child-suite path so header typing can never become a body-entry condition. if (forStatement.iteratorType() != null) { runSupportedRoot(context, forStatement.iteratorType()); } 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 7dadd257..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 @@ -546,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) { 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/test/java/gd/script/gdcc/frontend/sema/FrontendBodySemanticSupportPolicyTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendBodySemanticSupportPolicyTest.java index d8f80d7a..ca037b4a 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendBodySemanticSupportPolicyTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendBodySemanticSupportPolicyTest.java @@ -73,4 +73,77 @@ void forHeaderUsesOuterExecutableDomainWithoutOwningBodyInventory() { 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() + ); + } + } } From 4341f9dc3368365be90a039a5d5c51759ab526f2 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Thu, 23 Jul 2026 01:49:08 +0800 Subject: [PATCH 25/27] docs(frontend): freeze dedicated hidden iterator slot design and atomic PR boundary for range/int route - Define loop-carried iterator state as graph-owned dedicated hidden slot, not CFG value id or MergeValueItem extension - Add atomic next-PR boundary requiring C/D0/D1/E/G/H closure before compile gate unlock - Specify FrontendForIteratorStateSlot registry, four for-loop CFG items, and cross-table validation contract - Require distinct next temp with temp-then-commit for next intrinsic to preserve future destroyable state lifecycle - Expand CFG/lowering test surface with hidden-slot validation, boundary negative tests, and source-order isolation --- ...tend_for_range_loop_implementation_plan.md | 167 +++++++++++++++--- 1 file changed, 143 insertions(+), 24 deletions(-) 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 index bd2cb838..1c9bf99f 100644 --- a/doc/module_impl/frontend/frontend_for_range_loop_implementation_plan.md +++ b/doc/module_impl/frontend/frontend_for_range_loop_implementation_plan.md @@ -1,12 +1,12 @@ # Frontend for-in loop 实施计划 -> 本文档记录 `for iterator[: Type] in expr` 的分阶段实施。阶段 B 已把所有 `for-in` body 转为 frontend shared semantic 结构支持面;D0 已落地 ordinary iterable 的 header/body 调度骨架,但 bare `range(...)` 仍缺少进入 ordinary owner pipeline 前的专用预路由,因此 D0 尚未完成。iterator 先以保守 source-facing type 进入 lexical inventory;后续 `SuiteResolver` iteration-planning 阶段再发布 `FrontendForIterationPlan`,并在可静态确定 element type 时精化 iterator slot。lowering 最终根据 iteration plan 选择 `range(...)` / known iterable 专用 helper 或 generic Variant iterator helper。 +> 本文档记录 `for iterator[: Type] in expr` 的分阶段实施。阶段 B 已把所有 `for-in` body 转为 frontend shared semantic 结构支持面;D0 已落地 ordinary iterable 的 header/body 调度骨架,但 bare `range(...)` 仍缺少进入 ordinary owner pipeline 前的专用预路由,因此 D0 尚未完成。iterator 先以保守 source-facing type 进入 lexical inventory;后续 `SuiteResolver` iteration-planning 阶段再发布 `FrontendForIterationPlan`,并在可静态确定 element type 时精化 iterator slot。lowering 最终根据 iteration plan 选择 `range(...)` / known iterable 专用 helper 或 generic Variant iterator helper。loop-carried iterator state 使用 dedicated hidden mutable slot,不作为 CFG value id,也不通过 `MergeValueItem` 表达。 ## 文档状态 -- 状态:实施中(阶段 B 已完成;阶段 D0 仅完成结构性 header/body path,bare `range(...)` header 预路由与 canonical regression tests 尚未完成;阶段 C/D1 及后续 route、CFG、lowering 尚未实施) +- 状态:实施中(阶段 B 已完成;阶段 D0 仅完成结构性 header/body path;阶段 C/D1 及后续 route、CFG、lowering 尚未实施。2026-07-23 已冻结 dedicated hidden iterator slot 方向,当前 PR 不实施代码,下一 PR 按下述原子范围完整接通 range/int route) - 创建日期:2026-07-03 -- 最近校对:2026-07-22(撤销 D0 完成声明:canonical `for i in range(3)` 当前仍产生 `sema.binding` / `sema.expression_resolution`;body structural entry 已落地,compile-only 仍由临时 root blocker 阻断) +- 最近校对:2026-07-23(确认阶段 G 不能脱离阶段 C/D0/D1 单独进入 production path;loop-carried iterator state 改用 graph-owned dedicated hidden slot,不扩展 `MergeValueItem`) - 适用范围: - `src/main/java/gd/script/gdcc/frontend/sema/**` - `src/main/java/gd/script/gdcc/frontend/lowering/**` @@ -43,6 +43,20 @@ - 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` +### 下一 PR 原子实施边界 + +下一 PR 不再提交一个只能由测试手工注入 fact 才可到达的阶段 G 半成品。它必须按依赖顺序完成以下闭环: + +1. 阶段 C:冻结 `FrontendForIterationPlan`、route、operation contract 与 `FrontendAnalysisData.forIterationPlans()` publication surface。 +2. 完成阶段 D0:bare `range(...)` header 预路由和 canonical regression tests。 +3. 阶段 D1:发布 iteration plan、iterator slot refinement 与 final source-facing `slotTypes()[ForStatement]`。 +4. 阶段 E 中 range/int route 必需的 arity、argument boundary、zero-step 与 iterator conversion type-check。 +5. 阶段 G:发布 `FrontendForRegion`、dedicated hidden-slot registry、for-loop CFG items、graph validation 与控制流。 +6. 阶段 H:把 range/int route 降低为既有 intrinsic,并用 distinct next temp 执行 temp-then-commit。 +7. 最后替换阶段 F 的临时 root blocker:只解封已有完整 sema/CFG/LIR/backend 测试闭环的 `RANGE_CALL` 与 `INT_SHORTHAND`;generic/known routes 继续给出 route-not-ready diagnostic。 + +以上步骤属于一个生产链路原子 PR。不得先放宽 compile gate,也不得在 plan producer 尚不存在时通过测试专用 side-table mutation 声称阶段 G 已完成。generic Variant route、known iterable 专用 route 仍留在后续阶段。 + ## 1. 范围与非目标 本计划把 `for-in` 拆成两个互不混淆的支持面: @@ -73,6 +87,8 @@ compile / lowering 最终目标必须覆盖: - 不在 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. 当前基线 @@ -90,6 +106,7 @@ compile / lowering 最终目标必须覆盖: - `FrontendBodyLocalDeclaration` 与 `FrontendBodyDeclarationIndex` 已支持 `Node` declaration identity,并以 `ForStatement` 作为 `ITERATOR` entry identity;ordinary local 继续使用 `VariableDeclaration` identity。 - `FrontendCompileCheckAnalyzer` 当前对每个 `ForStatement` root 发布一个临时、无条件的 `sema.compile_check` blocker,不读取 iterable typed fact、iteration plan 或 route,也不进入 body。 - `FrontendCfgGraphBuilder.processStatement(...)` 当前没有 `ForStatement` 分支;`FrontendCfgRegion` 当前只允许 `BlockRegion`、`FrontendIfRegion`、`FrontendElifRegion`、`FrontendWhileRegion`。 +- `FrontendCfgGraphBuilder.ExecutableBodyBuild` 与 `FunctionLoweringContext` 当前只有 graph/region publication surface,没有 compiler-only hidden-local registry;因此 dedicated state 不能退化为 processor 内临时拼接的字符串。 - `GdccForRangeIterType.FOR_RANGE_ITER` 已作为 compiler-only `GdCompilerType` 子类型存在。 - `doc/gdcc_lir_intrinsic.md` 已冻结四个 backend-owned range intrinsic:`gdcc.for_range_iter.init`、`gdcc.for_range_iter.should_continue`、`gdcc.for_range_iter.next`、`gdcc.for_range_iter.get`;C backend 与 runtime helper 已有对应实现。 - generic Variant iterator helper、typed container iterator helper、Object `_iter_*` helper 在本仓库 runtime / intrinsic 层尚未实现。 @@ -100,6 +117,7 @@ compile / lowering 最终目标必须覆盖: - loop-control semantic 已把 `for` 当成合法 loop boundary。 - backend/LIR 已具备 range iterator state 与 intrinsic。 - frontend shared semantic 已承认 `for-in` body 为 structural supported surface,但 bare `range(...)` header 仍未接通;compile gate、CFG 与 body lowering 也尚未承认任何 `for-in` route 为 compile-ready surface。 +- 当前 CFG value 模型只为 ordinary single-definition value、branch merge value 与 source-slot alias 建模。用同一个 result value id 表达 init/update 的 loop-carried state 会混淆 value producer 与 mutable storage;本计划改为显式 hidden-slot registry,不扩展 `MergeValueItem` 的 branch-result 合同。 本计划的实施目标就是把这个张力收口为:“所有 `for-in` 在 shared semantic 中都是 supported body;iteration plan 决定 iterator type refinement 与 lowering route;compile surface 按 route helper 准备度分阶段打开”。 @@ -243,7 +261,7 @@ record FrontendForIterationPlan( - `rawElementType` 是 runtime helper / intrinsic 产出的元素类型。 - `exposedIteratorType` 是 body 中 iterator local 的 source-facing type。 -- `iteratorStateType` 只允许出现在该 dedicated iteration plan 与 lowering-internal state,不得进入 ordinary expression / slot / binding tables。 +- `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。 @@ -255,11 +273,76 @@ record FrontendForIterationPlan( generic Variant route 需要新增 contract: -- `iteratorStateType` 使用新的 compiler-only generic iterator state type,或由 LIR item 隐藏为 backend-owned storage。 +- `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 `range(...)` header 预路由与 ordinary call route 隔离 +### 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: @@ -273,7 +356,7 @@ generic Variant route 需要新增 contract: 该分流在 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.6 Godot iteration 语义落点 +### 3.7 Godot iteration 语义落点 外部语义参考表明 Godot 对 `for-in` 使用统一 iteration 协议: @@ -527,6 +610,8 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR ### 阶段 F:compile gate 分阶段解封 +状态:未实施。下一 PR 中先实现 route-aware policy,但只有阶段 G/H 与对应测试完成后才实际解封 range/int route;不得让阶段编号顺序变成提前开放 production path 的理由。 + 目标: - shared semantic 支持与 compile-ready 支持分离。 @@ -563,21 +648,32 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR ### 阶段 G:frontend CFG graph +状态:未实施。2026-07-23 确认不得脱离阶段 C、完整 D0、D1 与 range/int 必要 type-check 单独进入 production path;下一 PR 与这些前置及阶段 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,产出 hidden iterator state。 + - 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。 @@ -585,21 +681,29 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR - active loop frame 对 `for` 应使用: - `breakTargetId = exitId` - `continueTargetId = updateEntryId` -- hidden iterator state value id / slot id 必须稳定命名,建议沿用现有 `cfg_tmp_` 风格或使用明确前缀,例如 `cfg_for_iter_`;一旦选择,测试锁定。 +- 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`。 +- `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 +状态:未实施。下一 PR 与阶段 C/D0/D1/E/G 和 range/int compile-gate 解封一起原子实施。 + 目标: - 先接通已有 backend/runtime 已支持的 `range(...)` 与 `int` shorthand route。 @@ -607,33 +711,31 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR 实施内容: -- 在 `frontend.lowering.cfg.item` 增加最小 dedicated item,保持通用命名: - - `ForLoopInitItem` - - `ForLoopShouldContinueItem` - - `ForLoopGetItem` - - `ForLoopNextItem` -- 这些 item 都实现现有 `ValueOpItem`,并持有: - - AST anchor - - operand value ids - - result value id - - route / iterator state type / element type / lowering operation 必要信息 +- 消费阶段 G 已冻结的 `ForLoopInitItem`、`ForLoopShouldContinueItem`、`ForLoopGetItem`、`ForLoopNextItem`,不在 body lowering 阶段重新解释 item shape。 - body lowering 在 `FrontendSequenceItemInsnLoweringProcessors` 中增加对应 processor。 -- processor 消费 `FrontendForIterationPlan` 并生成 `CallIntrinsicInsn`;range route 对应: +- 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 @@ -650,7 +752,7 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR - `gdcc.for_variant_iter.should_continue` - `gdcc.for_variant_iter.next` - `gdcc.for_variant_iter.get` -- 新增 compiler-only iterator state type,或明确由 backend helper 持有 opaque state;无论哪种形态,都不得进入 ordinary type tables。 +- 新增 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` @@ -743,6 +845,11 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR ### 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` 能正确连边。 @@ -751,7 +858,11 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR ### 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。 @@ -799,6 +910,12 @@ script/run-gradle-targeted-tests.sh --tests GdCompilerTypeTest,GdccForRangeIterT - `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。下一 PR 必须按依赖链提交,不能用手工注入 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。 @@ -813,8 +930,10 @@ script/run-gradle-targeted-tests.sh --tests GdCompilerTypeTest,GdccForRangeIterT 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、hidden LIR local / intrinsic operand-result / backend C storage 路径。 -7. CFG 中存在独立 `FrontendForRegion`,且 `continue` / `break` 连边正确;同一 region/item/processor 基础设施能承载 range、generic Variant 与 known iterable route。 +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 完成定义。 + +下一 PR 的 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,不得把局部绿色测试标记为阶段完成。 From 73dac3ee3e190566419249395d01f84d1d03e3ef Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Thu, 23 Jul 2026 02:19:38 +0800 Subject: [PATCH 26/27] docs(frontend): archive pipeline plan as implementation source of truth and refresh for-range plan - Replace the segmented pipeline plan with a concise implementation doc retaining only invariants, core design, risks, and completion criteria - Strip completed phase logs (A-L) and transitional asset classification from the pipeline doc - Reorganize the for-range plan: compress completed phases A/B, add atomic PR boundary, remove outdated status entries and legacy pipeline references - Clean transitional language from code comments that referenced specific migration phases or legacy analyzer paths --- ...e_resolution_pipeline_execution_summary.md | 2 +- ...tend_for_range_loop_implementation_plan.md | 165 +- ...tend_resolution_pipeline_implementation.md | 425 ++++++ ...segmented_type_resolution_pipeline_plan.md | 1352 ----------------- .../analyzer/FrontendBodyOwnerProcedures.java | 4 +- .../analyzer/FrontendSemanticAnalyzer.java | 6 +- .../analyzer/FrontendStatementResolver.java | 2 +- .../sema/analyzer/FrontendSuiteContext.java | 2 +- .../sema/analyzer/FrontendSuiteResolver.java | 2 +- ...FrontendSemanticAnalyzerFrameworkTest.java | 2 +- .../FrontendSegmentedPipelineTestSupport.java | 2 +- 11 files changed, 473 insertions(+), 1491 deletions(-) create mode 100644 doc/module_impl/frontend/frontend_resolution_pipeline_implementation.md delete mode 100644 doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md diff --git a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md index 5716041c..c5a32911 100644 --- a/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md +++ b/doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md @@ -1,6 +1,6 @@ # Frontend Segmented Type Resolution Pipeline Execution Summary -本文总结 `doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md` 执行完成后的前端分析流水线形态。内容只描述目标架构、执行顺序与不变量,不展开旧 whole-module 流水线或过渡实现资产。 +本文总结 `doc/module_impl/frontend/frontend_resolution_pipeline_implementation.md` 所述前端分析流水线的目标架构形态。内容只描述目标架构、执行顺序与不变量,不展开旧 whole-module 流水线或过渡实现资产。 ## 1. 总体形态 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 index 1c9bf99f..66beee62 100644 --- a/doc/module_impl/frontend/frontend_for_range_loop_implementation_plan.md +++ b/doc/module_impl/frontend/frontend_for_range_loop_implementation_plan.md @@ -1,12 +1,12 @@ # Frontend for-in loop 实施计划 -> 本文档记录 `for iterator[: Type] in expr` 的分阶段实施。阶段 B 已把所有 `for-in` body 转为 frontend shared semantic 结构支持面;D0 已落地 ordinary iterable 的 header/body 调度骨架,但 bare `range(...)` 仍缺少进入 ordinary owner pipeline 前的专用预路由,因此 D0 尚未完成。iterator 先以保守 source-facing type 进入 lexical inventory;后续 `SuiteResolver` iteration-planning 阶段再发布 `FrontendForIterationPlan`,并在可静态确定 element type 时精化 iterator slot。lowering 最终根据 iteration plan 选择 `range(...)` / known iterable 专用 helper 或 generic Variant iterator helper。loop-carried iterator state 使用 dedicated hidden mutable slot,不作为 CFG value id,也不通过 `MergeValueItem` 表达。 +> 本文档是 `for iterator[: Type] in expr` 的长期实施事实源,定义 shared semantic / compile / lowering 三层支持面的架构合同、核心设计与分阶段实施步骤。不再保留已完成阶段的验收流水账。 ## 文档状态 -- 状态:实施中(阶段 B 已完成;阶段 D0 仅完成结构性 header/body path;阶段 C/D1 及后续 route、CFG、lowering 尚未实施。2026-07-23 已冻结 dedicated hidden iterator slot 方向,当前 PR 不实施代码,下一 PR 按下述原子范围完整接通 range/int route) +- 状态:实施中(shared semantic 结构支持已完成;bare `range(...)` header 预路由、iteration plan、CFG、lowering 尚未实施) - 创建日期:2026-07-03 -- 最近校对:2026-07-23(确认阶段 G 不能脱离阶段 C/D0/D1 单独进入 production path;loop-carried iterator state 改用 graph-owned dedicated hidden slot,不扩展 `MergeValueItem`) +- 更新时间:2026-07-23 - 适用范围: - `src/main/java/gd/script/gdcc/frontend/sema/**` - `src/main/java/gd/script/gdcc/frontend/lowering/**` @@ -20,7 +20,7 @@ - 关联事实源: - `doc/module_impl/common_rules.md` - `doc/module_impl/frontend/frontend_rules.md` - - `doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.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` @@ -43,19 +43,10 @@ - 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` -### 下一 PR 原子实施边界 - -下一 PR 不再提交一个只能由测试手工注入 fact 才可到达的阶段 G 半成品。它必须按依赖顺序完成以下闭环: - -1. 阶段 C:冻结 `FrontendForIterationPlan`、route、operation contract 与 `FrontendAnalysisData.forIterationPlans()` publication surface。 -2. 完成阶段 D0:bare `range(...)` header 预路由和 canonical regression tests。 -3. 阶段 D1:发布 iteration plan、iterator slot refinement 与 final source-facing `slotTypes()[ForStatement]`。 -4. 阶段 E 中 range/int route 必需的 arity、argument boundary、zero-step 与 iterator conversion type-check。 -5. 阶段 G:发布 `FrontendForRegion`、dedicated hidden-slot registry、for-loop CFG items、graph validation 与控制流。 -6. 阶段 H:把 range/int route 降低为既有 intrinsic,并用 distinct next temp 执行 temp-then-commit。 -7. 最后替换阶段 F 的临时 root blocker:只解封已有完整 sema/CFG/LIR/backend 测试闭环的 `RANGE_CALL` 与 `INT_SHORTHAND`;generic/known routes 继续给出 route-not-ready diagnostic。 - -以上步骤属于一个生产链路原子 PR。不得先放宽 compile gate,也不得在 plan producer 尚不存在时通过测试专用 side-table mutation 声称阶段 G 已完成。generic Variant route、known iterable 专用 route 仍留在后续阶段。 +- 明确非目标: + - 不在这里定义 generic Variant iterator route 的 runtime helper 或 C backend 实现细节 + - 不在这里定义 known iterable 专用 route 的 helper 准备度 + - 不在这里改变 Godot range runtime 语义 ## 1. 范围与非目标 @@ -92,34 +83,27 @@ compile / lowering 最终目标必须覆盖: ## 2. 当前基线 -当前代码库已经具备的基础: +已完成: -- `gdparser` 的 `ForStatement` AST 已包含 `iterator`、`iteratorType`、`iterable`、`body`、`range` 字段;其中 `range()` 是 AST source-location anchor,不是 `range(...)` builtin classifier,D0 pre-route 必须检查 `iterable` expression shape。 -- `GdScriptParserService` 只是外部 parser adapter,本仓库没有本地 parser grammar 可改;若 parse smoke 发现 `for` AST 形态不足,应先升级或修复外部 parser,而不是在 frontend analyzer 中猜文本。 -- `FrontendScopeAnalyzer` 已为 `ForStatement` 建立 `FOR_BODY` scope,并保证 `iteratorType` 与 `iterable` 在外层 scope 下遍历,`body` 在独立 `FOR_BODY` scope 下遍历。 -- `FrontendLoopControlFlowAnalyzer` 已把 `for` 与 `while` 一样视为 loop boundary,`break` / `continue` 在 `for` body 内不再报 `sema.loop_control_flow`。 -- `FrontendSemanticAnalyzer` 的 shared phase 顺序固定为 skeleton -> scope -> variable inventory -> interface surface -> `SuiteResolver` -> diagnostics-only analyzers。`SuiteResolver` 是逐语句运行的 typed fact owner,能在 `var limit := 3` flush 后让后续 `for i in limit:` 读取 `limit:int`。 -- `FrontendTypedLexicalEnvironment` 已支持 pending / committed overlay、per-owner patch transaction、`Variant -> exact` 的 local slot update 校验;但当前 API 只允许 `LOCAL_TYPE_STABILIZATION` owner 发布 local slot update。 +- `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。 -- 当前 `FrontendStatementResolver.resolveForStatement(...)` 仍无条件对整个 `forStatement.iterable()` 调用 ordinary `runSupportedRoot(...)`。由于 `range` 不是当前 registry 中可绑定的 ordinary value / utility function,canonical `for i in range(3)` 会先为 callee 发布 unknown binding,再让 identifier 与 call root expression typing 失败;argument `3` 也不会获得可供 planning 消费的 stable expression type。 -- 上述 header 错误不会关闭 body:iterator 与 `var x := i` 仍以 `Variant` baseline 进入 body。这只证明 structural child-suite entry 已落地,不证明 `range(...)` header 已受 shared semantic 支持。 -- `FrontendBodyLocalDeclaration` 与 `FrontendBodyDeclarationIndex` 已支持 `Node` declaration identity,并以 `ForStatement` 作为 `ITERATOR` entry identity;ordinary local 继续使用 `VariableDeclaration` identity。 -- `FrontendCompileCheckAnalyzer` 当前对每个 `ForStatement` root 发布一个临时、无条件的 `sema.compile_check` blocker,不读取 iterable typed fact、iteration plan 或 route,也不进入 body。 -- `FrontendCfgGraphBuilder.processStatement(...)` 当前没有 `ForStatement` 分支;`FrontendCfgRegion` 当前只允许 `BlockRegion`、`FrontendIfRegion`、`FrontendElifRegion`、`FrontendWhileRegion`。 -- `FrontendCfgGraphBuilder.ExecutableBodyBuild` 与 `FunctionLoweringContext` 当前只有 graph/region publication surface,没有 compiler-only hidden-local registry;因此 dedicated state 不能退化为 processor 内临时拼接的字符串。 -- `GdccForRangeIterType.FOR_RANGE_ITER` 已作为 compiler-only `GdCompilerType` 子类型存在。 -- `doc/gdcc_lir_intrinsic.md` 已冻结四个 backend-owned range intrinsic:`gdcc.for_range_iter.init`、`gdcc.for_range_iter.should_continue`、`gdcc.for_range_iter.next`、`gdcc.for_range_iter.get`;C backend 与 runtime helper 已有对应实现。 -- generic Variant iterator helper、typed container iterator helper、Object `_iter_*` helper 在本仓库 runtime / intrinsic 层尚未实现。 - -当前实现张力: - -- 早期文档曾把 `for` 放在 post-MVP / deferred 范围;阶段 B 与 D0 的结构性子集落地后,shared semantic 文档已改为 body 结构支持,但 D0 range header 预路由、compile/lowering 仍分阶段开放。 -- loop-control semantic 已把 `for` 当成合法 loop boundary。 -- backend/LIR 已具备 range iterator state 与 intrinsic。 -- frontend shared semantic 已承认 `for-in` body 为 structural supported surface,但 bare `range(...)` header 仍未接通;compile gate、CFG 与 body lowering 也尚未承认任何 `for-in` route 为 compile-ready surface。 -- 当前 CFG value 模型只为 ordinary single-definition value、branch merge value 与 source-slot alias 建模。用同一个 result value id 表达 init/update 的 loop-carried state 会混淆 value producer 与 mutable storage;本计划改为显式 hidden-slot registry,不扩展 `MergeValueItem` 的 branch-result 合同。 - -本计划的实施目标就是把这个张力收口为:“所有 `for-in` 在 shared semantic 中都是 supported body;iteration plan 决定 iterator type refinement 与 lowering route;compile surface 按 route helper 准备度分阶段打开”。 +- `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. 核心设计 @@ -372,87 +356,12 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR ## 4. 分阶段实施步骤 -### 阶段 A:parse / AST / scope 基线测试 - -目标: - -- 锁住外部 parser 对 `for-in` 的 AST shape。 -- 确认 `FrontendScopeAnalyzer` 的 header/body scope 分层满足 all for-in 支持面。 - -实施内容: - -- 增加或扩展 parse smoke / scope 测试,覆盖: - - `for i in range(3):` - - `for i in 3:` - - `for i in limit:` - - `for i in values:` - - `for i: int in values:` -- 不修改 `GdScriptParserService`,除非 parser diagnostic mapping 本身有问题。 -- 确认 `iteratorType` 与 `iterable` 仍在外层 scope 解析,`body` 仍在独立 `FOR_BODY` scope 下解析。 +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)留在后续独立实施。 -验收细则: - -- `ForStatement.iterator()` 返回源码 iterator name。 -- `ForStatement.iteratorType()` 对 typed iterator 非空,对 untyped iterator 为空。 -- `ForStatement.iterable()` 保持原始 expression shape,不被 rewrite。 -- `ForStatement.body()` 已由 `FrontendScopeAnalyzer` 发布 `BlockScopeKind.FOR_BODY`。 -- nested `for` 的内层 body 也有独立 `FOR_BODY`,且 parent scope 链正确。 - -### 阶段 B:body inventory 与 declaration index 解封 - -状态:已完成(2026-07-20)。 - -目标: -- 把 `for` 从 shared semantic 的 unsupported boundary 中移除。 -- 为所有 `for-in` 发布 iterator binding 与完整 body local inventory。 -- 扩展 body declaration index,使 iterator 不绕过 published inventory guard。 +### 阶段 A/B:parse / scope 基线与 body inventory 解封(已完成) -实施内容: - -- `FrontendExecutableInventorySupport.canPublishCallableLocalValueInventory(BlockScopeKind)` 增加 `FOR_BODY`。这是 all for-in semantic supported 的明确边界变化;`MATCH_SECTION_BODY`、`LAMBDA_BODY` 等仍不得一起打开。 -- `FrontendVariableAnalyzer.handleForStatement(...)` 改为 supported path: - - 找到 `forStatement.body()` 对应 `BlockScope`。 - - 用 `ForStatement` 作为 declaration identity 定义 iterator local。 - - 无显式 iterator type 时使用 `GdVariantType.VARIANT` baseline。 - - 有显式 iterator type 时通过既有 declared-type resolver 解析 source-facing type。 - - 使用现有 duplicate / same-callable shadow 规则检查 iterator 与参数、外层 local、同 body local 的冲突。 - - 遍历 body,发布 body 内 ordinary local `var`。 -- 在移除 shared semantic 的 `ForStatement` unsupported diagnostic 前,先让 - `FrontendCompileCheckAnalyzer.handleForStatement(...)` 对所有 `for-in` 发布临时、无条件的 - statement-root `sema.compile_check` blocker: - - blocker 只锚定 `ForStatement`,不得进入 body 重扫 semantic facts。 - - blocker 不读取 iteration plan、iterable type 或 route readiness。 - - 阶段 F 以 route-aware compile policy 替换该临时 blocker;在此之前任何 `for-in` 都不能进入 - `FrontendCfgGraphBuilder`。 -- 移除 `UnsupportedVariableBoundaryReporter` 对 `ForStatement` 的 unsupported diagnostic。 -- `FrontendBodyLocalDeclaration` / `FrontendBodyDeclarationIndex` 扩展为支持 iterator declaration identity: - - declaration key 使用 `Node` 或 `Object` identity,而不是只接受 `VariableDeclaration`。 - - iterator entry kind 为 `ITERATOR`,作为 synthetic 第 0 项固定 `sourceOrder == 0`,表示 body-start visible。 - - ordinary `var` entry kind 为 `ORDINARY_VAR`,继续使用 source-order visibility。 -- `FrontendInterfacePhase.handleForStatement(...)` 不再注册 `FOR_SUBTREE` pending gate;它应: - - walk `iteratorType` 与 `iterable`。 - - 记录 iterator baseline 到 `FrontendTypedLexicalBaseline`。 - - `enterSupportedBlock(forStatement.body())`,使 nested body local 与 nested for 都进入 interface surface。 -- `FrontendVisibleValueResolver` 删除 for-specific body/header deferred boundary: - - `ForStatement.body()` edge 不再返回 `FOR_SUBTREE`。 - - `ForStatement.iteratorType()` / `iterable()` header edge 不再被 deferred。 - - `BlockScopeKind.FOR_BODY` current-scope gate 不再 fail-closed。 - -验收细则: - -- `for i in values: print(i)` 中 `i` 在 body 内解析为 `LOCAL`,baseline type 为 `Variant`。 -- `for i in range(3): print(i)` 在本阶段也至少解析为 `LOCAL`,即使还未精化为 `int`。 -- `i` 在 loop 后不可见。 -- body 内 `var local := i` 正常发布为 `FOR_BODY` ordinary local。 -- `for i in values: var item := i` 的 `item` 出现在 body declaration index 中。 -- iterator entry 出现在 body declaration index 中,且 resolver 的 published inventory guard 覆盖该 entry。 -- 同一 callable 内 iterator name 与已有 parameter/local 冲突时按现有 local conflict 规则报错。 -- body 内 `var i := ...` 与 iterator `i` 冲突,不能覆盖 iterator。 -- `match`、lambda、block-local `const` 的旧 unsupported / deferred 行为不因 `FOR_BODY` 解封而改变。 -- 默认 shared `analyze(...)` 不再为 `for-in` 产生 `FOR_SUBTREE` / variable-inventory unsupported - diagnostic;同一 source 通过 `analyzeForCompile(...)` 时由临时 `sema.compile_check` blocker - 明确阻断,且 lowering pipeline 不进入 CFG build。 +阶段 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 @@ -493,14 +402,14 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR ### 阶段 D0:SuiteResolver header-only for path 与 baseline body entry -状态:未完成。2026-07-20 已落地 ordinary iterable 的结构性 header/body path;2026-07-22 复核确认 bare `range(...)` 仍被错误送入 ordinary owner pipeline,canonical range header 不可用,因此不得保留“D0 已完成”声明。 +状态:未完成。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。 -- 为 segmented pipeline 阶段 L 提供“没有 typed gate 也能进入 for body”的 production path。 +- 为 resolution pipeline 提供“没有 typed gate 也能进入 for body”的 production path。 实施内容: @@ -610,7 +519,7 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR ### 阶段 F:compile gate 分阶段解封 -状态:未实施。下一 PR 中先实现 route-aware policy,但只有阶段 G/H 与对应测试完成后才实际解封 range/int route;不得让阶段编号顺序变成提前开放 production path 的理由。 +状态:未实施。先实现 route-aware policy,但只有阶段 G/H 与对应测试完成后才实际解封 range/int route。 目标: @@ -648,7 +557,7 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR ### 阶段 G:frontend CFG graph -状态:未实施。2026-07-23 确认不得脱离阶段 C、完整 D0、D1 与 range/int 必要 type-check 单独进入 production path;下一 PR 与这些前置及阶段 H 一起原子实施。 +状态:未实施。不得脱离阶段 C、完整 D0、D1 与 range/int 必要 type-check 单独进入 production path;必须与这些前置及阶段 H 一起原子实施。 目标: @@ -702,7 +611,7 @@ GDCC 第一批 route 不必全部专用化,但必须让 `FrontendForIterationR ### 阶段 H:range route LIR lowering -状态:未实施。下一 PR 与阶段 C/D0/D1/E/G 和 range/int compile-gate 解封一起原子实施。 +状态:未实施。与阶段 C/D0/D1/E/G 和 range/int compile-gate 解封一起原子实施。 目标: @@ -915,7 +824,7 @@ script/run-gradle-targeted-tests.sh --tests GdCompilerTypeTest,GdccForRangeIterT - 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。下一 PR 必须按依赖链提交,不能用手工注入 plan 的 builder test 替代真实 source -> sema -> CFG/LIR integration test。 +- 阶段 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。 @@ -936,4 +845,4 @@ script/run-gradle-targeted-tests.sh --tests GdCompilerTypeTest,GdccForRangeIterT 阶段性完成门槛: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 完成定义。 -下一 PR 的 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,不得把局部绿色测试标记为阶段完成。 +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_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_segmented_type_resolution_pipeline_plan.md b/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md deleted file mode 100644 index e146e59e..00000000 --- a/doc/module_impl/frontend/frontend_segmented_type_resolution_pipeline_plan.md +++ /dev/null @@ -1,1352 +0,0 @@ -# Frontend Segmented Type Resolution Pipeline Plan - -## 1. 文档状态 - -- 性质:重启后的实施计划。 -- 目标模块:`src/main/java/gd/script/gdcc/frontend/sema/**`,并影响 `frontend/scope/**` 与 frontend lowering 的 analysis contract。 -- 直接动机:用 Godot 风格的 interface/body 分层与 source-order suite 解析替代原先 statement-window segmented runner 路线,使 `var limit := 3; for i in limit:` 这类依赖前缀 typed fact 的语义支持拥有稳定架构基础。阶段 I 后,旧 runner 与 shared analyzer legacy whole-phase bypass 已从代码中删除。 -- 主体方案:以 interface phase + body `SuiteResolver` 为主线,即先建立 class/callable/block 的 lexical 与 signature/interface 事实,再按 body suite 源码顺序解析 statement。 -- 辅助方案:引入 `TypedLexicalEnvironment` overlay,使当前 statement / 当前 suite 内的 local slot typed fact 能被后续 semantic step 读取,而不提前污染最终 stable side table。 -- 实施策略:允许把阶段 A-D 的已有实现视为“可保留资产 / 可重写参考 / 可回退资产”。本文不假设当前 segmented scheduler 已经是新路线的可继续扩展基础,也不把现有 `analyzeInWindow(...)` 当作可抽取的 statement runner。 -- 非目标:本文不直接完成 `for-range` lowering,不改变 Godot range runtime 语义,不一次性转正 `lambda` / `match` / block-local `const`。 - -关联文档: - -- `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_semantic_analyzer_research_report.md` - -### 1.1 架构决定:无条件结构性 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。 - -阶段 L 已删除早期 synthetic typed-dependent gate 试验。第 4.5 节现定义 structural policy 与 -completeness certificate,第 4.8 节现定义 resolver 的纯结构边界,阶段 F 只保留迁移历史结论。 -生产代码不存在 registry、readiness fallback、pending-gate 注册或 synthetic classifier;后续 feature -不得恢复等价 lifecycle 或 publication protocol。 - -## 2. 背景与问题 - -当前 `FrontendSemanticAnalyzer.analyze(...)` 是 whole-module phase pipeline: - -1. skeleton -2. scope -3. variable inventory -4. top binding -5. local type stabilization -6. chain binding -7. expr typing -8. var type post -9. annotation usage -10. virtual override -11. type check -12. loop control -13. compile-only gate,仅 `analyzeForCompile(...)` - -这条顺序让每个 phase 都能消费前一个 phase 的完整 module 事实,但它无法自然表达 Godot 的 body 解析模型: - -- Godot parser 只建立 AST / suite / local name 结构,不做类型解析。 -- 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。 - -这里的 Godot 对照只用于说明 interface/body 边界与 `resolve_suite()` 的 source-order 解析形状,不用于把“完整 local inventory”与“增量/source-order analysis”对立起来。Godot parser/suite local registry 与 analyzer source-order 解析可以同时成立;GDCC 的完整 inventory 要求来自自己的 resolver filtered-hit 模型,见第 3.2 节。 - -原 statement-window segmented runner 方案试图在现有 phase pipeline 上模拟 source-order,但实际暴露出两个结构性阻塞: - -- 已提取的 `analyzeInWindow(...)` 只改变发布表面,不限制 analyzer 遍历范围,仍偏 whole-module。 -- `FrontendLocalTypeStabilizationAnalyzer` 的 window 路径把 slot update 延迟到 patch commit,整模块运行时会让后续 `var b := a` 读不到前序 `a` 的稳定类型。 -- `FrontendVarTypePostAnalyzer.analyzeInWindow(...)` 不是纯 scratch-over-stable:它取得 stable `analysisData.slotTypes()` 引用、直接 `clear()` 并把该引用传给 `SlotTypePublisher` 写入,然后才复制到 `window.publications().slotTypes()`。因此现有 window publication 路径已经能在 patch commit / discard 前污染 stable side table。 - -现有 analyzer 不是“window runner”。它们的 `analyze(...)` / `analyzeInWindow(...)` 入口仍以 `moduleSkeleton.sourceClassRelations()` 为根,创建内部 `AstWalker...` / visitor 并遍历完整 `SourceFile` AST。`FrontendWindowAnalysisContext` 只把发布目标换成 scratch surface;它没有把遍历 root 限制到当前 statement、当前 suite 或当前 body。因此,新的 body `SuiteResolver` 不能通过简单迁移这些入口获得。它需要在现有语义规则与 owner 合同之上,重写每个 owner 的 statement-local 调度、显式上下文状态和发布逻辑。 - -这也是一次执行框架重写,而不是 statement-window 方案的续作:`FrontendSegmentedSemanticScheduler` 曾只作为证明 patch merge / stable reference 行为的过渡资产,阶段 I 后已删除。最终 pipeline 不再保留 shared analyzer 级别的 legacy whole-phase body semantic bypass。 - -因此,`FrontendWindowPublicationSurface` / `FrontendWindowAnalysisContext` 不能作为新 overlay/export 的正确性参考。类型本身的 API 试图表达 scratch view,但现有 analyzer 使用方式已经破坏该承诺;计划只能把它们作为 legacy comparison / targeted regression 的输入,不能把它们当作可复用设计。 - -因此新计划改为 interface/body 双层结构: - -- interface 层基于基础结构层已发布的 lexical inventory 建立 declaration index、signature/interface facts 与 typed baseline。 -- body 层用 `SuiteResolver` 按源码顺序解析 supported body statements。 -- `TypedLexicalEnvironment` overlay 在 body 层提供 Godot 风格的“当前语句已知 typed fact 对后续语义立即可见”能力,同时保留 GDCC 的 side-table owner、patch conflict 与 compiler-only type 隔离。 -- Suite 收敛后的 stable export 采用按 owner 有序的 patch transaction:base owner facts 按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 顺序提交;已实现的 feature-specific semantic route owner 可以在 expr typing 与对应 var type post 之间插入独立 patch,例如后续 `FOR_ITERATION_PLANNING`。不能把这些 owner 合并成一个多 owner `FrontendAnalysisPatch`。 -- Patch 相关类型统一迁入 `gd.script.gdcc.frontend.sema.patch` 包,包括旧 `FrontendAnalysisPatch`、`FrontendLocalSlotTypeUpdate` 与新建 per-owner patch / transaction 类型;window publication 类型若保留,只能作为 legacy shim。 - -## 3. 不变量 - -### 3.1 Phase owner 边界仍然保留 - -重启计划不是允许任意 resolver 写任意表。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。 - -上述 owner 边界是语义合同,不是当前 analyzer 遍历代码可直接复用的证明。当前 top binding、local stabilization、chain binding、expr typing、var type post analyzer 都把遍历控制、隐式状态和 side-table 发布耦合在 whole-module AST walker 内。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 要求不是因为 Godot 缺少前向 local 检测,也不是把 complete inventory 与 source-order analysis 当成二选一。Godot parser / suite local registry 先让 analyzer 阶段能查询完整 local set,`resolve_suite()` 再按 source order 解析;前向 local usage 以 `CONFUSABLE_LOCAL_USAGE` warning 表达。GDCC 与 Godot 的真实差异在诊断级别和实现模型:GDCC 把 future declaration 编码为 resolver filtered hit,并把它作为 binding / diagnostics provenance 的一部分。 - -在 GDCC 当前模型中,已支持 executable block 的 ordinary local inventory 由 `FrontendVariableAnalyzer` + scope graph 发布。`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。 - -正确模型是: - -- 对已支持的 block,基础结构层沿用现有 `FrontendVariableAnalyzer` + scope graph 发布完整 ordinary local inventory;interface 层建立 body declaration index / typed baseline view,而不是另起一套重复发布通道。生产 resolver 仍由 scope 完成按名称查找,并以 index 的 declaration identity 验证命中的 ordinary local 属于已发布 inventory;现有 source byte range filter 继续负责 declaration-order 与 initializer self-reference 语义。 -- 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;header typed fact 只能影响后续 type refinement、semantic route 或 lowering。 - -这保证 `var x := y; var y := 1` 仍能产生 declaration-after-use filtered hit,而不是把 `y` 误判成普通 miss 或外层 fallback。 - -### 3.3 `FrontendAnalysisData` 稳定引用合同保持不变 - -现有测试要求 `FrontendAnalysisData.updateXxx(...)` 保留 side table 对象引用,并通过 clear + putAll 清理 stale entry。新计划不能破坏这个外部合同。 - -必须做到: - -- 保留现有 `updateXxx(...)` whole-table publication API 和测试。 -- 保留 `applyPatch(...)` 或等价 merge API 表达部分提交,但其输入必须是单一 owner patch;suite export 通过有序 patch transaction 依次调用 merge API,不能把跨 owner facts 塞进同一个 patch。 -- 同一个 side table 的 stable reference 仍不替换。 -- 增量 merge 必须检测冲突,不能静默覆盖不兼容 fact。 -- `TypedLexicalEnvironment` 的 overlay fact 只有在 owner 合法、冲突校验和 compiler-only guard 通过后,才能封装为对应 owner patch 并导出到 stable side table。 -- 保留 `updateXxx(...)` 不等于允许它继续作为 source-facing typed publication 的自由入口。只要某个 `updateXxx(...)` 仍会接收 `symbolBindings()`、`resolvedMembers()`、`resolvedCalls()`、`expressionTypes()`、`slotTypes()` 这类用户可见 typed facts,它就必须先复用与 overlay write / patch merge 相同的 compiler-only guard;否则它只能继续作为 legacy whole-table publication API 存在,不能出现在 production SuiteResolver path。 -- 所有 production patch carrier、patch transaction 与 local slot update carrier 都位于 `gd.script.gdcc.frontend.sema.patch` 包;`FrontendAnalysisData` 保留 stable data ownership,只暴露 merge entrypoint。Window publication 类型若迁入该包,也必须标记为 legacy shim。 - -### 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 消费,不能通过普通 expression typing、ordinary local slot publication 或 user-visible binding payload 泄漏。 - -当前实现的 `FrontendAnalysisData.checkPatchDoesNotLeakCompilerOnlyTypes(...)` 只扫描 `expressionTypes()` 与 `slotTypes()`;`refreshPublishedLocalBindingPayloads(...)` 只覆盖 local slot update 后刷新 binding payload 的路径。因此本节是不变量目标,不是现有 `applyPatch` guard 已经完整覆盖的事实。阶段 C 必须把 patch-commit guard 与新 typed overlay guard 扩展到上面的所有 type-bearing publication surfaces,至少先关闭 `symbolBindings().resolvedValue.type()` 的直接 patch / overlay bypass;不得用 legacy window publication 行为证明该 guard 完整。 - -这里的“统一 guard”不是一句抽象约束,而是同一个 type-bearing field walker / validator 合同: - -- patch commit、overlay pending write、overlay flush、任何仍保留的 source-facing `updateXxx(...)` whole-table publish,都必须复用同一套 field walker,而不是各自挑几张表做局部检查。 -- walker 至少要能递归访问 `FrontendBinding.resolvedValue().type()`、`FrontendResolvedMember.receiverType()` / `resultType()`、`FrontendResolvedCall.receiverType()` / `returnType()` / `argumentTypes()` / exact callable boundary parameter types、`FrontendExpressionType.publishedType()`、`slotTypes()` value、`FrontendLocalSlotTypeUpdate.type()`。 -- 只检查顶层 side table name 不足以证明安全;必须检查 fact payload 中所有 user-visible `GdType` 字段。 -- 任何依赖 `FrontendAnalysisData.symbolBindings()` / `resolvedMembers()` / `resolvedCalls()` / `expressionTypes()` / `slotTypes()` 返回的可变 stable 引用来直接 `put()` / `clear()` / `putAll()` 的路径,都不能被视为满足本节不变量的 publication path。 -- Godot 在这里没有等价的 compiler-only publication guard 或 overlay export 机制;本节约束完全由 GDCC 自己的 side-table / lowering 边界不变量驱动,不能靠 Godot 对照“类比证明”。 - -### 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 已实施资产可回退但不能静默改变语义 - -阶段 A-D 已有代码可以按新计划保留、移动、作为重写参考、废弃或回退,但任何回退必须保持: - -- `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` guard-only 合同。 -- `FrontendAnalysisData` stable reference 合同。 -- patch merge 的冲突检测与 compiler-only guard。 -- unsupported / deferred subtree fail-closed 合同。 - -## 4. 核心设计 - -### 4.1 新的三层 pipeline - -重启后的 shared semantic pipeline 分成三层。 - -基础结构层: - -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 - -Interface 层借鉴 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` 或等价 coordinator。它不取代 skeleton/scope/variable analyzer,而是在它们之后建立 body 解析所需的 interface surface。 - -Interface phase 输入: - -- `FrontendModuleSkeleton` -- `scopesByAst()` -- baseline parameter / ordinary local inventory -- current diagnostics snapshot -- `ClassRegistry` - -Interface phase 输出: - -- `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 根列表。 - -Interface phase 不得: - -- 发布 `expressionTypes()`。 -- 发布 `resolvedMembers()` / `resolvedCalls()`。 -- 将已转正 AST 节点的 body inventory 延迟到 typed fact 可用后;每个转正节点必须在本阶段无条件发布其 body scope、完整 declaration index 与 baseline。 -- 将 `GdCompilerType` 写入 source-facing lexical baseline。 - -### 4.3 Body `SuiteResolver` - -新增 `FrontendSuiteResolver` 或等价 body coordinator。它按 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()) - -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 循环前写入 pending overlay 并 flush 到 current-suite committed overlay。该步骤与 -statement-local var type post procedure 互补而非绕过:二者共享 `VAR_TYPE_POST` owner stage、 -冲突检查、compiler-only guard 与 callable-scoped export batch;此前不得写 stable `slotTypes()`。 - -`resolveStatement(...)` 不是新的 owner。它只负责按 statement 结构调用 body-aware owner 子过程: - -- identifier / route head 绑定仍由 top binding owner 产出。 -- local `:=` slot rewrite 仍由 local stabilization owner 产出。 -- chain member / chain call 仍由 chain binding owner 产出。 -- expression type / bare call 仍由 expr typing owner 产出。 -- source slot final publication 仍由 var type post owner 产出。 - -这些 owner 子过程是新实现,不是现有 `analyzeInWindow(...)` 的改名。实现时可以抽取纯 helper(例如 binding 分类、chain reduction、expression semantic support),但不能复用 whole-module `AstWalker...walk(sourceFile)` 入口作为 production SuiteResolver procedure。当前 analyzer 内部的大型 visitor 状态机必须被拆成显式 context + statement-local dispatch;否则 source-order prefix facts、pending overlay 与 per-owner patch transaction 都无法保证。 - -`resolveStatement(...)` 内部的 owner 子过程顺序是新的硬不变量,必须保持 legacy whole-phase 的可见性顺序: - -0. Callable-entry var type post pre-publication:在进入 statement 循环前,为每个 parameter 发布 `VAR_TYPE_POST` slot fact,并调用 `flushPendingFacts()` 使其进入 current-suite committed overlay。它不是 statement-local procedure 的例外。 -1. Top binding runner:为当前 statement 内的 bare identifier / chain head use-site 写入 binding overlay。 -2. Local stabilization runner:在 top binding overlay、当前 suite 已提交 typed fact、stable lexical inventory 之上解析 eligible `:=` initializer,并写入当前 statement 的 local slot pending overlay。 -3. Chain binding runner:消费 top binding overlay 与 local stabilization pending / committed slot fact,发布 `resolvedMembers()` 与 chain-owned `resolvedCalls()` overlay。 -4. Expr typing runner:消费 binding、member、call 与 local slot overlay,发布 `expressionTypes()` 与 bare-call `resolvedCalls()` overlay;`backfillInferredLocalType(...)` 仍只做 guard-only 检查。 -5. Feature-specific semantic route planning:仅在该 statement kind 已有 concrete owner 时运行,例如后续 `FOR_ITERATION_PLANNING`;消费 header expression facts,发布 dedicated route fact / source-facing refinement,但不能控制 body entry。普通 statement 省略本步。 -6. Var type post procedure:消费 expression type、feature-specific refinement 与 source-facing local slot overlay,发布 final `slotTypes()` overlay。 -7. Pending fact flush:把当前 statement pending overlay 转入 current-suite committed overlay,供后续 statement 读取;不得在这一步写 stable side table。 - -`SuiteResolver` 必须把 interface surface 的 declaration index 交给 visible-value resolver,以检查 scope 选中的 ordinary local 的 declaration identity 已由 baseline inventory 发布;这不是重新扫描普通 `var`,也不改变 resolver 现有 filtered-hit 规则。它还必须把 typed lexical baseline 传给 root 与 child typed environment,使参数和 ordinary local 在没有更新 overlay / stable slot fact 时仍有 immutable source-facing 初始类型。 - -Suite 收敛后,不能把 current-suite committed overlay 整体打包为单个多 owner `FrontendAnalysisPatch`。它必须构造按 owner 有序的 patch transaction。嵌套 suite 只把该 transaction 追加到同一 callable-scoped export batch,不得在 child suite 边界直接 apply;根 callable suite 收敛后才由 batch 按追加顺序 apply 各 transaction。每个 transaction 内按 top binding -> local stabilization -> chain binding -> expr typing -> optional feature-specific semantic route planning -> var type post 顺序 apply owner patch;缺少 feature-specific owner 的 statement 直接跳过该 stage。这样既保留 source-order suite 的前缀可见性,又不破坏 `FrontendAnalysisPatch` 现有单 stage / 单 owner 约束。 - -当前阶段的 transaction / batch 只表达分组、owner 顺序与推迟 stable publication,不表达原子提交。`FrontendPatchTransaction.applyTo(...)` 逐 patch 立即提交;后续 patch 失败时,先前 patch 已写入的 stable side table、`BlockScope` slot 与 binding payload 不回滚。`FrontendCallableExportBatch.applyTo(...)` 同样逐 transaction 提交,且不在 apply 前预检 queued transaction 之间的冲突。由于 child / sibling suite overlay 彼此隔离,已加入 batch 但尚未 apply 的 fact 不参与另一个 transaction 的 overlay conflict check;若错误 traversal、重复 publication 或未来 feature 使两个 transaction 对同一 key 发布不兼容 fact,冲突可能延迟到后一个 transaction apply 时才暴露。该 corner case 本阶段明确保留、不做 prepare/commit 修复;任何 apply 异常后,当前 `FrontendAnalysisData` 及其 stable scope 状态必须整体丢弃,不能继续进入 diagnostics-only phase、compile gate、lowering 或增量复用。 - -不得重排 1-6,且 feature-specific stage 只能插在 expr typing 与对应 var type post 之间。尤其是 chain binding 读取 receiver local slot 时,必须先看到 local stabilization 对前序 statement 或当前 statement 前序子过程写入的 exact slot fact;否则会重新打开 receiver 被误读成 `Variant` 的历史回归。 - -Nested chain / argument retry 只能发生在当前 owner 子过程内部的非导出 transient cache 中。当前实现由 `FrontendBodyOwnerProcedures.BodyExpressionResolver` 的 `expressionTypes` / `finalizedExpressionTypes` / `resolvedCalls` 缓存、`FrontendChainReductionFacade.reducedChains` 以及 `FrontendChainReductionHelper` 的 bounded `finalizeWindow` retry 共同承担。Retry 可读取本次 reduction 已经推导出的 receiver / argument / step 临时事实,也可读取当前 statement 之前 owner 子过程已发布到 pending / committed overlay 的事实;但 retry 产生的中间 facts 不写入 `expressionTypes()` overlay、statement pending overlay、current-suite committed overlay 或 stable side table,也不能被 `TypedLexicalEnvironment` 的普通 lookup 读取。它们在当前 owner 子过程结束时丢弃。 - -`FrontendExprTypeAnalyzer` 对同一 expression / step key 只能在完成 retry、选定最终 `FrontendExpressionType` 后写入一次 expression type overlay。若同一 key 需要先得到 `DEFERRED`、暂定 `Variant` 或其他非最终状态,再得到 exact result,必须把中间值保存在 owner-local transient cache 或专用非导出状态里,而不是发布为 `expressionTypes()` 后再 narrowing / status upgrade。 - -Feature-specific semantic route planning 若需要插入 statement owner 顺序,只能位于其 header 所需的 top binding、local stabilization、chain binding 与 expr typing 之后,并位于对应 var type post / statement flush 之前。它可以读取当前 statement pending overlay 与 current-suite committed overlay,但不能读取后续 statement facts,也不能控制 body inventory 或 child-suite entry。 - -第一版 body statement 支持面: - -- `VariableDeclaration`,仅 ordinary local `var` 与 supported property initializer。 -- `ExpressionStatement`。 -- `ReturnStatement`。 -- `AssertStatement`,保持现有 compile gate blocker。 -- `IfStatement` / `ElifClause` / `else`,header 先解析,body 后递归。 -- `WhileStatement`,condition 先解析,body 后递归。 -- `ForStatement` 由 for-range 阶段 B/D0 转为 structural supported:header-first,body 通过普通 child-suite path 进入;iteration planning 与 compile readiness 仍由后续 for-range 阶段独立处理。 -- `MatchStatement`、`LambdaExpression`、block-local `const` 继续 deferred / unsupported,除非后续阶段显式转正。 - -### 4.4 `TypedLexicalEnvironment` overlay - -新增 `FrontendTypedLexicalEnvironment`,作为 body 层所有 value/type lookup 的 effective view。它不替换 `Scope`,而是包装 `Scope` 与当前 suite 的 typed overlay。 - -这不是给现有 resolver 增加一个可选参数那么简单。当前 `FrontendVisibleValueResolver`、`FrontendChainReductionFacade`、`FrontendExpressionSemanticSupport` 与各 analyzer 内部回调都默认读取 stable `FrontendAnalysisData` / `Scope` / analyzer-local state。SuiteResolver 路线必须为这些 lookup 建立新的 effective view 入口,使 identifier binding、receiver type、argument type、call/member result 与 local slot type 读取都能先看 pending / committed overlay,再回退到完整 lexical inventory 与 stable side table。 - -读取顺序: - -1. 当前 statement pending overlay。 -2. 当前 suite committed overlay。 -3. 已发布 stable slot facts。 -4. parent typed lexical environment。 -5. interface typed baseline 提供的冻结 source-facing fallback;`BlockScope` / `CallableScope` 继续提供 lexical inventory 名称查找。 -6. class/global/singleton/type-meta lookup。 - -Pending overlay 只对当前 statement 内后续 owner 子过程可见。`flushPendingFacts()` 后,它才变成 current-suite committed overlay,并对后续 statement / feature-specific semantic route planning 可见。Committed overlay 仍不是 stable publication。 - -Parent environment 只能沿 parent 链读取上层 overlay,不能读取 child environment 的 pending 或 committed facts。Child suite 保持独立 overlay;其 facts 仅以 per-owner patch transaction 追加到 shared callable export batch,并在根 callable 完成前保持不在 stable side table 中。 - -写入规则: - -- 只有 `FrontendLocalTypeStabilizationAnalyzer` 可写 source-facing local slot overlay。 -- 只有 `FrontendTopBindingAnalyzer` 可写 binding overlay。 -- 只有 `FrontendChainBindingAnalyzer` 可写 member / chain-call overlay。 -- 只有 `FrontendExprTypeAnalyzer` 可写 expression type / bare-call overlay。 -- 只有 var type post owner 可写 source-facing slot type overlay;它包括 callable-entry 参数预发布与 statement-local local-`var` publication,两者都必须使用 `VAR_TYPE_POST` stage。 -- overlay fact 必须带 owner metadata,并在导出到 stable side table 前执行冲突检测、idempotent 检查和 compiler-only guard。 -- compiler-only guard 必须检查该 fact 可达的每个 user-visible `GdType` payload;不能只检查 `expressionTypes()` / `slotTypes()` 两个表。 -- compiler-only guard 必须在 pending overlay write 时就 fail-fast,而不是等 suite export 时才补救。任何 scratch 写入如果命中 `GdCompilerType`,必须在 write API 返回前拒绝该 fact,不能先写入 pending / committed overlay 再在 export 时回滚。 -- `expressionTypes()` overlay 只接受当前 statement 内每个 AST key 的最终 publication fact。retry 中间计算(包括首 pass 的 `DEFERRED`、暂定 `Variant`、临时 status / detailReason)必须留在 owner procedure 的非导出 transient cache,不得作为 overlay fact 写入。 -- `expressionTypes()` overlay 不提供 `Variant -> exact`、parent -> child 或 terminal status -> success 的 narrowing 例外。这不是 overlay 的局部规则,而是对 `FrontendAnalysisData.sameExpressionType` 严格判据(status + publishedType + detailReason 全等,见第 4.6 节)的直接引用。需要 narrowing 的 local slot 变化必须走 `FrontendLocalSlotTypeUpdate`,不得绕道 `expressionTypes()` republish。 - -四层事实可见性模型固定为: - -- Owner procedure transient cache:owner 子过程私有,只给当前 chain / expr reduction 的 retry 回调读取;不属于 `TypedLexicalEnvironment`,不参与 flush / export,owner 子过程结束即丢弃。 -- 当前 statement pending overlay:只给当前 statement 后续 owner 子过程读取;只接受每个 AST key 的最终 publication fact。 -- current-suite committed overlay:由 pending fact flush 合并而来,给后续 statement 与 feature-specific semantic route planning 读取;仍不是 stable publication。 -- `FrontendAnalysisData` stable side tables / `BlockScope` stable slot:只在 suite export 的 per-owner patch apply / stable export helper 后更新,供 diagnostics-only phase、compile gate 与 lowering 使用。 - -Overlay 的目标是模拟 Godot “当前 statement 已解析出的类型可被后续 statement 使用”的效果,同时避免提前污染 `FrontendAnalysisData` stable tables。 - -写入与导出时机: - -- Statement owner runner 只能写当前 statement pending overlay;callable-entry var type post 在 statement 循环前写参数 pending overlay。 -- `flushPendingFacts(...)` 只把 pending overlay 合并到 current-suite committed overlay,并执行 owner、conflict、idempotent、exact-type 与 compiler-only guard。该 guard 必须与 suite export 使用同一 type-bearing field walker,避免 scratch 层接受 stable merge 层会拒绝的 compiler-only payload。 -- `flushPendingFacts(...)` 不得通过 stable side table 的临时 whole-table publish 再“读回 overlay”。如果当前实现为了复用旧 analyzer helper 需要 `updateXxx(...)` / stable side table 作为中转,则该 helper 必须先被改写或隔离为 legacy path,不能把中转污染当作阶段性可接受状态。 -- Suite 收敛时,current-suite committed overlay 只能导出为按 owner 有序的 patch transaction,不能导出为一个跨 owner `FrontendAnalysisPatch`。 -- 嵌套 suite 的 transaction 必须追加到 root callable 共享的 `FrontendCallableExportBatch`,不得在 child suite 收敛时独立 apply。根 callable suite 返回后,batch 才按追加顺序逐个 apply 已累积的 transaction;该顺序提交不构成原子批次,也不提供跨 queued transaction 预检或失败回滚。 -- 每个 patch transaction 固定按 top binding -> local stabilization -> chain binding -> expr typing -> optional feature-specific semantic route planning -> var type post apply;每个 step 只包含该 owner 的 facts。 -- Stable side table 与 `BlockScope.resetLocalType(...)` 只能在 callable export batch 的 per-owner patch apply / stable export helper 中更新。 -- Diagnostics-only phase、compile gate 与 lowering 只能读取 suite export 后的 stable facts。 -- Nested supported suite 收敛后,必须把其 per-owner patch transaction 追加到外层 callable export batch;lexical visibility 仍由 scope graph 与 resolver filter 决定,不能因为 transaction 合并放宽 local 可见性。 - -### 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 是双向的——每个 index entry 必须能回到 scope/baseline, - 且 body `BlockScope` 中每条已发布 `LOCAL` binding 必须出现在 declaration index 中;index 的 - `sourceOrder` 必须从 0 连续,并与 AST start-byte 非降序一致;`FOR_BODY` 还必须恰好包含一个以 - `ForStatement` 为 identity 的 iterator entry,且该 entry 位于 inventory 列表头部、`sourceOrder == 0`。 - -Policy 不读取 expression type、typed overlay、iteration plan、diagnostic state 或 compile surface; -certificate 不读取 pending/committed overlay、slot refinement 或 semantic/lowering route。Typed fact 只能 -驱动 refinement、type diagnostic 与 route planning,不能改变 body inventory 或 child-suite dispatch。 - -未转正的 lambda、match、block-local `const`、parameter default 与 unknown/skipped structure 由 policy、 -AST boundary 和 current-scope backstop 直接 fail-closed,不存在 body lifecycle 或 publication setter。 - -### 4.6 Stable export 与 per-owner patch transaction - -`TypedLexicalEnvironment` 不是 public publication。只有导出并通过 stable merge 后,事实才成为 diagnostics-only phase、compile gate 和 lowering 可消费的事实。 - -新增 `gd.script.gdcc.frontend.sema.patch` 包承载 patch 相关类型: - -- `FrontendOwnerPatch` 或等价 sealed interface,表达“单一 semantic owner 的 publication delta”。这里的 owner 是 analyzer / publication owner,不是 `FrontendResolvedMember.ownerKind()` / `FrontendResolvedCall.ownerKind()` 中的 `ScopeOwnerKind`。 -- `FrontendTopBindingPatch`:只包含 `symbolBindings()` delta。 -- `FrontendLocalTypeStabilizationPatch`:只包含 `FrontendLocalSlotTypeUpdate` delta,不直接携带刷新后的 `symbolBindings()` entry。 -- `FrontendChainBindingPatch`:只包含 `resolvedMembers()` 与 chain-owned `resolvedCalls()` delta。 -- `FrontendExprTypePatch`:只包含 `expressionTypes()` 与 bare-call `resolvedCalls()` delta。 -- `FrontendVarTypePostPatch`:只包含 `slotTypes()` delta。 -- `FrontendPatchTransaction` 或等价 ordered patch carrier,保存上述 patch 的有序列表并负责按固定 owner 顺序 apply;`Transaction` 在这里不表示原子提交,类型不提供跨 patch 预检或失败回滚。 -- 旧 `FrontendAnalysisPatch`、`FrontendLocalSlotTypeUpdate` 迁入同一 package。旧 `FrontendAnalysisPatch` 只能作为 legacy single-stage patch / 测试兼容层或被拆解;suite export 生产路径不得再用它承载多 owner facts。`FrontendWindowPublicationSurface`、`FrontendWindowAnalysisContext` 若迁入该包,必须单独标记为 legacy shim,不属于 production overlay/export 参考资产。 - -保留 `FrontendLocalSlotTypeUpdate` / `FrontendAnalysisData.applyPatch(...)` 的核心规则,但 `applyPatch` 的输入必须是单一 owner patch。以下规则适用于每个 per-owner patch 的 merge: - -- 新 key 直接写入 stable side table。 -- 旧 key + 相同 value 视为 idempotent,允许。 -- 旧 key + 不同 value 默认 fail-fast。 -- `symbolBindings()` 允许因 `FrontendLocalSlotTypeUpdate` 的 commit helper 刷新同一 declaration 的 `resolvedValue` payload,但 local stabilization patch 本身不得携带独立的 `symbolBindings()` delta。刷新必须由 `BlockScope.resetLocalType(...)` 与 binding payload refresh 的同一个 helper 派生完成,并保持 binding kind、name、declaration identity 不变。 -- `resolvedCalls()` 中 chain-owned call 与 bare-call 由 semantic owner patch 与 key-space contract 区分,不能相互覆盖;不得把 `ScopeOwnerKind` 当成 semantic publication owner。 -- `expressionTypes()` 的同 key republish 只有 `FrontendAnalysisData.sameExpressionType(...)` 判定为同值时允许;stable 判据保持 status + publishedType + detailReason 严格相等。 -- `expressionTypes()` 不允许同一 key 出现 status change、same-status + different publishedType、same-status + different detailReason,也不允许从发布 `Variant` 的 fact 变成 exact `int` fact。 -- `FrontendExprTypePatch.expressionTypes()` 必须是 expr typing owner overlay 收敛后的 per-key final view。Patch 内同一 key 不得出现不同 logical value,suite export 也不得把 retry 中间事实导出为 stable fact 后再 narrowing / status upgrade。 -- `slotTypes()` 不允许同一 source slot 被不同类型覆盖;同类型 idempotent 允许。 -- merge 前统一检查 compiler-only type 不泄漏。当前 `FrontendAnalysisData.checkPatchDoesNotLeakCompilerOnlyTypes(...)` 只覆盖 `expressionTypes()` 与 `slotTypes()`,阶段 C 必须把该检查扩展到 `symbolBindings().resolvedValue.type()`、`resolvedMembers()` 的 `receiverType` / `resultType`、`resolvedCalls()` 的 `receiverType` / `returnType` / `argumentTypes` / callable boundary parameter types。否则不能宣称 overlay export 已复用完整 compiler-only guard。 - -上述 conflict、idempotent、local-slot 与 compiler-only 校验只约束当前 per-owner patch。`FrontendAnalysisData.applyPatch(...)` 在当前 patch 的 merge 前完成这些检查,但连续调用多个 patch 不会形成新的原子边界;fail-fast 只中止当前 apply 链路,不撤销同一 transaction 内已提交的 patch,也不撤销同一 callable batch 内已提交的 transaction。 - -新增或扩展统一 helper,例如 `checkNoCompilerOnlyLeakInPublishedFact(...)` / `FrontendPublishedFactTypeGuard`,用于 patch commit 与 typed overlay 写入两处。它必须遍历 `FrontendBinding`、`FrontendResolvedMember`、`FrontendResolvedCall`、`FrontendExpressionType`、`slotTypes()` value 与 `FrontendLocalSlotTypeUpdate` 中所有 user-visible `GdType`,并复用同一错误策略。Legacy window publication surface 不能作为该 helper 的正确性基线。 - -阶段 C 实施前,至少要明确并锁定下面的 guard 扩展 checklist: - -- `FrontendAnalysisData.checkPatchDoesNotLeakCompilerOnlyTypes(...)` 从只看 expression/slot 扩展为调用 shared walker,覆盖 binding/member/call payload。 -- `FrontendWindowPublicationSurface.WindowSideTableView` 中 `symbolBindings()`、`resolvedMembers()`、`resolvedCalls()` 若仍保留,必须接入同一 walker;不能继续用 `null` guard 只保护 expression/slot。 -- `FrontendAnalysisData.updateSymbolBindings(...)`、`updateResolvedMembers(...)`、`updateResolvedCalls(...)`、`updateExpressionTypes(...)`、`updateSlotTypes(...)` 若还保留 source-facing publication 语义,必须先经过 shared walker;否则文档上必须把它们限定为 legacy whole-table path,并从 production SuiteResolver path 排除。 -- `refreshPublishedLocalBindingPayloads(...)` 只是一条 local slot commit helper 派生路径,不能被误写成“symbolBindings 已完整受保护”的证明。 -- 任何仍暴露可变 stable 引用的 API,都只能依赖调用方 contract 保证“不直接写 stable table”;计划的完成标准必须通过 rewrite/inspection/tests 确保 production body path 不再走这些 bypass。 - -可调整的是通信路径:body layer 可以先写 `TypedLexicalEnvironment` overlay,再在 suite / callable / module 边界导出 per-owner patch transaction。不得用 `updateXxx(...)` 表达部分提交,也不得用一个 single-stage patch 表达多 owner suite export。 - -这意味着 Stage E 的 “nested chain / argument retry 读己写” 不是 stable side-table rewrite 能力。Retry 的读己写发生在 chain/expr owner 的 procedure-local transient cache 与当前 statement effective view 中;stable `expressionTypes()` 仍只看到每个 key 的最终一次 publication。 - -### 4.7 Scope slot mutation - -`BlockScope.resetLocalType(...)` 不是 side-table 写入,但它会影响后续 resolver 和 binding payload。新计划必须把这类 mutation 建模为 owner-controlled slot update。 - -`FrontendLocalSlotTypeUpdate` 至少记录: - -```java -record FrontendLocalSlotTypeUpdate( - @NotNull BlockScope scope, - @NotNull String name, - @NotNull Object declaration, - @NotNull GdType type -) {} -``` - -`FrontendLocalSlotTypeUpdate` 迁入 `gd.script.gdcc.frontend.sema.patch` 包,并由 `FrontendLocalTypeStabilizationPatch` 携带。它仍然是 local stabilization owner 的专用 carrier,不得被 expr typing 或 var type post patch 复用。 - -应用规则: - -- `FrontendLocalTypeStabilizationAnalyzer` 是唯一允许产生 source-facing `FrontendLocalSlotTypeUpdate` 的 analyzer。 -- 只允许 `Variant -> exact` 或 exact same-type no-op。 -- 不允许 exact A -> exact B。 -- 不允许写入 `GdVoidType`。 -- 不允许写入 `GdCompilerType` 到 source-facing local。 -- 应用后必须刷新已发布且指向同一 declaration 的 `symbolBindings()` payload。 - -Overlay 可在导出前提供 effective type,但最终 stable `BlockScope.resetLocalType(...)` 和 binding payload refresh 仍必须走同一个 commit helper,避免出现第二条 slot mutation side channel。 - -`FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 必须继续保持 guard-only: - -- 不得调用 `BlockScope.resetLocalType(...)`。 -- 不得刷新 `symbolBindings()` payload。 -- 不得产生 `FrontendLocalSlotTypeUpdate`。 -- 已稳定同类型 no-op,已稳定异类型 fail-fast。 -- compiler-only initializer fact fail-fast。 - -### 4.8 Resolver 复用规则 - -`FrontendVisibleValueResolver` 继续一次性索引完整 source AST,并继续读取已经发布的完整 lexical inventory。完整 inventory 是 `DECLARATION_AFTER_USE_SITE` filtered hit 的前提条件;重构时不得让 resolver 自己扫描 AST 补找普通 `var`,也不得把 supported block inventory 缩成只包含当前 source-order 前缀的 incremental view。 - -新增 resolver 能力: - -- 接受 `TypedLexicalEnvironment` 或等价 effective lexical view,用于读取当前 statement / current suite 的 typed overlay。 -- 继续通过 declaration-order filter 处理 future local 与 initializer self-reference。 -- request-domain hard boundary、AST boundary 与 current-scope backstop 三道结构检查必须保留。 -- `FOR_BODY` 与 for header edge 直接允许 ordinary lookup;lambda、match、block-local `const`、parameter - default 与 unknown/skipped structure 使用 structural policy 的精确 deferred domain。 - -三道结构检查的规则: - -1. Request-domain hard boundary:`EXECUTABLE_BODY` 才进入 ordinary lookup;其它 domain 直接返回 - `DEFERRED_UNSUPPORTED`,不做 readiness normalization。 -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 即使缺少 AST - edge 也继续 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。 -- 当前 statement pending slot fact 可被同一 statement 后续 owner 子过程消费,但不能让 `var x := x` 的右侧 `x` 通过 self-reference 过滤。 -- 跨 statement committed fact 可被后续 statement 消费,但 future declaration 仍必须报告 `DECLARATION_AFTER_USE_SITE` filtered hit。 -- Feature-specific header planning 只能读取 header use-site 当前可见的 overlay fact,不能扫描后续 - suite statement,也不能以 typed result 改写 body entry。 - -### 4.9 已实施资产分类 - -允许代码回退或重新开始实施,但已有资产必须按类别处理。分类标准必须区分“可保留的语义规则 / helper / 测试”与“不可复用的 whole-module traversal 入口”。 - -保留资产: - -- `FrontendSemanticStage` -- `FrontendAnalysisData.applyPatch(...)`,但要泛化为消费 `FrontendOwnerPatch` 或等价 per-owner patch carrier。 -- `FrontendAnalysisData.refreshPublishedLocalBindingPayloads(...)` -- `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 的 guard-only 收口 -- `FrontendAnalysisDataTest` 中关于 stable reference、conflict、idempotent、compiler-only guard 的测试 -- analyzer 内部可抽取的纯语义 helper,如 binding 分类、type compatibility、chain reduction 与 expression support 的无遍历部分;只能在去除 whole-module AST walker 依赖后复用 - -新增资产: - -- `gd.script.gdcc.frontend.sema.patch` 包。 -- `FrontendOwnerPatch` 或等价 sealed interface。 -- `FrontendPatchTransaction` 或等价有序 transaction 类型。 -- `FrontendCallableExportBatch`,累积单个 callable root 及其 nested suite 的 transaction,并只在 root suite 收敛后按追加顺序逐个 apply;当前不做跨 queued transaction 预检,也不提供 batch 原子性或回滚。 -- `FrontendTopBindingPatch`、`FrontendLocalTypeStabilizationPatch`、`FrontendChainBindingPatch`、`FrontendExprTypePatch`、`FrontendVarTypePostPatch` 等 per-owner patch carrier。 -- patch package 内的 shared merge helper / type-bearing compiler-only guard / owner-field validator。 -- `FrontendSuiteResolver` / `FrontendStatementResolver` 的 root-bounded statement dispatch 子框架。 -- statement-local owner procedure interface 或等价显式调用约定,接收 `FrontendSuiteContext`、当前 statement root 与 `FrontendTypedLexicalEnvironment`。 -- 每个 owner 的显式上下文状态 record / stack,例如 restriction、static context、property initializer context、block scope stack、diagnostic suppression state、procedure-local transient retry cache。 - -移动 / 兼容资产: - -- `FrontendAnalysisPatch` 迁入 patch package;若保留,它只能是 legacy single-stage patch / 测试兼容层,不能作为 suite export 生产路径的 multi-owner carrier。 -- `FrontendLocalSlotTypeUpdate` 迁入 patch package,并只由 `FrontendLocalTypeStabilizationPatch` 携带。 -- `FrontendWindowPublicationSurface` 与 `FrontendWindowAnalysisContext` 若迁入 patch package,也只能作为 legacy comparison shim。它们不再是 overlay/export 参考实现;若最终删除这两个类型,必须把仍然有效的 surface API tests 拆到 overlay 与 per-owner patch transaction 测试,并新增 VarTypePost window contamination regression test 记录旧路径问题。 -- 已删除的 `FrontendSemanticAnalyzer.analyzeWithLegacySharedSemanticPublication(...)` package-private 测试旁路。阶段 I 后不再通过 shared analyzer 进入 legacy whole-phase baseline;需要 owner-level legacy 行为时只能在 focused legacy shim tests 中直接使用对应 analyzer/window API。 - -重写参考资产: - -- `FrontendTopBindingAnalyzer.AstWalkerTopBindingBinder`、`FrontendLocalTypeStabilizationAnalyzer.AstWalkerLocalTypeStabilizer`、`FrontendChainBindingAnalyzer.AstWalkerChainBinder`、`FrontendExprTypeAnalyzer.AstWalkerExprTypePublisher`、`FrontendVarTypePostAnalyzer.SlotTypePublisher` 等内部 walker 只能作为语义规则参考。它们的 whole-module `walk(sourceFile)` 入口、隐式字段状态与整表发布策略不得作为 production SuiteResolver procedure 复用。 -- 各 analyzer 的 `analyzeInWindow(...)` 方法名称具有误导性:它只改变 publication surface,不改变 traversal root。它可以保留为 legacy / comparison test path,但不能被归类为可迁移 runner。 -- `FrontendVarTypePostAnalyzer.analyzeInWindow(...)` 更不能作为参考:它直接清空并写入 stable `slotTypes()`,再事后复制到 window scratch。新 var type post procedure 必须从这个实现重新写起,而不是修饰调用路径。 -- `FrontendChainReductionFacade` 与 `FrontendExpressionSemanticSupport` 可保留算法价值,但它们当前通过 analyzer-local 回调读取 dependency type;SuiteResolver 下必须改为从 `FrontendTypedLexicalEnvironment` 与 owner-local transient cache 读取。 - -可回退或废弃资产: - -- 已删除的 `FrontendSegmentedSemanticScheduler` 过渡实现。阶段 I 后它不再作为代码资产存在。 -- 已删除的 `FrontendSemanticAnalyzer.segmentedSemanticRunner` 生产路径开关与 `analyzeWithLegacySharedSemanticPublication(...)` 测试旁路。默认入口只运行 interface/body pipeline。 -- 把 `analyzeInWindow(...)` 作为 production body procedure 的任何调用路径。 - -## 5. 分步骤实施 - -### 阶段 A:资产盘点与回退边界冻结 - -实施内容: - -- 明确阶段 A-D 已实施代码的处理方式:保留、移动、重写参考、废弃或回退。 -- 将 `FrontendSegmentedSemanticScheduler` 标记为过渡实现并在最终阶段删除,不再作为新路线的主体。 -- 保留 `FrontendAnalysisPatch`、`FrontendLocalSlotTypeUpdate`、`FrontendAnalysisData.applyPatch(...)` 的测试价值。`FrontendWindowPublicationSurface` 只保留 API-level / legacy comparison 测试价值,不能作为 overlay 隔离参考;patch 相关类型是否迁入 `gd.script.gdcc.frontend.sema.patch` 包需与其 legacy shim 定位一致。 -- 明确 per-owner patch 类型与 `FrontendPatchTransaction` 命名方案,旧 `FrontendAnalysisPatch` 不再作为 suite export 生产路径的 multi-owner carrier。 -- 明确 `FrontendSemanticAnalyzer.segmentedSemanticRunner` 生产路径开关最终要移除;阶段 I 后该开关及 shared analyzer legacy bypass 均已删除。 -- 明确 `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` guard-only 合同不可回退。 -- 为五个 owner analyzer 建立 walker-state inventory:列出当前内部 visitor 的 traversal root、隐式字段状态、读取的 stable side table、写入的 side table、diagnostic emission 与可抽取 helper。该 inventory 是重写输入,不是迁移完成标志。 -- 为 compiler-only guard 建立 payload matrix:逐项记录 `expressionTypes()`、`slotTypes()`、`localSlotTypeUpdates()`、`symbolBindings()`、`resolvedMembers()`、`resolvedCalls()` 当前由谁检查、谁未检查、哪些 API 仍可直接绕过 guard。该 matrix 必须与阶段 C shared walker 设计一起冻结。 - -当前状态(2026-07-09): - -- [x] A1 在文档中完成资产分类。 -- [x] A2 在代码中将 `FrontendSegmentedSemanticScheduler` 标记为 deprecated 或 legacy comparison entry。 -- [x] A3 保留 `FrontendAnalysisDataTest`,并将 `FrontendWindowPublicationSurfaceTest` 降级为 API-level / legacy contamination regression 测试,不再作为新 overlay 正确性的参考测试。 -- [x] A4 移除或隔离 `segmentedSemanticRunner` 对生产路径的影响。 -- [x] A5 确定 patch package 迁移计划、per-owner patch 命名与 transaction apply 顺序。 -- [x] A6 完成 whole-module walker state inventory,并标记 `analyzeInWindow(...)` 只允许作为 legacy comparison path。 -- [x] A7 记录 `FrontendVarTypePostAnalyzer.analyzeInWindow(...)` 的 stable `slotTypes()` 污染路径,并决定修复、隔离或删除该 legacy path。 -- [x] A8 完成 compiler-only guard payload matrix,并明确 `updateXxx(...)` / direct stable side-table 引用哪些是 legacy-only,哪些必须在后续阶段接入 shared walker。 - -阶段 A 完成记录: - -- A1:第 4.9 节的资产分类冻结为阶段 A 基线。`FrontendSemanticStage`、`FrontendAnalysisData.applyPatch(...)`、`refreshPublishedLocalBindingPayloads(...)`、backfill guard 与 `FrontendAnalysisDataTest` 保留;patch carrier 与 transaction 类型进入 `gd.script.gdcc.frontend.sema.patch` 迁移计划;现有 analyzer walker 只作为语义 helper / rewrite reference;`FrontendSegmentedSemanticScheduler`、`segmentedSemanticRunner` 和 production `analyzeInWindow(...)` 调用路径归类为可回退或废弃资产。 -- A2/I2:`FrontendSegmentedSemanticScheduler` 在阶段 A 被标记为 `@Deprecated` legacy comparison entry;阶段 I 已删除该过渡实现,SuiteResolver 成为唯一 shared body publication path。 -- A3:`FrontendAnalysisDataTest` 继续作为 stable reference、conflict、idempotent 与 compiler-only guard 的 focused tests。`FrontendWindowPublicationSurfaceTest` 的类级注释已降级其解释范围:它只证明 legacy shim API 的 scratch write 隔离与 guard,不证明所有 legacy `analyzeInWindow(...)` 都 scratch-safe。 -- A4/H3/I1:阶段 A 先隔离 `segmentedSemanticRunner` 的生产影响;阶段 H 已删除该字段、构造参数和 `withSegmentedSemanticRunnerForTesting()` 工厂;阶段 I 已删除 shared analyzer 的 package-private legacy whole-phase bypass 与 test-source bridge。 -- A5:patch package 迁移计划冻结为:新建 `gd.script.gdcc.frontend.sema.patch`;以 `FrontendOwnerPatch` 或等价 sealed interface 作为单 owner patch 根类型;新增 `FrontendTopBindingPatch`、`FrontendLocalTypeStabilizationPatch`、`FrontendChainBindingPatch`、`FrontendExprTypePatch`、`FrontendVarTypePostPatch`;新增 `FrontendPatchTransaction` 按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 顺序 apply。旧 `FrontendAnalysisPatch` 若迁移则只能作为 legacy single-stage compatibility carrier,不能继续作为 SuiteResolver export 的 multi-owner carrier;`FrontendLocalSlotTypeUpdate` 随 local stabilization patch 迁入 patch 包。 -- A6:whole-module walker state inventory 见下表。所有当前 `analyzeInWindow(...)` 都只允许作为 legacy comparison path;其 whole-module traversal root、隐式 visitor 字段和整表发布策略都不得直接复用为 SuiteResolver statement-local owner procedure。 -- A7:`FrontendVarTypePostAnalyzer.analyzeInWindow(...)` 的 legacy 污染路径为:读取 `analysisData.slotTypes()` 稳定表 -> `clear()` -> 将稳定表传入 `SlotTypePublisher` 逐项写入 -> 再复制到 `window.publications().slotTypes()`。阶段 A 决定是隔离而非修补该 legacy path:保留测试比较价值,但新 var type post procedure 必须从 statement-local scratch/overlay 写入重新实现。 -- A8:compiler-only guard payload matrix 见下表。阶段 C 的 shared type-bearing field walker 必须成为 overlay write、patch merge、overlay flush 与任何保留 source-facing `updateXxx(...)` 的共同 guard 基线;不能先写 overlay / stable 再在 export 时补查。 - -Whole-module walker state inventory: - -| Owner analyzer | 当前 traversal root | 隐式 walker state | 读取的 stable side table / state | 写入 side table / patch payload | Diagnostics owner | 可抽取 helper / 不可复用部分 | -| --- | --- | --- | --- | --- | --- | --- | -| `FrontendTopBindingAnalyzer` | 每个 `FrontendSourceClassRelation.unit().ast()` 的 `SourceFile` whole-module walk | `sourcePath`、`moduleSkeleton`、`visibleValueResolver`、`classRegistry`、`reportedUnsupportedRoots`、`ASTWalker` | `moduleSkeleton()`、`scopesByAst()`、parse/skeleton diagnostics snapshot、class registry | `symbolBindings()` | `sema.binding` 与 unsupported binding routes | binding 分类与 visible resolver provenance 可抽取;whole-module walk 和整表 `updateSymbolBindings(...)` 不可作为 SuiteResolver procedure | -| `FrontendLocalTypeStabilizationAnalyzer` | 每个 source file whole-module walk;按 visitor 状态维护 source-order probe | `SilentExpressionResolver`、`probes`、`writeBackStableSlots`、可选 `FrontendWindowAnalysisContext`、probe-local expression memo | `scopesByAst()`、`symbolBindings()`、`resolvedMembers()` / `resolvedCalls()` / `expressionTypes()` 已发布事实、当前 scope slot state | legacy path 直接更新 local `ScopeValue.type()`;window path 产生 `FrontendLocalSlotTypeUpdate` | 不拥有 source-facing diagnostics;只在写回边界 fail-fast 拒绝非法 slot type | initializer probe、compatibility 与 slot rewrite guard 可抽取;whole-module delayed probe 收集和直接 stable scope mutation 不可复用 | -| `FrontendChainBindingAnalyzer` | 每个 source file whole-module walk | `chainReduction`、assignment / expression semantic support、`reportedDeferredRoots`、`reportedUnsupportedRoots`、`ASTWalker` | `scopesByAst()`、`symbolBindings()`、stable slot types、已发布 member/call facts、class registry | `resolvedMembers()`、chain-owned `resolvedCalls()` | `sema.member_resolution`、`sema.call_resolution`、deferred / unsupported routes | chain reduction 与 call/member classification 可抽取;通过 analyzer-local callback 读取 dependency type 与 whole-module transient retry cache 不可复用 | -| `FrontendExprTypeAnalyzer` | 每个 source file whole-module walk | `chainReduction`、assignment / expression semantic support、`parentByNode`、reported expression/deferred/unsupported/discarded roots | `scopesByAst()`、`symbolBindings()`、`resolvedMembers()`、`resolvedCalls()`、slot types、class registry | `expressionTypes()`、bare-call `resolvedCalls()`;`backfillInferredLocalType(...)` 只能 guard-only | `sema.expression_resolution`、`sema.deferred_expression_resolution`、`sema.unsupported_expression_route`、`sema.discarded_expression` | expression support、type compatibility 与 discarded-expression rules 可抽取;backfill 不得重新成为 slot mutation owner,whole-module expression walk 不可复用 | -| `FrontendVarTypePostAnalyzer` | 每个 source file whole-module walk | `sourcePath`、`blockScopeStack`、`currentCallableOwner`、`supportedExecutableBlockDepth`、`ASTWalker` | `moduleSkeleton()`、`scopesByAst()`、callable/block scope inventory、current scope slot types | `slotTypes()` final callable-local snapshot | `sema.variable_slot_publication` fail-fast / invariant violations | slot publication eligibility rules 可抽取;legacy `analyzeInWindow(...)` 的 stable `slotTypes()` clear/write 与 whole-table snapshot 不可复用 | - -Compiler-only guard payload matrix: - -| Publication surface | User-visible type-bearing payload | 当前 guard 覆盖 | 当前绕过点 / legacy-only API | 后续 shared walker 要求 | -| --- | --- | --- | --- | --- | -| `expressionTypes()` | `FrontendExpressionType.publishedType()` | 阶段 C 起由 `FrontendPublishedFactTypeGuard` 统一覆盖 overlay write / flush / export、`applyPatch(...)`、legacy window put 与 `updateExpressionTypes(...)` | direct stable table mutation 仍是 legacy-only 可变引用 bypass,production SuiteResolver path 不得使用 | 新增 typed overlay 与 patch transaction 测试锚定 same walker 覆盖 | -| `slotTypes()` | local / parameter / iterator slot `GdType` value | 阶段 C 起由 `FrontendPublishedFactTypeGuard` 覆盖 overlay write / flush / export、owner patch、legacy patch、window put 与 `updateSlotTypes(...)`;local slot update 仍额外拒绝 void | `analysisData.slotTypes().put/clear` direct mutation 与 VarTypePost legacy contamination path 是 legacy-only bypass | VarTypePost 新 procedure 后续只能写 overlay,不得复用旧 `analyzeInWindow(...)` contamination path | -| `localSlotTypeUpdates()` | `FrontendLocalSlotTypeUpdate.type()` | 阶段 C 起迁入 `gd.script.gdcc.frontend.sema.patch`,由 `FrontendPublishedFactTypeGuard` 覆盖 overlay / owner patch / legacy patch;`FrontendAnalysisData` 继续负责 void / exact rewrite / declaration conflict | legacy `FrontendAnalysisPatch` 仍保留为 compatibility carrier,但 production suite export 使用 `FrontendLocalTypeStabilizationPatch` | `FrontendLocalTypeStabilizationPatch` 独占携带,并在 transaction apply 前后复用 shared guard | -| `symbolBindings()` | `FrontendBinding.resolvedValue().type()` | 阶段 C 起由 `FrontendPublishedFactTypeGuard` 覆盖 overlay write / flush / export、owner patch、legacy patch、window put 与 `updateSymbolBindings(...)`;local slot refresh 仍有额外 payload guard | `analysisData.symbolBindings().put` direct mutation 是 legacy-only 可变引用 bypass,production SuiteResolver path 不得使用 | `FrontendTopBindingPatch` 是 production suite export 的唯一 top-binding carrier | -| `resolvedMembers()` | `FrontendResolvedMember.receiverType()` / `resultType()` | 阶段 C 起由 `FrontendPublishedFactTypeGuard` 覆盖 overlay write / flush / export、owner patch、legacy patch、window put 与 `updateResolvedMembers(...)` | direct stable table mutation 仍是 legacy-only 可变引用 bypass,production SuiteResolver path 不得使用 | `FrontendChainBindingPatch` 是 production suite export 的 member carrier | -| `resolvedCalls()` | `FrontendResolvedCall.receiverType()` / `returnType()` / `argumentTypes()` / callable boundary parameter types | 阶段 C 起由 `FrontendPublishedFactTypeGuard` 覆盖 overlay write / flush / export、owner patch、legacy patch、window put 与 `updateResolvedCalls(...)` | direct stable table mutation 仍是 legacy-only 可变引用 bypass,production SuiteResolver path 不得使用 | chain-owned call 与 bare-call 由 `FrontendChainBindingPatch` / `FrontendExprTypePatch` 分 owner 携带 | - -验收细则: - -- 已实施 A-D 资产在文档中均有归类。 -- 所有 patch 相关类型都被归类为保留、移动、新增或可删除资产。 -- 不执行 destructive git 回退也能清晰说明哪些代码可删除,哪些代码需保留。 -- 现有 patch/backfill guard focused tests 继续通过;surface tests 不再被解释为“所有 analyzer window path 都 scratch-safe”。 -- 没有任何阶段把 `analyzeInWindow(...)` 声明为 production SuiteResolver procedure。 -- 文档明确 `FrontendWindowPublicationSurface` 的 API scratch contract 与 VarTypePost legacy 调用行为不一致。 -- 文档明确 shared walker 是 overlay write、patch merge 与保留 whole-table publish 的共同 guard 基线,不存在“先写 overlay / stable,export 时再补查”的宽松解释。 - -### 阶段 B:建立 Interface phase 数据结构 - -状态同步(2026-07-07): - -- [x] B1 新增 `FrontendInterfacePhase` coordinator;它在 skeleton / scope / variable inventory 之后构建独立 `FrontendInterfaceSurface`,不写入 stable typed side table,也不改变 legacy analyzer 顺序。 -- [x] B2 新增 `FrontendBodyDeclarationIndex`,按 supported body root 记录已发布 ordinary local declaration 与 body-local source order;该 index 是 baseline inventory 的只读 view,不重新发布 local。 -- [x] B3 早期 synthetic typed-dependent registry 试验已完成并在阶段 L 物理删除;最终 `FrontendInterfaceSurface` 不持有 lifecycle registry。 -- [x] B4 新增 `FrontendTypedLexicalBaseline`,记录 parameter / ordinary local source-facing slot baseline,并在写入时 fail-fast 拒绝 `GdCompilerType`。 -- [x] B5 新增 `FrontendSuiteEntryRoots`,列出 body layer 可进入的 callable / property initializer / structural supported block roots;entry 不依赖 typed fact。 -- [x] B6 保持 skeleton / scope / variable analyzer public contract 不变;新增 `FrontendInterfacePhaseTest` 只调用已发布基础结构层并断言 Interface phase 不写 typed stable side table,现有 phase-boundary / variable inventory focused tests 继续作为回归锚点。 - -实施内容: - -- 新增 `FrontendInterfacePhase` 或等价 coordinator。 -- 新增 `FrontendBodyDeclarationIndex`,按 callable/block 记录完整 ordinary local declaration 与 source order。 -- 使用 `FrontendBodySemanticSupportPolicy` 表达结构支持面,并由 `FrontendBodyStructuralCompleteness` 验证本次 interface surface 的 scope、suite entry、declaration index 与 baseline 完整性。 -- 新增 `FrontendTypedLexicalBaseline`,记录 interface 层可确定的 source-facing typed baseline。 -- 新增 `FrontendSuiteEntryRoots`,列出 body layer 可进入的 callable/property initializer/supported block roots。 -- 保持 skeleton/scope/variable analyzer 的 public contract 不变。 -- 阶段 B 的 `FrontendInterfaceSurface` 暂时仍是可手动构建的数据结构,不是 `FrontendSemanticAnalyzer.analyze()` 主 pipeline 的真实产物;阶段 D 引入 `SuiteResolver` 时必须补齐主流程集成。 - -验收细则: - -- `FrontendSemanticAnalyzerFrameworkTest` 继续证明 skeleton/scope/variable phase boundary 未漂移。 -- `FrontendVariableAnalyzerTest` 继续证明 supported block 的完整 local inventory 先发布。 -- `var x := y; var y := 1` 仍能通过 resolver 看到 future declaration 并过滤为 `DECLARATION_AFTER_USE_SITE`。 -- `for` 已通过后续阶段 B/D0 与 L 系列转正;`match` / lambda / block-local `const` 继续 structural deferred / unsupported。 - -### 阶段 C:引入 `TypedLexicalEnvironment` overlay - -状态同步(2026-07-07): - -- [x] C1 新增 `FrontendTypedLexicalEnvironment`,包装当前 `Scope`、stable `FrontendAnalysisData`、parent environment、statement pending overlay 与 current-suite committed overlay;pending write / flush / export 均不提前修改 stable side table 或 `BlockScope`。 -- [x] C2 新增 `gd.script.gdcc.frontend.sema.patch` 包,迁入 legacy `FrontendAnalysisPatch` / `FrontendLocalSlotTypeUpdate`,并新增 `FrontendOwnerPatch`、`FrontendTopBindingPatch`、`FrontendLocalTypeStabilizationPatch`、`FrontendChainBindingPatch`、`FrontendExprTypePatch`、`FrontendVarTypePostPatch` 与 `FrontendPatchTransaction`。 -- [x] C3 新增 `FrontendPublishedFactTypeGuard` shared walker,覆盖 binding/member/call/expression/slot/update 六类 source-facing typed payload;`FrontendAnalysisData.applyPatch(...)`、owner patch、legacy window scratch put 与保留的 `updateXxx(...)` whole-table publish 均接入该 guard。 -- [x] C4 Overlay 写入 API 必须显式传入 `FrontendSemanticStage` owner metadata;错误 owner fail-fast,local slot overlay 继续执行 `Variant -> exact` / exact-same-only / no-void / no-compiler-only 规则。 -- [x] C5 `flushPendingFacts()` 只把 pending overlay 合并到 committed overlay;`exportPatchTransaction()` 只导出 fixed-order per-owner patch transaction,并由 `FrontendPatchTransaction` 拒绝 owner 顺序回退或重复 owner。 -- [x] C6 移除未接入生产路径的 `FrontendOwnerRetryMemo`,并把合同明确为 owner procedure 内部非导出 transient cache:`BodyExpressionResolver` 双 expression cache / call cache、`FrontendChainReductionFacade.reducedChains` 与 helper bounded retry 均不属于 typed lexical environment,不参与 flush / export。 -- [x] C7 `FrontendVisibleValueResolver` 新增 overlay-aware `resolve(request, environment)` overload;它只在 declaration-order / self-reference filter 之后替换 returned local 的 effective type,不绕过 future-local filtered hit。 -- [ ] C8 Expression semantic support 与 chain reduction facade 的 dependency-type callback 尚未替换为 explicit environment lookup;这是阶段 E owner procedure 重写的一部分,本阶段只提供可接入入口。 - -实施内容: - -- 新增 `FrontendTypedLexicalEnvironment`,包装 `Scope`、suite-local typed facts、pending local slot updates。 -- 为 visible value resolver、expression semantic support、chain reduction facade 提供 effective local type 读取入口;现有 stable-data / analyzer-local callback 读取路径必须逐步替换为 explicit environment lookup。 -- Overlay 写入必须带 owner metadata。 -- Overlay export 必须先按 owner 拆成 per-owner patch,再复用 `FrontendAnalysisData.applyPatch(...)` 的冲突检测;compiler-only guard 必须先扩展为第 4.6 节的全 type-bearing fact guard 后才能复用。 -- 为 overlay scratch 写入补同等 compiler-only guard,覆盖 `symbolBindings()`、`resolvedMembers()`、`resolvedCalls()`、`expressionTypes()`、`slotTypes()` 与 `localSlotTypeUpdates()`。 -- Overlay write / flush / export 必须复用同一个 shared walker;测试要能证明 pending write fail-fast 与 export-time fail-fast 的覆盖面完全一致,而不是两套各自演化的 guard 名单。 -- 不以 `FrontendWindowPublicationSurface` 作为 overlay 实现参考。它的 API 形状可作为反例/legacy comparison,但 Stage C overlay 必须独立实现并独立证明:写入 pending / committed overlay 时 stable side table 与 `BlockScope` 保持不变。 - -验收细则: - -- 当前 statement pending local slot update 对同一 statement 后续 semantic step 可见。 -- 前一 statement committed typed fact 对后一 statement 可见。 -- Overlay 不修改 stable side table,直到 export / apply patch。 -- Overlay isolation tests 不能复用 VarTypePost window path 作为等价 oracle;必须直接断言 stable `symbolBindings()`、`resolvedMembers()`、`resolvedCalls()`、`expressionTypes()`、`slotTypes()` 与 local slot backing scope 在 overlay 生命周期内不变。 -- Overlay export 不允许跨 owner 混合在同一 patch 中;suite 收敛后必须按固定顺序 apply per-owner patches。 -- Overlay 不允许 exact A -> exact B。 -- Overlay 不允许 source-facing `GdCompilerType`,测试必须覆盖 binding/member/call/expression/slot/update 六类写入面。 -- 若保留 `updateXxx(...)` whole-table publish 参与 legacy flow,必须额外证明这些入口对六类 source-facing typed payload 使用与 overlay/export 相同的 walker;否则它们只能被标记为 non-production compatibility API。 - -### 阶段 D:实现 body `SuiteResolver` 骨架 - -状态同步(2026-07-08): - -- [x] D1 新增 `FrontendSuiteResolver` skeleton;默认 owner procedures 为 no-op,只从 `FrontendInterfaceSurface.suiteEntryRoots()` 进入 callable / property initializer / supported child block roots,并在 suite 收敛后通过 `FrontendTypedLexicalEnvironment.exportPatchTransaction()` apply stable facts。 -- [x] D2 `FrontendSemanticAnalyzer.analyze()` 已在 skeleton / scope / variable inventory 之后构建真实 `FrontendInterfaceSurface`,并在 legacy body analyzers 之前传入 `FrontendSuiteResolver`;legacy analyzer 顺序保留,SuiteResolver 目前是 shadow no-op body path。 -- [x] D3 新增 `FrontendSuiteContext`,显式携带 source path、callable owner、current block scope / scope、restriction、static context、property initializer context、interface surface、typed lexical environment、analysis data、diagnostic manager 与 class registry。 -- [x] D4 新增 `FrontendStatementResolver` dispatcher;supported roots 按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 固定顺序调用 injected owner hooks,并在每个 statement/header boundary flush pending overlay。 -- [x] D5 `if` / `elif` / `else` / `while` 的 Phase D traversal 已建立 header-first / child-suite-after-header 形状;Phase D 当时让 `for` / `match` / block-local `const` 走 fail-closed hook。最终状态由 for-range B/D0 与阶段 L 覆盖:`for` 已进入普通 child-suite path,match/const 仍 structural fail-closed。 -- [x] D6 新增并运行 `FrontendSuiteResolverTest` 及相关 targeted regressions,覆盖 source-order、owner order、header-before-body、unsupported body not entered、overlay export boundary、main pipeline surface hand-off;`FrontendInterfacePhaseTest`、`FrontendTypedLexicalEnvironmentTest`、`FrontendVisibleValueResolverTest`、`FrontendAnalysisDataTest`、`FrontendSemanticAnalyzerFrameworkTest`、`FrontendVariableAnalyzerTest`、`FrontendLocalTypeStabilizationAnalyzerTest`、`FrontendVarTypePostAnalyzerTest`、`FrontendChainBindingAnalyzerTest`、`FrontendExprTypeAnalyzerTest` 均通过。 - -实施内容: - -- 新增 `FrontendSuiteResolver`。 -- 在 `FrontendSemanticAnalyzer.analyze()` 主 pipeline 中,于 skeleton / scope / variable inventory 之后构建 `FrontendInterfaceSurface`,并把它作为 `FrontendSuiteResolver` 的输入;不能继续只依赖测试或手动 fixture 构造 interface surface。 -- 新增 `FrontendSuiteContext`,携带 source path、callable owner、current block scope / scope、restriction、static context、property initializer context、interface surface、typed lexical environment、analysis data、diagnostic manager 与 class registry。 -- 新增 `FrontendStatementResolver` 或等价 statement dispatcher。 -- 新增 owner procedure registry / dispatch contract,但第一版只接线 no-op 或 fail-closed hook,不复用 whole-module `analyzeInWindow(...)`。 -- `FrontendSuiteContext` / owner procedure registry 必须把 `FrontendTypedLexicalEnvironment` 作为显式依赖暴露给后续 owner procedure;阶段 C8 的 expression semantic support 与 chain reduction facade 接入点由阶段 E 真正替换,阶段 D 只建立可传递该依赖的骨架。 -- 第一版只遍历当前已支持 body 结构:ordinary statements、`if` / `elif` / `else`、`while`、property initializer。 -- `for` / `match` / lambda / block-local `const` 继续 fail-closed。 -- 暂不删除 legacy whole-phase analyzer wrappers。 - -阶段 D 是新 analyzer 子框架的骨架阶段,不是把现有 analyzer 包一层调度器。它必须建立 root-bounded statement traversal:外层 SuiteResolver 决定进入哪个 statement / header / child suite,owner procedure 只能处理传入 root 及其允许的子表达式,不能重新从 `SourceFile` root walk。Godot 的 `resolve_suite()` / `resolve_node()` 只作为 dispatch 形状参考;GDCC 仍必须保留自己的完整 inventory、filtered-hit resolver 与 side-table publication contracts。 - -验收细则: - -- `SuiteResolver` 只进入 `FrontendSuiteEntryRoots` 标记为可进入的 body。 -- `FrontendSemanticAnalyzer.analyze()` 必须发布并传递真实的 `FrontendInterfaceSurface` 给 `SuiteResolver`;targeted test 必须证明 interface surface 是主 pipeline 中 skeleton / scope / variable inventory 之后、body suite 解析之前产生的。 -- source-order traversal 与 AST statement order 一致。 -- child block 递归顺序为 header 先解析,body 后解析。 -- `FrontendVisibleValueResolver` 的 declaration-after-use 与 self-reference 测试继续通过。 -- Body-aware resolver 调用必须传入当前 `FrontendSuiteContext` 的 `TypedLexicalEnvironment`。 -- Statement flush 前 stable side table 与 `BlockScope` 不变;flush 后仅 current-suite committed overlay 可见。 -- Suite 收敛后,stable side table 只能通过按序 apply per-owner patch transaction 更新。 -- Production SuiteResolver path 不调用任何 analyzer 的 `analyzeInWindow(...)`,也不调用内部 `AstWalker...walk(sourceFile)`。 - -### 阶段 E:重写 owner 子过程到 suite/body 上下文 - -实施内容: - -- 为 top binding、local stabilization、chain binding、expr typing、var type post 重写 statement-local owner procedure。 -- Owner procedure 接收 `FrontendSuiteContext`、当前 statement/header/expression root 与 `FrontendTypedLexicalEnvironment`,不再依赖 whole-module AST traversal 建立隐式上下文。 -- `FrontendStatementResolver` 必须按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 的顺序调用 owner procedure。 -- 保留现有 analyzer class 名称和 owner 边界。 -- `FrontendLocalTypeStabilizationAnalyzer` 不再通过整模块 legacy direct phase 表达 source-order 行为。 -- 完成阶段 C8:`FrontendExpressionSemanticSupport`、`FrontendChainReductionFacade` 与相关 chain-head / dependency-type callback 必须改为显式读取 `FrontendTypedLexicalEnvironment`,替换 stable-data / analyzer-local callback 的 dependency type 读取路径。 -- `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 继续 guard-only。 -- Chain / argument retry 的中间 expression facts 必须保存在 owner procedure 内部非导出 transient cache,不得写入 `expressionTypes()` overlay 后再以 narrowing / status upgrade 方式覆盖。 - -每个 owner 的重写范围必须显式记录: - -- Top binding:把 `AstWalkerTopBindingBinder` 的 use-site binding 规则拆为 statement-local binding procedure,并把 restriction、static context、property initializer context 由 `FrontendSuiteContext` 显式传入。 -- Local stabilization:把 `AstWalkerLocalTypeStabilizer` 的 eligible `:=` initializer 解析改为立即写 pending overlay,使同 statement 后续 owner 与后续 statement 可按 flush 规则读取;禁止继续依赖整模块 direct phase 更新 `BlockScope`。 -- Chain binding:把 chain reduction 的 dependency type 回调改为读取 `FrontendTypedLexicalEnvironment` 与 owner-local transient cache,而不是 analyzer-local stable side table snapshot。 -- Expr typing:把 expression fact 发布改为 statement-local final fact publication;父索引、duplicate-report state、retry state 都必须显式化,不能藏在 whole-module walker 字段里。 -- Expression semantic support / chain reduction facade:identifier binding、receiver type、argument type、bare-call callee type 与 nested dependency type 读取必须先走 owner-local transient cache / pending overlay / committed overlay 的 effective view,再回退 stable side table;不得继续把 `FrontendAnalysisData.symbolBindings()` / `expressionTypes()` 作为 body procedure 的第一读取源。 -- Var type post:把 slot type publication 改为消费当前 statement / current-suite typed facts 的 statement-local publication,不再通过整表 `updateSlotTypes(...)` 表达 body 结果,也不得复用旧 `analyzeInWindow(...)` 的“stable `slotTypes()` clear/write 后再复制到 window”模式。 -- Resolver request:request-domain gate、AST boundary gate 与 current-scope gate 的创建必须由 `FrontendSuiteContext` 统一完成,不能继续由各 analyzer 手写 deferred domain。 - -阶段 E 状态同步: - -- [x] E0 重新读取 `AGENTS.md`,用 MCP 列出 `doc` 与 `doc/module_impl`,并并行子代理调研阶段 E 相关文档、owner analyzer 代码与测试基线。 -- [x] E1 完成 C8 overlay-aware dependency lookup:`FrontendExpressionSemanticSupport` / `FrontendChainReductionFacade` / chain-head receiver 可通过注入 lookup 读取 `FrontendTypedLexicalEnvironment` 的 effective binding 与 exact local slot fact;`FrontendChainReductionHelper` 的 argument dependency lookup 不再先读 stable `expressionTypes()`,而是委托注入 resolver 保持 overlay-first 合同;旧构造器继续保持 stable-table 兼容。 -- [x] E2 新增 `FrontendBodyOwnerProcedures`,以 root-bounded DFS 实现 top binding、local stabilization、chain binding、expr typing、var type post 的核心 statement-local publication,不调用 whole-module analyzer entrypoint。 -- [x] E3 `FrontendSuiteResolver` 默认接入真实 owner procedure;阶段 I 后 `FrontendSemanticAnalyzer` 默认无条件运行真实 SuiteResolver,不再保留 legacy-compatible no-op / whole-phase fallback 分支。 -- [x] E4 新增正反向 targeted tests,锚定 source-order alias、child-prefix visibility、chain receiver exactness、transient cache isolation、var type post export boundary、C8 overlay lookup 与 framework-level real owner path hand-off。 -- [x] E5 已运行格式化、IDE 增量编译与问题检查、`FrontendSuiteResolverTest`、`FrontendExpressionSemanticSupportTest`、`FrontendChainReductionFacadeTest`、`FrontendChainReductionHelperTest`、阶段 E 相关 targeted regression batch,以及 `git diff --check`。 -- [x] E6 原通过显式注入真实 `FrontendBodyOwnerProcedures` 的 `FrontendSuiteResolver` 证明 D/E body owner path 在 framework hand-off 中先于 legacy whole-phase publication 执行;阶段 I 后该证据已收口为 `FrontendSemanticAnalyzerFrameworkTest` 中的默认 SuiteResolver body publication、nested source-order facts 与 unsupported fail-closed tests,且不再依赖 legacy baseline。 - -验收细则: - -- `var a := typed_value; var b := a; var c := b` 在 body resolver 下稳定为同一 exact type。 -- child block 可读取 parent 前缀 stable local。 -- 对 `var b := a`,`b` 的 local stabilization 必须读取前一 statement 已 committed 的 `a` exact slot fact。 -- 对 `var x := receiver.member`,chain binding 必须在 local stabilization 子过程之后运行,并消费已稳定的 `receiver` slot fact;不能直接读取 interface baseline `Variant`。 -- rejected shadow declaration 不污染 parent slot。 -- nested chain / argument retry 保持读己写能力,但这个能力只存在于 owner-local transient cache;同一 expression / step key 在 statement flush 和 suite export 中只产生一条最终 `expressionTypes()` fact。 -- retry 过程中出现的任何非最终 expression fact(含 `DEFERRED` 代理类型、暂定 `Variant`、中间 status / detailReason)不得先发布到 pending overlay、committed overlay 或 stable side table 再被最终 fact 覆盖。 -- C8 回归测试必须证明:当 stable side table / baseline 仍是 `Variant` 而 pending 或 committed overlay 已有 exact local slot fact 时,`FrontendExpressionSemanticSupport` 与 `FrontendChainReductionFacade` 的 dependency-type callback 消费 overlay exact type,而不是 stable `Variant`。 -- C8 negative path 必须证明:support / facade 不能直接读取 owner-local transient cache 以外的非最终 expression fact,也不能绕过 `FrontendTypedLexicalEnvironment` 直接读取 stable-only side table 后发布 stale receiver / argument / bare-call result。 -- owner 以外的 procedure 不能写对应 side table 或 slot update。 -- 每个 owner 子过程的 suite export 以独立 per-owner patch 出现在 transaction 中;transaction coordinator 按 top binding -> local stabilization -> chain binding -> expr typing -> var type post 顺序 apply。 -- Var type post procedure 在 statement flush 前不得改变 stable `slotTypes()`;targeted test 必须在 procedure 运行、flush、suite export 三个点分别断言 stable table 只在 export/apply patch 后变化。 - -### 阶段 F:历史 typed-dependent gate 试验(已由阶段 L 删除) - -阶段 F 曾用于验证 synthetic body-entry lifecycle,但没有 production feature consumer。阶段 L 已完成清理: - -- [x] registry、status/readiness 类型、publication setter 与 synthetic classifier 已物理删除。 -- [x] Interface surface 不产出 body gate;SuiteResolver、body-local stabilization 与 visible-value resolver - 不接收或查询 registry。 -- [x] F 中仍有价值的 overlay 顺序测试保留在普通 statement/header 测试中,但 typed result 只能驱动 - refinement 或 route planning,不能决定 child body entry。 -- [x] 未转正 feature 通过 structural policy、AST boundary 与 current-scope backstop fail-closed。 -- [x] source-facing facts 继续拒绝 feature-specific `GdCompilerType`。 - -### 阶段 G:收敛 diagnostics 与 compile gate - -实施内容: - -- [x] G1 确定 interface phase、body suite statement、suite export、diagnostics-only phase 的 diagnostics snapshot 刷新点。`FrontendStatementResolver.flushStatementBoundary(...)` 在每个 body statement flush typed facts 后同步刷新 diagnostics snapshot;`FrontendSuiteResolver` 保留 suite export snapshot;`FrontendSemanticAnalyzer` 保留 interface/suite hand-off 后、annotation/virtual/type/loop diagnostics-only phase 后、compile-only gate 后的 snapshot。 -- [x] G2 重新定义 diagnostics 可见性:body statement 解析期间的诊断写入仍进入 `DiagnosticManager`,但 duplicate suppression 必须能区分 statement-local upstream、current-suite upstream 与稳定 phase upstream;不能继续假设每个 whole-module analyzer 结束后才刷新一次 snapshot。`FrontendSuiteResolverTest.statementBoundaryPublishesDiagnosticsSnapshotForLaterStatements` 锚定同 statement 内只能通过 live manager snapshot 读取 statement-local upstream,后一 statement 可通过 `analysisData.diagnostics()` 读取 current-suite snapshot。 -- [x] G3 保持 compile gate 只在 `analyzeForCompile(...)` 运行。代码路径未把 `FrontendCompileCheckAnalyzer` 接入 shared `analyze(...)`、suite resolver 或 inspection;既有 `FrontendCompileCheckAnalyzerTest` / `FrontendSemanticAnalyzerFrameworkTest` / `FrontendAnalysisInspectionToolTest` split 覆盖继续作为锚点。 -- [x] G4 检查 compile gate 的 duplicate suppression 是否仍能识别 interface/body upstream diagnostics。`FrontendCompileCheckAnalyzer` 在 gate 入口先要求 stable diagnostics boundary 已发布,再冻结 live `DiagnosticManager.snapshot()` 作为 upstream 去重输入;`FrontendCompileCheckAnalyzerTest.analyzeDeduplicatesAgainstLiveManagerSnapshotWhenAnalysisDataSnapshotIsStale` 锚定 manager 中存在但 stable snapshot 尚未刷新的 upstream error 仍会抑制同 anchor `sema.compile_check`。 -- [x] G5 对缺失 `slotTypes()`、`DEFERRED`、`FAILED`、`UNSUPPORTED` 的 final facts 保持现有 compile blocking 规则。compile gate 仅调整 upstream snapshot 来源,generic published-fact blocker 与 non-error slot publication blocker 逻辑未改变;`FrontendCompileCheckAnalyzerTest` 中 slot publication、deferred/failed/unsupported/dynamic 覆盖继续通过。 - -验收细则: - -- `FrontendCompileCheckAnalyzerTest` 中已有 compile gate 去重测试继续通过。 -- upstream diagnostic 已存在时,下游 phase 不补同级重复错误。 -- `analyze(...)` 不运行 compile gate。 -- `analyzeForCompile(...)` 在 interface + body facts 完成后运行 compile gate。 -- parse / skeleton / scope diagnostics 的顺序与可见性不变。 -- 同一 statement 内 owner procedure 产生 upstream diagnostic 后,后续 owner procedure 不再补同 anchor / 同类别重复错误;后一 statement 仍能读取 current-suite diagnostics snapshot 进行去重。 - -### 阶段 H:切换默认 shared semantic pipeline - -实施内容: - -- [x] H1 `FrontendSemanticAnalyzer.analyze(...)` 默认运行新 interface/body pipeline。当前生产 `analyze(...)` 在 interface phase 后只调用真实 `FrontendSuiteResolver`,不再追加 legacy whole-phase owner publication,避免 body facts 双重发布。 -- [x] H2 legacy whole-phase runner 在阶段 H 只保留为 package-private 测试旁路;阶段 I 已删除该旁路与 test bridge。 -- [x] H3 移除 `segmentedSemanticRunner` 生产路径开关。当前已删除 `segmentedSemanticRunner` 字段、构造参数与 `withSegmentedSemanticRunnerForTesting()` 工厂,生产路径不再能切到 deprecated segmented scheduler。 -- [x] H4 新增 legacy vs interface/body pipeline 等价测试,等价基线以 guard-only backfill 合同为准;阶段 I 后这些测试已迁移为默认 interface/body pipeline 的 body fact、nested source-order 与 unsupported fail-closed 合同测试。 -- [x] H5 broad regression parity follow-up:body owner top-binding 现在与 legacy 共享 dual-role singleton/type-meta 路偏和 global enum type-meta preference,body expression publication 补齐 builtin Variant constructor unsafe-call warning,missing / blocked binding diagnostics 与 legacy binding owner 对齐;阶段 I 后 owner-specific `FrontendExprTypeAnalyzerTest` 使用测试内显式 owner-analyzer baseline helper,不再通过 shared analyzer test bridge 进入 legacy whole-phase baseline。 - -验收细则: - -- 对同一输入,legacy 与新 pipeline 的 `symbolBindings()`、`resolvedMembers()`、`resolvedCalls()`、`expressionTypes()`、`slotTypes()` 等价,或文档明确接受的 diagnostics 顺序差异有测试锚定。 -- unsupported `for` / `match` / lambda / block-local `const` 行为不变,除非对应 gate 已在本阶段显式转正。 -- `FrontendVisibleValueResolver` declaration-after-use 与 initializer self-reference 测试继续通过。 - -### 阶段 I:移除 legacy whole-phase 旁路并更新相关文档 - -实施内容: - -- [x] I1 删除 legacy whole-phase body semantic 旁路。`FrontendSemanticAnalyzer.analyze(...)` 现在无条件执行 interface phase + `FrontendSuiteResolver`;`analyzeWithLegacySharedSemanticPublication(...)` 与 test bridge 已删除。 -- [x] I2 删除 `FrontendSegmentedSemanticScheduler` 过渡实现。代码中不再存在 scheduler 文件或 runner 开关。 -- [x] I3 保留 patch/overlay/export 基础设施,并移除 single-stage `FrontendAnalysisPatch` 作为 suite export 生产路径的用途。Suite export 继续通过 `FrontendTypedLexicalEnvironment.exportPatchTransaction()` 产生 per-owner transaction;`FrontendAnalysisPatch` 仅保留为 legacy shim / focused tests 兼容载体。 -- [x] I4 更新 variable analyzer、visible resolver、local stabilization、chain/expr typing、compile check、for-range plan,明确最终 production shared analyzer 只通过 interface/body + SuiteResolver 发布 body facts。 -- [x] I5 更新 `doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md`,记录 Phase I 删除项、final owner 顺序、fact 生命周期、per-owner patch/export、compiler-only guard 与 diagnostics / compile gate 边界。 -- 更新 variable analyzer、visible resolver、local stabilization、chain/expr typing、compile check、for-range plan。 -- 更新 `doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md`,使其反映最终实现的层级职责、owner 顺序、fact 生命周期、patch/export 合同、compiler-only guard 与 diagnostics / compile gate 边界,并继续保持为目标架构摘要而非旧流水线或过渡资产说明。 - -验收细则: - -- 所有 frontend semantic focused tests 通过。 -- 新 pipeline 测试覆盖 per-owner patch merge、patch transaction 顺序、typed overlay、backfill guard-only、source-order typed fact、pending overlay flush、resolver filtered hit、diagnostic dedup、compile gate。 -- `doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md` 已与最终代码行为和本计划完成定义同步,且未把 legacy whole-phase 或 window / scheduler 过渡资产写成目标流水线的一部分。 -- `./gradlew classes --no-daemon --info --console=plain` 通过。 -- 相关 targeted tests 使用 `script/run-gradle-targeted-tests.sh --tests ...` 通过。 - -### 阶段 J:收口 `FrontendSemanticAnalyzer` 的 legacy-owner constructor 注入 - -阶段 I 已删除 shared pipeline 的 legacy whole-phase bypass,但不包含无效 constructor -注入的物理删除。本阶段负责删除 `FrontendSemanticAnalyzer` 中所有 legacy-owner -constructor overload,清理 constructor chaining 中被丢弃的 analyzer 实例,并将 -framework tests 从“断言 legacy analyzer 未运行”改为验证默认 -`FrontendSuiteResolver` 的 body fact publication。 - -- [x] J1 删除所有接收 `FrontendTopBindingAnalyzer`、 - `FrontendLocalTypeStabilizationAnalyzer`、`FrontendChainBindingAnalyzer`、 - `FrontendExprTypeAnalyzer` 或 `FrontendVarTypePostAnalyzer` 的 public constructor - overload。这五类参数此前只作 null-check,既不保存为 field,也不进入 production - pipeline。现有 public API 只保留 default、active phase、interface/body resolver 与 - skeleton/scope/variable 的注入边界。 -- [x] J2 清理其余 constructor chaining 中无意义的上述 analyzer `new` 操作;默认与 - active-dependency 注入入口不再构造被丢弃的 legacy owner analyzer。 -- [x] J3 将 framework tests 从“注入 probe 后断言 legacy analyzer 未运行”改为验证默认 - `FrontendSuiteResolver` 的 body fact publication;继续保留对 skeleton、scope、variable - inventory、diagnostics-only phase 与 compile-only gate 的 active dependency probes。 - `activeDependencyConstructorExcludesLegacyBodyOwnerParameters` 正向锚定 active-only - constructor,并反向检查全部 public constructor 均不含 legacy owner 参数; - `activeDependencyConstructorUsesDefaultSuiteResolverAndPreservesPhaseBoundaries` 保留 - diagnostics boundary probes 与 callable-entry slot publication,既有 default - SuiteResolver body-fact / unsupported-subtree tests 继续锚定 export 与 fail-closed 合同。 - -验收细则: - -- `FrontendSemanticAnalyzer` 不再暴露或构造被丢弃的 legacy owner analyzer;其 production - pipeline 仍只通过 interface/body + `FrontendSuiteResolver` 发布 body facts。 -- `./gradlew classes --no-daemon --info --console=plain` 通过。 -- 相关 targeted tests 使用 `script/run-gradle-targeted-tests.sh --tests ...` 通过。 - -### 阶段 K:删除 legacy whole-phase 比较路径并迁移测试基线 - -本阶段在前一阶段删除 legacy-owner constructor 注入的基础上,继续删除 legacy -whole-phase analyzer comparison path 及 window/patch compatibility shim。必须先完成测试 -基线迁移与常量收口再执行 K3-K4 的物理删除,不能因为 production 路径已切换就跳过这些 -前置条件。 - -- [x] K1 迁移或删除五个 legacy owner analyzer 的 standalone/cross-analyzer tests,使新 - `FrontendBodyOwnerProcedures`、overlay 与 per-owner patch transaction 覆盖仍有效的 - semantic contracts;不得仅因 production 无调用者就删除这些测试。chain、expression - 与 var-post 基线已迁入 `FrontendBodyOwnerProcedures*Test` 并通过默认 segmented pipeline - 或真实 root-bounded owner procedure 运行;纯 whole-module boundary/probe/整表清空断言已 - 删除。local stabilization 的 source-order/parent-prefix 合同继续由 suite/framework/expr - tests 锚定,overlay 另补错误 owner、stable conflict 与无副作用负向覆盖。 -- [x] K2 将 `FrontendVarTypePostAnalyzer.VARIABLE_SLOT_PUBLICATION_CATEGORY` 迁移到 - 非 legacy analyzer 的语义常量位置,并更新 `FrontendBodyOwnerProcedures` 与 - `FrontendCompileCheckAnalyzer` 的消费者。常量现由实际发布该诊断的 - `FrontendBodyOwnerProcedures.VARIABLE_SLOT_PUBLICATION_CATEGORY` 持有;compile gate、 - lowering gate 与 focused tests 均不再依赖 legacy analyzer 类型。 -- [x] K3 删除 `FrontendTopBindingAnalyzer`、`FrontendLocalTypeStabilizationAnalyzer`、 - `FrontendChainBindingAnalyzer`、`FrontendExprTypeAnalyzer` 与 - `FrontendVarTypePostAnalyzer` 的 whole-module `analyze(...)` / - `analyzeInWindow(...)` entrypoints、内部 `AstWalker...walk(sourceFile)` traversal - 与 legacy stable-table publication path。仍有效的 owner 语义边界必须由 - `FrontendBodyOwnerProcedures` 保持。五个 legacy 类已物理删除;active expression owner - 保留 discarded-result diagnostic 与 inferred-local consistency guard,但该 guard 只检查 - local stabilization 结果,不写 slot、不刷新 binding payload。framework negative test 同时 - 锚定这些类不再出现在 constructor API 或 runtime classpath。 -- [x] K4 在 K3 后删除 `FrontendWindowAnalysisContext`、 - `FrontendWindowPublicationSurface`、legacy `FrontendAnalysisPatch`、 - `FrontendAnalysisData.applyPatch(FrontendAnalysisPatch)` 与对应的 - `FrontendPublishedFactTypeGuard.checkAnalysisPatch(...)` compatibility path; - `FrontendLocalSlotTypeUpdate`、`FrontendOwnerPatch`、`FrontendPatchTransaction` 与 - `FrontendAnalysisPatchException` 仍是 active patch infrastructure,不得删除。window 与 - multi-owner shim 及其 direct API tests 已删除;`FrontendAnalysisDataTest` 现只构造 - owner-specific patches,并补充 binding/member/call conflict 无副作用测试。framework - negative regression 同时验证三个 shim 类和 legacy `applyPatch` overload 均不存在。 - -验收细则: - -- 不存在任何 production 或 focused test 对 legacy whole-module `analyze(...)` / - `analyzeInWindow(...)`、window shim 或 `FrontendAnalysisPatch` 的引用。 -- per-owner patches 与 `FrontendPatchTransaction` 是唯一的 body-fact export path,且 - `FrontendVarTypePostAnalyzer` 删除前完成 category 常量迁移。 -- `./gradlew classes --no-daemon --info --console=plain` 通过。 -- 相关 targeted tests 使用 `script/run-gradle-targeted-tests.sh --tests ...` 通过。 - -### 阶段 L 系列:删除 typed-dependent body gate 历史架构债务 - -第 1.1 节已确定:body 是否进入 shared semantic 只能由结构性支持面和完整 lexical -inventory 决定,typed fact 只能影响后续 refinement、semantic route 与 lowering。本阶段系列 -删除阶段 F 为 synthetic fixture 建立、但从未成为 production feature consumer 的 -typed-dependent gate scaffolding;不得将其扩展为 publication protocol、failure state 或新的 -feature integration API。 - -阶段 L 系列只依赖 `frontend_for_range_loop_implementation_plan.md` 的阶段 B 与阶段 D0 结构性 header/body 子集,不依赖 bare `range(...)` header 预路由: - -- 阶段 B 已为 `FOR_BODY` 发布 iterator、完整 ordinary local inventory、declaration index 与 - source-facing baseline。 -- 阶段 D0 的结构性子集已建立 header-only dispatch,并在 header facts flush 后通过普通 - `resolveChildSuite(...)` 进入 body;iterator 此时允许保持 `Variant` 或显式 declared baseline。 -- 阶段 B 在移除 shared semantic unsupported diagnostic 前,已经安装临时、无条件的 - `ForStatement` compile blocker,确保 for-range 阶段 F/G 尚未完成时不会进入 CFG/lowering。 - -阶段 L 系列**不依赖** for-range 阶段 C、D1、type-check、route-aware compile 解封、CFG 或 -lowering。iteration planning 与 iterator slot refinement 可以晚于 gate 删除;它们只能改变 typed -fact 和 lowering route,不能改变阶段 D0 已建立的 body entry。 - -对于阶段 L 时仍未转正的 lambda、match、block-local `const`、parameter default 等节点,必须 -保持纯结构性 unsupported / deferred boundary:不创建 `PENDING` gate、不查询 typed readiness、 -不因前缀或 header expression 的类型变化而改变 body entry。这里的“跳过 inventory”必须按 -feature-owned surface 理解:lambda 包括 parameter/capture/body,match 包括 pattern binding、guard -与 section body,block-local `const` 包括 declaration/initializer boundary;不能把它们错误归一化 -成只有 `BlockScopeKind` 的普通 block body。 - -实施规则: - -- 每个子阶段结束时保持 main/test source 可编译;不得先物理删除类型,再等待后续阶段清理消费者。 -- 每个 patch batch 最多修改三个文件;一个子阶段可以拆为多个 batch,并在 behavior batch 后运行 - focused tests、在子阶段结束时运行 compile check。 -- “structural support”只表示某种 AST/scope kind 的 inventory path 已实现;“structural - completeness”表示本次 interface surface 确实发布了完整事实。二者不得合并为一个 boolean。 -- compile readiness 不得进入 structural semantic support matrix。 - -#### 阶段 L0:冻结 for 前置与 compile safety bridge - -- [x] 确认 for-range 阶段 B 与 D0 结构性 header/body 子集已完成(bare `range(...)` pre-route 不属于阶段 L 依赖),且 `FOR_BODY` 不再查询 gate registry。 -- [x] 确认临时 `FrontendCompileCheckAnalyzer.handleForStatement(...)` blocker 只锚定 - `ForStatement`、不进入 body、不读取 iterable type / iteration plan / route。 -- [x] 增加 for-only characterization:shared `analyze(...)` 无 `FOR_SUBTREE` unsupported; - `analyzeForCompile(...)` 有明确 `sema.compile_check` error,lowering pipeline 在 CFG 前停止。 - -验收细则: - -- `for item in values: var copy := item` 已进入 shared semantic,且即使 `values` 仍是 `Variant` - 也不改变 body entry。 -- `FrontendCfgGraphBuilder` 尚无 `ForStatement` 分支时,任何 `for-in` 都不能从 compile-only - boundary 泄漏到 CFG。 - -#### 阶段 L1:建立不可变 structural semantic support matrix - -- [x] 新增 `FrontendBodySemanticSupportPolicy` 或等价不可变 helper,统一表达当前结构支持面; - `FrontendExecutableInventorySupport` 必须委托该事实源或被其替代,不能保留平行 allowlist。 -- [x] policy 只能由 AST/scope kind 决定,不能读取 `GdType`、expression type、typed overlay、 - iteration plan、diagnostic state、compile surface 或 gate lifecycle。 -- [x] policy 必须覆盖下列结构位置,且每一行都有 focused test。 - -结构支持矩阵最低合同: - -- function/constructor/if/elif/else/while/ordinary block body:发布 lexical inventory,进入 - `SuiteResolver`,不使用 deferred domain。 -- `for` header:使用外层 scope,只运行 header-only dispatch,不使用 deferred domain。 -- `FOR_BODY`:发布 lexical inventory,进入 `SuiteResolver`,不使用 deferred domain。 -- lambda parameter/capture/body:不发布 feature-owned inventory,不进入 `SuiteResolver`,domain 为 - `LAMBDA_SUBTREE`。 -- match pattern/guard/section body:不发布 feature-owned inventory,不进入 `SuiteResolver`,domain 为 - `MATCH_SUBTREE`。 -- block-local `const` declaration/initializer:不发布 local-const inventory,不进入 - `SuiteResolver`,domain 为 `BLOCK_LOCAL_CONST_SUBTREE`。 -- parameter default:不进入 body pipeline,domain 为 `PARAMETER_DEFAULT`。 -- 未知或 skipped structure:不发布 inventory,不进入 `SuiteResolver`,domain 为 - `UNKNOWN_OR_SKIPPED_SUBTREE`。 - -- [x] enum switch 必须 exhaustive;新增 `BlockScopeKind` / `CallableScopeKind` 时必须显式选择 - policy,不能通过 `default -> EXECUTABLE_BODY` 自动放行。 - -验收细则: - -- `FOR_BODY` 的 policy 是 structural supported;lambda/match 不因 for 解封而改变。 -- policy 类型和测试中不存在 `PENDING`、`PUBLISHED`、readiness、route 或 compile-ready 状态。 - -#### 阶段 L2:增加 structural completeness fail-fast certificate - -- [x] 在 `FrontendSuiteResolver` 进入 root/child body 前,通过 - `requireStructurallyCompleteBody(...)` 或等价 helper 验证: - - `scopesByAst()` 中 body root 映射到预期 identity / kind 的 `BlockScope`。 - - `FrontendSuiteEntryRoots` 明确包含该 supported body。 - - `FrontendBodyDeclarationIndex.containsBodyRoot(body)` 为 true,即使 body 没有 ordinary local。 - - **正向**(index → scope/baseline):每个 index entry 的 declaration/binding/scope identity 一致, - 且 typed baseline 包含该 source-facing declaration;`sourceOrder` 从 0 连续,并与 AST start-byte - 非降序一致。 - - **反向**(scope → index):`expectedScope` 中每条已发布 `LOCAL` binding(ordinary `var` 与 - for iterator)都必须有对应 declaration-index entry,且 declaration identity / binding kind 与 - scope inventory 一致。反向检查只覆盖该 body-root scope,不遍历 child scope;不包含尚未发布的 - block-local `const`。 - - `FOR_BODY` inventory 契约:恰好一个 iterator entry,且必须是 inventory 列表首项、 - `sourceOrder == 0`;ordinary body local 仅能跟在其后并占用连续 `sourceOrder >= 1`。Interface - 层 `FrontendInterfacePhase` 通过“先 record iterator、再 walk body statements”发布该形状, - certificate 在 suite entry 再次钉死。 -- [x] certificate 只读取 scope graph、suite entry roots、declaration index、baseline 与 body scope - inventory 枚举;不得读取 pending/committed overlay、expression type、slot refinement 或 iteration - plan。 -- [x] 结构事实缺洞属于 phase protocol / programmer error,必须 fail-fast,不得伪装成源码 - diagnostic,也不得静默跳过 body。 - -验收细则: - -- scope 存在但 suite-entry、body-index、iterator entry、ordinary inventory entry 或 baseline 任一 - 缺失时 focused test fail-fast。 -- 仅 index 列表重编号连续但 AST 顺序错乱、或 for iterator 不在 `sourceOrder` 0 时 fail-fast。 -- header resolved type 变化不会改变 certificate 结果。 - -#### 阶段 L3:迁移 SuiteResolver 与 body-local consumer - -- [x] `FrontendSuiteResolver`、`FrontendSuiteContext` 与 - `FrontendBodyOwnerProcedures.eligibleInferredLocalScope(...)` 改为消费 structural policy 与 - completeness certificate,不再用 gate readiness 决定 suite entry / ordinary local - stabilization。 -- [x] `FrontendSuiteContext.visibleValueDomainForCurrentBody()` 对 supported body 直接生成 - `EXECUTABLE_BODY`;unsupported body 根据 policy 返回精确 deferred domain,未知 kind 不得 - fallback 为 `EXECUTABLE_BODY`。 -- [x] 本阶段允许 `FrontendInterfaceSurface` 暂时继续携带 registry 供尚未迁移的 resolver 使用; - 不得为了提前删除字段形成不可编译的中间状态。 - -#### 阶段 L4:迁移 FrontendVisibleValueResolver 的结构封口 - -- [x] 删除 resolver 的 gate registry constructor dependency、request-domain readiness - normalization、`gateBodyBoundary(...)`、`isNearestOwningGateReady(...)`、 - `nearestOwningGate(...)` 及所有 `SUPPORTED + PUBLISHED` 分支。 -- [x] 保留 request-domain hard boundary、AST boundary 与 current-scope fail-closed。删除的是 typed - readiness 条件,不是这三类结构检查本身;AST boundary 与 current-scope 两层结构封口都不得 - 删除。 -- [x] `ForStatement.body()`、for header edge 与 `FOR_BODY` current scope 直接允许 normal lookup; - lambda/match/const/parameter-default 根据 structural policy 直接返回 deferred boundary。 -- [x] 保留 declaration-order、initializer self-reference、skipped subtree 以及 visible block-local - `const` 不得 fallback 到 outer same-name binding 的合同。 -- [x] 同批更新 `FrontendBodyOwnerProcedures` 的 resolver 构造点和 focused resolver tests。 - -#### 阶段 L5:删除 synthetic statement classifier - -- [x] 删除 `FrontendStatementResolver.OwnerProcedures.runGateClassifier(...)`、 - `resolveSupportedRoot(...)` 中的调用及 synthetic classifier fixtures。 -- [x] 该删除只针对 body-entry classifier。for-range 阶段 D1 后续可以在 header expr typing 后、 - iterator var type post 前新增 concrete `runForIterationPlanning(...)`;不得复用 classifier 名称、 - lifecycle 或 readiness 语义。 -- [x] 真实 feature typed result 只能驱动 refinement、semantic route、type diagnostic 或 lowering - route,不能决定是否调用 `resolveChildSuite(...)`。 - -#### 阶段 L6:删除 interface gate producer 与 surface dependency - -- [x] 移除 `FrontendInterfacePhase` 的 `FrontendInventoryGateRegistry.Builder`、 - `addPendingGate(...)`、match registration 及 lambda/match/block-local `const` 调用点。 -- [x] 已转正 `for` 继续使用阶段 B/D0 的 structural path;未转正节点只跳过其 feature-owned - inventory,并由 structural policy / AST boundary 表达限制。 -- [x] `FrontendInterfaceSurface`、`FrontendSuiteEntryRoots` 的字段、构造器与文档不再产出、持有 - 或描述 gate registry。 - -#### 阶段 L7:迁移测试合同 - -- [x] 重写 `FrontendInterfacePhaseTest`、`FrontendSuiteResolverTest`、 - `FrontendVisibleValueResolverTest` 中 pending/published gate、registry 与 classifier fixture。 -- [x] 新增/保留以下结构合同: - - `FOR_BODY` 在 typed resolution 前已有 iterator、ordinary local、index、baseline 与 suite entry。 - - supported kind 但 certificate 任一事实缺失时 fail-fast。 - - iterable 分别为 exact、`Variant`、error fact 时,body inventory 与 entry 保持不变。 - - nested for 中 lambda/match/const 仍保持 structural deferred。 - - visible block-local `const` 不 fallback;future declaration 与 initializer self-reference filtered - hit 不退化。 - - for-only compile path 在 CFG 未支持时命中临时 blocker。 -- [x] 删除手工推进 gate lifecycle 的 fixture 与 `FrontendInventoryGateRegistryTest`。 - -#### 阶段 L8:物理删除 gate lifecycle 类型 - -- [x] 仅在 L3-L7 的生产/测试消费者全部清除后,物理删除 - `FrontendInventoryGate`、`FrontendInventoryGateRegistry`、 - `FrontendInventoryGateStatus`、`FrontendBodyInventoryReadiness`。 -- [x] 不保留 compatibility shim、empty registry、反射检查或公开 - `markSupported(...)` / `markBodyInventoryPublished(...)` setter。 -- [x] `src/main/java` 与 `src/test/java` 中不存在上述类型、`runGateClassifier`、 - `markBodyInventoryPublished` 或 `isBodyInventoryReady` 引用。 - -#### 阶段 L9:同步文档、风险与完成定义 - -- [x] 删除或改写本文第 4.5、4.8、阶段 F 中把 `SUPPORTED + PUBLISHED` 作为 body entry 条件的 - 历史设计;复核第 6/7/8 节不再保留 readiness registry 不变量。 -- [x] 更新 `frontend_rules.md`:`for` 已进入 shared semantic,但在 route-aware compile gate 落地前 - 由临时 blocker 阻断;compile gate 不得把该 blocker 反向变成 semantic body gate。 -- [x] 更新 `frontend_segmented_type_resolution_pipeline_execution_summary.md`、visible-value - resolver、interface phase 与 for-range 文档,明确 structural policy、completeness certificate、 - semantic body entry 与 compile route readiness 四者分离。 - -阶段 L 系列完成验收: - -- `FrontendInterfaceSurface`、`FrontendSuiteContext`、`FrontendSuiteResolver`、 - `FrontendBodyOwnerProcedures` 与 `FrontendVisibleValueResolver` 均不接收、持有或查询 gate - registry;不存在带 body root / registry 参数的 readiness API。 -- 已转正 body 的 resolver/suite entry 不依赖 typed fact;改变 header expression 的 resolved type - 不会改变 scope inventory、declaration index、completeness certificate 或 child-suite dispatch。 -- `for i in values: var local := i` 在 iterable typing 前已有 `i` baseline 与 `local` 完整 inventory; - 两者都由 declaration index 覆盖,body 解析后 `local` 通过既有 owner/export 路径发布 slot type。 -- 未转正节点保持 fail-closed,但没有 `PENDING` / `PUBLISHING` / `PUBLISHED` lifecycle;其 - deferred diagnostic/domain 只由 AST/scope 的结构性 unsupported boundary 决定。 -- 相关 focused tests 与 `./gradlew classes --no-daemon --info --console=plain` 通过。 - -## 6. 必须新增或调整的测试 - -基础设施测试: - -- `FrontendAnalysisDataTest`:patch merge 新 key / idempotent / conflict / stable reference。 -- `FrontendAnalysisDataTest`:`FrontendOwnerPatch` / per-owner patch merge 时只能携带该 owner 允许的 side table 或 local slot update;跨 owner payload fail-fast。 -- `FrontendAnalysisDataTest`:`FrontendLocalTypeStabilizationPatch` 是唯一允许携带 `FrontendLocalSlotTypeUpdate` 的 patch,且不能同时携带独立 `symbolBindings()` delta。 -- `FrontendAnalysisDataTest`:`FrontendPatchTransaction` 按 top binding -> local stabilization -> chain binding -> expr typing -> optional feature-specific semantic route planning -> var type post 顺序 apply;乱序或重复 owner patch fail-fast。 -- `FrontendAnalysisDataTest`:`expressionTypes()` patch 对同一 stable key 的 same-status different publishedType、status change、Variant-published fact -> exact fact 仍 fail-fast,证明没有 slot-style narrowing 例外。 -- `FrontendAnalysisDataTest`:compiler-only type 泄漏 guard 覆盖 `expressionTypes()`、`slotTypes()`、`localSlotTypeUpdates()`。 -- `FrontendAnalysisDataTest`:`applyPatch` 拒绝 `symbolBindings()` 中 `resolvedValue.type()` 为 `GdCompilerType` 的 patch entry。 -- `FrontendAnalysisDataTest`:`applyPatch` 拒绝 `resolvedMembers()` 中 `receiverType` / `resultType` 为 `GdCompilerType` 的 patch entry。 -- `FrontendAnalysisDataTest`:`applyPatch` 拒绝 `resolvedCalls()` 中 `receiverType` / `returnType` / `argumentTypes` / callable boundary parameter types 含 `GdCompilerType` 的 patch entry。 -- `FrontendAnalysisDataTest`:shared walker 对 `FrontendBinding` / `FrontendResolvedMember` / `FrontendResolvedCall` / `FrontendExpressionType` / `FrontendLocalSlotTypeUpdate` 的 type-bearing field coverage 有明确 regression tests,防止未来新增字段后 guard 漏扫。 -- `FrontendAnalysisDataTest`:若 `updateSymbolBindings()` / `updateResolvedMembers()` / `updateResolvedCalls()` / `updateExpressionTypes()` / `updateSlotTypes()` 仍保留 source-facing publication 语义,它们必须拒绝 compiler-only payload;若不做该保护,则测试与文档必须把这些入口显式限定为 legacy non-production path。 -- `FrontendAstSideTableTest`:identity key 语义不变。 -- `FrontendTypedLexicalEnvironmentTest`:overlay read order、owner metadata、source-facing compiler-only guard、exact type conflict、export 前 stable 不变。 -- `FrontendTypedLexicalEnvironmentTest`:pending overlay 只对当前 statement 后续子过程可见,flush 后才进入 current-suite committed overlay。 -- `FrontendTypedLexicalEnvironmentTest`:suite export 前 stable side table 与 `BlockScope` 不变,export 后只通过 patch apply 更新。 -- `FrontendTypedLexicalEnvironmentTest`:suite export 生成 per-owner patch transaction,而不是单个 multi-owner patch。 -- `FrontendTypedLexicalEnvironmentTest`:`expressionTypes()` overlay 同 key 同值幂等,不同值 fail-fast;retry 中间 fact 不能作为可导出的 expression type fact 留存。 -- `FrontendSuiteResolverTest` / `FrontendChainReductionHelperTest`:owner procedure transient caches 与 bounded `finalizeWindow` retry 中的临时 facts 不会进入 pending overlay、committed overlay 或 stable side table;只有 owner 显式 publication 后才进入 typed overlay。 -- `FrontendTypedLexicalEnvironmentTest`:overlay 写入拒绝 `symbolBindings()`、`resolvedMembers()`、`resolvedCalls()` 中所有 type-bearing payload 的 `GdCompilerType`;不得用 `FrontendWindowPublicationSurfaceTest` 代替该覆盖。 -- `FrontendTypedLexicalEnvironmentTest`:pending write、statement flush、suite export 三个时点对 compiler-only payload 的拒绝集合一致;不能出现 pending 接受、export 才拒绝的 coverage drift。 -- `FrontendWindowPublicationSurfaceTest`:保留为 API-level / legacy shim 测试,只验证 surface 自身 direct API 的 scratch / discard / conflict 语义;不得声明所有 `analyzeInWindow(...)` caller 都满足 scratch-over-stable。 -- `FrontendVarTypePostAnalyzerTest` 或 dedicated legacy regression:直接调用旧 `analyzeInWindow(...)` 后,即使调用 `window.discard()`,stable `slotTypes()` 已被 clear/write 污染;该测试用于记录旧路径不可作为 overlay 参考,而不是把污染行为转正为新 pipeline 行为。 -- `FrontendExprTypeAnalyzerTest` 或 dedicated legacy regression:旧 whole-module flow 中 `updateExpressionTypes(...)` 先于 `applyPatch(...)` 的 whole-table replace 行为只能作为 legacy snapshot path 记录,不得被新 overlay/export 方案复用为“先写 stable 再校验”先例。 -- Patch package 迁移测试:`FrontendAnalysisPatch`、`FrontendLocalSlotTypeUpdate` 的引用改为 `gd.script.gdcc.frontend.sema.patch`,旧 package 不保留同名生产类型;`FrontendWindowPublicationSurface` / `FrontendWindowAnalysisContext` 若保留或迁入,也必须标记为 legacy shim,不得被 production overlay 引用。 -- Analyzer rewrite inventory test / inspection:五个 owner analyzer 的 production SuiteResolver path 不调用 `analyzeInWindow(...)`,不从 `SourceFile` root 调用内部 `AstWalker...walk(...)`。 - -Interface phase 测试: - -- `FrontendInterfacePhaseTest.buildsSupportedBodyDeclarationIndexTypedBaselineAndSuiteEntryRoots`:构建 `FrontendBodyDeclarationIndex`,完整记录 supported block local declaration source order,同时锚定 typed baseline 与 suite entry roots 不写 stable typed side table。 -- `FrontendInterfacePhaseTest.keepsFutureDeclarationVisibleToResolverThroughCompleteBodyIndex`:`var first := second; var second := 1` 仍通过完整 inventory 被 resolver 过滤为 `DECLARATION_AFTER_USE_SITE`。 -- `FrontendInterfacePhaseTest.recordsStructuralSupportWithoutTypedBodyGates`:`FOR_BODY` 已进入 - suite entry / declaration index / baseline;lambda、match、block-local `const` 仍不发布其 - feature-owned inventory,且 interface surface 不产出 gate registry。 -- `FrontendInterfacePhaseTest.typedLexicalBaselineRejectsCompilerOnlySourceFacingTypes`:source-facing typed baseline 写入 `GdCompilerType` 时 fail-fast。 -- `FrontendSemanticAnalyzerFrameworkTest`:skeleton / scope / variable diagnostics snapshot boundary 不漂移。 - -Suite/body pipeline 测试: - -- `FrontendSuiteResolverTest`:source-order statement traversal 与 AST order 一致。 -- `FrontendSuiteResolverTest`:fake owner procedure 只收到当前 statement/header/expression root,不能拿到 module `SourceFile` root 重新 whole-module walk。 -- `FrontendSuiteResolverTest`:普通 statement 的 owner procedure 顺序固定为 top binding -> local stabilization -> chain binding -> expr typing -> var type post;for-range D1 完成后,`FOR_ITERATION_PLANNING` 只能插在 header expr typing 与 iterator var type post 之间。 -- `FrontendSuiteResolverTest`:chain binding 消费 receiver local 时看到 local stabilization 已写入的 exact slot fact。 -- `FrontendSuiteResolverTest`:nested chain / argument retry 可读取 owner-local transient facts,但这些 facts 不对其他 owner procedure 或 `TypedLexicalEnvironment` 普通 lookup 可见。 -- `FrontendSuiteResolverTest`:retry 后 final result 只写入同一 key 的最终 fact,不会把 earlier intermediate fact 写入 stable 或 committed overlay。 -- `FrontendSuiteResolverTest`:suite export 后 stable side table 状态等同于按固定顺序 apply 对应 per-owner patches 的结果。 -- `FrontendSuiteResolverTest`:local stabilization patch apply 后由 commit helper 派生刷新同 declaration 的 `symbolBindings()` payload,而不是通过同一个 patch 携带 binding delta。 -- `FrontendSuiteResolverTest`:`if` / `elif` / `else` / `while` header 先解析,body 后递归。 -- `FrontendSuiteResolverTest`:unsupported body 不进入 resolver。 -- `FrontendSemanticAnalyzerFrameworkTest`:默认 interface/body pipeline 发布 body facts、nested source-order facts,`for` 进入 shared semantic,match / lambda / block-local `const` 继续 structural fail-closed;测试不再通过 shared analyzer legacy whole-phase baseline。 -- `FrontendExprTypeAnalyzerTest`:owner-specific baseline 只能在测试内显式串联 focused owner analyzers;不得恢复 shared analyzer legacy bridge 或旧 expr-phase slot mutation。 - -Resolver 测试: - -- 同 block future local:`var x := y; var y := 1`,必须得到 `DECLARATION_AFTER_USE_SITE` filtered hit。 -- initializer self-reference:`var x := x`,必须得到 `SELF_REFERENCE_IN_INITIALIZER` filtered hit。 -- Structural request-domain 测试:supported body request 直接使用 `EXECUTABLE_BODY`;lambda、match、 - block-local `const`、parameter default 与 unknown/skipped request 使用精确 deferred domain,不存在 - readiness normalization。 -- AST boundary 测试:`ForStatement.body()` 与 for header edge 不再封口;lambda body、match - pattern/guard/body、block-local `const` initializer 与 parameter default 继续 structural - fail-closed。 -- Current-scope 测试:真实 `FOR_BODY` current scope 允许 normal lookup;synthetic - `LAMBDA_BODY`、`MATCH_SECTION_BODY` 与 lambda callable scope 即使缺少 AST edge 也继续 - fail-closed。 -- 双重结构封口测试:unsupported feature 同时覆盖 AST boundary 与 current-scope backstop,防止 - 只改一处导致 outer binding fallback。 -- Structural completeness 测试:supported body 的 scope、suite entry、body index、baseline 或 - iterator identity 任一缺失时 fail-fast,不能返回普通 `NOT_FOUND` 或静默跳过 suite。 - -Local stabilization 测试: - -- source-order alias chain 在 interface/body pipeline 下保持稳定。 -- child block 读取 parent 前缀稳定 local。 -- exact type 不允许被后续 statement / overlay export 改写为另一个 exact type。 -- 同一 statement 内,local stabilization pending slot 对后续 chain binding / expr typing 可见,但不允许 initializer self-reference 借此通过过滤。 -- assignment initializer / bare `TYPE_META` / dynamic fallback 保持 `Variant`。 -- `FrontendExprTypeAnalyzerTest` 继续覆盖 backfill guard:inventory-seeded `Variant` 不被 expr phase narrowing,已稳定同类型 no-op,已稳定异类型 fail-fast,compiler-only initializer fact fail-fast。 - -Structural body support 测试: - -- structural semantic support matrix 不读取 typed fact、overlay、iteration plan、diagnostic state 或 - compile readiness。 -- `FOR_BODY` 是 structural supported;lambda/match/const/parameter default 的 policy 保持精确 - unsupported/deferred domain,新增 scope kind 没有 allow-by-default。 -- 前缀 `:=` local 稳定后,后续 for header planning 可以读取 typed fact,但该事实不会改变 body - inventory、completeness certificate 或 suite entry。 -- for iterator 与 ordinary local 都有 declaration-index entry 和 source-facing baseline;缺失任一 - entry 时 fail-fast。 -- feature-specific `GdCompilerType` 不出现在 `expressionTypes()` / source-facing `slotTypes()` / - ordinary `ScopeValue.type()`,也不进入 structural policy/certificate。 - -Compile gate 测试: - -- interface/body facts 中残留 `DEFERRED` / `FAILED` / `UNSUPPORTED` 时仍被 compile gate 阻断。 -- for-range route-aware compile policy 尚未落地时,for-only source 由临时 statement-root - `sema.compile_check` blocker 阻断,不能进入 `FrontendCfgGraphBuilder`。 -- upstream diagnostic 去重跨 interface/body phase 生效。 -- `analyze(...)` 与 `analyzeForCompile(...)` split 不变。 - -## 7. 风险与缓解 - -### R1:side-table 冲突被静默覆盖 - -缓解:overlay export 必须通过 per-owner patch merge API;默认不同 value fail-fast。需要覆盖 overlay shadow stable、idempotent、conflict tests 与 patch transaction 顺序 tests。这里的 fail-fast 只阻止当前冲突 patch 覆盖既有 value,不意味着 transaction 或 callable batch 失败后无部分提交;非原子失败语义见第 4.3、4.6 节与 R18。 - -### R2:resolver 看不到未来声明 - -缓解:interface phase 必须基于基础结构层已发布的 inventory 为 supported suite 建立完整 local declaration index。禁止 resolver 自己扫描普通 `var` 弥补缺口;resolver 通过 scope 的已发布 inventory 完成名称查找,并读取 index 与 structural completeness certificate 验证该命中属于可解析 body。现有 source byte range filter 继续产出 declaration-after-use 与 initializer self-reference provenance;仅有 local declaration `sourceOrder` 不能替代任意 use-site 的位置模型。参见第 3.2 节:完整 inventory 是 resolver filtered-hit 模型的前提,不是为了与 Godot source-order analysis 对立。 - -### R3:scope slot mutation 与 published binding payload 脱节 - -缓解:local slot rewrite 通过 `FrontendLocalTypeStabilizationPatch` 携带的 `FrontendLocalSlotTypeUpdate` 统一应用,并由同一个 commit helper 派生刷新同 declaration 的 `symbolBindings()`。禁止 local stabilization patch 同时携带独立 `symbolBindings()` delta。 - -### R4:历史 backfill 路径恢复第二个 slot mutation owner - -缓解:`FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 必须是 strict no-op / guard-only;测试同时锁住“不调用 `BlockScope.resetLocalType(...)`”、“不刷新 `symbolBindings()` payload”、“不生成 `FrontendLocalSlotTypeUpdate`”。需要新的 narrowing 能力时只能扩展 local stabilization。 - -### R5:structural capability 与实际 inventory completeness 被混为一谈 - -缓解:immutable structural semantic support matrix 只回答某种 AST/scope kind 的 inventory path -是否已实现;`FrontendSuiteEntryRoots`、`FrontendBodyDeclarationIndex`、typed baseline 与 scope -identity 共同证明本次 interface surface 的 structural completeness,且 declaration index 对 body -scope inventory 必须双向一致(index entry 回到 scope/baseline,scope 中每条 published `LOCAL` -都能在 index 找到)。`canPublish...` 或等价 allowlist 不能单独充当 body-entry certificate。缺失 -结构事实必须 fail-fast,不能静默跳过 body,也不能重新引入 `PENDING` / `PUBLISHED` lifecycle。 - -### R6:`SuiteResolver` 绕过 phase owner 边界 - -缓解:`SuiteResolver` 只编排 owner 子过程,不直接写 owner side table。每个 side table 的写入 API 应保留 owner 意图,`FrontendSuiteContext` 必须校验当前 runner identity 与目标 overlay owner 匹配,per-owner patch 类型也必须编码 semantic owner identity 并在 merge-time 校验,不能复用 `ScopeOwnerKind`,测试覆盖错误 owner 写入 fail-fast 或不可达。 - -### R7:diagnostics 重复或顺序漂移 - -缓解:interface phase、body statement、suite export、diagnostics-only phase 都有明确 diagnostics snapshot 边界;新增跨 phase duplicate suppression tests。若顺序需要调整,必须更新 framework probe tests 与文档。 - -### R8:unsupported subtree 被 structural support matrix 过早打开 - -缓解:support matrix 对 lambda parameter/capture/body、match pattern/guard/body、block-local -`const` initializer、parameter default 和 unknown/skipped structure 显式返回各自 deferred domain; -新增 scope/AST kind 必须通过 exhaustive mapping 显式选择 policy。AST boundary 与 current-scope -backstop 同时保留,不能依赖 `default -> EXECUTABLE_BODY`。 - -### R9:compiler-only type 泄漏 - -缓解:阶段 C3 已通过 `FrontendPublishedFactTypeGuard` shared walker 覆盖 binding/member/call/expression/slot/update 六类 source-facing typed payload,并接入 patch、overlay 与保留的 whole-table publication API。feature-specific compiler state 仍只能通过专用 contract 给 CFG/lowering;新增 type-bearing payload 时必须同步扩展 shared walker 与 regression tests,否则本风险重新打开。`FrontendWindowPublicationSurface` 不能作为 guard 完整性的基线,因为历史 VarTypePost window caller 曾在 surface guard 生效前直接写入 stable `slotTypes()`。 - -补充:如果保留 `updateXxx(...)` whole-table publish 或可变 stable side-table 引用,它们也必须被纳入同一风险面。只要 production body path 还能通过 `analysisData.xxx().put()/clear()/putAll()`、`updateXxx(...)` whole-table replace 或 window caller 中转直接触达 stable typed table,就不能宣称 overlay export 安全性已经被证明。 - -### R10:已实施过渡资产继续扩大影响面 - -缓解:阶段 A 必须冻结资产分类。`FrontendSegmentedSemanticScheduler` 与 `segmentedSemanticRunner` 只能作为迁移测试旁路或删除对象,不能继续承载新功能。 - -### R11:实现一次性改动过大 - -缓解:先冻结资产边界,再落地 interface 数据结构、overlay、suite skeleton、owner 子过程、diagnostics 与默认切换;历史 typed-dependent gate 按 L0-L9 的 compile-safe 小阶段迁移并最后删除类型。每个子阶段都应有 targeted tests,单个 patch batch 不超过三个文件。 - -### R12:statement 内 owner 顺序或 overlay 导出时机漂移 - -缓解:第 4.3 节的 base owner procedure 顺序不可重排;feature-specific semantic route owner 只能在 expr typing 与对应 var type post 之间插入。第 4.4 节的 pending -> committed -> stable export 时机不可折叠。Statement flush 不写 stable side table,suite export 只能通过按 owner 有序的 patch transaction 更新 stable facts,suite export 后 diagnostics-only phase 才能运行。测试必须覆盖 chain binding 读取 receiver local 时已经看到 local stabilization 的 exact slot fact、for planning 读取 header expr fact,以及 suite export 前后 stable side table / `BlockScope` 状态。 - -### R13:resolver structural domain、AST edge 与 current scope 漂移 - -缓解:supported `FOR_BODY` 的 request domain、`ForStatement.body()`/header AST edge 与 current -scope 必须同时允许 ordinary executable lookup;unsupported lambda/match/const/parameter-default -则由同一 structural policy 返回精确 deferred domain。request-domain hard boundary、AST boundary -与 current-scope backstop 仍是三类独立检查,但不再读取 readiness registry。测试必须同时覆盖 -supported for 的三处放行与 unsupported feature 的 AST/current-scope 双重封口。 - -### R14:retry 中间 expression type 被导出导致 patch 冲突 - -缓解:`expressionTypes()` stable merge 保持严格 `sameExpressionType` 判据,不增加 `Variant -> exact`、parent -> child 或 status upgrade 例外。Chain / argument retry 的中间 expression facts 只能存放在 owner procedure 内部非导出 transient cache;statement pending overlay、current-suite committed overlay 与 `FrontendExprTypePatch.expressionTypes()` 都只能包含每 key 最终单条 fact。测试必须覆盖 same key different value fail-fast,以及 retry 后 stable / committed table 不含 stale intermediate fact。 - -### R15:single-stage patch 被误用为 multi-owner suite export - -缓解:suite export 生产路径不得构造跨 owner `FrontendAnalysisPatch`。旧 `FrontendAnalysisPatch` 迁入 patch package 后只能作为 legacy single-stage patch / 测试兼容层或被拆解;`FrontendPatchTransaction` 必须按固定 owner 顺序 apply per-owner patches,并拒绝乱序、重复 owner 或单一 patch 内跨 owner 的 payload。该 per-patch owner 校验不构成 callable batch 的跨 transaction 预检。 - -### R16:把 whole-module analyzer 包装成 body runner - -缓解:阶段 A 必须完成 walker-state inventory,阶段 D 必须建立不调用 `analyzeInWindow(...)` 的 root-bounded SuiteResolver skeleton,阶段 E 才能接入真实 owner procedure。任何 production SuiteResolver path 若调用 `analyzeInWindow(...)`、内部 `AstWalker...walk(sourceFile)` 或整表 `updateXxx(...)` 来表达 body result,都视为计划违约而非阶段性完成。 - -### R17:`FrontendWindowPublicationSurface` 被误认为纯 scratch 参考 - -缓解:`FrontendWindowPublicationSurface` 的 direct API 可以表达 scratch-over-stable,但现有 analyzer caller 已经违反该模型:`FrontendVarTypePostAnalyzer.analyzeInWindow(...)` 直接 clear/write stable `slotTypes()`,再复制到 window scratch。阶段 A 必须把该类型降级为 legacy shim;阶段 C overlay 必须独立实现和验证;阶段 E var type post 重写不得复用旧 window path。任何以 `FrontendWindowPublicationSurfaceTest` 代替 overlay isolation test 的验收都无效。 - -### R18:ordered transaction / callable batch 被误认为原子提交 - -当前限制:`FrontendPatchTransaction` 只保证 owner 顺序,`FrontendCallableExportBatch` 只保证延迟到 root callable 返回后按追加顺序 apply。二者都不做整体 prepare、不提供失败回滚;batch 也看不到 queued transaction 之间尚未进入 stable state 的冲突。后续 patch / transaction 失败时,之前已经 apply 的 stable side table、local slot 与 binding payload 会保留。本阶段不修复该 corner case;代码与文档必须持续声明 non-atomic 合同,production path 必须传播 apply 异常并丢弃整个 `FrontendAnalysisData`。未来若引入增量复用、可恢复 diagnostics 或继续分析语义,必须先落地覆盖整个 callable batch 的两阶段 prepare/commit,而不能只给单个 patch 增加预检。 - -## 8. 完成定义 - -本计划完成时应满足: - -- frontend shared semantic 默认使用 interface/body pipeline,且现有 supported surface 行为等价。 -- 过渡用 `FrontendSegmentedSemanticScheduler` 与 `segmentedSemanticRunner` 已删除,或只作为明确隔离的测试辅助存在。 -- interface phase 建立完整 local declaration index、typed baseline 与 suite entry roots,但不做 body typed resolution,也不产出 gate registry。 -- `SuiteResolver` 按 source order 解析 supported body,并在 child body 前验证 structural completeness certificate;typed refinement 不参与 body-entry 决策。 -- `SuiteResolver` 的 base statement owner 顺序固定为 top binding -> local stabilization -> chain binding -> expr typing -> var type post;concrete feature-specific semantic route owner 只能插在 expr typing 与对应 var type post 之间。 -- Production SuiteResolver path 不调用 `analyzeInWindow(...)`、不从 module `SourceFile` root 启动内部 `AstWalker...walk(...)`,也不通过整表 `updateXxx(...)` 表达 body typed result。 -- 五个 owner analyzer 的 whole-module walker state inventory 已关闭,并已用 statement-local rewrite 替代 production body path;现有 walker 只可作为 legacy comparison path 或删除对象存在。 -- typed overlay 能区分 current statement pending facts 与 current suite committed facts,export 前不污染 stable side table 或 `BlockScope`;该标准明确排除旧 `FrontendVarTypePostAnalyzer.analyzeInWindow(...)` 的 stable `slotTypes()` clear/write 模式。 -- `FrontendAnalysisData` 支持带 conflict / guard 校验的 per-owner patch merge,并保持 stable reference 合同;这里的安全边界只覆盖当前 patch,不包含跨 patch 原子性。 -- Suite export 使用按 top binding -> local stabilization -> chain binding -> expr typing -> optional feature-specific semantic route planning -> var type post 顺序 apply 的 `FrontendPatchTransaction` 或等价机制;生产路径不使用 single-stage `FrontendAnalysisPatch` 承载多 owner facts。该 transaction 与 callable batch 明确为 non-atomic ordered apply,失败后必须丢弃当前 analysis state。 -- 所有 production patch 相关类型位于 `gd.script.gdcc.frontend.sema.patch` 包,包括旧 `FrontendAnalysisPatch`、`FrontendLocalSlotTypeUpdate`、新建 per-owner patch 类型、transaction 与 shared merge / guard helper。`FrontendWindowPublicationSurface` / `FrontendWindowAnalysisContext` 若保留或迁入该包,也必须标记为 legacy shim,不能作为 production overlay/export 参考。 -- patch commit 与 typed overlay 写入的 compiler-only guard 覆盖所有 user-visible type-bearing publication surfaces:`symbolBindings()`、`resolvedMembers()`、`resolvedCalls()`、`expressionTypes()`、`slotTypes()`、`localSlotTypeUpdates()`;guard 完整性不得以 `FrontendWindowPublicationSurface` 行为作为证明。 -- shared compiler-only walker 同时用于 patch commit、overlay pending write、overlay flush,以及任何保留的 source-facing whole-table publication API;新增 type-bearing payload 时必须同步更新该 walker 与对应 regression tests。 -- production body path 不通过 `FrontendAnalysisData.symbolBindings()/resolvedMembers()/resolvedCalls()/expressionTypes()/slotTypes()` 返回的可变 stable 引用直接 `put()` / `clear()` / `putAll()`,也不通过 `updateXxx(...)` whole-table replace 把 body typed facts 中转到 stable side table 后再校验。 -- `FrontendExprTypeAnalyzer.backfillInferredLocalType(...)` 不改写 `BlockScope`、不刷新 `symbolBindings()` payload、不产生 slot update,只保留 guard-only 协议检查。 -- supported suite 的完整 local inventory 先于 body typed resolution,production resolver 使用 declaration index 验证 scope local identity,且 declaration-after-use filtered hit 行为不退化。 -- local `:=` 的 source-order type stabilization 可被后续 statement 与 feature-specific semantic route planning 消费,但不能改变 structural body entry。 -- chain binding 消费 receiver local slot 时,必须看到 local stabilization 已发布到 overlay 的 exact type,而不是 interface baseline `Variant`。 -- immutable structural semantic support matrix 是 AST/scope kind 支持面的单一事实源;它不读取 typed fact、compile readiness 或 lifecycle state。 -- supported body 只有在 scope identity、suite entry root、body declaration index 与 typed baseline 全部满足 structural completeness certificate 后才进入 `SuiteResolver`;certificate 对 published body inventory 做 index↔scope 双向校验与 AST 源序 `sourceOrder` 校验,缺洞 fail-fast。 -- resolver 的 request-domain hard boundary、AST boundary 与 current-scope backstop 不读取 registry;supported `FOR_BODY` 三处均放行,unsupported lambda/match/const/parameter-default 继续 structural fail-closed。 -- for header 与 body edge 可区分:header typed resolution 可读取前缀 facts并驱动后续 iteration planning,但 body entry 已由无条件 structural inventory 与 D0 child-suite path 决定。 -- nested chain / argument retry 不会产生 stable `expressionTypes()` narrowing rewrite 或 status upgrade;每个 expression / step key 在 overlay export 与 stable table 中最多有一个最终 fact。 -- `applyPatch` 对 `FrontendExprTypePatch.expressionTypes()` 的 conflict 检测保持 status + publishedType + detailReason 严格相等,只有 local slot update 保留 `Variant -> exact` 例外;如未来需要表达受控 expression narrowing,必须新增显式 merge/upgrade 机制,而不是复用当前 republish 路径。 -- unsupported subtree 仍通过 structural policy、AST boundary 与 current-scope backstop fail-closed,不能 fallback 或误发布 body facts。 -- compile gate、lowering-ready fact 边界和 compiler-only type 隔离不变。 -- `doc/analysis/frontend_segmented_type_resolution_pipeline_execution_summary.md` 已根据最终实现同步更新,用作目标架构摘要,并与本完成定义中的 pipeline 层级、owner 顺序、overlay 生命周期、patch/export 合同、compiler-only guard 与 diagnostics / compile gate 边界保持一致。 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 index 1bc31784..650a2502 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProcedures.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProcedures.java @@ -70,10 +70,10 @@ import java.util.Objects; import java.util.function.Consumer; -/// Statement-local owner procedures used by the new body SuiteResolver path. +/// 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 call any legacy whole-module analyzer entrypoint. Facts +/// 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 { 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 686c96f7..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 @@ -224,9 +224,9 @@ private FrontendSemanticAnalyzer( variableAnalyzer.analyze(analysisData, diagnosticManager); analysisData.updateDiagnostics(diagnosticManager.snapshot()); - // Interface analysis freezes callable/property entry roots before the only body owner - // publication path runs. Phase I removes the legacy whole-phase bypass so shared facts can - // only enter stable storage through SuiteResolver's per-owner export transaction. + // 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()); 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 index 303ed084..f9a5a0c0 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendStatementResolver.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendStatementResolver.java @@ -20,7 +20,7 @@ import java.util.Objects; -/// Root-bounded statement dispatcher for the new body SuiteResolver skeleton. +/// 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 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 index f1076757..dca3d664 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteContext.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteContext.java @@ -21,7 +21,7 @@ import java.nio.file.Path; import java.util.Objects; -/// Statement-local context passed through the new body SuiteResolver skeleton. +/// 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 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 index 29fbb62d..e499f4a7 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolver.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolver.java @@ -29,7 +29,7 @@ /// Body-suite coordinator for the staged semantic pipeline. /// -/// The default resolver now uses statement-local owner procedures. Tests may still inject a custom +/// 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 { 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 f66daa43..576086cd 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendSemanticAnalyzerFrameworkTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendSemanticAnalyzerFrameworkTest.java @@ -779,7 +779,7 @@ func ping(value): assertFalse(result.slotTypes().isEmpty()); } - /// Locks the Stage K boundary: active phase injection remains available while legacy body-owner + /// 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 { diff --git a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSegmentedPipelineTestSupport.java b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSegmentedPipelineTestSupport.java index 5ac98ba6..785780bb 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSegmentedPipelineTestSupport.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSegmentedPipelineTestSupport.java @@ -11,7 +11,7 @@ import java.util.EnumSet; import java.util.Set; -/// Test harness for running real root-bounded owner procedures without restoring whole-module analyzers. +/// Test harness for running real root-bounded owner procedures through the body SuiteResolver path. final class FrontendSegmentedPipelineTestSupport { private FrontendSegmentedPipelineTestSupport() { } From 1f824a1829018bc618f7614ffe91a68c5eac8a69 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Thu, 23 Jul 2026 13:07:29 +0800 Subject: [PATCH 27/27] refactor(frontend): align semantic tests with suite resolver pipeline - Replace legacy segmented test support with suite resolver stage infrastructure - Update focused analyzer regressions to use the consolidated pipeline - Add explicit null-safety checks and null-safe diagnostic comparisons - Document intentional test helper parameter reuse for clean IDE inspections - Verify compilation and targeted frontend test coverage --- .../sema/FrontendAnalysisDataTest.java | 11 ++++- ...ontendBodyOwnerProceduresExprTypeTest.java | 11 +++-- .../FrontendCompileCheckAnalyzerTest.java | 42 +++++++++++++------ ...rontendSuiteResolverStageTestSupport.java} | 4 +- .../FrontendTypeCheckAnalyzerTest.java | 40 +++++++++++------- .../FrontendVirtualOverrideAnalyzerTest.java | 42 +++++++++++++------ 6 files changed, 104 insertions(+), 46 deletions(-) rename src/test/java/gd/script/gdcc/frontend/sema/analyzer/{FrontendSegmentedPipelineTestSupport.java => FrontendSuiteResolverStageTestSupport.java} (97%) 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 8e400c62..3475969f 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendAnalysisDataTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendAnalysisDataTest.java @@ -603,7 +603,7 @@ void applyPatchSkipsBindingRefreshForNoOpUpdateAndExprTypeOnlyPatch() throws Exc } @Test - void applyPatchRejectsWrongStageLocalSlotUpdatesAndSourceFacingCompilerOnlyLeaks() throws Exception { + void applyPatchRejectsCompilerOnlyLeaksAcrossExpressionAndSlotTypePayloads() throws Exception { var analysisData = FrontendAnalysisData.bootstrap(); var expressionTypes = new FrontendAstSideTable(); expressionTypes.put(identifier("iter"), FrontendExpressionType.resolved(GdccForRangeIterType.FOR_RANGE_ITER)); @@ -791,7 +791,10 @@ void ownerPatchTransactionAppliesInFixedOwnerOrderAndRejectsRegressions() { )).applyTo(analysisData); assertSame(binding, analysisData.symbolBindings().get(bindingNode)); - assertEquals(GdIntType.INT, analysisData.expressionTypes().get(expressionNode).publishedType()); + assertEquals( + GdIntType.INT, + Objects.requireNonNull(analysisData.expressionTypes().get(expressionNode)).publishedType() + ); assertThrows( FrontendAnalysisPatchException.class, () -> new FrontendPatchTransaction(List.of( @@ -863,6 +866,7 @@ private static PassStatement passNode() { ); } + @SuppressWarnings("SameParameterValue") private static @NotNull FrontendBinding localBinding( @NotNull String name, @NotNull Object declaration, @@ -877,6 +881,7 @@ private static PassStatement passNode() { ); } + @SuppressWarnings("SameParameterValue") private static @NotNull ScopeValue requireLocal(@NotNull BlockScope scope, @NotNull String name) { var value = scope.resolveValueHere(name); if (value == null) { @@ -893,6 +898,7 @@ private static PassStatement passNode() { return new BlockScope(callableScope, BlockScopeKind.FUNCTION_BODY); } + @SuppressWarnings("SameParameterValue") private static @NotNull FrontendOwnerPatch patch( @NotNull FrontendSemanticStage stage, @NotNull FrontendAstSideTable symbolBindings, @@ -911,6 +917,7 @@ private static PassStatement passNode() { }; } + @SuppressWarnings("SameParameterValue") private static @NotNull FrontendOwnerPatch patch( @NotNull FrontendSemanticStage stage, @NotNull FrontendAstSideTable symbolBindings diff --git a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProceduresExprTypeTest.java b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProceduresExprTypeTest.java index 171b07b6..dd75ef92 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProceduresExprTypeTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendBodyOwnerProceduresExprTypeTest.java @@ -47,6 +47,7 @@ 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; @@ -60,6 +61,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +@SuppressWarnings("DataFlowIssue") class FrontendBodyOwnerProceduresExprTypeTest { @Test void analyzePublishesResolvedAtomicAndChainExpressionTypes() throws Exception { @@ -2486,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") ); @@ -2679,11 +2681,11 @@ func ping(): var diagnostics = new DiagnosticManager(); var parserService = new GdScriptParserService(); var unit = parserService.parseUnit(Path.of("tmp", fileName), source, diagnostics); - var analysisData = analyzeWithSegmentedPipeline(unit, registry, diagnostics, topLevelCanonicalNameMap); + var analysisData = analyzeWithFrontendPipeline(unit, registry, diagnostics, topLevelCanonicalNameMap); return new AnalyzedScript(unit.ast(), analysisData); } - private static @NotNull FrontendAnalysisData analyzeWithSegmentedPipeline( + private static @NotNull FrontendAnalysisData analyzeWithFrontendPipeline( @NotNull FrontendSourceUnit unit, @NotNull ClassRegistry classRegistry, @NotNull DiagnosticManager diagnostics, @@ -2696,6 +2698,7 @@ func ping(): ); } + @SuppressWarnings("SameParameterValue") private static @NotNull PreparedExpressionInput prepareInputBeforeExpressionTyping( @NotNull String fileName, @NotNull String source @@ -2732,7 +2735,7 @@ func ping(): FrontendSemanticStage.CHAIN_BINDING ) : Set.of(FrontendSemanticStage.TOP_BINDING, FrontendSemanticStage.CHAIN_BINDING); - FrontendSegmentedPipelineTestSupport.resolveOwners( + FrontendSuiteResolverStageTestSupport.resolveOwners( classRegistry, analysisData, diagnostics, 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 edbc1727..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 @@ -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"))); @@ -787,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, @@ -918,7 +928,7 @@ 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); @@ -947,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") ); @@ -992,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") ); @@ -1110,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)); @@ -1289,7 +1307,7 @@ func ping(flag): analysisData.updateDiagnostics(diagnosticManager.snapshot()); new FrontendVariableAnalyzer().analyze(analysisData, diagnosticManager); analysisData.updateDiagnostics(diagnosticManager.snapshot()); - FrontendSegmentedPipelineTestSupport.resolveAllOwners(classRegistry, analysisData, diagnosticManager); + 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/FrontendSegmentedPipelineTestSupport.java b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolverStageTestSupport.java similarity index 97% rename from src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSegmentedPipelineTestSupport.java rename to src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolverStageTestSupport.java index 785780bb..881e48d2 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSegmentedPipelineTestSupport.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/FrontendSuiteResolverStageTestSupport.java @@ -12,8 +12,8 @@ import java.util.Set; /// Test harness for running real root-bounded owner procedures through the body SuiteResolver path. -final class FrontendSegmentedPipelineTestSupport { - private FrontendSegmentedPipelineTestSupport() { +final class FrontendSuiteResolverStageTestSupport { + private FrontendSuiteResolverStageTestSupport() { } static void resolveAllOwners( 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 30e4125d..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,7 +1308,7 @@ private static void assertEvent( analysisData.updateDiagnostics(diagnosticManager.snapshot()); new FrontendVariableAnalyzer().analyze(analysisData, diagnosticManager); analysisData.updateDiagnostics(diagnosticManager.snapshot()); - FrontendSegmentedPipelineTestSupport.resolveAllOwners(classRegistry, analysisData, diagnosticManager); + 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 04eefe74..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,7 +298,7 @@ func _process(delta) -> void: analysisData.updateDiagnostics(diagnosticManager.snapshot()); new FrontendVariableAnalyzer().analyze(analysisData, diagnosticManager); analysisData.updateDiagnostics(diagnosticManager.snapshot()); - FrontendSegmentedPipelineTestSupport.resolveAllOwners(classRegistry, analysisData, diagnosticManager); + FrontendSuiteResolverStageTestSupport.resolveAllOwners(classRegistry, analysisData, diagnosticManager); new FrontendAnnotationUsageAnalyzer().analyze(classRegistry, analysisData, diagnosticManager); analysisData.updateDiagnostics(diagnosticManager.snapshot()); return new PreparedVirtualOverrideInput(units, analysisData, diagnosticManager, classRegistry); @@ -326,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 @@ -377,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"); } } @@ -391,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"); } } }