From 18591531e9260d97b29cbbce08452a56d09e9a31 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Mon, 29 Jun 2026 16:14:27 +0800 Subject: [PATCH 01/16] docs(frontend): add GdCompilerType design risk analysis and plan - Document the design risk analysis for the compiler-only type branch, including sealed-interface risks, fact sources reviewed, and recommended boundaries. - Outline the implementation plan covering target boundary, scope, cross-module contract, and acceptance criteria. --- .../gdcompiler_type_design_risk_analysis.md | 466 +++++++++++++++ .../frontend/frontend_gdcompiler_type_plan.md | 544 ++++++++++++++++++ 2 files changed, 1010 insertions(+) create mode 100644 doc/analysis/gdcompiler_type_design_risk_analysis.md create mode 100644 doc/module_impl/frontend/frontend_gdcompiler_type_plan.md diff --git a/doc/analysis/gdcompiler_type_design_risk_analysis.md b/doc/analysis/gdcompiler_type_design_risk_analysis.md new file mode 100644 index 00000000..641549eb --- /dev/null +++ b/doc/analysis/gdcompiler_type_design_risk_analysis.md @@ -0,0 +1,466 @@ +# GdCompilerType 设计风险调研报告 + +- 日期:2026-06-28 +- 范围:本报告只做设计与风险调研,不实现源码改动。事实来源包括当前仓库文档、当前仓库源码、并行子代理调研摘要,以及上游 Godot 公开仓库中与 GDScript 内部运行时状态相关的实现线索。 + +--- + +## 1. 执行摘要 + +计划新增的 `GdCompilerType` 需求可以成立,但它不能被当作普通 GDScript/Godot 类型接入现有 `GdType` 流水线。当前代码库里 `GdType` 同时承担了用户可见语义类型、LIR XML 文本类型、C 类型渲染、Variant pack/unpack helper 命名、binding metadata 和生命周期判断等职责。新增 compiler-only 分支时,最大风险不是 sealed interface 上多一个 permits,而是它被这些默认路径静默当作 Godot builtin 或普通 `Variant` 可转换类型处理。 + +推荐的设计边界是: + +1. `GdCompilerType` 只允许出现在 compiler/lowering/LIR/backend 内部变量或 intrinsic operand/result 上。 +2. 它不能进入 source-facing type namespace、用户声明类型解析、`expressionTypes()` 用户语义事实、function/property/signal/callable outward metadata、implicit conversion matrix、`Variant` pack/unpack、engine method/utility/global call ABI。 +3. 每个具体 compiler-only 类型必须显式给出 C 类型名、初始化 helper 名和销毁 helper 名。helper 应使用 `gdcc_*` 命名空间,不应复用或伪造 `godot_*` generated binding surface。 +4. 如果 compiler-only 类型是 destroyable 非对象值,生命周期必须走非对象 destroyable 路径;如果它按设计是不可变按值传递,代码仍需要避免现有 value-wrapper 复制路径自动拼 `godot_new__with_`。 +5. 实现时应优先在边界入口 fail-fast,而不是依赖 `getGdExtensionType() == null` 被后续 metadata helper 偶然拦住。 + +上游 Godot 的实现也支持这个边界:`GDScriptFunctionState`、iterator state、VM stack slots 属于 runtime/VM/codegen 内部机制,并没有扩展为 `GDScriptDataType` 的公开类型分类。GDCC 如果需要 C runtime 的迭代器、函数状态等结构,应把它们保持在 IR/backend/runtime 私有模型中。 + +--- + +## 2. 已复核事实源 + +### 2.1 文档事实源 + +- `AGENTS.md` +- `doc/gdcc_type_system.md` +- `doc/gdcc_low_ir.md` +- `doc/gdcc_lir_intrinsic.md` +- `doc/gdcc_c_backend.md` +- `doc/gdcc_runtime_lib.md` +- `doc/gdcc_ownership_lifecycle_spec.md` +- `doc/module_impl/common_rules.md` +- `doc/module_impl/backend/backend_ownership_lifecycle_contract.md` +- `doc/module_impl/backend/lifecycle_instruction_restriction.md` +- `doc/module_impl/backend/variant_abi_contract.md` +- `doc/module_impl/backend/implicit_conversion_implementation.md` +- `doc/module_impl/backend/builtin_builder_implementation.md` +- `doc/module_impl/frontend/frontend_rules.md` +- `doc/module_impl/frontend/frontend_type_check_analyzer_implementation.md` +- `doc/module_impl/frontend/frontend_implicit_conversion_matrix.md` +- `doc/module_impl/frontend/frontend_dynamic_call_lowering_implementation.md` +- `doc/module_impl/frontend/frontend_builtin_constructor_variant_implementation.md` +- `doc/module_impl/frontend/frontend_builtin_property_access_implementation.md` +- `doc/module_impl/frontend/frontend_lowering_plan.md` +- `doc/module_impl/frontend/frontend_lowering_func_pre_pass_implementation.md` +- `doc/module_impl/frontend/scope_type_resolver_implementation.md` +- `doc/module_impl/frontend/scope_analyzer_implementation.md` +- `doc/module_impl/frontend/frontend_visible_value_resolver_implementation.md` +- `doc/module_impl/frontend/frontend_exact_call_extension_metadata_contract.md` +- `doc/module_impl/frontend/runtime_name_mapping_implementation.md` + +备注:`AGENTS.md` 中提到 `doc/module_impl/common_rule.md`,实际仓库文件名是 `doc/module_impl/common_rules.md`。 + +### 2.2 源码事实源 + +- `src/main/java/gd/script/gdcc/type/GdType.java` +- `src/main/java/gd/script/gdcc/type/GdVariantType.java` +- `src/main/java/gd/script/gdcc/type/GdVoidType.java` +- `src/main/java/gd/script/gdcc/type/GdExtensionTypeEnum.java` +- `src/main/java/gd/script/gdcc/scope/ClassRegistry.java` +- `src/main/java/gd/script/gdcc/scope/resolver/ScopeTypeResolver.java` +- `src/main/java/gd/script/gdcc/lir/LirVariable.java` +- `src/main/java/gd/script/gdcc/lir/LirFunctionDef.java` +- `src/main/java/gd/script/gdcc/lir/LirParameterDef.java` +- `src/main/java/gd/script/gdcc/lir/parser/DomLirParser.java` +- `src/main/java/gd/script/gdcc/lir/parser/DomLirSerializer.java` +- `src/main/java/gd/script/gdcc/lir/validation/LifecycleInstructionRestrictionValidator.java` +- `src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendVariantBoundaryCompatibility.java` +- `src/main/java/gd/script/gdcc/frontend/lowering/pass/body/FrontendBodyLoweringSession.java` +- `src/main/java/gd/script/gdcc/frontend/lowering/FrontendSubscriptAccessSupport.java` +- `src/main/java/gd/script/gdcc/frontend/lowering/FrontendWritableTypeWritebackSupport.java` +- `src/main/java/gd/script/gdcc/backend/c/gen/CGenHelper.java` +- `src/main/java/gd/script/gdcc/backend/c/gen/CCodegen.java` +- `src/main/java/gd/script/gdcc/backend/c/gen/CBodyBuilder.java` +- `src/main/java/gd/script/gdcc/backend/c/gen/CIntrinsicManager.java` +- `src/main/java/gd/script/gdcc/backend/c/gen/intrinsic/CIntToFloatIntrinsic.java` +- `src/main/java/gd/script/gdcc/backend/c/gen/insn/CallIntrinsicInsnGen.java` +- `src/main/java/gd/script/gdcc/backend/c/gen/insn/PackUnpackVariantInsnGen.java` +- `src/main/java/gd/script/gdcc/backend/c/gen/insn/CallMethodInsnGen.java` +- `src/main/java/gd/script/gdcc/backend/c/gen/insn/BackendMethodCallResolver.java` +- `src/main/c/codegen/template_451/func.ftl` + +### 2.3 上游 Godot 参考线索 + +并行调研覆盖了 `godotengine/godot` 的以下路径: + +- `modules/gdscript/gdscript_function.h` +- `modules/gdscript/register_types.cpp` +- `modules/gdscript/gdscript_byte_codegen.cpp` +- `modules/gdscript/gdscript_vm.cpp` +- `modules/gdscript/gdscript_analyzer.cpp` +- `core/variant/variant_setget.cpp` +- `modules/gdscript/tests/scripts/analyzer/errors/invalid_identifier.out` + +结论是:Godot 的 iterator/function-state 机制停留在 VM/runtime 层。`GDScriptFunctionState` 是 internal registered class,脚本中不可直接命名;for-loop iterator state 是 codegen/VM 临时槽位组合,不是公开 `GDScriptDataType`。 + +--- + +## 3. 当前 GdType 的耦合面 + +### 3.1 sealed 根接口 + +`GdType` 当前是 sealed interface,permits 列表只允许 container/meta/nil/object/primitive/rid/string-like/variant/vector/void。新增 `GdCompilerType` 必须改 permits,否则无法实现根接口。 + +`GdType` 的协议方法很少: + +- `getTypeName()` +- `isNullable()` +- `getGdExtensionType()` +- `isDestroyable()` + +问题在于 `getTypeName()` 已经被多个层当作不同语义使用:诊断名、LIR XML 文本、C symbol 片段、Godot helper 名称片段、typed container hint 片段。`GdCompilerType` 如果只返回一个普通名字,就会被许多默认路径自动拼成不存在的 Godot wrapper。 + +### 3.2 ClassRegistry 解析与 assignability + +`ClassRegistry.findType(...)` 是 LIR XML 反序列化和兼容解析入口。它先走 strict declared-type resolver,再对兼容模式中的未知 bare identifier 猜成 object。`tryParseStrictTextType(...)` 列出了所有用户/严格声明可识别类型。`ScopeTypeResolver` 也是 source-facing type namespace 的入口。 + +因此: + +- compiler-only 类型不应加入 strict declared type 解析,否则用户声明位置可见。 +- 如果 compiler-only 类型需要参与 LIR XML 往返,应新增 LIR-only 解析策略,不能把它注册进普通 type namespace。 +- `ClassRegistry.checkAssignable(...)` 当前第一条规则是 `from.getTypeName().equals(to.getTypeName())` 即 assignable。具体 compiler-only 子类型的 `getTypeName()` 必须稳定且唯一;如果未来有参数化内部类型,只靠字符串同名可能过弱。 + +### 3.3 LIR 类型承载面 + +`LirVariable`、`LirFunctionDef`、`LirParameterDef` 都直接持有 `GdType`。这使 compiler-only 类型一旦进入 LIR,就天然可能进入: + +- 函数参数和返回类型。 +- 变量 prepare block。 +- return flow 和 `_return_val`。 +- C 函数签名模板。 +- binding data collection。 +- LIR XML serializer/parser。 + +需求明确“不会出现在自定义函数签名中”。这不等于“不会进入 LIR”;它更准确地要求:若 compiler-only 类型进入 LIR,只能作为内部 local/temp 或 backend-owned hidden surface,不能进入公开 class/function/property/signal ABI。 + +--- + +## 4. 前端与用户可见边界 + +### 4.1 不进入 source-facing 类型解析 + +frontend/scope 文档都强调 source-facing lookup、canonical identity、human display 和 lowering/private facts 的分层。`ScopeTypeResolver` 只处理源码声明类型文本和 type-meta namespace,`ScopeTypeMeta` 是用户 lexical type namespace 的产物。 + +`GdCompilerType` 不应出现在: + +- `ScopeTypeMeta` +- `ClassRegistry.resolveTypeMetaHere(...)` +- `ScopeTypeResolver.tryResolveDeclaredType(...)` +- frontend skeleton declared type +- extension metadata parser 的普通输出 +- source-facing runtime name mapping + +如果这些入口能返回 `GdCompilerType`,用户代码就可能声明、引用、诊断展示或通过 `TYPE_META` 路由观察到它。 + +### 4.2 不进入 expressionTypes 和 ordinary typed boundary + +`FrontendVariantBoundaryCompatibility.determineFrontendBoundaryDecision(...)` 目前有两处会误接纳 compiler-only 类型: + +1. target 是 `Variant` 时,除 source 本身是 `Variant` 外,一律 `ALLOW_WITH_PACK`。 +2. default 分支回到 `ClassRegistry.checkAssignable(...)`,同名 compiler-only 类型会 `ALLOW_DIRECT`。 + +`FrontendBodyLoweringSession.materializeFrontendBoundaryValue(...)` 会把这些 decision 物化成 `PackVariantInsn`、`UnpackVariantInsn`、`CallIntrinsicInsn` 或 `ConstructBuiltinInsn`。所以 `GdCompilerType` 不能进入 ordinary frontend typed boundary 的 source/target。若实现层允许某些内部 lowering 生成 compiler-only LIR slot,应绕开用户 typed boundary matrix,而不是给 matrix 加普通兼容规则。 + +需要显式保持非法的地方: + +- `expressionTypes()` 用户语义结果。 +- `slotTypes()` 用户 declared/local/property/return slot。 +- fixed call arguments。 +- return contract。 +- property store。 +- subscript key/index。 +- constructor resolution。 +- member/property/call route facts。 + +### 4.3 不复用 Variant/DYNAMIC/TYPE_META + +`Variant` 是用户可见 runtime-open carrier,`DYNAMIC` 是 frontend 对 runtime-open route 的语义事实,`TYPE_META` 是源码层面的类型元值路由。`GdCompilerType` 不是三者中的任何一个。 + +错误复用的后果: + +- 复用 `Variant` 会触发 pack/unpack 和 outward ABI。 +- 复用 `DYNAMIC` 会让 dynamic call 结果固定通过 `Variant` 流动。 +- 复用 `TYPE_META` 会让源码可见 binding/member/static route 看到 compiler-only 类型。 + +--- + +## 5. LIR 与 intrinsic 边界 + +### 5.1 call_intrinsic 是正确操作通道 + +`doc/gdcc_lir_intrinsic.md` 和源码 `CIntrinsicManager` / `CallIntrinsicInsnGen` 已经把 `call_intrinsic` 定义为 backend-owned 白名单机制。intrinsic name 是数据,不是任意 C symbol escape hatch;每个 intrinsic 自己校验 result/argument 类型合同。 + +这正好匹配需求中的“只通过 intrinsic 进行操作”。后续新增涉及 compiler-only 类型的 intrinsic 时,应遵守现有规则: + +- 更新 intrinsic catalog。 +- 在 `CIntrinsicManager` 注册白名单。 +- 每个 intrinsic 显式检查 result/argument arity、ref、类型。 +- 不在 intrinsic 中复制变量解析、registry lookup 或 slot lifecycle。 +- 使用 `CBodyBuilder.assignVar(...)` / `callAssign(...)` 等统一写入 API,除非该 compiler-only 类型需要专门的值语义分支。 +- destroyable 或 object-like result 必须单独审计 ownership/lifecycle,不能照抄 primitive cast。 + +### 5.2 pack_variant / unpack_variant 必须拒绝 + +`PackUnpackVariantInsnGen` 当前只校验 unpack 的 source 是 `Variant`,然后直接调用: + +- `helper.renderUnpackFunctionName(resultVar.type())` +- `InsnGenSupport.packVariantAssign(...)` + +`CGenHelper.renderPackFunctionName(...)` 对非 nil/object 默认拼 `godot_new_Variant_with_`;`renderUnpackFunctionName(...)` 对非 object 默认拼 `godot_new__with_Variant`。这会为 compiler-only 类型生成不存在且语义错误的 helper。 + +因此应在 pack/unpack instruction generator 或更早的 LIR validator 中显式拒绝 `GdCompilerType`。 + +### 5.3 call_global / call_method / call_static_method 边界 + +需求要求 compiler-only 类型“不传入引擎函数中”。当前风险点包括: + +- `BackendMethodCallResolver.resolve(...)` 只拒绝 void/nil receiver,其他 receiver 会进入普通 method resolver。 +- `CallMethodInsnGen` 的动态参数路径会把非 `Variant` 参数 pack 成 `Variant`。 +- known signature call 通过 `checkAssignable(...)` 验证参数和返回。 +- `CallGlobalInsnGen` / utility/global helper 路径也依赖 `checkAssignable(...)`、`renderGdTypeInC(...)` 和 pack helper。 + +因此,compiler-only 类型应在普通 call instruction 边界 fail-fast: + +- 不能作为 engine/builtin/object method receiver。 +- 不能作为普通 method/global/static call argument。 +- 不能作为普通 method/global/static call return target。 +- 只能作为特定 intrinsic 的 operand/result,或未来明确标注的 backend-owned hidden helper route。 + +--- + +## 6. C 后端冲突点 + +### 6.1 C 类型名渲染默认危险 + +`CGenHelper.renderGdTypeInC(...)` 当前 default 返回 `godot_` + `getTypeName()`。`renderGdTypeRefInC(...)` 当前 default 返回 `godot_` + `getTypeName()` + `*`。模板 `func.ftl` 直接用这两个 helper 渲染函数返回值和参数。 + +如果 `GdCompilerType` 进入这些路径且没有专门分支,就会生成类似 `godot_` 的伪 Godot 类型。 + +建议: + +- `GdCompilerType` 提供明确 `cTypeName()` 或类似协议。 +- `CGenHelper.renderGdTypeInC(...)` 对 `GdCompilerType` 使用该 C 类型名,或在公开 ABI 上 fail-fast。 +- `renderGdTypeRefInC(...)` 不能默认沿用 value-wrapper 指针规则;按“按值传递不可变”的设计,compiler-only 类型参数是否需要指针必须由 compiler-only 类型或 intrinsic 合同显式决定。 + +### 6.2 默认初始化会误走 ConstructBuiltinInsn + +`CCodegen.generateFunctionPrepareBlock(...)` 对非参数、非 ref、非 void 变量生成默认初始化。未知类型 default 到 `new ConstructBuiltinInsn(variable.id(), List.of())`。如果 compiler-only local/temp 进入变量表,它会被当作 Godot builtin 构造。 + +建议: + +- 对 `GdCompilerType` 使用其 initialization helper 生成专门 init instruction/code path。 +- 如果某个 compiler-only 类型不允许普通变量 prepare,则在 prepare block 生成前 fail-fast。 +- 不要把 compiler-only init 塞进 `CBuiltinBuilder` 普通 constructor matcher;这会污染 Godot builtin constructor 合同。 + +### 6.3 copy/destroy 默认会拼 Godot helper + +`CGenHelper.renderCopyAssignFunctionName(...)` 对非 object/primitive/void/nil 默认拼 `godot_new__with_`。`renderDestroyFunctionName(...)` 对 destroyable 非 object 默认拼 `godot__destroy`。`CBodyBuilder.prepareRhsValue(...)` 和 `emitDestroy(...)` 会消费这些 helper。 + +这与需求“每个继承自 `GdCompilerType` 的类型返回初始化函数名和销毁函数名,都是后端 helper 中手动编写的函数”不匹配。应避免 compiler-only 类型落入 `godot_*` 默认命名。 + +建议: + +- `GdCompilerType` 或其具体子类型提供 `initFunctionName()`、`destroyFunctionName()`,必要时还需要 `copyFunctionName()` 或明确“按值浅拷贝合法”。 +- 如果类型是不可变按值数据且浅拷贝合法,赋值路径应是 primitive-like direct assignment,而不是 Godot value-wrapper copy ctor。 +- 如果类型有 destroy helper,`isDestroyable()` 应为 true,并由 `renderDestroyFunctionName(...)` 走 `gdcc_*` helper。 +- 若类型需要 init 但不需要 destroy,也要单独覆盖 prepare block,不能因为 `isDestroyable()==false` 就跳过初始化。 + +### 6.4 outward metadata 不能接收 compiler-only 类型 + +`CGenHelper.renderBoundMetadata(...)` 会调用 `requireBoundMetadataType(...)`,该 helper 在 `getGdExtensionType() == null` 时 fail-fast。虽然这可以阻止 compiler-only 类型生成 metadata,但这是较晚的失败点。 + +更好的防线是在以下更早边界禁止: + +- 函数参数/返回。 +- property type。 +- signal parameter。 +- callable outward ABI。 +- generated `call_func` wrapper。 + +否则错误会表现为 backend metadata 生成失败,而不是清晰的 “compiler-only type leaked into public ABI”。 + +### 6.5 wrapper cleanup 与普通 slot lifecycle 分离 + +`variant_abi_contract.md` 明确 `call_func` wrapper cleanup 由模板和 `renderCallWrapperDestroyStmt(...)` 管理,普通函数体 slot lifecycle 由 `CBodyBuilder` 管理。compiler-only 类型不应出现在 wrapper local 中;若误出现且 `isDestroyable()==true`,`renderCallWrapperDestroyStmt(...)` 会尝试调用 destroy helper。 + +这再次说明:仅设置 `getGdExtensionType()==null` 不足以表达 compiler-only。需要 public ABI 前置禁止。 + +--- + +## 7. 生命周期与按值不可变设计 + +用户已经明确说明 compiler-only 子类型都是按值传递的不可变数据,这一点无需检查。实现上仍要处理三个问题: + +1. 按值传递不等于无需初始化。prepare block 或 intrinsic result materialization 需要调用 init helper,避免未初始化 C storage。 +2. 不可变不等于无需销毁。若具体类型持有 runtime resource,discard、overwrite、scope cleanup 仍必须调用 destroy helper。 +3. 按值浅拷贝是否合法需要成为设计事实。如果所有 compiler-only 类型都支持 C struct assignment,`assignVar` 可 direct assign;如果某些类型未来需要引用计数或 deep copy,就需要 copy helper,而不是当前 `godot_new_*_with_*` 默认。 + +当前 lifecycle 文档对非对象 destroyable 的规则是: + +- prepare/copy RHS。 +- 必要时 destroy old value。 +- 再 assign。 +- discard destroyable non-object return value 时立即 destroy。 + +compiler-only 类型应沿用非对象生命周期,而不是对象 ownership。不要通过 `try_own_object` / `try_release_object` 表达它的生命周期。 + +如果需要显式 `destruct`,`LifecycleInstructionRestrictionValidator` 的 `INTERNAL` provenance 只允许 compiler internal/temp variable。compiler-only lifecycle 指令应使用 internal/temp 路径,不能暴露成用户变量生命周期操作。 + +--- + +## 8. Godot 对齐结论 + +上游 Godot 的 GDScript 类型层没有 `Iterator`、`FunctionState` 这类公开类型分类。相关机制位于执行层: + +- `GDScriptFunctionState` / `CallState` 保存 await/coroutine 恢复状态。 +- for-loop 通过 `@counter_pos`、`@container_pos`、`@iterator_temp` 等内部槽位和 VM opcode 实现。 +- `Variant::iter_init` / `iter_next` / `iter_get` 是 Variant 层通用迭代 API。 +- object iteration 通过 `_iter_init`、`_iter_next`、`_iter_get` 进入对象协议。 +- analyzer 只关心用户可见 loop variable 的元素类型,而不是 iterator state 的内部表示。 + +这给 GDCC 的启发是边界划分,而不是具体类名迁移: + +- GDScript 语义类型系统只表达用户能声明、观察、检查、转换的类型。 +- iterator/function state 是 backend/runtime 私有状态。 +- 如果需要 `GdCompilerType` 表达 C runtime storage,它应被明确标记为 compiler-only,且不得注册进用户 type namespace。 + +--- + +## 9. 建议设计形态 + +### 9.1 类型层 + +可以新增一个 sealed interface 或 abstract class: + +```java +public sealed interface GdCompilerType extends GdType + permits GdIteratorStateType, GdFunctionStateType { + @NotNull String getCTypeName(); + + @NotNull String getInitFunctionName(); + + @NotNull String getDestroyFunctionName(); + + @Override + default boolean isNullable() { + return false; + } + + @Override + default @Nullable GdExtensionTypeEnum getGdExtensionType() { + return null; + } +} +``` + +这只是形态建议,不是实现要求。需要注意: + +- 不要把只有一个实现的额外 interface 拆出来;如果短期只有一种 compiler-only 类型,可以先用一个 concrete type 承载,等第二种出现再抽出 sealed 分支。 +- 如果所有具体类型都必须 init/destroy,那么协议方法放在 `GdCompilerType` 上是合理的。 +- 如果某些 compiler-only 类型没有 destroy 语义,需要决定是返回 no-op helper,还是允许 `destroyFunctionName()` 为空。当前需求说“返回初始化函数名和销毁函数名”,报告按必有 helper 处理。 + +### 9.2 解析层 + +建议拆分: + +- 用户 declared type:继续使用 `ScopeTypeResolver` / `ClassRegistry.tryParseStrictTextType(...)`,不支持 compiler-only。 +- LIR/internal type:如确实要序列化 compiler-only 类型,新增明确前缀或 grammar,例如 `compiler::`,并只在 LIR parser 的 internal type parser 中识别。 + +不要把 compiler-only 名称作为普通 bare identifier 加入 `tryParseStrictTextType(...)`。 + +### 9.3 后端 helper 命名 + +根据 `doc/gdcc_runtime_lib.md`,手写 backend helper 应保持 `gdcc_*` / local namespace,除非明确扩展 runtime-provided `godot_*` surface。compiler-only init/destroy helper 应使用 `gdcc_*`,例如: + +- `gdcc_iterator_state_init` +- `gdcc_iterator_state_destroy` +- `gdcc_function_state_init` +- `gdcc_function_state_destroy` + +不建议使用 `godot_new_` / `godot__destroy`,否则会和 generated Godot binding symbol ownership 混淆。 + +### 9.4 禁止边界 + +实现时建议集中增加 fail-fast 检查,错误信息明确使用 “compiler-only type leaked into ...” 语义。至少应覆盖: + +- `FrontendVariantBoundaryCompatibility` +- `PackUnpackVariantInsnGen` +- `CallMethodInsnGen` / `BackendMethodCallResolver` +- `CallGlobalInsnGen` +- `OperatorInsnGen` +- `IndexLoadInsnGen` / `IndexStoreInsnGen` +- `LoadPropertyInsnGen` / `StorePropertyInsnGen` +- `CGenHelper.renderBoundMetadata(...)` +- `CCodegen.generateFunctionPrepareBlock(...)` +- LIR parser/serializer public ABI sections + +--- + +## 10. 实现前检查清单 + +### 10.1 必须先决定的问题 + +1. `GdCompilerType` 是否允许进入 LIR XML?如果允许,需要独立 type text grammar;如果不允许,parser/serializer 应 fail-fast。 +2. compiler-only 类型是否允许作为普通 local variable?如果允许,prepare block 应调用 init helper;如果只允许 intrinsic result temp,应限制变量来源。 +3. compiler-only 类型赋值是否一律 C struct assignment?如果不是,需要 copy helper 协议。 +4. destroy helper 是否必定存在且非空?如果是,`isDestroyable()` 应为 true;如果不是,需要 no-op policy。 +5. hidden/internal function 是否允许使用 compiler-only 参数/返回?如果允许,必须和 public custom function ABI 明确区分。 + +### 10.2 代码同步点 + +- `GdType` permits。 +- 新增 compiler-only type 类。 +- `ClassRegistry`:普通 declared type 禁止,LIR-only 解析策略另设。 +- `DomLirParser` / `DomLirSerializer`:如果 LIR XML 支持内部类型,需要 focused 往返测试。 +- `FrontendVariantBoundaryCompatibility`:显式拒绝 compiler-only typed boundary,尤其 target `Variant` pack 分支。 +- `FrontendBodyLoweringSession.materializeFrontendBoundaryValue(...)`:禁止 ordinary boundary materialization。 +- `CGenHelper`:C 类型名、ref 类型名、pack/unpack、copy、destroy、metadata helper。 +- `CCodegen.generateFunctionPrepareBlock(...)`:compiler-only init helper。 +- `CBodyBuilder`:needs-address、RHS prepare、non-object assignment、discard、return、temp destroy。 +- `CIntrinsicManager` 与具体 `CIntrinsicFunction`:注册和 narrow type contract。 +- 普通 call/property/operator/index instruction generators:禁止 compiler-only 类型进入 Godot/Variant/engine path。 +- runtime `src/main/c/codegen/include_451/gdcc/**`:新增手写 helper 声明/实现。 +- `doc/gdcc_lir_intrinsic.md`:新增 intrinsic catalog。 +- `doc/gdcc_type_system.md`、`doc/gdcc_low_ir.md`、`doc/gdcc_c_backend.md`、`doc/gdcc_runtime_lib.md`:同步 compiler-only 边界。 + +### 10.3 测试建议 + +重点不是测“能创建一个类型类”,而是测边界不会泄漏: + +- `GdCompilerType` 不能由用户 declared type resolver 解析。 +- `GdCompilerType` 不能进入 `Variant` pack/unpack。 +- `GdCompilerType` 不能作为 public function/property/signal metadata。 +- `GdCompilerType` 不能传给 call_method/call_global/operator/index/property engine path。 +- compiler-only local 初始化调用 `gdcc_*_init`。 +- compiler-only destroyable local cleanup 调用 `gdcc_*_destroy`。 +- intrinsic 成功路径和坏 arity/result/ref/arg 类型都 fail-fast。 +- LIR XML 若支持 internal type,往返稳定;若不支持,错误清晰。 + +--- + +## 11. 主要风险列表 + +1. `target Variant` 自动 pack:`FrontendVariantBoundaryCompatibility` 会直接允许 source -> `Variant`,必须显式排除 compiler-only。 +2. 同名赋值:`ClassRegistry.checkAssignable(...)` 以 `getTypeName()` 同名为第一优先,可能让 compiler-only 类型参与 ordinary boundary。 +3. C 类型渲染 default:`CGenHelper.renderGdTypeInC(...)` / `renderGdTypeRefInC(...)` 会拼 `godot_*`。 +4. prepare block default:未知类型会变成 `ConstructBuiltinInsn`。 +5. copy/destroy helper default:会拼 `godot_new_*_with_*` 和 `godot_*_destroy`。 +6. pack/unpack helper default:会拼 `godot_new_Variant_with_*` 和 `godot_new_*_with_Variant`。 +7. public ABI late failure:`getGdExtensionType()==null` 只会在 metadata helper 较晚失败,不足以表达设计边界。 +8. dynamic call 参数打包:普通动态 dispatch 会把非 `Variant` 参数 pack 成 `Variant`。 +9. typed container hint:如果 compiler-only 类型进入 `Array[T]` / `Dictionary[K,V]`,typed container metadata 需要 GDExtension type/hint,会失败或误导。 +10. wrapper cleanup:如果 compiler-only destroyable 类型进入 generated `call_func` wrapper,模板可能生成不应存在的 cleanup。 +11. `DestructInsnGen` 现有分类只显式处理 object/variant/string-like/meta/container,其他类型默认 no-op;compiler-only destroyable 需要新增分支或走统一 destroy helper。 +12. Godot surface 混淆:helper 使用 `godot_*` 会和 generated binding/provided symbol 机制混淆。 + +--- + +## 12. 结论 + +`GdCompilerType` 的正确定位应是“GDCC backend/lowering 内部 runtime storage 类型”,不是 GDScript 用户语义类型。它可以作为 `GdType` 的子分支来复用 LIR variable typing,但必须同步收紧所有用户可见和 Godot ABI 边界。实现时最关键的工作是封住默认路径:Variant pack/unpack、C helper 命名、prepare block builtin construction、metadata generation、engine call、普通 frontend typed boundary。 + +如果后续实现目标只是 for-loop iterator 或 async function state,建议先从一个具体 compiler-only type 和一组 intrinsic 做最小闭环:定义 C type/init/destroy、LIR internal local、intrinsic 操作、C backend init/destroy/assign 策略,并用 focused tests 证明它不会流入用户签名、Variant、engine call 和 outward metadata。 diff --git a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md new file mode 100644 index 00000000..68b8f689 --- /dev/null +++ b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md @@ -0,0 +1,544 @@ +# Frontend GdCompilerType Plan + +> 本文档记录 `GdCompilerType` 的实施计划、跨模块边界与验收细则。 +> `GdCompilerType` 只用于 GDCC compiler / lowering / LIR / backend 内部 runtime storage typing, +> 不属于 GDScript source-facing 类型系统。 + +## 文档状态 + +- 状态:计划维护中 +- 更新时间:2026-06-29 +- 适用范围: + - `src/main/java/gd/script/gdcc/type/**` + - `src/main/java/gd/script/gdcc/scope/**` + - `src/main/java/gd/script/gdcc/frontend/**` + - `src/main/java/gd/script/gdcc/lir/**` + - `src/main/java/gd/script/gdcc/backend/c/**` + - `src/main/c/codegen/**` +- 关联文档: + - `doc/analysis/gdcompiler_type_design_risk_analysis.md` + - `doc/gdcc_type_system.md` + - `doc/gdcc_low_ir.md` + - `doc/gdcc_lir_intrinsic.md` + - `doc/gdcc_c_backend.md` + - `doc/gdcc_runtime_lib.md` + - `doc/gdcc_ownership_lifecycle_spec.md` + - `doc/module_impl/common_rules.md` + - `doc/module_impl/frontend/frontend_rules.md` + - `doc/module_impl/frontend/frontend_implicit_conversion_matrix.md` + - `doc/module_impl/frontend/frontend_lowering_(un)pack_implementation.md` + - `doc/module_impl/frontend/frontend_dynamic_call_lowering_implementation.md` + - Godot docs:`tutorials/scripting/gdscript/gdscript_advanced.rst` 的 `range(...)` / custom iterator 说明 + +--- + +## 0. 维护合同 + +- 本文档是 `GdCompilerType` 实施顺序、禁止边界和验收细则的计划事实源。 +- `frontend_implicit_conversion_matrix.md` 仍是 ordinary typed-boundary compatibility 的唯一真源;本文档不得维护第二份 source/target conversion 矩阵。 +- `gdcc_lir_intrinsic.md` 仍是 `call_intrinsic` surface、backend registry 和 intrinsic catalog 的事实源;本文档只规定 compiler-only 类型必须通过该通道操作。 +- `gdcc_low_ir.md` 仍是 LIR XML surface 的事实源;本文档的 LIR XML 计划落地后必须同步更新该文档。 +- 实现时不得用 `Variant`、`DYNAMIC`、`TYPE_META` 或 Godot object metadata 伪装 compiler-only storage。 +- 若实现过程中发现某个计划项需要改变 source-facing typing、ordinary boundary、public ABI 或 runtime helper 命名,必须先更新对应事实源文档,再修改代码与测试。 + +--- + +## 1. 目标边界 + +`GdCompilerType` 的设计目标: + +- 表示 compiler/backend 为运行时实现需要保存的 C storage 类型。 +- 允许作为 LIR 内部 local/temp variable 的类型。 +- 允许作为 backend-owned intrinsic 的 operand / result type。 +- 每个具体 compiler-only 类型必须提供: + - C storage type name。 + - init helper function name。 + - destroy helper function name。 +- compiler-only 类型按值传递且不可变,这是设计前提,不在实现中额外证明。 + +`GdCompilerType` 明确不允许进入: + +- source-facing declared type parser。 +- `ScopeTypeMeta` / type-meta namespace。 +- 用户可见 `expressionTypes()` 语义事实。 +- 用户 ordinary `slotTypes()`,包括 local / parameter / property / return slot。 +- 自定义函数公开签名。 +- property / signal / callable outward ABI。 +- `Variant` pack / unpack。 +- engine method / utility / global / property / index / operator ABI。 +- generated `call_func` wrapper metadata、argument unpack 或 cleanup surface。 + +Godot upstream 的对齐依据是:`GDScriptFunctionState` 注册为 internal class,脚本 analyzer 不把它作为可声明标识符;for-loop iterator state 也停留在 bytecode / VM stack slot / opcode operand 层。GDCC 的 iterator、function state 等 C runtime storage 同样应保持在 IR / backend / runtime support 层,而不是扩展成 GDScript source-facing 类型。 + +首个 concrete compiler-only type 固定为 `GdccForRangeIterType`,服务未来 GDScript `for i in range(...)` lowering。Godot 文档说明 `range` 支持 `range(n)`、`range(b, n)`、`range(b, n, s)` 三种形态,起点 inclusive、终点 exclusive,默认起点为 `0`、默认步长为 `1`,负步长用于反向迭代;custom iterator 合同也把初始化、推进、取值拆成 `_iter_init()`、`_iter_next()`、`_iter_get()` 三类动作。当前阶段不实现 `for` lowering,只用 `GdccForRangeIterType` 与对应 intrinsics 验证 compiler-only type 的 LIR/backend 内部通路。 + +--- + +## 2. 当前冲突面 + +当前 `GdType` 被多个层面共同消费。新增 compiler-only 分支时,以下默认路径会静默误处理: + +- `GdType` 当前 sealed permits 不包含 compiler-only 分支。 +- `ClassRegistry.tryParseStrictTextType(...)` 和 `ScopeTypeResolver.tryResolveDeclaredType(...)` 是 source-facing declared type 入口,不能认识 compiler-only 名称。 +- `ClassRegistry.findType(...)` 当前被 `DomLirParser` 用于 LIR XML 类型文本解析;若 LIR XML 需要 compiler-only 类型,不能把它塞进普通 declared type parser。 +- `ClassRegistry.checkAssignable(...)` 第一条规则是 `getTypeName()` 同名即 assignable,会让 compiler-only 类型穿过 ordinary typed boundary。 +- `FrontendVariantBoundaryCompatibility` 当前会允许 stable type -> `Variant` pack、`Variant` -> concrete unpack,并在 default 分支回退 `checkAssignable(...)`。 +- `FrontendBodyLoweringSession.materializeFrontendBoundaryValue(...)` 会把 accepted boundary 物化为 `PackVariantInsn`、`UnpackVariantInsn`、`CallIntrinsicInsn` 或 `ConstructBuiltinInsn`。 +- condition lowering 对非 `bool` / `Variant` stable type 当前可能走 `pack_variant -> unpack_variant(bool)`。 +- dynamic call backend path 会把非 `Variant` 参数 pack 成 `Variant`,也可能把 dynamic result unpack 到 target type。 +- `DomLirSerializer` 直接写 `GdType.getTypeName()`;`DomLirParser` 直接用 `ClassRegistry.findType(...)` 读回类型文本。 +- `CGenHelper.renderGdTypeInC(...)` / `renderGdTypeRefInC(...)` default 会生成 `godot_` / `godot_*`。 +- `CGenHelper.renderPackFunctionName(...)` / `renderUnpackFunctionName(...)` default 会生成 `godot_new_Variant_with_` / `godot_new__with_Variant`。 +- `CGenHelper.renderCopyAssignFunctionName(...)` / `renderDestroyFunctionName(...)` default 会生成 `godot_new__with_` / `godot__destroy`。 +- `CCodegen.generateFunctionPrepareBlock(...)` 对未知非 void local default 到 `ConstructBuiltinInsn`。 +- `CBodyBuilder.renderDefaultValueExpr(...)` 对未知 type default 到 `godot_new_()`。 +- `DestructInsnGen` 当前只显式处理 Godot value/object/meta/container family;compiler-only destroyable type 不能落到 no-op。 +- `CGenHelper.renderBoundMetadata(...)` 会通过 `getGdExtensionType() == null` late fail,但这不是足够早的 public ABI 边界。 +- `func.ftl` 对函数返回和参数直接调用 `renderGdTypeInC(...)` / `renderGdTypeRefInC(...)`,public 函数签名若混入 compiler-only type 会生成错误 C ABI。 + +--- + +## 3. 实施原则 + +- 先定义边界,再放开路径。任何默认兼容、默认 C helper 命名、默认 metadata 生成都必须对 compiler-only type 显式处理。 +- 实现应从 `GdccForRangeIterType` 和一组 range iterator intrinsic 闭环开始。不要在只有一个实现时额外制造一层抽象;如果后续已经有两个以上具体 compiler-only 类型,才引入 `GdCompilerType` sealed interface。 +- compiler-only 类型不参与 ordinary frontend typed-boundary matrix。内部 LIR slot 的 direct assignment 由 backend/LIR 合同处理,不由 source-facing semantic compatibility 扩面。 +- helper 命名使用 `gdcc_*`,不得伪造 `godot_*` generated binding helper。 +- lifecycle 走非对象 destroyable value 路径,不走 object ownership。 +- parser、serializer、backend 和 frontend boundary 的错误信息应明确说明 `compiler-only type leaked into ...`,避免让使用者看到晚期 `getGdExtensionType()==null` 或 C symbol 缺失。 + +--- + +## 4. LIR XML 策略 + +MVP 采用以下策略: + +- 允许 compiler-only 类型出现在 LIR XML 的 function `` 中。 +- 类型文本使用 LIR-only grammar:`compiler::`;本阶段唯一合法实例是 `compiler::GdccForRangeIter`。 +- 该 grammar 只由 LIR parser / serializer 识别,不进入 `ScopeTypeResolver`、`ClassRegistry.tryParseStrictTextType(...)` 或 source-facing type-meta namespace。 +- compiler-only 类型禁止出现在以下 LIR XML surface: + - function ``。 + - function ``。 + - ``。 + - `` parameter。 + - lambda ``。 +- `is_hidden=true` 函数在 MVP 中也不允许 compiler-only parameter / return type。若未来 backend-owned hidden helper 需要 compiler-only ABI,必须先单独更新本文档和 `gdcc_low_ir.md`,并证明不会生成 outward binding metadata 或 call wrapper。 + +验收细则: + +- happy path: + - `` 可解析为 `GdccForRangeIterType`。 + - `DomLirSerializer` 对该 compiler-only local 输出稳定的 `compiler::GdccForRangeIter`。 + - parser / serializer round-trip 不通过 ordinary source-facing resolver。 +- negative path: + - 用户 declared type resolver 不能解析 `compiler::GdccForRangeIter` 或 bare `GdccForRangeIter`。 + - property / signal / parameter / capture / return type 使用 `compiler::GdccForRangeIter` 时 fail-fast。 + - malformed `compiler::` grammar 报错清晰,不退化成 `GdObjectType("compiler::...")`。 + +--- + +## 5. 分步骤实施计划 + +### 5.1 阶段一:类型协议与 `GdccForRangeIterType` + +目标: + +- 让 `GdType` 能承载 compiler-only storage type,但不把它暴露给用户类型系统。 +- 锁定 `GdccForRangeIterType` 的 C storage、init、destroy 协议。 +- 为未来 `for i in range(...)` lowering 准备内部状态类型;本阶段不实现 `for` 语义 lowering。 + +建议实施内容: + +- 更新 `GdType` sealed permits。 +- 新增首个具体 compiler-only type:`GdccForRangeIterType`。在只有这一种具体类型时,优先让它直接进入 `GdType` sealed hierarchy;不要为了一个实现先制造单独 `GdCompilerType` 抽象层。 +- 类型协议至少覆盖: + - `getTypeName()`:建议稳定为 `GdccForRangeIter`,用于内部 identity,不作为 source-facing declared type text。 + - LIR-only text:`compiler::GdccForRangeIter`。 + - C storage type name:建议为 `gdcc_for_range_iter`。 + - init helper function name:建议为 `gdcc_for_range_iter_init`。 + - destroy helper function name:建议为 `gdcc_for_range_iter_destroy`。 + - `isNullable() == false`。 + - `getGdExtensionType() == null`。 + - `isDestroyable()` 根据 destroy helper 策略返回 true。 +- `GdccForRangeIterType` 代表按值保存的 range iterator state,不表示 GDScript `range(...)` 调用返回的 `Array`。 +- 如需 direct C struct assignment,明确该类型不使用 `renderCopyAssignFunctionName(...)` 的 `godot_new_*_with_*` 路径。 + +验收细则: + +- happy path: + - type unit test 覆盖 `GdccForRangeIterType` 的 stable internal name、LIR-only text、C type name、init helper、destroy helper、nullability、extension metadata。 + - Java sealed switch 编译强制覆盖新增分支。 +- negative path: + - `GdccForRangeIterType` 不被当作 `GdPrimitiveType`、`GdObjectType`、`GdVariantType` 或 `GdMetaType`。 + - 不出现 `godot_GdccForRangeIter`、`godot_new_GdccForRangeIter...`、`godot_GdccForRangeIter_destroy` 形式的默认 helper。 + - 不新增任何 `for` parser / analyzer / lowering 行为。 + +测试锚点: + +- 新增 `src/test/java/gd/script/gdcc/type/GdccForRangeIterTypeTest.java` 或同等具体 type test。 +- 更新需要覆盖 sealed switch 的现有 type/backend tests。 + +### 5.2 阶段二:source-facing resolver 禁止与 LIR-only parser + +目标: + +- source-facing type namespace 完全看不到 compiler-only type。 +- LIR XML 只在 `` 中通过 `compiler::` grammar 承载 compiler-only type。 + +建议实施内容: + +- 保持 `ClassRegistry.tryParseStrictTextType(...)` 不识别 compiler-only type。 +- 保持 `ScopeTypeResolver.tryResolveDeclaredType(...)` 不识别 compiler-only type。 +- 不向 `ClassRegistry.resolveTypeMetaHere(...)` 注册 compiler-only `ScopeTypeMeta`。 +- 给 `DomLirParser` 增加 LIR-only type parser,并带 use-site 参数区分 public ABI surface 和 variable surface。 +- 给 `DomLirSerializer` 增加 compiler-only type text renderer。 +- 在 parser 层提前禁止 public ABI surface 出现 compiler-only type。 + +验收细则: + +- happy path: + - local variable 的 `compiler::` XML round-trip 稳定。 + - ordinary builtin / object / container type XML 行为保持不变。 +- negative path: + - `ScopeTypeResolverTest` 证明 declared type 文本无法解析 compiler-only type。 + - `ClassRegistryTypeMetaTest` 证明 registry 不发布 compiler-only type-meta。 + - `DomLirParserTest` 证明 parameter / return / property / signal / capture surface 拒绝 compiler-only type。 + - `ClassRegistry.findType(...)` 不把 `compiler::` 猜成 object。 + +测试锚点: + +- `ScopeTypeResolverTest` +- `ScopeTypeParsersTest` +- `ClassRegistryTypeMetaTest` +- `FrontendDeclaredTypeSupportTest` +- `DomLirParserTest` +- `DomLirSerializerTest` + +### 5.3 阶段三:frontend semantic 与 lowering 边界封堵 + +目标: + +- compiler-only type 不进入用户 semantic facts。 +- ordinary typed boundary 不接受 compiler-only source/target。 +- lowering 不生成 pack/unpack/construct_builtin 来处理 compiler-only type。 + +建议实施内容: + +- 在 `FrontendVariantBoundaryCompatibility.determineFrontendBoundaryDecision(...)` 最前面拒绝 compiler-only source 或 target。 +- 明确 `ClassRegistry.checkAssignable(...)` 的 source-facing consumer 不得把 compiler-only 同名视作 ordinary boundary success;可以通过 frontend helper 前置拒绝实现,不必污染 strict backend assignability 基线。 +- 在 `FrontendLocalTypeStabilizationAnalyzer` 和 `FrontendExprTypeAnalyzer` 的 local backfill 风险点增加 fail-closed / fail-fast 策略,防止 compiler-only published expression type 写回 ordinary local slot。 +- 在 condition lowering 的非 bool / non Variant pack path 前显式拒绝 compiler-only type。 +- 在 `FrontendBodyLoweringSession.materializeFrontendBoundaryValue(...)` 增加 invariant guard,作为 shared helper 的二次防线。 +- 在 call materialization 中,fixed args、vararg tail、dynamic route 均不得接受 compiler-only value。 + +验收细则: + +- happy path: + - 现有 ordinary boundary:`Variant` pack/unpack、`int -> float`、`Vector*i -> Vector*`、`String <-> StringName` 测试保持通过。 +- negative path: + - compiler-only -> `Variant` reject,不生成 `PackVariantInsn`。 + - `Variant` -> compiler-only reject,不生成 `UnpackVariantInsn`。 + - compiler-only -> compiler-only 不通过 ordinary user boundary。 + - compiler-only condition 不生成 `pack_variant -> unpack_variant(bool)`。 + - fixed call argument、vararg tail、return slot、property store、subscript key/index 都不能 materialize compiler-only boundary。 + - artificial published compiler-only expression type 不能写回 user local slot。 + +测试锚点: + +- `FrontendVariantBoundaryCompatibilityTest` +- `FrontendTypeCheckAnalyzerTest` +- `FrontendLocalTypeStabilizationAnalyzerTest` +- `FrontendBodyLoweringSupportTest` +- `FrontendLoweringBodyInsnPassTest` +- `FrontendWritableTypeWritebackSupportTest` + +### 5.4 阶段四:LIR public ABI validator + +目标: + +- compiler-only type 即使由手写 LIR 或 parser 注入,也不能流入 public ABI。 + +建议实施内容: + +- 增加 LIR validator 或在现有 validation/codegen 前置流程中增加 pass。 +- 校验范围至少覆盖: + - class property type。 + - signal parameter type。 + - public and hidden function parameter type。 + - public and hidden function return type。 + - lambda capture type。 + - generated call wrapper / binding data collection surface。 +- MVP 允许 compiler-only type 只出现在 function variables。 +- 错误信息使用 `compiler-only type leaked into public ABI` 或具体 surface 名称。 + +验收细则: + +- happy path: + - 含 compiler-only local variable 和 intrinsic 的 LIR module 可以进入 backend codegen。 +- negative path: + - function parameter / return / property / signal / capture 使用 compiler-only type fail-fast。 + - hidden function parameter / return 在 MVP 中同样 fail-fast。 + - failure 早于 `CGenHelper.renderBoundMetadata(...)` 的 `getGdExtensionType()==null`。 + +测试锚点: + +- 新增或扩展 LIR validation tests。 +- `DomLirParserTest` +- `CGenHelperTest` +- backend integration shape tests 中加入 public ABI negative cases。 + +### 5.5 阶段五:C 后端类型渲染、初始化、赋值与销毁 + +目标: + +- compiler-only storage 在 C 中使用显式 `gdcc_*` helper 和 C type。 +- 封堵所有 `godot_*` 默认 helper 路径。 + +建议实施内容: + +- `CGenHelper.renderGdTypeInC(...)` 对 compiler-only type 使用其 C storage type name。 +- `CGenHelper.renderGdTypeRefInC(...)` 仅在允许的 internal function / intrinsic helper surface 使用明确策略;public ABI 已由 validator 禁止。 +- `renderPackFunctionName(...)` / `renderUnpackFunctionName(...)` 对 compiler-only type fail-fast。 +- `renderCopyAssignFunctionName(...)` 对 compiler-only type 不返回 `godot_new_*_with_*`。若所有 compiler-only type 允许 C struct assignment,返回空字符串并让 assignment 走 direct path;若未来需要 deep copy,先扩展 copy helper 协议。 +- `renderDestroyFunctionName(...)` 对 compiler-only type 返回其 `gdcc_*_destroy` helper。 +- `CCodegen.generateFunctionPrepareBlock(...)` 对 compiler-only local 生成专用初始化路径,不生成 `ConstructBuiltinInsn`。 +- `CBodyBuilder.renderDefaultValueExpr(...)` 对 compiler-only type fail-fast,除非该 use-site 明确是 internal helper 且能调用 init helper。 +- `CBodyBuilder.needsAddressOf(...)`、`prepareRhsValue(...)`、`prepareReturnValue(...)`、`emitDestroy(...)` 明确处理 compiler-only direct assignment 与 destroy。 +- `DestructInsnGen` 对 compiler-only destroyable type 调用 `gdcc_*_destroy`,不能 default no-op。 + +验收细则: + +- happy path: + - compiler-only local declaration 使用 C storage type name。 + - prepare block 调用 `gdcc_*_init` 或等价专用 init instruction/code path。 + - overwrite、scope cleanup、discarded destroyable value 调用 `gdcc_*_destroy`。 + - direct assignment 不调用 `godot_new_*_with_*`。 +- negative path: + - 生成结果中不出现 `godot_`。 + - 不出现 `godot_new_()`。 + - 不出现 `godot_new_Variant_with_`。 + - 不出现 `godot_new__with_Variant`。 + - 不出现 `godot_new__with_`。 + - 不出现 `godot__destroy`。 + +测试锚点: + +- `CGenHelperTest` +- `CConstructInsnGenTest` +- `CDestructInsnGenTest` +- `CAssignInsnGenTest` +- `CBodyBuilderPhaseBTest` +- `CBodyBuilderPhaseCTest` + +### 5.6 阶段六:`GdccForRangeIterType` intrinsic 最小闭环 + +目标: + +- compiler-only type 只通过 backend-owned intrinsic 操作。 +- 每个 intrinsic 的类型合同窄而明确。 +- 用 range iterator 的初始化、继续判断、推进、取值闭环验证 compiler-only type 可以支撑未来 `for i in range(...)` lowering。 + +建议实施内容: + +- 为 `GdccForRangeIterType` 新增一组最小 intrinsic。推荐命名如下,最终名称可按现有 intrinsic 命名风格调整,但语义必须保持: + - `gdcc.for_range_iter.init`:根据 `start`、`end`、`step` 初始化 iterator state。 + - `gdcc.for_range_iter.should_continue`:读取当前 state,返回是否还有当前值可用。 + - `gdcc.for_range_iter.next`:推进到下一个值,并返回推进后的新 iterator state。 + - `gdcc.for_range_iter.get`:读取当前迭代值。 +- intrinsic 类型合同: + - `init` 的 result 必须是非 ref 的 `compiler::GdccForRangeIter`;arguments 为三个 `int` 值,对应 normalized `start`、`end`、`step`。 + - `should_continue` 的 result 必须是非 ref 的 `bool`;argument 为一个 `compiler::GdccForRangeIter`。 + - `next` 的 result 必须是非 ref 的 `compiler::GdccForRangeIter`;argument 为一个 `compiler::GdccForRangeIter`,返回推进后的新 iterator state。 + - `get` 的 result 必须是非 ref 的 `int`;argument 为一个 `compiler::GdccForRangeIter`。 +- 未来 lowering 的基本形状应为:`iter = init(start,end,step)`,每轮先 `should_continue(iter)`,再 `value = get(iter)`,循环体结束后 `iter = next(iter)`。这保证 `GdccForRangeIterType` 仍按不可变值语义传递,不需要 ref mutation。 +- `range(n)` / `range(b, n)` / `range(b, n, s)` 的参数归一化属于未来 frontend lowering:本阶段只要求 intrinsic 能接受已归一化的三个 `int` 参数。 +- `step == 0` 的处理策略必须在 intrinsic catalog 中写清。建议 fail-fast 或 runtime error helper,不允许生成无限循环语义。 +- 负步长必须作为合法输入进入 helper 语义:`should_continue` / `next` 对正 step 使用 `< end`,对负 step 使用 `> end`。 +- 更新 `doc/gdcc_lir_intrinsic.md` catalog,记录: + - intrinsic name。 + - LIR textual shape。 + - result 是否必须存在。 + - result 是否允许 ref。 + - argument 数量与类型。 + - C backend 语义。 + - lifecycle / ownership 说明。 +- 在 `CIntrinsicManager` 注册白名单。 +- 每个 `CIntrinsicFunction` 自己校验 result / arg / arity / ref。 +- 成功路径优先复用 `CBodyBuilder.assignVar(...)` 或 `callAssign(...)`,除非该 compiler-only type 需要更窄的写入策略。 + +验收细则: + +- happy path: + - parser / serializer 保留 `call_intrinsic` textual shape。 + - backend registry 能找到 `gdcc.for_range_iter.init`、`gdcc.for_range_iter.should_continue`、`gdcc.for_range_iter.next`、`gdcc.for_range_iter.get`。 + - 成功 codegen 使用 `gdcc_*` helper,不绕过 slot lifecycle。 + - `range(10)` 归一化后的 `start=0,end=10,step=1`、`range(5,10)` 归一化后的 `start=5,end=10,step=1`、`range(10,0,-1)` 归一化后的 `start=10,end=0,step=-1` 都能用手写 LIR intrinsic 序列生成预期 C helper 调用形状。 +- negative path: + - unknown intrinsic fail-fast。 + - bad arity fail-fast。 + - missing result / unexpected result fail-fast。 + - result ref 不符合合同 fail-fast。 + - result type 错误 fail-fast。 + - argument type 错误 fail-fast。 + - literal operand fail-fast。 + - `step == 0` 的手写 LIR 用例按 catalog 约定 fail-fast 或进入明确 runtime error helper,不允许静默生成无限循环 helper 调用。 + - `GdccForRangeIterType` 不能被传给非上述四个 range iterator intrinsic。 + +测试锚点: + +- `SimpleLirBlockInsnParserTest` +- `SimpleLirBlockInsnSerializerTest` +- `CIntrinsicManagerTest` +- `CallIntrinsicInsnGenTest` +- 新增 `GdccForRangeIter` intrinsic tests,覆盖正步长、负步长、exclusive end、zero step 策略和类型错误。 + +### 5.7 阶段七:普通 Godot / Variant / engine 路径封堵 + +目标: + +- 即使手写 LIR 绕过 frontend,compiler-only type 也不能进入普通 Godot runtime ABI。 + +建议实施内容: + +- `PackUnpackVariantInsnGen` 显式拒绝 compiler-only pack / unpack。 +- `CallMethodInsnGen`、`BackendMethodCallResolver`、`CallGlobalInsnGen`、static call path 显式拒绝 compiler-only receiver / argument / return target。 +- operator / index / property instruction generators 显式拒绝 compiler-only operand / receiver / value。 +- typed array / dictionary metadata 和 runtime guard leaf 渲染拒绝 compiler-only leaf。 +- generated `call_func` wrapper argument gate、unpack expression、destroy stmt 不接受 compiler-only type。 + +验收细则: + +- negative path: + - `PACK_VARIANT` source 是 compiler-only type fail-fast。 + - `UNPACK_VARIANT` result 是 compiler-only type fail-fast。 + - dynamic call argument 是 compiler-only type 不被 pack 成 `Variant`。 + - dynamic result 不可 unpack 到 compiler-only type。 + - method/global/static/operator/index/property path 遇到 compiler-only type fail-fast。 + - `Array[compiler::]` / `Dictionary[String, compiler::]` 不能生成 outward metadata。 + +测试锚点: + +- `CPackUnpackVariantInsnGenTest` +- `CallMethodInsnGenTest` +- `CallGlobalInsnGenTest` +- `COperatorInsnGenTest` +- `IndexLoadInsnGenTest` +- `IndexStoreInsnGenTest` +- `CLoadPropertyInsnGenTest` +- `CStorePropertyInsnGenTest` +- `CGenHelperTest` + +### 5.8 阶段八:文档同步与回归收口 + +目标: + +- 实现后所有事实源一致。 +- 回归覆盖证明 compiler-only type 是内部 storage typing,而不是 source-facing type。 + +建议实施内容: + +- 更新 `doc/gdcc_type_system.md`: + - 增加 compiler-only type 的定位。 + - 明确它不属于 GDScript source-facing type set。 + - 明确 ordinary compatibility matrix 不接受它。 +- 更新 `doc/gdcc_low_ir.md`: + - 记录 `compiler::` LIR-only type grammar。 + - 记录只允许 function variables 的 MVP 约束。 +- 更新 `doc/gdcc_lir_intrinsic.md`: + - 增加 `GdccForRangeIterType` 的四个 intrinsic catalog:init、should_continue、next、get。 +- 更新 `doc/gdcc_c_backend.md`: + - 记录 `gdcc_for_range_iter` C storage type、init/destroy helper、禁止 `godot_*` default helper。 +- 更新 `doc/gdcc_runtime_lib.md`: + - 记录新增 `gdcc_for_range_iter_*` helper 声明/实现边界。 +- 如 lifecycle 行为扩面,更新 `doc/gdcc_ownership_lifecycle_spec.md` 或 backend lifecycle contract。 + +验收细则: + +- 所有新增文档不维护第二份 frontend conversion matrix。 +- 所有新增 intrinsic 均能从 `gdcc_lir_intrinsic.md` 找到 catalog 条目。 +- 计划文档、类型系统、Low IR、C backend、runtime helper 文档中的边界措辞一致。 + +--- + +## 6. 非目标 + +当前计划不覆盖: + +- 把 compiler-only type 暴露给 GDScript 用户声明。 +- 让 `GdCompilerType` 参与 ordinary implicit conversion。 +- 让 compiler-only storage 和 `Variant` 相互转换。 +- 把 compiler-only type 传给 engine method、utility function、global function、property、index 或 operator。 +- 支持 `Array[CompilerOnly]` / `Dictionary[K, CompilerOnly]` 作为 typed container ABI。 +- 支持 public 或 hidden function 使用 compiler-only parameter / return type。 +- 把 Godot internal class,例如 `GDScriptFunctionState`,建模为 source-facing GDCC type。 +- 一次性实现完整 iterator / async lowering。 +- 在本阶段实现 `for` parser / analyzer / lowering,或把 GDScript `range(...)` ordinary call 改写为内部 iterator;当前只实现 `GdccForRangeIterType` 与四个 range iterator intrinsics,作为未来任务的实现与验收锚点。 + +--- + +## 7. 建议 targeted test 命令 + +优先从纯单元测试开始: + +```bash +script/run-gradle-targeted-tests.sh --tests "gd.script.gdcc.frontend.sema.analyzer.support.FrontendVariantBoundaryCompatibilityTest" +``` + +```bash +script/run-gradle-targeted-tests.sh --tests "gd.script.gdcc.scope.resolver.ScopeTypeResolverTest,gd.script.gdcc.scope.resolver.ScopeTypeParsersTest,gd.script.gdcc.scope.ClassRegistryTypeMetaTest,gd.script.gdcc.frontend.sema.FrontendDeclaredTypeSupportTest" +``` + +```bash +script/run-gradle-targeted-tests.sh --tests "gd.script.gdcc.lir.parser.DomLirParserTest,gd.script.gdcc.lir.parser.DomLirSerializerTest,gd.script.gdcc.lir.parser.SimpleLirBlockInsnParserTest,gd.script.gdcc.lir.parser.SimpleLirBlockInsnSerializerTest" +``` + +```bash +script/run-gradle-targeted-tests.sh --tests "gd.script.gdcc.backend.c.gen.CGenHelperTest,gd.script.gdcc.backend.c.gen.CPackUnpackVariantInsnGenTest,gd.script.gdcc.backend.c.gen.CIntrinsicManagerTest,gd.script.gdcc.backend.c.gen.CallIntrinsicInsnGenTest" +``` + +`GdccForRangeIterType` 与四个 intrinsic 实现后补充运行: + +```bash +script/run-gradle-targeted-tests.sh --tests "gd.script.gdcc.type.GdccForRangeIterTypeTest,gd.script.gdcc.backend.c.gen.GdccForRangeIterIntrinsicTest" +``` + +生命周期和 C body shape 实现后再跑: + +```bash +script/run-gradle-targeted-tests.sh --tests "gd.script.gdcc.backend.c.gen.CConstructInsnGenTest,gd.script.gdcc.backend.c.gen.CDestructInsnGenTest,gd.script.gdcc.backend.c.gen.CAssignInsnGenTest,gd.script.gdcc.backend.c.gen.CBodyBuilderPhaseBTest,gd.script.gdcc.backend.c.gen.CBodyBuilderPhaseCTest" +``` + +普通 Godot/backend path 封堵实现后再跑: + +```bash +script/run-gradle-targeted-tests.sh --tests "gd.script.gdcc.backend.c.gen.CallMethodInsnGenTest,gd.script.gdcc.backend.c.gen.CallGlobalInsnGenTest,gd.script.gdcc.backend.c.gen.COperatorInsnGenTest,gd.script.gdcc.backend.c.gen.IndexLoadInsnGenTest,gd.script.gdcc.backend.c.gen.IndexStoreInsnGenTest,gd.script.gdcc.backend.c.gen.CLoadPropertyInsnGenTest,gd.script.gdcc.backend.c.gen.CStorePropertyInsnGenTest" +``` + +frontend lowering shape 实现后再跑: + +```bash +script/run-gradle-targeted-tests.sh --tests "gd.script.gdcc.frontend.lowering.FrontendBodyLoweringSupportTest,gd.script.gdcc.frontend.lowering.FrontendLoweringBodyInsnPassTest,gd.script.gdcc.frontend.lowering.FrontendLoweringFunctionPreparationPassTest" +``` + +最终可以用一个较保守的非 runtime fixture 组合验证边界闭环: + +```bash +script/run-gradle-targeted-tests.sh --tests "gd.script.gdcc.frontend.sema.analyzer.support.FrontendVariantBoundaryCompatibilityTest,gd.script.gdcc.scope.resolver.ScopeTypeResolverTest,gd.script.gdcc.lir.parser.DomLirParserTest,gd.script.gdcc.lir.parser.SimpleLirBlockInsnParserTest,gd.script.gdcc.backend.c.gen.CGenHelperTest,gd.script.gdcc.backend.c.gen.CPackUnpackVariantInsnGenTest,gd.script.gdcc.backend.c.gen.CallIntrinsicInsnGenTest" +``` + +--- + +## 8. 推进顺序 + +建议按以下顺序实施并提交,每一步都保持可编译、可回归: + +1. 类型协议与 `GdccForRangeIterType`。 +2. LIR-only parser / serializer grammar,并先冻结 source-facing resolver 禁止。 +3. frontend ordinary typed-boundary、local stabilization、condition lowering、call materialization 的封堵。 +4. LIR public ABI validator。 +5. C 后端 C type / init / destroy / assignment / pack-unpack / metadata 封堵。 +6. `GdccForRangeIterType` 的 init / should_continue / next / get intrinsic 和 runtime `gdcc_for_range_iter_*` helper。 +7. 普通 method / global / operator / index / property / wrapper path 负例补齐。 +8. 文档同步和 targeted regression。 + +这条顺序的核心约束是:先让 compiler-only type 不能从用户世界进入,再允许它在 LIR/backend 内部出现;先封掉默认 `Variant` / `godot_*` 路径,再接入具体 intrinsic 成功路径。 From b32ecb91499c2dbe83356d27397648c6c32e58f5 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Mon, 29 Jun 2026 16:33:29 +0800 Subject: [PATCH 02/16] =?UTF-8?q?feat(type):=20implement=20phase=201=20of?= =?UTF-8?q?=20GdCompilerType=20plan=20=E2=80=94=20GdccForRangeIterType=20p?= =?UTF-8?q?rotocol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add GdccForRangeIterType to GdType sealed permits - Define stable internal name, LIR-only text, C storage/init/destroy helpers - Fail-fast on godot_* default helpers: pack/unpack, default-value, engine ABI - Render C storage as gdcc_for_range_iter and destroy as gdcc_for_range_iter_destroy - Reject compiler-only types in frontend ordinary boundary and writeback analysis - Add positive protocol tests and negative family/helper barrier tests Affected packages: type, backend/c/gen, frontend/sema/lowering --- .../frontend/frontend_gdcompiler_type_plan.md | 12 ++++- .../gdcc/backend/c/gen/CBodyBuilder.java | 3 ++ .../script/gdcc/backend/c/gen/CCodegen.java | 6 +++ .../script/gdcc/backend/c/gen/CGenHelper.java | 13 +++++ .../c/gen/binding/EngineMethodAbiCodec.java | 4 ++ .../backend/c/gen/insn/DestructInsnGen.java | 5 ++ .../FrontendWritableTypeWritebackSupport.java | 4 ++ .../FrontendVariantBoundaryCompatibility.java | 4 ++ src/main/java/gd/script/gdcc/type/GdType.java | 2 +- .../gdcc/type/GdccForRangeIterType.java | 51 +++++++++++++++++++ .../gdcc/backend/c/gen/CGenHelperTest.java | 17 +++++++ .../gen/binding/EngineMethodAbiCodecTest.java | 16 ++++++ ...ntendWritableTypeWritebackSupportTest.java | 14 +++++ ...ntendVariantBoundaryCompatibilityTest.java | 42 +++++++++++++++ .../gdcc/type/GdccForRangeIterTypeTest.java | 45 ++++++++++++++++ 15 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 src/main/java/gd/script/gdcc/type/GdccForRangeIterType.java create mode 100644 src/test/java/gd/script/gdcc/type/GdccForRangeIterTypeTest.java diff --git a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md index 68b8f689..76d36ef2 100644 --- a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md +++ b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md @@ -6,7 +6,7 @@ ## 文档状态 -- 状态:计划维护中 +- 状态:阶段一已完成,后续阶段计划维护中 - 更新时间:2026-06-29 - 适用范围: - `src/main/java/gd/script/gdcc/type/**` @@ -141,6 +141,16 @@ MVP 采用以下策略: ### 5.1 阶段一:类型协议与 `GdccForRangeIterType` +状态:已完成(2026-06-29)。 + +产出: + +- `GdType` sealed hierarchy 已直接允许 `GdccForRangeIterType`,未新增 `GdCompilerType` 抽象层。 +- `GdccForRangeIterType` 固定内部名、LIR-only text、C storage/init/destroy helper、不可空、无 GDExtension metadata、destroyable 协议。 +- C helper 对该类型使用 `gdcc_for_range_iter` / `gdcc_for_range_iter_destroy`,Variant pack/unpack 和 default-value 路径 fail-fast,copy assignment 不走 `godot_new_*_with_*`。 +- Frontend ordinary boundary/writeback helper 对该类型显式拒绝,避免阶段一后由默认兼容路径泄漏到用户语义。 +- 本阶段未新增任何 `for` parser / analyzer / lowering 行为;prepare 初始化仍按计划留给阶段五专用 init 路径。 + 目标: - 让 `GdType` 能承载 compiler-only storage type,但不把它暴露给用户类型系统。 diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/CBodyBuilder.java b/src/main/java/gd/script/gdcc/backend/c/gen/CBodyBuilder.java index f38d8f69..3bbbd28c 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/CBodyBuilder.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/CBodyBuilder.java @@ -889,6 +889,9 @@ public static String renderStaticStringNameLiteral(@NotNull String value) { @NotNull public static String renderDefaultValueExpr(@NotNull GdType type) { return switch (type) { + case GdccForRangeIterType _ -> throw new IllegalArgumentException( + "compiler-only type leaked into default value expression: " + type.getTypeName() + ); case GdVoidType _ -> ""; case GdBoolType _ -> "false"; case GdIntType _ -> "0"; diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/CCodegen.java b/src/main/java/gd/script/gdcc/backend/c/gen/CCodegen.java index 2f289c7f..31933dd2 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/CCodegen.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/CCodegen.java @@ -157,6 +157,9 @@ private void generateDefaultGetterSetterInitialization() { var bb = new LirBasicBlock("entry"); func.addBasicBlock(bb); switch (propertyDef.getType()) { + case GdccForRangeIterType _ -> throw new IllegalStateException( + "compiler-only type leaked into property initializer: " + propertyDef.getType().getTypeName() + ); case GdObjectType _ -> bb.appendNonTerminatorInstruction(new LiteralNullInsn(tmpVar.id())); case GdVariantType _, GdNilType _ -> bb.appendNonTerminatorInstruction(new LiteralNilInsn(tmpVar.id())); @@ -209,6 +212,9 @@ private void generateFunctionPrepareBlock() { continue; } var initInsn = switch (variable.type()) { + case GdccForRangeIterType _ -> throw new IllegalStateException( + "compiler-only type requires dedicated prepare initialization: " + variable.type().getTypeName() + ); case GdObjectType _ -> new LiteralNullInsn(variable.id()); case GdVariantType _, GdNilType _ -> new LiteralNilInsn(variable.id()); case GdBoolType _ -> new LiteralBoolInsn(variable.id(), false); diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/CGenHelper.java b/src/main/java/gd/script/gdcc/backend/c/gen/CGenHelper.java index 440734f4..33cf8bd3 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/CGenHelper.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/CGenHelper.java @@ -265,6 +265,7 @@ private void collectBindingData(@NotNull List classDefs) { public @NotNull String renderGdTypeInC(@NotNull GdType gdType) { return switch (gdType) { + case GdccForRangeIterType forRangeIterType -> forRangeIterType.getCStorageTypeName(); case GdContainerType gdContainerType -> switch (gdContainerType) { case GdArrayType gdArrayType -> { if (gdArrayType.getValueType() instanceof GdVariantType) { @@ -298,6 +299,7 @@ private void collectBindingData(@NotNull List classDefs) { public @NotNull String renderGdTypeRefInC(@NotNull GdType gdType) { return switch (gdType) { + case GdccForRangeIterType forRangeIterType -> forRangeIterType.getCStorageTypeName() + "*"; case GdContainerType gdContainerType -> switch (gdContainerType) { case GdArrayType gdArrayType -> { if (gdArrayType.getValueType() instanceof GdVariantType) { @@ -467,6 +469,7 @@ public boolean checkEngineMethodHelperRequiresLocalValueSlot(@NotNull EngineMeth public @NotNull String renderGdTypeName(@NotNull GdType gdType) { return switch (gdType) { + case GdccForRangeIterType _ -> gdType.getTypeName(); case GdContainerType gdContainerType -> switch (gdContainerType) { case GdArrayType _ -> "Array"; case GdDictionaryType _ -> "Dictionary"; @@ -516,6 +519,9 @@ public boolean checkEngineMethodHelperRequiresLocalValueSlot(@NotNull EngineMeth } public @NotNull String renderUnpackFunctionName(@NotNull GdType type) { + if (type instanceof GdccForRangeIterType) { + throw new IllegalArgumentException("compiler-only type leaked into Variant unpack: " + type.getTypeName()); + } if (type instanceof GdObjectType objectType) { if (objectType.checkGdccType(context.classRegistry())) { return "(" + objectType.getTypeName() + "*)godot_new_gdcc_Object_with_Variant"; @@ -603,6 +609,9 @@ public boolean checkEngineMethodHelperRequiresLocalValueSlot(@NotNull EngineMeth /// Ordinary pack helpers are the unary `godot_new_Variant_with_` family. /// `Nil` is excluded because it uses the dedicated nullary `godot_new_Variant_nil()`. public @NotNull String renderPackFunctionName(@NotNull GdType type) { + if (type instanceof GdccForRangeIterType) { + throw new IllegalArgumentException("compiler-only type leaked into Variant pack: " + type.getTypeName()); + } if (type instanceof GdNilType) { throw new IllegalArgumentException("Nil uses dedicated godot_new_Variant_nil() materialization"); } @@ -618,6 +627,7 @@ public boolean checkEngineMethodHelperRequiresLocalValueSlot(@NotNull EngineMeth public @NotNull String renderCopyAssignFunctionName(@NotNull GdType type) { return switch (type) { + case GdccForRangeIterType _ -> ""; case GdObjectType _, GdPrimitiveType _ -> ""; case GdVoidType _, GdNilType _ -> throw new IllegalArgumentException("Type " + type.getTypeName() + " does not support copy assignment"); @@ -632,6 +642,9 @@ public boolean checkEngineMethodHelperRequiresLocalValueSlot(@NotNull EngineMeth if (!type.isDestroyable()) { throw new IllegalArgumentException("Type " + type.getTypeName() + " is not destroyable"); } + if (type instanceof GdccForRangeIterType forRangeIterType) { + return forRangeIterType.getCDestroyHelperName(); + } if (type instanceof GdObjectType) { return "godot_object_destroy"; } else { diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/binding/EngineMethodAbiCodec.java b/src/main/java/gd/script/gdcc/backend/c/gen/binding/EngineMethodAbiCodec.java index 7df3c71c..a846d201 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/binding/EngineMethodAbiCodec.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/binding/EngineMethodAbiCodec.java @@ -7,6 +7,7 @@ import gd.script.gdcc.type.GdCallableType; import gd.script.gdcc.type.GdColorType; import gd.script.gdcc.type.GdDictionaryType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdFloatType; import gd.script.gdcc.type.GdFloatVectorType; import gd.script.gdcc.type.GdIntType; @@ -102,6 +103,9 @@ private static void appendTypeDescriptor(@NotNull StringBuilder builder, } builder.append('V'); } + case GdccForRangeIterType _ -> throw new IllegalArgumentException( + "compiler-only type leaked into engine method ABI descriptor: " + type.getTypeName() + ); case GdVariantType _ -> builder.append('R'); case GdNilType _ -> builder.append('n'); case GdObjectType objectType -> appendObjectDescriptor(builder, objectType); diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/insn/DestructInsnGen.java b/src/main/java/gd/script/gdcc/backend/c/gen/insn/DestructInsnGen.java index 27ad8bb4..f5d15ecc 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/insn/DestructInsnGen.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/insn/DestructInsnGen.java @@ -8,6 +8,7 @@ import gd.script.gdcc.lir.insn.DestructInsn; import gd.script.gdcc.scope.RefCountedStatus; import gd.script.gdcc.type.GdContainerType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdMetaType; import gd.script.gdcc.type.GdObjectType; import gd.script.gdcc.type.GdStringLikeType; @@ -33,6 +34,10 @@ public void generateCCode(@NotNull CBodyBuilder bodyBuilder) { case GdVoidType _ -> throw bodyBuilder.invalidInsn("Cannot destruct variable of type " + variable.type().getTypeName()); case GdObjectType objectType -> generateObjectDestruct(bodyBuilder, insn, objectType, variable); + case GdccForRangeIterType _ -> { + var destroyFunc = bodyBuilder.helper().renderDestroyFunctionName(variable.type()); + bodyBuilder.callVoid(destroyFunc, List.of(bodyBuilder.valueOfVar(variable))); + } case GdVariantType _, GdStringLikeType _, GdMetaType _, GdContainerType _ -> { var destroyFunc = bodyBuilder.helper().renderDestroyFunctionName(variable.type()); bodyBuilder.callVoid(destroyFunc, List.of(bodyBuilder.valueOfVar(variable))); diff --git a/src/main/java/gd/script/gdcc/frontend/lowering/FrontendWritableTypeWritebackSupport.java b/src/main/java/gd/script/gdcc/frontend/lowering/FrontendWritableTypeWritebackSupport.java index ae62adec..ada8c252 100644 --- a/src/main/java/gd/script/gdcc/frontend/lowering/FrontendWritableTypeWritebackSupport.java +++ b/src/main/java/gd/script/gdcc/frontend/lowering/FrontendWritableTypeWritebackSupport.java @@ -2,6 +2,7 @@ import gd.script.gdcc.type.GdArrayType; import gd.script.gdcc.type.GdDictionaryType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdObjectType; import gd.script.gdcc.type.GdPrimitiveType; import gd.script.gdcc.type.GdType; @@ -23,6 +24,9 @@ private FrontendWritableTypeWritebackSupport() { public static boolean requiresReverseCommitForCarrierType(@NotNull GdType carrierType) { return switch (Objects.requireNonNull(carrierType, "carrierType must not be null")) { + case GdccForRangeIterType _ -> throw new IllegalArgumentException( + "compiler-only type leaked into frontend writeback analysis: " + carrierType.getTypeName() + ); case GdPrimitiveType _, GdObjectType _, GdArrayType _, GdDictionaryType _ -> false; default -> true; }; diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendVariantBoundaryCompatibility.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendVariantBoundaryCompatibility.java index 8cc822fc..594b88ed 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendVariantBoundaryCompatibility.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendVariantBoundaryCompatibility.java @@ -1,6 +1,7 @@ package gd.script.gdcc.frontend.sema.analyzer.support; import gd.script.gdcc.scope.ClassRegistry; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdFloatType; import gd.script.gdcc.type.GdFloatVectorType; import gd.script.gdcc.type.GdIntType; @@ -65,6 +66,9 @@ private FrontendVariantBoundaryCompatibility() { var registry = Objects.requireNonNull(classRegistry, "classRegistry must not be null"); var source = Objects.requireNonNull(sourceType, "sourceType must not be null"); var target = Objects.requireNonNull(targetType, "targetType must not be null"); + if (source instanceof GdccForRangeIterType || target instanceof GdccForRangeIterType) { + return Decision.REJECT; + } if (target instanceof GdVariantType) { return source instanceof GdVariantType ? Decision.ALLOW_DIRECT : Decision.ALLOW_WITH_PACK; } diff --git a/src/main/java/gd/script/gdcc/type/GdType.java b/src/main/java/gd/script/gdcc/type/GdType.java index 918df941..4e9384f4 100644 --- a/src/main/java/gd/script/gdcc/type/GdType.java +++ b/src/main/java/gd/script/gdcc/type/GdType.java @@ -4,7 +4,7 @@ import org.jetbrains.annotations.Nullable; public sealed interface GdType - permits GdContainerType, GdMetaType, GdNilType, GdObjectType, GdPrimitiveType, GdRidType, GdStringLikeType, GdVariantType, GdVectorType, GdVoidType { + permits GdContainerType, GdMetaType, GdNilType, GdObjectType, GdPrimitiveType, GdRidType, GdStringLikeType, GdVariantType, GdVectorType, GdVoidType, GdccForRangeIterType { @NotNull String getTypeName(); boolean isNullable(); diff --git a/src/main/java/gd/script/gdcc/type/GdccForRangeIterType.java b/src/main/java/gd/script/gdcc/type/GdccForRangeIterType.java new file mode 100644 index 00000000..c8354423 --- /dev/null +++ b/src/main/java/gd/script/gdcc/type/GdccForRangeIterType.java @@ -0,0 +1,51 @@ +package gd.script.gdcc.type; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/// Compiler-only storage type for future lowered `for i in range(...)` iterator state. +/// It is intentionally not source-facing and must only be serialized through LIR-only text. +public final class GdccForRangeIterType implements GdType { + public static final @NotNull GdccForRangeIterType FOR_RANGE_ITER = new GdccForRangeIterType(); + + public static final @NotNull String LIR_TYPE_TEXT = "compiler::GdccForRangeIter"; + public static final @NotNull String C_STORAGE_TYPE_NAME = "gdcc_for_range_iter"; + public static final @NotNull String C_INIT_HELPER_NAME = "gdcc_for_range_iter_init"; + public static final @NotNull String C_DESTROY_HELPER_NAME = "gdcc_for_range_iter_destroy"; + + @Override + public @NotNull String getTypeName() { + return "GdccForRangeIter"; + } + + public @NotNull String getLirTypeText() { + return LIR_TYPE_TEXT; + } + + public @NotNull String getCStorageTypeName() { + return C_STORAGE_TYPE_NAME; + } + + public @NotNull String getCInitHelperName() { + return C_INIT_HELPER_NAME; + } + + public @NotNull String getCDestroyHelperName() { + return C_DESTROY_HELPER_NAME; + } + + @Override + public boolean isNullable() { + return false; + } + + @Override + public @Nullable GdExtensionTypeEnum getGdExtensionType() { + return null; + } + + @Override + public boolean isDestroyable() { + return true; + } +} diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/CGenHelperTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CGenHelperTest.java index d84a539d..c9389ad4 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/CGenHelperTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/CGenHelperTest.java @@ -11,6 +11,7 @@ import gd.script.gdcc.scope.ClassRegistry; import gd.script.gdcc.type.GdArrayType; import gd.script.gdcc.type.GdDictionaryType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdFloatVectorType; import gd.script.gdcc.type.GdFloatType; import gd.script.gdcc.type.GdIntVectorType; @@ -60,6 +61,22 @@ void setUp() throws IOException { helper = new CGenHelper(context, List.of(gdccBase, gdccChild, gdccInner)); } + @Test + @DisplayName("compiler-only range iterator should use gdcc storage helpers and reject Variant helpers") + void compilerOnlyRangeIteratorShouldUseGdccHelpersAndRejectVariantHelpers() { + var type = GdccForRangeIterType.FOR_RANGE_ITER; + + assertEquals("gdcc_for_range_iter", helper.renderGdTypeInC(type)); + assertEquals("gdcc_for_range_iter*", helper.renderGdTypeRefInC(type)); + assertEquals("GdccForRangeIter", helper.renderGdTypeName(type)); + assertEquals("", helper.renderCopyAssignFunctionName(type)); + assertEquals("gdcc_for_range_iter_destroy", helper.renderDestroyFunctionName(type)); + assertFalse(helper.renderGdTypeInC(type).contains("godot_")); + assertFalse(helper.renderDestroyFunctionName(type).contains("godot_")); + assertThrows(IllegalArgumentException.class, () -> helper.renderPackFunctionName(type)); + assertThrows(IllegalArgumentException.class, () -> helper.renderUnpackFunctionName(type)); + } + @Test @DisplayName("checkVirtualMethod should accept exact engine virtual signatures") void checkVirtualMethodShouldAcceptExactEngineVirtualSignatures() { diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/binding/EngineMethodAbiCodecTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/binding/EngineMethodAbiCodecTest.java index 62d1ffc3..b5c4a89a 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/binding/EngineMethodAbiCodecTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/binding/EngineMethodAbiCodecTest.java @@ -7,6 +7,7 @@ import gd.script.gdcc.type.GdCallableType; import gd.script.gdcc.type.GdColorType; import gd.script.gdcc.type.GdDictionaryType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdFloatType; import gd.script.gdcc.type.GdFloatVectorType; import gd.script.gdcc.type.GdIntType; @@ -63,6 +64,21 @@ void codecShouldGenerateTheDocumentedDescriptorExamples() { ); } + @Test + @DisplayName("codec should reject compiler-only types on engine helper ABI") + void codecShouldRejectCompilerOnlyTypesOnEngineHelperAbi() { + var ex = assertThrows( + IllegalArgumentException.class, + () -> EngineMethodAbiCodec.generate(new EngineMethodAbiSignature( + List.of(GdccForRangeIterType.FOR_RANGE_ITER), + GdVoidType.VOID, + false + )) + ); + + assertTrue(ex.getMessage().contains("compiler-only type leaked into engine method ABI descriptor"), ex.getMessage()); + } + @Test @DisplayName("codec should round-trip nested container signatures and object names with underscores") void codecShouldRoundTripNestedContainerSignaturesAndObjectNamesWithUnderscores() { diff --git a/src/test/java/gd/script/gdcc/frontend/lowering/FrontendWritableTypeWritebackSupportTest.java b/src/test/java/gd/script/gdcc/frontend/lowering/FrontendWritableTypeWritebackSupportTest.java index fef63adf..2719ef36 100644 --- a/src/test/java/gd/script/gdcc/frontend/lowering/FrontendWritableTypeWritebackSupportTest.java +++ b/src/test/java/gd/script/gdcc/frontend/lowering/FrontendWritableTypeWritebackSupportTest.java @@ -2,6 +2,7 @@ import gd.script.gdcc.type.GdArrayType; import gd.script.gdcc.type.GdDictionaryType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdIntType; import gd.script.gdcc.type.GdObjectType; import gd.script.gdcc.type.GdPackedNumericArrayType; @@ -10,6 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; class FrontendWritableTypeWritebackSupportTest { @@ -32,4 +34,16 @@ void requiresReverseCommitForCarrierTypeMatchesSharedTypeMatrix() { )) ); } + + @Test + void compilerOnlyTypeCannotEnterFrontendWritebackAnalysis() { + var ex = assertThrows( + IllegalArgumentException.class, + () -> FrontendWritableTypeWritebackSupport.requiresReverseCommitForCarrierType( + GdccForRangeIterType.FOR_RANGE_ITER + ) + ); + + assertTrue(ex.getMessage().contains("compiler-only type leaked into frontend writeback analysis"), ex.getMessage()); + } } diff --git a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendVariantBoundaryCompatibilityTest.java b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendVariantBoundaryCompatibilityTest.java index 9a89096a..4edd9a86 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendVariantBoundaryCompatibilityTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendVariantBoundaryCompatibilityTest.java @@ -3,6 +3,7 @@ import gd.script.gdcc.gdextension.ExtensionApiLoader; import gd.script.gdcc.scope.ClassRegistry; import gd.script.gdcc.type.GdBoolType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdFloatType; import gd.script.gdcc.type.GdFloatVectorType; import gd.script.gdcc.type.GdIntType; @@ -80,6 +81,47 @@ void decideSeparatesDirectPackUnpackAndRejectedPairs() throws Exception { ); } + @Test + void compilerOnlyTypesAreRejectedAtOrdinaryFrontendBoundary() throws Exception { + var classRegistry = new ClassRegistry(ExtensionApiLoader.loadDefault()); + var compilerOnlyType = GdccForRangeIterType.FOR_RANGE_ITER; + + assertAll( + () -> assertEquals( + FrontendVariantBoundaryCompatibility.Decision.REJECT, + FrontendVariantBoundaryCompatibility.determineFrontendBoundaryDecision( + classRegistry, + compilerOnlyType, + compilerOnlyType + ) + ), + () -> assertEquals( + FrontendVariantBoundaryCompatibility.Decision.REJECT, + FrontendVariantBoundaryCompatibility.determineFrontendBoundaryDecision( + classRegistry, + compilerOnlyType, + GdVariantType.VARIANT + ) + ), + () -> assertEquals( + FrontendVariantBoundaryCompatibility.Decision.REJECT, + FrontendVariantBoundaryCompatibility.determineFrontendBoundaryDecision( + classRegistry, + GdVariantType.VARIANT, + compilerOnlyType + ) + ), + () -> assertEquals( + 0, + FrontendVariantBoundaryCompatibility.frontendBoundarySpecificityRank( + classRegistry, + compilerOnlyType, + compilerOnlyType + ) + ) + ); + } + @Test void intrinsicCastDecisionAllowsOnlyDocumentedScalarAndVectorWidening() throws Exception { var classRegistry = new ClassRegistry(ExtensionApiLoader.loadDefault()); diff --git a/src/test/java/gd/script/gdcc/type/GdccForRangeIterTypeTest.java b/src/test/java/gd/script/gdcc/type/GdccForRangeIterTypeTest.java new file mode 100644 index 00000000..6974b9d1 --- /dev/null +++ b/src/test/java/gd/script/gdcc/type/GdccForRangeIterTypeTest.java @@ -0,0 +1,45 @@ +package gd.script.gdcc.type; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class GdccForRangeIterTypeTest { + @Test + void exposesOnlyStableCompilerInternalProtocol() { + var type = GdccForRangeIterType.FOR_RANGE_ITER; + + assertEquals("GdccForRangeIter", type.getTypeName()); + assertEquals("compiler::GdccForRangeIter", type.getLirTypeText()); + assertEquals("gdcc_for_range_iter", type.getCStorageTypeName()); + assertEquals("gdcc_for_range_iter_init", type.getCInitHelperName()); + assertEquals("gdcc_for_range_iter_destroy", type.getCDestroyHelperName()); + assertFalse(type.isNullable()); + assertNull(type.getGdExtensionType()); + assertTrue(type.isDestroyable()); + } + + @Test + void staysOutOfUserFacingTypeFamilies() { + assertNotUserFacingTypeFamily(GdccForRangeIterType.FOR_RANGE_ITER); + } + + private static void assertNotUserFacingTypeFamily(GdType type) { + assertInstanceOf(GdType.class, type); + var userFacingFamilies = List.>of( + GdPrimitiveType.class, + GdObjectType.class, + GdVariantType.class, + GdMetaType.class + ); + for (var family : userFacingFamilies) { + assertFalse(family.isAssignableFrom(type.getClass()), family.getSimpleName()); + } + } +} From 4f7e10ddb0535cfd5455d35292d2d166dbc3c81a Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Mon, 29 Jun 2026 19:01:13 +0800 Subject: [PATCH 03/16] feat(lir): restrict compiler-only type to function local variables - Add use-site aware type parsing/serialization for LIR XML: only function `` may carry `compiler::GdccForRangeIter`; signal/property/parameter/ return/capture surfaces fail-fast on compiler-only type leak. - Reject unknown `compiler::...` grammar with explicit error instead of guessing Object type. - Cover source-facing resolvers/registries and LIR round-trip with new tests. - Mark stage 2 of frontend_gdcompiler_type_plan as completed. --- .../frontend/frontend_gdcompiler_type_plan.md | 10 ++ .../script/gdcc/lir/parser/DomLirParser.java | 82 +++++++--- .../gdcc/lir/parser/DomLirSerializer.java | 40 ++++- .../sema/FrontendDeclaredTypeSupportTest.java | 28 ++++ .../gdcc/lir/parser/DomLirParserTest.java | 143 ++++++++++++++++++ .../gdcc/lir/parser/DomLirSerializerTest.java | 32 ++++ .../gdcc/scope/ClassRegistryTypeMetaTest.java | 9 ++ .../scope/resolver/ScopeTypeResolverTest.java | 13 ++ 8 files changed, 328 insertions(+), 29 deletions(-) diff --git a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md index 76d36ef2..080ac202 100644 --- a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md +++ b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md @@ -190,6 +190,16 @@ MVP 采用以下策略: ### 5.2 阶段二:source-facing resolver 禁止与 LIR-only parser +状态:已完成(2026-06-29)。 + +产出: + +- `ClassRegistry.tryParseStrictTextType(...)`、`ScopeTypeResolver.tryResolveDeclaredType(...)` 与 `ClassRegistry.resolveTypeMetaHere(...)` 继续保持 source-facing strict namespace,不识别 `compiler::GdccForRangeIter` 或 bare `GdccForRangeIter`。 +- `DomLirParser` 新增按 use-site 区分的 LIR type parser:只有 function `` 可解析 `compiler::GdccForRangeIter`,其余 public ABI-like surface 统一以 `compiler-only type leaked into ...` fail-fast。 +- `DomLirSerializer` 新增对应的 compiler-only type text renderer:function local variable 稳定输出 `compiler::GdccForRangeIter`,同时拒绝把 compiler-only 类型序列化到 parameter / return / property / signal surface。 +- ordinary builtin / object / container 的 XML 解析与序列化继续复用既有 `ClassRegistry.findType(...)` / `getTypeName()` 路径,阶段二不改变非 compiler-only 类型行为。 +- 针对 source-facing declared type、registry type-meta、frontend declared type fallback、LIR parser/serializer round-trip 与 leak negative path 的测试已补齐,明确锚定“变量面可用、ABI 面禁止、未知 `compiler::` grammar 不退化为 object guess”。 + 目标: - source-facing type namespace 完全看不到 compiler-only type。 diff --git a/src/main/java/gd/script/gdcc/lir/parser/DomLirParser.java b/src/main/java/gd/script/gdcc/lir/parser/DomLirParser.java index 03ee966a..babc00a5 100644 --- a/src/main/java/gd/script/gdcc/lir/parser/DomLirParser.java +++ b/src/main/java/gd/script/gdcc/lir/parser/DomLirParser.java @@ -5,6 +5,7 @@ import gd.script.gdcc.scope.ClassRegistry; import gd.script.gdcc.type.*; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.w3c.dom.*; @@ -15,6 +16,23 @@ /// DOM-based implementation of LirParser. Parses the XML structure into LIR entities. public final class DomLirParser implements LirParser { + private enum TypeUseSite { + SIGNAL_PARAMETER("signal parameter", false), + PROPERTY("property", false), + FUNCTION_PARAMETER("function parameter", false), + FUNCTION_CAPTURE("function capture", false), + FUNCTION_RETURN("function return", false), + FUNCTION_VARIABLE("function variable", true); + + private final @NotNull String displayName; + private final boolean allowCompilerOnlyType; + + TypeUseSite(@NotNull String displayName, boolean allowCompilerOnlyType) { + this.displayName = displayName; + this.allowCompilerOnlyType = allowCompilerOnlyType; + } + } + private ClassRegistry classRegistry; public DomLirParser(@NotNull ClassRegistry classRegistry) { @@ -75,10 +93,7 @@ public void setClassRegistry(ClassRegistry classRegistry) { for (int pi = 0; pi < params.getLength(); pi++) { var pEl = (Element) params.item(pi); var pname = pEl.getAttribute("name"); - var ptype = classRegistry.findType(pEl.getAttribute("type")); - if (ptype == null) { - throw new IllegalArgumentException("Cannot parse type for signal parameter: " + pEl.getAttribute("type")); - } + var ptype = parseTypeText(pEl.getAttribute("type"), TypeUseSite.SIGNAL_PARAMETER); signal.addParameter(new LirParameterDef(pname, ptype, null, signal)); } signals.add(signal); @@ -94,10 +109,7 @@ public void setClassRegistry(ClassRegistry classRegistry) { for (int pi = 0; pi < pList.getLength(); pi++) { var pEl = (Element) pList.item(pi); var pname = pEl.getAttribute("name"); - var ptype = classRegistry.findType(pEl.getAttribute("type")); - if (ptype == null) { - throw new IllegalArgumentException("Cannot parse type for property: " + pEl.getAttribute("type")); - } + var ptype = parseTypeText(pEl.getAttribute("type"), TypeUseSite.PROPERTY); var isStatic = Boolean.parseBoolean(pEl.getAttribute("is_static")); var init = pEl.hasAttribute("init_func") ? pEl.getAttribute("init_func") : null; var getter = pEl.hasAttribute("getter_func") ? pEl.getAttribute("getter_func") : null; @@ -150,10 +162,7 @@ public void setClassRegistry(ClassRegistry classRegistry) { for (int pi = 0; pi < pList.getLength(); pi++) { var pEl = (Element) pList.item(pi); var pname = pEl.getAttribute("name"); - var ptype = classRegistry.findType(pEl.getAttribute("type")); - if (ptype == null) { - throw new IllegalArgumentException("Cannot parse type for function parameter: " + pEl.getAttribute("type")); - } + var ptype = parseTypeText(pEl.getAttribute("type"), TypeUseSite.FUNCTION_PARAMETER); var defFunc = pEl.hasAttribute("default_value_func") ? pEl.getAttribute("default_value_func") : null; fn.addParameter(new LirParameterDef(pname, ptype, defFunc, fn)); } @@ -167,10 +176,7 @@ public void setClassRegistry(ClassRegistry classRegistry) { for (int ci = 0; ci < cList.getLength(); ci++) { var cEl = (Element) cList.item(ci); var cname = cEl.getAttribute("name"); - var ctype = classRegistry.findType(cEl.getAttribute("type")); - if (ctype == null) { - throw new IllegalArgumentException("Cannot parse type for function capture: " + cEl.getAttribute("type")); - } + var ctype = parseTypeText(cEl.getAttribute("type"), TypeUseSite.FUNCTION_CAPTURE); fn.addCapture(new LirCaptureDef(cname, ctype, fn)); } } @@ -179,10 +185,7 @@ public void setClassRegistry(ClassRegistry classRegistry) { var retNodes = fEl.getElementsByTagName("return_type"); if (retNodes.getLength() > 0) { var rEl = (Element) retNodes.item(0); - var rtype = classRegistry.findType(rEl.getAttribute("type")); - if (rtype == null) { - throw new IllegalArgumentException("Cannot parse return type for function: " + rEl.getAttribute("type")); - } + var rtype = parseTypeText(rEl.getAttribute("type"), TypeUseSite.FUNCTION_RETURN); fn.setReturnType(rtype); } @@ -194,10 +197,7 @@ public void setClassRegistry(ClassRegistry classRegistry) { for (int vi = 0; vi < vList.getLength(); vi++) { var vEl = (Element) vList.item(vi); var id = vEl.getAttribute("id"); - var t = classRegistry.findType(vEl.getAttribute("type")); - if (t == null) { - throw new IllegalArgumentException("Cannot parse type for variable: " + vEl.getAttribute("type")); - } + var t = parseTypeText(vEl.getAttribute("type"), TypeUseSite.FUNCTION_VARIABLE); fn.createAndAddVariable(id, t); } } @@ -253,4 +253,38 @@ public void setClassRegistry(ClassRegistry classRegistry) { public @NotNull LirModule parse(@NotNull java.io.Reader reader) throws Exception { return parse(reader, ""); } + + /// LIR XML keeps compiler-only types on a dedicated grammar and only accepts them for local + /// function variables. All public ABI-like surfaces continue to reuse source-facing parsing. + private @NotNull GdType parseTypeText(@NotNull String rawTypeText, @NotNull TypeUseSite useSite) { + var typeText = rawTypeText.trim(); + if (typeText.isEmpty()) { + throw new IllegalArgumentException("Cannot parse type for " + useSite.displayName + ": blank type text"); + } + var compilerOnlyType = tryParseCompilerOnlyType(typeText, useSite); + if (compilerOnlyType != null) { + return compilerOnlyType; + } + + var parsedType = classRegistry.findType(typeText); + if (parsedType != null) { + return parsedType; + } + throw new IllegalArgumentException("Cannot parse type for " + useSite.displayName + ": " + rawTypeText); + } + + private @Nullable GdType tryParseCompilerOnlyType(@NotNull String typeText, @NotNull TypeUseSite useSite) { + if (!typeText.startsWith("compiler::")) { + return null; + } + if (!useSite.allowCompilerOnlyType) { + throw new IllegalArgumentException( + "compiler-only type leaked into " + useSite.displayName + ": " + typeText + ); + } + if (GdccForRangeIterType.LIR_TYPE_TEXT.equals(typeText)) { + return GdccForRangeIterType.FOR_RANGE_ITER; + } + throw new IllegalArgumentException("Unknown compiler-only type text: " + typeText); + } } diff --git a/src/main/java/gd/script/gdcc/lir/parser/DomLirSerializer.java b/src/main/java/gd/script/gdcc/lir/parser/DomLirSerializer.java index 0997c3e4..00578187 100644 --- a/src/main/java/gd/script/gdcc/lir/parser/DomLirSerializer.java +++ b/src/main/java/gd/script/gdcc/lir/parser/DomLirSerializer.java @@ -21,6 +21,22 @@ /// DOM-based implementation of LirSerializer. public final class DomLirSerializer implements LirSerializer { + private enum TypeUseSite { + SIGNAL_PARAMETER("signal parameter", false), + PROPERTY("property", false), + FUNCTION_PARAMETER("function parameter", false), + FUNCTION_RETURN("function return", false), + FUNCTION_VARIABLE("function variable", true); + + private final @NotNull String displayName; + private final boolean allowCompilerOnlyType; + + TypeUseSite(@NotNull String displayName, boolean allowCompilerOnlyType) { + this.displayName = displayName; + this.allowCompilerOnlyType = allowCompilerOnlyType; + } + } + @Override public void serialize(@NotNull LirModule module, @NotNull Writer out) throws Exception { var docFactory = DocumentBuilderFactory.newInstance(); @@ -57,7 +73,7 @@ public void serialize(@NotNull LirModule module, @NotNull Writer out) throws Exc var p = s.getParameter(i); Element pEl = doc.createElement("parameter"); pEl.setAttribute("name", Objects.requireNonNull(p).name()); - pEl.setAttribute("type", p.type().getTypeName()); + pEl.setAttribute("type", renderTypeText(p.type(), TypeUseSite.SIGNAL_PARAMETER)); sEl.appendChild(pEl); } signalsEl.appendChild(sEl); @@ -69,7 +85,7 @@ public void serialize(@NotNull LirModule module, @NotNull Writer out) throws Exc for (var prop : cls.getProperties()) { Element pEl = doc.createElement("property"); pEl.setAttribute("name", prop.getName()); - pEl.setAttribute("type", prop.getType().getTypeName()); + pEl.setAttribute("type", renderTypeText(prop.getType(), TypeUseSite.PROPERTY)); pEl.setAttribute("is_static", Boolean.toString(prop.isStatic())); if (prop.getInitFunc() != null) pEl.setAttribute("init_func", prop.getInitFunc()); if (prop.getGetterFunc() != null) pEl.setAttribute("getter_func", prop.getGetterFunc()); @@ -111,7 +127,7 @@ public void serialize(@NotNull LirModule module, @NotNull Writer out) throws Exc } Element pEl = doc.createElement("parameter"); pEl.setAttribute("name", p.name()); - pEl.setAttribute("type", p.type().getTypeName()); + pEl.setAttribute("type", renderTypeText(p.type(), TypeUseSite.FUNCTION_PARAMETER)); if (p.defaultValueFunc() != null) pEl.setAttribute("default_value_func", p.defaultValueFunc()); paramsEl.appendChild(pEl); } @@ -123,7 +139,7 @@ public void serialize(@NotNull LirModule module, @NotNull Writer out) throws Exc // return type Element retEl = doc.createElement("return_type"); - retEl.setAttribute("type", fn.getReturnType().getTypeName()); + retEl.setAttribute("type", renderTypeText(fn.getReturnType(), TypeUseSite.FUNCTION_RETURN)); fEl.appendChild(retEl); // variables @@ -131,7 +147,7 @@ public void serialize(@NotNull LirModule module, @NotNull Writer out) throws Exc for (var v : fn.getVariables().values()) { Element vEl = doc.createElement("variable"); vEl.setAttribute("id", v.id()); - vEl.setAttribute("type", v.type().getTypeName()); + vEl.setAttribute("type", renderTypeText(v.type(), TypeUseSite.FUNCTION_VARIABLE)); varsEl.appendChild(vEl); } fEl.appendChild(varsEl); @@ -174,4 +190,18 @@ public void serialize(@NotNull LirModule module, @NotNull Writer out) throws Exc transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); transformer.transform(new DOMSource(doc), new StreamResult(out)); } + + /// Serializer keeps compiler-only types on the dedicated `compiler::...` grammar and rejects any + /// attempt to leak them into outward-facing metadata surfaces. + private @NotNull String renderTypeText(@NotNull GdType type, @NotNull TypeUseSite useSite) { + if (type instanceof GdccForRangeIterType compilerOnlyType) { + if (!useSite.allowCompilerOnlyType) { + throw new IllegalArgumentException( + "compiler-only type leaked into " + useSite.displayName + ": " + compilerOnlyType.getLirTypeText() + ); + } + return compilerOnlyType.getLirTypeText(); + } + return type.getTypeName(); + } } diff --git a/src/test/java/gd/script/gdcc/frontend/sema/FrontendDeclaredTypeSupportTest.java b/src/test/java/gd/script/gdcc/frontend/sema/FrontendDeclaredTypeSupportTest.java index b2584e5c..80a11dd3 100644 --- a/src/test/java/gd/script/gdcc/frontend/sema/FrontendDeclaredTypeSupportTest.java +++ b/src/test/java/gd/script/gdcc/frontend/sema/FrontendDeclaredTypeSupportTest.java @@ -8,6 +8,7 @@ import gd.script.gdcc.lir.LirClassDef; import gd.script.gdcc.scope.ClassRegistry; import gd.script.gdcc.type.GdArrayType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdIntType; import gd.script.gdcc.type.GdObjectType; import gd.script.gdcc.type.GdVariantType; @@ -144,6 +145,33 @@ void resolveTypeOrVariantWarnsAndFallsBackForUnsupportedStructuredTypeText() thr assertTrue(diagnostic.message().contains("Array[Array[int]]")); } + @Test + void resolveTypeOrVariantWarnsAndFallsBackForCompilerOnlyDeclaredTypeTexts() throws IOException { + var diagnostics = new DiagnosticManager(); + var registry = new ClassRegistry(ExtensionApiLoader.loadDefault()); + + var compilerPrefixedType = FrontendDeclaredTypeSupport.resolveTypeOrVariant( + new TypeRef(GdccForRangeIterType.LIR_TYPE_TEXT, SYNTHETIC_RANGE), + registry, + Map.of(), + SOURCE_PATH, + diagnostics + ); + var bareCompilerType = FrontendDeclaredTypeSupport.resolveTypeOrVariant( + new TypeRef(GdccForRangeIterType.FOR_RANGE_ITER.getTypeName(), SYNTHETIC_RANGE), + registry, + Map.of(), + SOURCE_PATH, + diagnostics + ); + + assertEquals(GdVariantType.VARIANT, compilerPrefixedType); + assertEquals(GdVariantType.VARIANT, bareCompilerType); + assertEquals(2, diagnostics.snapshot().size()); + assertTrue(diagnostics.snapshot().asList().getFirst().message().contains(GdccForRangeIterType.LIR_TYPE_TEXT)); + assertTrue(diagnostics.snapshot().asList().getLast().message().contains(GdccForRangeIterType.FOR_RANGE_ITER.getTypeName())); + } + @Test void resolveTypeOrVariantRemapsMappedTopLevelSourceNameAfterStrictLexicalMiss() throws IOException { var diagnostics = new DiagnosticManager(); diff --git a/src/test/java/gd/script/gdcc/lir/parser/DomLirParserTest.java b/src/test/java/gd/script/gdcc/lir/parser/DomLirParserTest.java index 0c8fb635..f36ac9f2 100644 --- a/src/test/java/gd/script/gdcc/lir/parser/DomLirParserTest.java +++ b/src/test/java/gd/script/gdcc/lir/parser/DomLirParserTest.java @@ -3,6 +3,7 @@ import gd.script.gdcc.gdextension.ExtensionApiLoader; import gd.script.gdcc.type.GdArrayType; import gd.script.gdcc.type.GdDictionaryType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdObjectType; import gd.script.gdcc.type.GdStringType; import gd.script.gdcc.lir.LirModule; @@ -217,4 +218,146 @@ public void parse_preservesMappedTopLevelCanonicalClassIdentity() throws Excepti assertNotEquals("MappedOuter", cls.getName()); assertNotEquals("BaseBySource", cls.getSuperName()); } + + @Test + public void parse_allowsCompilerOnlyTypeOnlyOnFunctionVariables() throws Exception { + var xml = """ + + + + + + + + + + + + + return; + + + + + + + """; + + var parser = new DomLirParser(new ClassRegistry(ExtensionApiLoader.loadDefault())); + var mod = parser.parse(new StringReader(xml)); + var fn = mod.getClassDefs().getFirst().getFunctions().getFirst(); + + assertEquals(GdccForRangeIterType.FOR_RANGE_ITER, fn.getVariableById("iter").type()); + } + + @Test + public void parse_rejectsCompilerOnlyTypeOnPublicAbiAndLambdaCaptureSurfaces() throws Exception { + var surfaces = java.util.Map.of( + "signal parameter", """ + + + + + + + + + + + """, + "property", """ + + + + + + + + + """, + "function parameter", """ + + + + + + + + + + + return; + + + + + """, + "function return", """ + + + + + + + + + return; + + + + + """, + "function capture", """ + + + + + + + + + + + return; + + + + + """ + ); + + for (var entry : surfaces.entrySet()) { + var parser = new DomLirParser(new ClassRegistry(ExtensionApiLoader.loadDefault())); + var exception = assertThrows(IllegalArgumentException.class, () -> parser.parse(new StringReader(entry.getValue())), entry.getKey()); + + assertTrue(exception.getMessage().contains("compiler-only type leaked into " + entry.getKey()), exception.getMessage()); + } + } + + @Test + public void parse_rejectsUnknownCompilerOnlyGrammarWithoutGuessingObjectType() throws Exception { + var xml = """ + + + + + + + + + + + return; + + + + + """; + + var parser = new DomLirParser(new ClassRegistry(ExtensionApiLoader.loadDefault())); + var exception = assertThrows(IllegalArgumentException.class, () -> parser.parse(new StringReader(xml))); + + assertTrue(exception.getMessage().contains("Unknown compiler-only type text: compiler::UnknownIter"), exception.getMessage()); + assertFalse(exception.getMessage().contains("GdObjectType"), exception.getMessage()); + } } diff --git a/src/test/java/gd/script/gdcc/lir/parser/DomLirSerializerTest.java b/src/test/java/gd/script/gdcc/lir/parser/DomLirSerializerTest.java index 9f1de404..3de45e79 100644 --- a/src/test/java/gd/script/gdcc/lir/parser/DomLirSerializerTest.java +++ b/src/test/java/gd/script/gdcc/lir/parser/DomLirSerializerTest.java @@ -6,6 +6,7 @@ import gd.script.gdcc.lir.*; import gd.script.gdcc.lir.insn.*; import gd.script.gdcc.type.GdFloatType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdObjectType; import org.junit.jupiter.api.Test; @@ -93,4 +94,35 @@ public void serialize_module_preservesMappedTopLevelCanonicalClassIdentity() thr assertFalse(xml.contains("MappedOuter"), xml); assertFalse(xml.contains("BaseBySource"), xml); } + + @Test + public void serialize_module_usesCompilerOnlyGrammarForFunctionVariables() throws Exception { + var fn = new LirFunctionDef("_init", "entry"); + fn.createAndAddVariable("iter", GdccForRangeIterType.FOR_RANGE_ITER); + fn.addBasicBlock(new LirBasicBlock("entry", List.of(new ReturnInsn(null)))); + + var cls = new LirClassDef("RotatingCamera", "Camera3D", false, false, Map.of(), List.of(), List.of(), List.of(fn)); + var module = new LirModule("m", List.of(cls)); + var serializer = new DomLirSerializer(); + + var xml = serializer.serializeToString(module); + + assertTrue(xml.contains("type=\"compiler::GdccForRangeIter\""), xml); + assertFalse(xml.contains("type=\"GdccForRangeIter\""), xml); + } + + @Test + public void serialize_module_rejectsCompilerOnlyTypeOnFunctionReturnSurface() { + var fn = new LirFunctionDef("helper", "entry"); + fn.setReturnType(GdccForRangeIterType.FOR_RANGE_ITER); + fn.addBasicBlock(new LirBasicBlock("entry", List.of(new ReturnInsn(null)))); + + var cls = new LirClassDef("RotatingCamera", "Camera3D", false, false, Map.of(), List.of(), List.of(), List.of(fn)); + var module = new LirModule("m", List.of(cls)); + var serializer = new DomLirSerializer(); + + var exception = assertThrows(IllegalArgumentException.class, () -> serializer.serializeToString(module)); + + assertTrue(exception.getMessage().contains("compiler-only type leaked into function return"), exception.getMessage()); + } } diff --git a/src/test/java/gd/script/gdcc/scope/ClassRegistryTypeMetaTest.java b/src/test/java/gd/script/gdcc/scope/ClassRegistryTypeMetaTest.java index 77fcacf9..4230cb09 100644 --- a/src/test/java/gd/script/gdcc/scope/ClassRegistryTypeMetaTest.java +++ b/src/test/java/gd/script/gdcc/scope/ClassRegistryTypeMetaTest.java @@ -4,6 +4,7 @@ import gd.script.gdcc.lir.LirClassDef; import gd.script.gdcc.type.GdArrayType; import gd.script.gdcc.type.GdDictionaryType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdIntType; import gd.script.gdcc.type.GdObjectType; import gd.script.gdcc.type.GdStringType; @@ -135,4 +136,12 @@ void resolveTypeMetaRejectsUnknownUtilityAndSingletonNames() throws IOException } assertNull(registry.resolveTypeMeta("DefinitelyMissingType")); } + + @Test + void resolveTypeMetaDoesNotPublishCompilerOnlyTypeMeta() throws IOException { + var registry = new ClassRegistry(ExtensionApiLoader.loadDefault()); + + assertNull(registry.resolveTypeMeta(GdccForRangeIterType.LIR_TYPE_TEXT)); + assertNull(registry.resolveTypeMeta(GdccForRangeIterType.FOR_RANGE_ITER.getTypeName())); + } } diff --git a/src/test/java/gd/script/gdcc/scope/resolver/ScopeTypeResolverTest.java b/src/test/java/gd/script/gdcc/scope/resolver/ScopeTypeResolverTest.java index bf1071d9..d2be980b 100644 --- a/src/test/java/gd/script/gdcc/scope/resolver/ScopeTypeResolverTest.java +++ b/src/test/java/gd/script/gdcc/scope/resolver/ScopeTypeResolverTest.java @@ -8,6 +8,7 @@ import gd.script.gdcc.scope.ScopeTypeMetaKind; import gd.script.gdcc.type.GdArrayType; import gd.script.gdcc.type.GdDictionaryType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdObjectType; import gd.script.gdcc.type.GdStringType; import gd.script.gdcc.scope.Scope; @@ -120,6 +121,18 @@ public GdObjectType mapUnresolvedType(Scope ignoredScope, String unresolvedTypeT assertEquals(0, mapperCalls.get()); } + @Test + void declaredTypeResolutionKeepsCompilerOnlyTextsOutOfSourceFacingNamespace() throws IOException { + var registry = new ClassRegistry(ExtensionApiLoader.loadDefault()); + var ownerClass = new LirClassDef("Owner", "Object"); + registry.addGdccClass(ownerClass); + var scope = new ClassScope(registry, registry, ownerClass); + + assertNull(ScopeTypeResolver.tryResolveTypeMeta(scope, GdccForRangeIterType.LIR_TYPE_TEXT)); + assertNull(ScopeTypeResolver.tryResolveDeclaredType(scope, GdccForRangeIterType.LIR_TYPE_TEXT)); + assertNull(ScopeTypeResolver.tryResolveDeclaredType(scope, GdccForRangeIterType.FOR_RANGE_ITER.getTypeName())); + } + @Test void classRegistryDeclaredTypeResolutionDelegatesToSharedResolver() throws IOException { var registry = new ClassRegistry(ExtensionApiLoader.loadDefault()); From 0f18d08867af5e6fa39fcfc3433ca1a9122f85d3 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Mon, 29 Jun 2026 20:05:44 +0800 Subject: [PATCH 04/16] feat(frontend): fail-fast guards preventing compiler-only type leaks into semantic facts and lowering boundaries - Reject compiler-only types in expression type publish/resolve and inferred-local backfill paths - Reject compiler-only resolved initializer in local slot stabilization - Reject compiler-only types in condition published facts and condition normalization - Reject compiler-only source/target in frontend boundary materialization - Add tests covering all new guard points --- .../frontend/frontend_gdcompiler_type_plan.md | 11 ++++ .../body/FrontendBodyLoweringSession.java | 17 ++++++ ...FrontendCfgNodeInsnLoweringProcessors.java | 30 ++++++----- .../analyzer/FrontendExprTypeAnalyzer.java | 19 +++++++ ...rontendLocalTypeStabilizationAnalyzer.java | 7 +++ .../analyzer/FrontendTypeCheckAnalyzer.java | 9 +++- .../FrontendLoweringBodyInsnPassTest.java | 43 ++++++++++++++- .../body/FrontendBodyLoweringSessionTest.java | 40 ++++++++++++++ .../FrontendExprTypeAnalyzerTest.java | 54 ++++++++++++++++++- ...endLocalTypeStabilizationAnalyzerTest.java | 32 +++++++++++ .../FrontendTypeCheckAnalyzerTest.java | 44 +++++++++++++-- 11 files changed, 285 insertions(+), 21 deletions(-) diff --git a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md index 080ac202..3e1052bd 100644 --- a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md +++ b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md @@ -236,6 +236,17 @@ MVP 采用以下策略: ### 5.3 阶段三:frontend semantic 与 lowering 边界封堵 +状态:已完成(2026-06-29)。 + +产出: + +- `FrontendExprTypeAnalyzer` 在 `expressionTypes()` 发布入口与 inferred-local backfill 路径上对 compiler-only type fail-fast,避免 compiler-only published fact 进入用户 expression/local semantic facts。 +- `FrontendLocalTypeStabilizationAnalyzer` 在 local slot 写回前显式拒绝 compiler-only resolved initializer,保持 ordinary local `:=` 只接受用户语义类型。 +- `FrontendTypeCheckAnalyzer` 对 condition published fact 新增 compiler-only guard,防止 leaked compiler-only condition 静默通过 shared type-check 消费。 +- `FrontendBodyLoweringSession.materializeFrontendBoundaryValue(...)` 新增 compiler-only source/target 二次防线;即使 shared boundary helper 未来回归或 synthetic CFG/value fact 被篡改,也不会生成 `PackVariantInsn`、`UnpackVariantInsn`、`ConstructBuiltinInsn` 或 intrinsic-cast boundary。 +- `FrontendCfgNodeInsnLoweringProcessors` 对 condition normalization 新增 compiler-only fail-fast,阻止 compiler-only condition 走 `pack_variant -> unpack_variant(bool)` lowering。 +- 针对 shared boundary、semantic facts、local stabilization、condition type-check、boundary materialization 与 condition lowering 的正反测试已补齐,并继续保留既有 ordinary boundary happy path。 + 目标: - compiler-only type 不进入用户 semantic facts。 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 c1d44a5a..ac1e4d8c 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 @@ -31,6 +31,7 @@ import gd.script.gdcc.scope.ParameterDef; import gd.script.gdcc.scope.PropertyDef; import gd.script.gdcc.type.GdContainerType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdFloatType; import gd.script.gdcc.type.GdFloatVectorType; import gd.script.gdcc.type.GdIntType; @@ -743,6 +744,22 @@ void ensureVariable(@NotNull String variableId, @NotNull GdType expectedType) { + "' must not materialize into target type void; value-required lowering sites must not request a concrete slot for void" ); } + if (source instanceof GdccForRangeIterType compilerOnlyType) { + throw new IllegalStateException( + "compiler-only type leaked into frontend boundary source '" + + use + + "': " + + compilerOnlyType.getTypeName() + ); + } + if (target instanceof GdccForRangeIterType compilerOnlyType) { + throw new IllegalStateException( + "compiler-only type leaked into frontend boundary target '" + + use + + "': " + + compilerOnlyType.getTypeName() + ); + } return switch (FrontendVariantBoundaryCompatibility.determineFrontendBoundaryDecision( classRegistry, source, diff --git a/src/main/java/gd/script/gdcc/frontend/lowering/pass/body/FrontendCfgNodeInsnLoweringProcessors.java b/src/main/java/gd/script/gdcc/frontend/lowering/pass/body/FrontendCfgNodeInsnLoweringProcessors.java index 2afeaf64..44cce9e3 100644 --- a/src/main/java/gd/script/gdcc/frontend/lowering/pass/body/FrontendCfgNodeInsnLoweringProcessors.java +++ b/src/main/java/gd/script/gdcc/frontend/lowering/pass/body/FrontendCfgNodeInsnLoweringProcessors.java @@ -9,6 +9,7 @@ import gd.script.gdcc.lir.insn.ReturnInsn; import gd.script.gdcc.lir.insn.UnpackVariantInsn; import gd.script.gdcc.type.GdBoolType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdType; import gd.script.gdcc.type.GdVariantType; import org.jetbrains.annotations.NotNull; @@ -96,19 +97,24 @@ private void emitConditionBranch( ) { var sourceSlotId = FrontendBodyLoweringSupport.cfgTempSlotId(conditionValueId); session.ensureVariable(sourceSlotId, conditionType); - if (conditionType instanceof GdBoolType) { - block.setTerminator(new GoIfInsn(sourceSlotId, trueBlockId, falseBlockId)); - return; - } - - if (conditionType instanceof GdVariantType) { - var boolSlotId = FrontendBodyLoweringSupport.conditionBoolSlotId(conditionValueId); - session.ensureVariable(boolSlotId, GdBoolType.BOOL); - block.appendNonTerminatorInstruction(new UnpackVariantInsn(boolSlotId, sourceSlotId)); - block.setTerminator(new GoIfInsn(boolSlotId, trueBlockId, falseBlockId)); - return; + switch (conditionType) { + case GdBoolType _ -> { + block.setTerminator(new GoIfInsn(sourceSlotId, trueBlockId, falseBlockId)); + return; + } + case GdVariantType _ -> { + var boolSlotId = FrontendBodyLoweringSupport.conditionBoolSlotId(conditionValueId); + session.ensureVariable(boolSlotId, GdBoolType.BOOL); + block.appendNonTerminatorInstruction(new UnpackVariantInsn(boolSlotId, sourceSlotId)); + block.setTerminator(new GoIfInsn(boolSlotId, trueBlockId, falseBlockId)); + return; + } + case GdccForRangeIterType compilerOnlyType -> throw new IllegalStateException( + "compiler-only type leaked into frontend condition normalization: " + + compilerOnlyType.getTypeName() + ); + default -> {} } - var variantSlotId = FrontendBodyLoweringSupport.conditionVariantSlotId(conditionValueId); var boolSlotId = FrontendBodyLoweringSupport.conditionBoolSlotId(conditionValueId); session.ensureVariable(variantSlotId, GdVariantType.VARIANT); 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 466de318..d52ee8c7 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 @@ -18,6 +18,7 @@ import gd.script.gdcc.scope.Scope; import gd.script.gdcc.scope.ScopeOwnerKind; import gd.script.gdcc.scope.ScopeValue; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdType; import gd.script.gdcc.type.GdVariantType; import gd.script.gdcc.type.GdVoidType; @@ -468,6 +469,12 @@ private void indexSubtree(@NotNull Node node, @Nullable Node parent) { } var published = expressionTypes.get(expression); if (published != null) { + if (published.publishedType() instanceof GdccForRangeIterType compilerOnlyType) { + throw new IllegalStateException( + "compiler-only type leaked into frontend expressionTypes(): " + + compilerOnlyType.getTypeName() + ); + } return published; } var computed = resolveExpressionType(expression, allowStatementResult); @@ -482,6 +489,12 @@ private void publishResolvedExpressionType( if (isRouteHeadOnlyTypeMeta(expression)) { return; } + if (computed.publishedType() instanceof GdccForRangeIterType compilerOnlyType) { + throw new IllegalStateException( + "compiler-only type leaked into frontend expressionTypes(): " + + compilerOnlyType.getTypeName() + ); + } expressionTypes.put(expression, computed); } @@ -534,6 +547,12 @@ private void backfillInferredLocalType(@NotNull VariableDeclaration variableDecl if (backfilledType == null || backfilledType instanceof GdVoidType) { return; } + if (backfilledType instanceof GdccForRangeIterType 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( 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 fde3ad7c..9e651464 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 @@ -47,6 +47,7 @@ import gd.script.gdcc.scope.ResolveRestriction; import gd.script.gdcc.scope.Scope; import gd.script.gdcc.scope.ScopeValue; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdType; import gd.script.gdcc.type.GdVoidType; import org.jetbrains.annotations.NotNull; @@ -191,6 +192,12 @@ static void probeStabilizeLocalSlot( if (publishedType instanceof GdVoidType) { return null; } + if (publishedType instanceof GdccForRangeIterType compilerOnlyType) { + throw new IllegalStateException( + "compiler-only type leaked into frontend local stabilization: " + + compilerOnlyType.getTypeName() + ); + } return publishedType; } diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTypeCheckAnalyzer.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTypeCheckAnalyzer.java index d7fa3fb9..42b918a6 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTypeCheckAnalyzer.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTypeCheckAnalyzer.java @@ -18,6 +18,7 @@ import gd.script.gdcc.scope.ResolveRestriction; import gd.script.gdcc.scope.Scope; import gd.script.gdcc.scope.ScopeValue; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdType; import gd.script.gdcc.type.GdVariantType; import gd.script.gdcc.type.GdVoidType; @@ -252,10 +253,16 @@ protected void visitConditionExpression( // Conditions only require a stable published typed fact here. // Godot-compatible truthiness stays a downstream lowering/runtime concern instead of being // reinterpreted here as a strict-bool source-language contract. - Objects.requireNonNull( + var conditionType = Objects.requireNonNull( publishedConditionType.publishedType(), "publishedType must not be null for stable condition expression" ); + if (conditionType instanceof GdccForRangeIterType compilerOnlyType) { + throw new IllegalStateException( + "compiler-only type leaked into frontend condition fact: " + + compilerOnlyType.getTypeName() + ); + } } protected record TypeCheckAccess( diff --git a/src/test/java/gd/script/gdcc/frontend/lowering/FrontendLoweringBodyInsnPassTest.java b/src/test/java/gd/script/gdcc/frontend/lowering/FrontendLoweringBodyInsnPassTest.java index e2c58b0d..a5271548 100644 --- a/src/test/java/gd/script/gdcc/frontend/lowering/FrontendLoweringBodyInsnPassTest.java +++ b/src/test/java/gd/script/gdcc/frontend/lowering/FrontendLoweringBodyInsnPassTest.java @@ -65,6 +65,7 @@ import gd.script.gdcc.lir.insn.VariantSetNamedInsn; import gd.script.gdcc.scope.ClassRegistry; import gd.script.gdcc.scope.ScopeOwnerKind; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdFloatType; import gd.script.gdcc.type.GdFloatVectorType; import gd.script.gdcc.type.GdObjectType; @@ -955,6 +956,46 @@ func branch_on_int(count: int) -> int: ); } + @Test + void runFailsFastWhenConditionFactLeaksCompilerOnlyType() throws Exception { + var prepared = prepareContext( + "body_insn_compiler_only_condition.gd", + """ + class_name BodyInsnCompilerOnlyCondition + extends RefCounted + + func branch_on_int(count: int) -> int: + if count: + return 1 + return 2 + """, + Map.of("BodyInsnCompilerOnlyCondition", "RuntimeBodyInsnCompilerOnlyCondition"), + true + ); + var functionContext = requireContext( + prepared.context().requireFunctionLoweringContexts(), + FunctionLoweringContext.Kind.EXECUTABLE_BODY, + "RuntimeBodyInsnCompilerOnlyCondition", + "branch_on_int" + ); + var root = assertInstanceOf(dev.superice.gdparser.frontend.ast.Block.class, functionContext.loweringRoot()); + var ifStatement = assertInstanceOf(dev.superice.gdparser.frontend.ast.IfStatement.class, root.statements().getFirst()); + functionContext.analysisData().expressionTypes().put( + ifStatement.condition(), + FrontendExpressionType.resolved(GdccForRangeIterType.FOR_RANGE_ITER) + ); + + var failure = assertThrows( + IllegalStateException.class, + () -> new FrontendLoweringBodyInsnPass().run(prepared.context()) + ); + + assertAll( + () -> assertFalse(prepared.diagnostics().hasErrors()), + () -> assertTrue(failure.getMessage().contains("compiler-only type leaked into frontend condition normalization")) + ); + } + @Test void runChoosesIndexedNamedAndKeyedSubscriptInstructionsFromPublishedKeyTypes() throws Exception { var prepared = prepareContext( @@ -2846,7 +2887,7 @@ void runLowersSingletonReceiverBeforeLaterLocalShadowAsGlobalScopeLoad() throws """ class_name BodyInsnSingletonReceiverLaterLocal extends RefCounted - + func frames() -> int: var frames := Engine.get_frames_drawn() var Engine: String = "" diff --git a/src/test/java/gd/script/gdcc/frontend/lowering/pass/body/FrontendBodyLoweringSessionTest.java b/src/test/java/gd/script/gdcc/frontend/lowering/pass/body/FrontendBodyLoweringSessionTest.java index b6a9c6e5..219c7c59 100644 --- a/src/test/java/gd/script/gdcc/frontend/lowering/pass/body/FrontendBodyLoweringSessionTest.java +++ b/src/test/java/gd/script/gdcc/frontend/lowering/pass/body/FrontendBodyLoweringSessionTest.java @@ -25,6 +25,7 @@ import gd.script.gdcc.scope.ClassRegistry; import gd.script.gdcc.scope.ScopeOwnerKind; import gd.script.gdcc.type.GdDictionaryType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdFloatType; import gd.script.gdcc.type.GdFloatVectorType; import gd.script.gdcc.type.GdIntType; @@ -624,6 +625,45 @@ void materializeFrontendBoundaryValueFailsFastForVoidSourceOrTarget() throws Exc ); } + @Test + void materializeFrontendBoundaryValueFailsFastWhenCompilerOnlyTypeLeaksIntoBoundary() throws Exception { + var session = prepareSession(); + var sourceLeakBlock = new LirBasicBlock("compiler_only_source"); + var targetLeakBlock = new LirBasicBlock("compiler_only_target"); + session.ensureVariable("iter_slot", GdccForRangeIterType.FOR_RANGE_ITER); + session.ensureVariable("value_slot", GdIntType.INT); + + var sourceFailure = assertThrows( + IllegalStateException.class, + () -> session.materializeFrontendBoundaryValue( + sourceLeakBlock, + "iter_slot", + GdccForRangeIterType.FOR_RANGE_ITER, + GdVariantType.VARIANT, + "compiler_only_source_boundary" + ) + ); + var targetFailure = assertThrows( + IllegalStateException.class, + () -> session.materializeFrontendBoundaryValue( + targetLeakBlock, + "value_slot", + GdVariantType.VARIANT, + GdccForRangeIterType.FOR_RANGE_ITER, + "compiler_only_target_boundary" + ) + ); + + assertAll( + () -> assertTrue(sourceFailure.getMessage().contains("compiler-only type leaked into frontend boundary source")), + () -> assertTrue(sourceFailure.getMessage().contains("compiler_only_source_boundary")), + () -> assertTrue(targetFailure.getMessage().contains("compiler-only type leaked into frontend boundary target")), + () -> assertTrue(targetFailure.getMessage().contains("compiler_only_target_boundary")), + () -> assertTrue(sourceLeakBlock.getNonTerminatorInstructions().isEmpty()), + () -> assertTrue(targetLeakBlock.getNonTerminatorInstructions().isEmpty()) + ); + } + @Test void loweringProcessorRegistryReturnsContinuationBlockChosenByProcessor() throws Exception { var session = prepareSession(); 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 7d6ab3f9..3155756d 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 @@ -17,6 +17,8 @@ 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 dev.superice.gdparser.frontend.ast.AttributeCallStep; import dev.superice.gdparser.frontend.ast.AttributeExpression; import dev.superice.gdparser.frontend.ast.AttributePropertyStep; @@ -154,7 +156,7 @@ void analyzeKeepsSingletonResolvedValueForLaterLocalAndInitializerSelfReferenceT """ class_name ExprTypeSingletonResolvedValueStability extends RefCounted - + func ping(): Engine Engine.get_frames_drawn() @@ -2323,7 +2325,7 @@ void analyzeBackfillRefreshesPublishedLocalResolvedValuePayload() throws Excepti """ class_name ExprTypeBackfillRefreshesBindingResolvedValue extends RefCounted - + func ping(): var value := 1 value @@ -2432,6 +2434,54 @@ func ping(): assertTrue(failure.getMessage().contains("value")); } + @Test + void analyzeFailsFastWhenResolvedCompilerOnlyExpressionFactWouldBePublished() throws Exception { + var input = prepareInputBeforeExpressionTyping( + "expr_type_compiler_only_expression_fact.gd", + """ + class_name ExprTypeCompilerOnlyExpressionFact + extends RefCounted + + func ping(): + var value: int = 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 failure = assertThrows( + IllegalStateException.class, + () -> new FrontendExprTypeAnalyzer().analyze( + input.classRegistry(), + input.analysisData(), + input.diagnosticManager() + ) + ); + + assertTrue(failure.getMessage().contains("compiler-only type leaked into frontend expressionTypes()")); + } + @Test void analyzeLeavesInferredLocalsAsVariantWhenInitializerCannotPublishStableType() throws Exception { var analyzed = analyze( 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 fb321839..2957725f 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 @@ -26,10 +26,12 @@ import gd.script.gdcc.frontend.scope.ClassScope; import gd.script.gdcc.frontend.sema.FrontendAnalysisData; import gd.script.gdcc.frontend.sema.FrontendClassSkeletonBuilder; +import gd.script.gdcc.frontend.sema.FrontendExpressionType; import gd.script.gdcc.frontend.sema.FrontendExpressionTypeStatus; import gd.script.gdcc.gdextension.ExtensionApiLoader; import gd.script.gdcc.lir.LirClassDef; import gd.script.gdcc.scope.ClassRegistry; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdType; import gd.script.gdcc.type.GdVariantType; import org.jetbrains.annotations.NotNull; @@ -45,6 +47,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.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; class FrontendLocalTypeStabilizationAnalyzerTest { @@ -180,6 +183,35 @@ void probeKeepsAssignmentExpressionInitializerAsVariantFallback() throws Excepti ); } + @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( 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 ef9f7e3a..f633092c 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 @@ -15,6 +15,7 @@ import gd.script.gdcc.scope.ClassRegistry; import gd.script.gdcc.scope.PropertyDef; import gd.script.gdcc.scope.ResolveRestriction; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdVariantType; import gd.script.gdcc.type.GdVoidType; import dev.superice.gdparser.frontend.ast.AssignmentExpression; @@ -715,24 +716,24 @@ void analyzeAcceptsStringFamilyBoundariesAtOrdinaryTypeCheckSlots() throws Excep var preparedInput = prepareTypeCheckInput("type_check_string_family_boundaries.gd", """ class_name TypeCheckStringFamilyBoundaries extends RefCounted - + var accepted_property_name: StringName = "" var accepted_property_text: String = &"property_text" var rejected_property: int = &"not_an_int" - + func ping(text: String, name: StringName) -> void: var accepted_local_name: StringName = text var accepted_local_text: String = name var rejected_local: int = text name = text text = name - + func return_name(text: String) -> StringName: return text - + func return_text(name: StringName) -> String: return name - + func reject_return(text: String) -> int: return text """); @@ -1211,6 +1212,39 @@ func ping(payload): ); } + @Test + void analyzeFailsFastWhenConditionFactLeaksCompilerOnlyType() throws Exception { + var preparedInput = prepareTypeCheckInput("type_check_compiler_only_condition.gd", """ + class_name TypeCheckCompilerOnlyCondition + extends RefCounted + + func ping(): + if 1: + pass + """); + var pingFunction = findFunction(preparedInput.unit().ast(), "ping"); + var ifStatement = findNode( + pingFunction, + dev.superice.gdparser.frontend.ast.IfStatement.class, + ignored -> true + ); + preparedInput.analysisData().expressionTypes().put( + ifStatement.condition(), + FrontendExpressionType.resolved(GdccForRangeIterType.FOR_RANGE_ITER) + ); + + var failure = assertThrows( + IllegalStateException.class, + () -> new FrontendTypeCheckAnalyzer().analyze( + preparedInput.classRegistry(), + preparedInput.analysisData(), + preparedInput.diagnosticManager() + ) + ); + + assertTrue(failure.getMessage().contains("compiler-only type leaked into frontend condition fact")); + } + private static void assertEvent( @NotNull ProbeEvent event, @NotNull String expectedKind, From 53094989aa0d5aae9820e6940d30771d6770eb02 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Mon, 29 Jun 2026 21:14:10 +0800 Subject: [PATCH 05/16] docs(frontend/type): add GdCompilerType abstraction phase and sync fact sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Insert Phase 4 (GdCompilerType sealed interface + GdccForRangeIterType migration) into the plan, renumber subsequent phases 5→9 - Update risk analysis: GdCompilerType is now a planned requirement, not an open question - Add compiler-only type category and compatibility boundary to gdcc_type_system.md - Document compiler:: grammar for backend-owned locals in gdcc_low_ir.md - Add compiler-only storage naming rules (gdcc_* over godot_*) to gdcc_c_backend.md --- .../gdcompiler_type_design_risk_analysis.md | 5 +- doc/gdcc_c_backend.md | 2 + doc/gdcc_low_ir.md | 2 + doc/gdcc_type_system.md | 8 +-- .../frontend/frontend_gdcompiler_type_plan.md | 54 ++++++++++++++++--- 5 files changed, 59 insertions(+), 12 deletions(-) diff --git a/doc/analysis/gdcompiler_type_design_risk_analysis.md b/doc/analysis/gdcompiler_type_design_risk_analysis.md index 641549eb..a4b522df 100644 --- a/doc/analysis/gdcompiler_type_design_risk_analysis.md +++ b/doc/analysis/gdcompiler_type_design_risk_analysis.md @@ -357,12 +357,13 @@ public sealed interface GdCompilerType extends GdType } ``` -这只是形态建议,不是实现要求。需要注意: +当前实施计划已经把 `GdCompilerType` 抽象层纳入后续阶段,因此这里应视为目标协议而不是是否抽象的开放问题。需要注意: -- 不要把只有一个实现的额外 interface 拆出来;如果短期只有一种 compiler-only 类型,可以先用一个 concrete type 承载,等第二种出现再抽出 sealed 分支。 - 如果所有具体类型都必须 init/destroy,那么协议方法放在 `GdCompilerType` 上是合理的。 - 如果某些 compiler-only 类型没有 destroy 语义,需要决定是返回 no-op helper,还是允许 `destroyFunctionName()` 为空。当前需求说“返回初始化函数名和销毁函数名”,报告按必有 helper 处理。 +计划文档已要求在首个 concrete compiler-only type 落地后补入该抽象层,因此后续实现应以 `GdCompilerType` 为准,不再沿用“只有一个实现时暂不抽象”的旧策略。 + ### 9.2 解析层 建议拆分: diff --git a/doc/gdcc_c_backend.md b/doc/gdcc_c_backend.md index 3afc1c66..dd29f17c 100644 --- a/doc/gdcc_c_backend.md +++ b/doc/gdcc_c_backend.md @@ -31,6 +31,7 @@ already exists, otherwise use the project's own `compiler-cache` directory. `REAL_T_IS_DOUBLE` and 32-bit Godot builds are outside the supported target matrix. - Any Object, including engine object and GDCC objects, are always passed as pointers, and the pointers are passed as values, usually we do not use pointers to pointers. - There are 3 types: built-in types, engine types, and GDCC types, see [gdcc_type_system.md](gdcc_type_system.md) for more details. +- Compiler-only storage types are backend-owned `GdType` implementations that do not belong to the source-facing type namespace. They are rendered with explicit `gdcc_*` storage/helper names and must never fall back to the default `godot_*` naming path. - Engine types and GDCC types are all Objects, built-in types are not Objects. - Only `godot_bool`, `godot_int` and `godot_float` can be used directly as C primitive types, they are always passed by value. - Variable `ref` semantics in generated C (mandatory): @@ -93,6 +94,7 @@ already exists, otherwise use the project's own `compiler-cache` directory. - If we can 100% sure that an object is ref-counted, we can use `own_object` and `release_object` directly, these 2 functions are faster. - For type mapping between compiler types and C types: - GDCC types are directly used as C types, e.g., `MyCustomGdClass` is used as `MyCustomGdClass*`. + - Compiler-only types are mapped to their own explicit C storage names and helper names, not to generated `godot_*` ABI symbols. - Other types are mapped with a `godot_` prefix, e.g., `int` is mapped to `godot_int`, `String` is mapped to `godot_String`. - Always remember GDExtension API does not receive GDCC object ptrs, convert them to `godot_Object*` using generated per-class helper functions first. - When receiving `godot_Object*` from GDExtension API that is actually a GDCC object, convert it to the correct GDCC type using `gdcc_object_from_godot_object_ptr(GDExtensionObjectPtr ptr)` if necessary. diff --git a/doc/gdcc_low_ir.md b/doc/gdcc_low_ir.md index 43118958..740ab967 100644 --- a/doc/gdcc_low_ir.md +++ b/doc/gdcc_low_ir.md @@ -24,6 +24,8 @@ manipulate variables. See [Types](gdcc_type_system.md) for details on the type system used in Low IR. +Low IR can also carry backend-owned compiler-only types. They use the `compiler::` textual grammar and are only valid on backend-owned local variables unless a dedicated validator explicitly allows a wider surface. + ## Instructions Each instruction has an optional return value, a string id, and a list of operands: diff --git a/doc/gdcc_type_system.md b/doc/gdcc_type_system.md index 703437f7..f43f488e 100644 --- a/doc/gdcc_type_system.md +++ b/doc/gdcc_type_system.md @@ -6,8 +6,9 @@ - Primitive types: `GdPrimitiveType` and subclasses (`GdIntType`, `GdFloatType`, `GdBoolType`, `GdStringType`, ...). - Vector/geometry types: `GdVectorType`, `GdFloatVectorType`, `GdQuaternionType`, `GdTransform3DType`, etc. - Container types: `GdArrayType`, `GdDictionaryType`, and `GdPacked*` variants. - - Object/reference types: `GdObjectType`, `GdNodePathType`, `GdRidType`, `GdSignalType`, `GdCallableType`. - - Meta/extension types: `GdMetaType`, `GdExtensionTypeEnum` for annotations and extension points. +- Object/reference types: `GdObjectType`, `GdNodePathType`, `GdRidType`, `GdSignalType`, `GdCallableType`. +- Meta/extension types: `GdMetaType`, `GdExtensionTypeEnum` for annotations and extension points. +- Compiler-only types: `GdCompilerType` as the shared abstraction for backend-only storage types, with `GdccForRangeIterType` as the first concrete example. ## Major Types (Summary) - `GdPrimitiveType`: atomic values. @@ -60,7 +61,8 @@ - `Array[SubClass]` -> `Array[SuperClass]` - `Dictionary[K, V]` -> `Dictionary` / `Dictionary[Variant, Variant]` - `Dictionary[K1, V1]` -> `Dictionary[K2, V2]` when both key/value directions are assignable. - - Other implicit promotions (for example numeric promotion) are not part of generic assignment compatibility and must be handled by dedicated lowering/instruction semantics. +- Other implicit promotions (for example numeric promotion) are not part of generic assignment compatibility and must be handled by dedicated lowering/instruction semantics. +- Compiler-only types are not part of the ordinary assignment-compatibility matrix; they are handled by LIR/backend-specific contracts and must not be treated as source-facing declared types. - For "TypeType", which is a type representing another type: - e.g. `var N = Node` where `N` is a "TypeType" representing the `Node` type. - We do not explicitly model "TypeType" in the type system; instead, we use a `StringName` to represent the type name as the implementation detail. diff --git a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md index 3e1052bd..b0f11322 100644 --- a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md +++ b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md @@ -6,7 +6,7 @@ ## 文档状态 -- 状态:阶段一已完成,后续阶段计划维护中 +- 状态:阶段一已完成,抽象层阶段已补入,后续阶段计划维护中 - 更新时间:2026-06-29 - 适用范围: - `src/main/java/gd/script/gdcc/type/**` @@ -101,7 +101,7 @@ Godot upstream 的对齐依据是:`GDScriptFunctionState` 注册为 internal c ## 3. 实施原则 - 先定义边界,再放开路径。任何默认兼容、默认 C helper 命名、默认 metadata 生成都必须对 compiler-only type 显式处理。 -- 实现应从 `GdccForRangeIterType` 和一组 range iterator intrinsic 闭环开始。不要在只有一个实现时额外制造一层抽象;如果后续已经有两个以上具体 compiler-only 类型,才引入 `GdCompilerType` sealed interface。 +- 实现应从 `GdccForRangeIterType` 和一组 range iterator intrinsic 闭环开始;随后补入 `GdCompilerType` 作为 compiler-only 类型的共同抽象层,并把已实现的具体类型迁移到该层下。 - compiler-only 类型不参与 ordinary frontend typed-boundary matrix。内部 LIR slot 的 direct assignment 由 backend/LIR 合同处理,不由 source-facing semantic compatibility 扩面。 - helper 命名使用 `gdcc_*`,不得伪造 `godot_*` generated binding helper。 - lifecycle 走非对象 destroyable value 路径,不走 object ownership。 @@ -283,7 +283,46 @@ MVP 采用以下策略: - `FrontendLoweringBodyInsnPassTest` - `FrontendWritableTypeWritebackSupportTest` -### 5.4 阶段四:LIR public ABI validator +### 5.4 阶段四:`GdCompilerType` 抽象层与既有实现回迁 + +状态:计划中。 + +产出: + +- 新增 `GdCompilerType` sealed interface,承载所有 compiler-only 类型的共同协议。 +- 将已实现的 `GdccForRangeIterType` 调整为 `GdCompilerType` 的具体实现,并保留其现有 internal name、LIR-only text、C helper 协议与 destroyable 语义。 +- 把 compiler-only 类型的共同约束从 `GdType` 直接实现中抽离到抽象层,避免后续新增 compiler-only 类型继续复制协议方法。 +- 更新相关既有实现,使 phase 1/2/3/5/6/7/8 继续以抽象层为事实源。 + +目标: + +- 让 compiler-only 类型共享统一协议,而不再依赖单个 concrete type 直接承载所有约定。 +- 为后续新增第二个及以上 compiler-only 类型预留扩展点。 +- 保持现有 `GdccForRangeIterType` 的行为和边界不变,只改变它的类型归属与共同协议来源。 + +建议实施内容: + +- 在 `src/main/java/gd/script/gdcc/type/` 中新增 `GdCompilerType` sealed interface,定义 compiler-only 类型共同协议。 +- 让 `GdccForRangeIterType` 实现 `GdCompilerType`,并把 shared contract 从 concrete type 的重复实现中收拢到接口默认实现或公共辅助方法中。 +- 更新 `GdType` permits,使其包含 `GdCompilerType`,而不是把 compiler-only concrete type 继续直接散落在根层 permits。 +- 调整阶段一里关于“暂不抽象化”的表述,改为“先完成首个 concrete type,再在当前计划中补入抽象层与迁移”。 +- 检查后续源码和测试中对 compiler-only 类型的判断,优先改为面向 `GdCompilerType` 的判断。 + +验收细则: + +- happy path: + - `GdccForRangeIterType` 继续通过所有现有 type/backend/serialization 边界测试。 + - 新增的抽象层测试覆盖 `GdCompilerType` 的共同契约。 +- negative path: + - 不再出现“所有 compiler-only 类型都必须直接挂到 `GdType` 根层”的实现假设。 + - `GdccForRangeIterType` 仍不进入 source-facing type namespace、public ABI 或 ordinary boundary。 + +测试锚点: + +- `GdType` / `GdccForRangeIterType` 相关单测。 +- 新增 `GdCompilerType` 契约测试。 + +### 5.5 阶段五:LIR public ABI validator 目标: @@ -318,7 +357,7 @@ MVP 采用以下策略: - `CGenHelperTest` - backend integration shape tests 中加入 public ABI negative cases。 -### 5.5 阶段五:C 后端类型渲染、初始化、赋值与销毁 +### 5.6 阶段六:C 后端类型渲染、初始化、赋值与销毁 目标: @@ -361,7 +400,7 @@ MVP 采用以下策略: - `CBodyBuilderPhaseBTest` - `CBodyBuilderPhaseCTest` -### 5.6 阶段六:`GdccForRangeIterType` intrinsic 最小闭环 +### 5.7 阶段七:`GdccForRangeIterType` intrinsic 最小闭环 目标: @@ -423,7 +462,7 @@ MVP 采用以下策略: - `CallIntrinsicInsnGenTest` - 新增 `GdccForRangeIter` intrinsic tests,覆盖正步长、负步长、exclusive end、zero step 策略和类型错误。 -### 5.7 阶段七:普通 Godot / Variant / engine 路径封堵 +### 5.8 阶段八:普通 Godot / Variant / engine 路径封堵 目标: @@ -459,7 +498,7 @@ MVP 采用以下策略: - `CStorePropertyInsnGenTest` - `CGenHelperTest` -### 5.8 阶段八:文档同步与回归收口 +### 5.9 阶段九:文档同步与回归收口 目标: @@ -471,6 +510,7 @@ MVP 采用以下策略: - 更新 `doc/gdcc_type_system.md`: - 增加 compiler-only type 的定位。 - 明确它不属于 GDScript source-facing type set。 + - 明确 `GdCompilerType` 是 compiler-only 共同抽象层,`GdccForRangeIterType` 是其首个具体实现。 - 明确 ordinary compatibility matrix 不接受它。 - 更新 `doc/gdcc_low_ir.md`: - 记录 `compiler::` LIR-only type grammar。 From 7cc045fe740759e41ee20771f7a58b3b48a34ebd Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Tue, 30 Jun 2026 00:11:39 +0800 Subject: [PATCH 06/16] feat(type): add compiler-only type abstraction layer - Introduce sealed interface for compiler-only storage types with shared protocol and design invariants - Migrate existing concrete type to the new abstraction, removing duplicated defaults - Update all consumer-side type checks across frontend, backend, and LIR to use the abstraction - Add contract tests and update existing tests to anchor abstraction membership - Sync implementation plan status --- .../frontend/frontend_gdcompiler_type_plan.md | 30 +++-- .../gdcc/backend/c/gen/CBodyBuilder.java | 2 +- .../script/gdcc/backend/c/gen/CCodegen.java | 4 +- .../script/gdcc/backend/c/gen/CGenHelper.java | 39 +++--- .../c/gen/binding/EngineMethodAbiCodec.java | 4 +- .../backend/c/gen/insn/DestructInsnGen.java | 4 +- .../FrontendWritableTypeWritebackSupport.java | 4 +- .../body/FrontendBodyLoweringSession.java | 6 +- ...FrontendCfgNodeInsnLoweringProcessors.java | 4 +- .../analyzer/FrontendExprTypeAnalyzer.java | 8 +- ...rontendLocalTypeStabilizationAnalyzer.java | 4 +- .../analyzer/FrontendTypeCheckAnalyzer.java | 4 +- .../FrontendVariantBoundaryCompatibility.java | 4 +- .../gdcc/lir/parser/DomLirSerializer.java | 2 +- .../gd/script/gdcc/type/GdCompilerType.java | 47 ++++++++ src/main/java/gd/script/gdcc/type/GdType.java | 2 +- .../gdcc/type/GdccForRangeIterType.java | 22 +--- .../script/gdcc/type/GdCompilerTypeTest.java | 112 ++++++++++++++++++ .../gdcc/type/GdccForRangeIterTypeTest.java | 4 + 19 files changed, 231 insertions(+), 75 deletions(-) create mode 100644 src/main/java/gd/script/gdcc/type/GdCompilerType.java create mode 100644 src/test/java/gd/script/gdcc/type/GdCompilerTypeTest.java diff --git a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md index b0f11322..5963f9fd 100644 --- a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md +++ b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md @@ -6,7 +6,7 @@ ## 文档状态 -- 状态:阶段一已完成,抽象层阶段已补入,后续阶段计划维护中 +- 状态:阶段一至四已完成,后续阶段计划维护中 - 更新时间:2026-06-29 - 适用范围: - `src/main/java/gd/script/gdcc/type/**` @@ -145,7 +145,7 @@ MVP 采用以下策略: 产出: -- `GdType` sealed hierarchy 已直接允许 `GdccForRangeIterType`,未新增 `GdCompilerType` 抽象层。 +- `GdType` sealed hierarchy 最初直接允许 `GdccForRangeIterType`,阶段四已补入 `GdCompilerType` 抽象层并完成回迁。 - `GdccForRangeIterType` 固定内部名、LIR-only text、C storage/init/destroy helper、不可空、无 GDExtension metadata、destroyable 协议。 - C helper 对该类型使用 `gdcc_for_range_iter` / `gdcc_for_range_iter_destroy`,Variant pack/unpack 和 default-value 路径 fail-fast,copy assignment 不走 `godot_new_*_with_*`。 - Frontend ordinary boundary/writeback helper 对该类型显式拒绝,避免阶段一后由默认兼容路径泄漏到用户语义。 @@ -160,7 +160,7 @@ MVP 采用以下策略: 建议实施内容: - 更新 `GdType` sealed permits。 -- 新增首个具体 compiler-only type:`GdccForRangeIterType`。在只有这一种具体类型时,优先让它直接进入 `GdType` sealed hierarchy;不要为了一个实现先制造单独 `GdCompilerType` 抽象层。 +- 新增首个具体 compiler-only type:`GdccForRangeIterType`。先完成首个 concrete type 再在阶段四补入 `GdCompilerType` 抽象层并回迁,避免为一个实现过早制造抽象层。 - 类型协议至少覆盖: - `getTypeName()`:建议稳定为 `GdccForRangeIter`,用于内部 identity,不作为 source-facing declared type text。 - LIR-only text:`compiler::GdccForRangeIter`。 @@ -285,14 +285,17 @@ MVP 采用以下策略: ### 5.4 阶段四:`GdCompilerType` 抽象层与既有实现回迁 -状态:计划中。 +状态:已完成(2026-06-29)。 产出: -- 新增 `GdCompilerType` sealed interface,承载所有 compiler-only 类型的共同协议。 -- 将已实现的 `GdccForRangeIterType` 调整为 `GdCompilerType` 的具体实现,并保留其现有 internal name、LIR-only text、C helper 协议与 destroyable 语义。 -- 把 compiler-only 类型的共同约束从 `GdType` 直接实现中抽离到抽象层,避免后续新增 compiler-only 类型继续复制协议方法。 -- 更新相关既有实现,使 phase 1/2/3/5/6/7/8 继续以抽象层为事实源。 +- 新增 `GdCompilerType` sealed interface,承载所有 compiler-only 类型的共同协议:`getLirTypeText()`、`getCStorageTypeName()`、`getCInitHelperName()`、`getCDestroyHelperName()`,以及共享默认实现 `isNullable() == false`、`getGdExtensionType() == null`、`isDestroyable() == true`。 +- `GdccForRangeIterType` 从直接 `implements GdType` 迁移为 `implements GdCompilerType`,移除了与抽象层默认实现重复的 `isNullable()`、`getGdExtensionType()`、`isDestroyable()` 覆写,保留其 internal name、LIR-only text、C helper 协议不变。 +- `GdType` permits 从直接包含 `GdccForRangeIterType` 改为包含 `GdCompilerType`,使 compiler-only 类型通过统一抽象层挂入 sealed hierarchy。 +- 所有 consumer 侧的 `instanceof GdccForRangeIterType` / `case GdccForRangeIterType` 判断已迁移为面向 `GdCompilerType` 的判断,覆盖 frontend leak guards(`FrontendVariantBoundaryCompatibility`、`FrontendExprTypeAnalyzer`、`FrontendLocalTypeStabilizationAnalyzer`、`FrontendTypeCheckAnalyzer`、`FrontendBodyLoweringSession`、`FrontendCfgNodeInsnLoweringProcessors`、`FrontendWritableTypeWritebackSupport`)、C backend guards(`CGenHelper`、`CCodegen`、`CBodyBuilder`、`DestructInsnGen`、`EngineMethodAbiCodec`)和 LIR serializer(`DomLirSerializer`)。 +- `DomLirParser` 的 text-to-instance dispatch 保留对 `GdccForRangeIterType.LIR_TYPE_TEXT` 的引用,因为该路径是文本到具体实例的映射,不是类型判断。 +- 新增 `GdCompilerTypeTest` 契约测试,覆盖抽象层协议方法、共享默认值、sealed hierarchy 归属、user-facing family 排除和 `gdcc_*` helper 命名约束的正反两面。 +- `GdccForRangeIterTypeTest` 已更新,增加 `assertInstanceOf(GdCompilerType.class, type)` 断言以锚定抽象层归属。 目标: @@ -606,10 +609,11 @@ script/run-gradle-targeted-tests.sh --tests "gd.script.gdcc.frontend.sema.analyz 1. 类型协议与 `GdccForRangeIterType`。 2. LIR-only parser / serializer grammar,并先冻结 source-facing resolver 禁止。 3. frontend ordinary typed-boundary、local stabilization、condition lowering、call materialization 的封堵。 -4. LIR public ABI validator。 -5. C 后端 C type / init / destroy / assignment / pack-unpack / metadata 封堵。 -6. `GdccForRangeIterType` 的 init / should_continue / next / get intrinsic 和 runtime `gdcc_for_range_iter_*` helper。 -7. 普通 method / global / operator / index / property / wrapper path 负例补齐。 -8. 文档同步和 targeted regression。 +4. `GdCompilerType` 抽象层与既有实现回迁(已完成)。 +5. LIR public ABI validator。 +6. C 后端 C type / init / destroy / assignment / pack-unpack / metadata 封堵。 +7. `GdccForRangeIterType` 的 init / should_continue / next / get intrinsic 和 runtime `gdcc_for_range_iter_*` helper。 +8. 普通 method / global / operator / index / property / wrapper path 负例补齐。 +9. 文档同步和 targeted regression。 这条顺序的核心约束是:先让 compiler-only type 不能从用户世界进入,再允许它在 LIR/backend 内部出现;先封掉默认 `Variant` / `godot_*` 路径,再接入具体 intrinsic 成功路径。 diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/CBodyBuilder.java b/src/main/java/gd/script/gdcc/backend/c/gen/CBodyBuilder.java index 3bbbd28c..1bc9413c 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/CBodyBuilder.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/CBodyBuilder.java @@ -889,7 +889,7 @@ public static String renderStaticStringNameLiteral(@NotNull String value) { @NotNull public static String renderDefaultValueExpr(@NotNull GdType type) { return switch (type) { - case GdccForRangeIterType _ -> throw new IllegalArgumentException( + case GdCompilerType _ -> throw new IllegalArgumentException( "compiler-only type leaked into default value expression: " + type.getTypeName() ); case GdVoidType _ -> ""; diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/CCodegen.java b/src/main/java/gd/script/gdcc/backend/c/gen/CCodegen.java index 31933dd2..44e3f220 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/CCodegen.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/CCodegen.java @@ -157,7 +157,7 @@ private void generateDefaultGetterSetterInitialization() { var bb = new LirBasicBlock("entry"); func.addBasicBlock(bb); switch (propertyDef.getType()) { - case GdccForRangeIterType _ -> throw new IllegalStateException( + case GdCompilerType _ -> throw new IllegalStateException( "compiler-only type leaked into property initializer: " + propertyDef.getType().getTypeName() ); case GdObjectType _ -> bb.appendNonTerminatorInstruction(new LiteralNullInsn(tmpVar.id())); @@ -212,7 +212,7 @@ private void generateFunctionPrepareBlock() { continue; } var initInsn = switch (variable.type()) { - case GdccForRangeIterType _ -> throw new IllegalStateException( + case GdCompilerType _ -> throw new IllegalStateException( "compiler-only type requires dedicated prepare initialization: " + variable.type().getTypeName() ); case GdObjectType _ -> new LiteralNullInsn(variable.id()); diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/CGenHelper.java b/src/main/java/gd/script/gdcc/backend/c/gen/CGenHelper.java index 33cf8bd3..734ac395 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/CGenHelper.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/CGenHelper.java @@ -265,7 +265,7 @@ private void collectBindingData(@NotNull List classDefs) { public @NotNull String renderGdTypeInC(@NotNull GdType gdType) { return switch (gdType) { - case GdccForRangeIterType forRangeIterType -> forRangeIterType.getCStorageTypeName(); + case GdCompilerType compilerType -> compilerType.getCStorageTypeName(); case GdContainerType gdContainerType -> switch (gdContainerType) { case GdArrayType gdArrayType -> { if (gdArrayType.getValueType() instanceof GdVariantType) { @@ -299,7 +299,7 @@ private void collectBindingData(@NotNull List classDefs) { public @NotNull String renderGdTypeRefInC(@NotNull GdType gdType) { return switch (gdType) { - case GdccForRangeIterType forRangeIterType -> forRangeIterType.getCStorageTypeName() + "*"; + case GdCompilerType compilerType -> compilerType.getCStorageTypeName() + "*"; case GdContainerType gdContainerType -> switch (gdContainerType) { case GdArrayType gdArrayType -> { if (gdArrayType.getValueType() instanceof GdVariantType) { @@ -469,7 +469,7 @@ public boolean checkEngineMethodHelperRequiresLocalValueSlot(@NotNull EngineMeth public @NotNull String renderGdTypeName(@NotNull GdType gdType) { return switch (gdType) { - case GdccForRangeIterType _ -> gdType.getTypeName(); + case GdCompilerType _ -> gdType.getTypeName(); case GdContainerType gdContainerType -> switch (gdContainerType) { case GdArrayType _ -> "Array"; case GdDictionaryType _ -> "Dictionary"; @@ -519,7 +519,7 @@ public boolean checkEngineMethodHelperRequiresLocalValueSlot(@NotNull EngineMeth } public @NotNull String renderUnpackFunctionName(@NotNull GdType type) { - if (type instanceof GdccForRangeIterType) { + if (type instanceof GdCompilerType) { throw new IllegalArgumentException("compiler-only type leaked into Variant unpack: " + type.getTypeName()); } if (type instanceof GdObjectType objectType) { @@ -609,25 +609,26 @@ public boolean checkEngineMethodHelperRequiresLocalValueSlot(@NotNull EngineMeth /// Ordinary pack helpers are the unary `godot_new_Variant_with_` family. /// `Nil` is excluded because it uses the dedicated nullary `godot_new_Variant_nil()`. public @NotNull String renderPackFunctionName(@NotNull GdType type) { - if (type instanceof GdccForRangeIterType) { - throw new IllegalArgumentException("compiler-only type leaked into Variant pack: " + type.getTypeName()); - } - if (type instanceof GdNilType) { - throw new IllegalArgumentException("Nil uses dedicated godot_new_Variant_nil() materialization"); - } - if (type instanceof GdObjectType objectType) { - if (objectType.checkGdccType(context.classRegistry())) { - return "gdcc_new_Variant_with_gdcc_Object"; + switch (type) { + case GdCompilerType _ -> + throw new IllegalArgumentException("compiler-only type leaked into Variant pack: " + type.getTypeName()); + case GdNilType _ -> + throw new IllegalArgumentException("Nil uses dedicated godot_new_Variant_nil() materialization"); + case GdObjectType objectType -> { + if (objectType.checkGdccType(context.classRegistry())) { + return "gdcc_new_Variant_with_gdcc_Object"; + } + return "godot_new_Variant_with_Object"; + } + default -> { + return "godot_new_Variant_with_" + renderGdTypeName(type); } - return "godot_new_Variant_with_Object"; - } else { - return "godot_new_Variant_with_" + renderGdTypeName(type); } } public @NotNull String renderCopyAssignFunctionName(@NotNull GdType type) { return switch (type) { - case GdccForRangeIterType _ -> ""; + case GdCompilerType _ -> ""; case GdObjectType _, GdPrimitiveType _ -> ""; case GdVoidType _, GdNilType _ -> throw new IllegalArgumentException("Type " + type.getTypeName() + " does not support copy assignment"); @@ -642,8 +643,8 @@ public boolean checkEngineMethodHelperRequiresLocalValueSlot(@NotNull EngineMeth if (!type.isDestroyable()) { throw new IllegalArgumentException("Type " + type.getTypeName() + " is not destroyable"); } - if (type instanceof GdccForRangeIterType forRangeIterType) { - return forRangeIterType.getCDestroyHelperName(); + if (type instanceof GdCompilerType compilerType) { + return compilerType.getCDestroyHelperName(); } if (type instanceof GdObjectType) { return "godot_object_destroy"; diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/binding/EngineMethodAbiCodec.java b/src/main/java/gd/script/gdcc/backend/c/gen/binding/EngineMethodAbiCodec.java index a846d201..b4924332 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/binding/EngineMethodAbiCodec.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/binding/EngineMethodAbiCodec.java @@ -7,7 +7,7 @@ import gd.script.gdcc.type.GdCallableType; import gd.script.gdcc.type.GdColorType; import gd.script.gdcc.type.GdDictionaryType; -import gd.script.gdcc.type.GdccForRangeIterType; +import gd.script.gdcc.type.GdCompilerType; import gd.script.gdcc.type.GdFloatType; import gd.script.gdcc.type.GdFloatVectorType; import gd.script.gdcc.type.GdIntType; @@ -103,7 +103,7 @@ private static void appendTypeDescriptor(@NotNull StringBuilder builder, } builder.append('V'); } - case GdccForRangeIterType _ -> throw new IllegalArgumentException( + case GdCompilerType _ -> throw new IllegalArgumentException( "compiler-only type leaked into engine method ABI descriptor: " + type.getTypeName() ); case GdVariantType _ -> builder.append('R'); diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/insn/DestructInsnGen.java b/src/main/java/gd/script/gdcc/backend/c/gen/insn/DestructInsnGen.java index f5d15ecc..ebc33f27 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/insn/DestructInsnGen.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/insn/DestructInsnGen.java @@ -8,7 +8,7 @@ import gd.script.gdcc.lir.insn.DestructInsn; import gd.script.gdcc.scope.RefCountedStatus; import gd.script.gdcc.type.GdContainerType; -import gd.script.gdcc.type.GdccForRangeIterType; +import gd.script.gdcc.type.GdCompilerType; import gd.script.gdcc.type.GdMetaType; import gd.script.gdcc.type.GdObjectType; import gd.script.gdcc.type.GdStringLikeType; @@ -34,7 +34,7 @@ public void generateCCode(@NotNull CBodyBuilder bodyBuilder) { case GdVoidType _ -> throw bodyBuilder.invalidInsn("Cannot destruct variable of type " + variable.type().getTypeName()); case GdObjectType objectType -> generateObjectDestruct(bodyBuilder, insn, objectType, variable); - case GdccForRangeIterType _ -> { + case GdCompilerType _ -> { var destroyFunc = bodyBuilder.helper().renderDestroyFunctionName(variable.type()); bodyBuilder.callVoid(destroyFunc, List.of(bodyBuilder.valueOfVar(variable))); } diff --git a/src/main/java/gd/script/gdcc/frontend/lowering/FrontendWritableTypeWritebackSupport.java b/src/main/java/gd/script/gdcc/frontend/lowering/FrontendWritableTypeWritebackSupport.java index ada8c252..678ffa51 100644 --- a/src/main/java/gd/script/gdcc/frontend/lowering/FrontendWritableTypeWritebackSupport.java +++ b/src/main/java/gd/script/gdcc/frontend/lowering/FrontendWritableTypeWritebackSupport.java @@ -2,7 +2,7 @@ import gd.script.gdcc.type.GdArrayType; import gd.script.gdcc.type.GdDictionaryType; -import gd.script.gdcc.type.GdccForRangeIterType; +import gd.script.gdcc.type.GdCompilerType; import gd.script.gdcc.type.GdObjectType; import gd.script.gdcc.type.GdPrimitiveType; import gd.script.gdcc.type.GdType; @@ -24,7 +24,7 @@ private FrontendWritableTypeWritebackSupport() { public static boolean requiresReverseCommitForCarrierType(@NotNull GdType carrierType) { return switch (Objects.requireNonNull(carrierType, "carrierType must not be null")) { - case GdccForRangeIterType _ -> throw new IllegalArgumentException( + case GdCompilerType _ -> throw new IllegalArgumentException( "compiler-only type leaked into frontend writeback analysis: " + carrierType.getTypeName() ); case GdPrimitiveType _, GdObjectType _, GdArrayType _, GdDictionaryType _ -> false; 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 ac1e4d8c..58643855 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 @@ -31,7 +31,7 @@ import gd.script.gdcc.scope.ParameterDef; import gd.script.gdcc.scope.PropertyDef; import gd.script.gdcc.type.GdContainerType; -import gd.script.gdcc.type.GdccForRangeIterType; +import gd.script.gdcc.type.GdCompilerType; import gd.script.gdcc.type.GdFloatType; import gd.script.gdcc.type.GdFloatVectorType; import gd.script.gdcc.type.GdIntType; @@ -744,7 +744,7 @@ void ensureVariable(@NotNull String variableId, @NotNull GdType expectedType) { + "' must not materialize into target type void; value-required lowering sites must not request a concrete slot for void" ); } - if (source instanceof GdccForRangeIterType compilerOnlyType) { + if (source instanceof GdCompilerType compilerOnlyType) { throw new IllegalStateException( "compiler-only type leaked into frontend boundary source '" + use @@ -752,7 +752,7 @@ void ensureVariable(@NotNull String variableId, @NotNull GdType expectedType) { + compilerOnlyType.getTypeName() ); } - if (target instanceof GdccForRangeIterType compilerOnlyType) { + if (target instanceof GdCompilerType compilerOnlyType) { throw new IllegalStateException( "compiler-only type leaked into frontend boundary target '" + use diff --git a/src/main/java/gd/script/gdcc/frontend/lowering/pass/body/FrontendCfgNodeInsnLoweringProcessors.java b/src/main/java/gd/script/gdcc/frontend/lowering/pass/body/FrontendCfgNodeInsnLoweringProcessors.java index 44cce9e3..9e760048 100644 --- a/src/main/java/gd/script/gdcc/frontend/lowering/pass/body/FrontendCfgNodeInsnLoweringProcessors.java +++ b/src/main/java/gd/script/gdcc/frontend/lowering/pass/body/FrontendCfgNodeInsnLoweringProcessors.java @@ -9,7 +9,7 @@ import gd.script.gdcc.lir.insn.ReturnInsn; import gd.script.gdcc.lir.insn.UnpackVariantInsn; import gd.script.gdcc.type.GdBoolType; -import gd.script.gdcc.type.GdccForRangeIterType; +import gd.script.gdcc.type.GdCompilerType; import gd.script.gdcc.type.GdType; import gd.script.gdcc.type.GdVariantType; import org.jetbrains.annotations.NotNull; @@ -109,7 +109,7 @@ private void emitConditionBranch( block.setTerminator(new GoIfInsn(boolSlotId, trueBlockId, falseBlockId)); return; } - case GdccForRangeIterType compilerOnlyType -> throw new IllegalStateException( + case GdCompilerType compilerOnlyType -> throw new IllegalStateException( "compiler-only type leaked into frontend condition normalization: " + compilerOnlyType.getTypeName() ); 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 d52ee8c7..7f4ce0d7 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 @@ -18,7 +18,7 @@ import gd.script.gdcc.scope.Scope; import gd.script.gdcc.scope.ScopeOwnerKind; import gd.script.gdcc.scope.ScopeValue; -import gd.script.gdcc.type.GdccForRangeIterType; +import gd.script.gdcc.type.GdCompilerType; import gd.script.gdcc.type.GdType; import gd.script.gdcc.type.GdVariantType; import gd.script.gdcc.type.GdVoidType; @@ -469,7 +469,7 @@ private void indexSubtree(@NotNull Node node, @Nullable Node parent) { } var published = expressionTypes.get(expression); if (published != null) { - if (published.publishedType() instanceof GdccForRangeIterType compilerOnlyType) { + if (published.publishedType() instanceof GdCompilerType compilerOnlyType) { throw new IllegalStateException( "compiler-only type leaked into frontend expressionTypes(): " + compilerOnlyType.getTypeName() @@ -489,7 +489,7 @@ private void publishResolvedExpressionType( if (isRouteHeadOnlyTypeMeta(expression)) { return; } - if (computed.publishedType() instanceof GdccForRangeIterType compilerOnlyType) { + if (computed.publishedType() instanceof GdCompilerType compilerOnlyType) { throw new IllegalStateException( "compiler-only type leaked into frontend expressionTypes(): " + compilerOnlyType.getTypeName() @@ -547,7 +547,7 @@ private void backfillInferredLocalType(@NotNull VariableDeclaration variableDecl if (backfilledType == null || backfilledType instanceof GdVoidType) { return; } - if (backfilledType instanceof GdccForRangeIterType compilerOnlyType) { + if (backfilledType instanceof GdCompilerType compilerOnlyType) { throw new IllegalStateException( "compiler-only type leaked into frontend local backfill: " + compilerOnlyType.getTypeName() 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 9e651464..66d49fc2 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 @@ -47,7 +47,7 @@ import gd.script.gdcc.scope.ResolveRestriction; import gd.script.gdcc.scope.Scope; import gd.script.gdcc.scope.ScopeValue; -import gd.script.gdcc.type.GdccForRangeIterType; +import gd.script.gdcc.type.GdCompilerType; import gd.script.gdcc.type.GdType; import gd.script.gdcc.type.GdVoidType; import org.jetbrains.annotations.NotNull; @@ -192,7 +192,7 @@ static void probeStabilizeLocalSlot( if (publishedType instanceof GdVoidType) { return null; } - if (publishedType instanceof GdccForRangeIterType compilerOnlyType) { + if (publishedType instanceof GdCompilerType compilerOnlyType) { throw new IllegalStateException( "compiler-only type leaked into frontend local stabilization: " + compilerOnlyType.getTypeName() diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTypeCheckAnalyzer.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTypeCheckAnalyzer.java index 42b918a6..ff6b3cbe 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTypeCheckAnalyzer.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/FrontendTypeCheckAnalyzer.java @@ -18,7 +18,7 @@ import gd.script.gdcc.scope.ResolveRestriction; import gd.script.gdcc.scope.Scope; import gd.script.gdcc.scope.ScopeValue; -import gd.script.gdcc.type.GdccForRangeIterType; +import gd.script.gdcc.type.GdCompilerType; import gd.script.gdcc.type.GdType; import gd.script.gdcc.type.GdVariantType; import gd.script.gdcc.type.GdVoidType; @@ -257,7 +257,7 @@ protected void visitConditionExpression( publishedConditionType.publishedType(), "publishedType must not be null for stable condition expression" ); - if (conditionType instanceof GdccForRangeIterType compilerOnlyType) { + if (conditionType instanceof GdCompilerType compilerOnlyType) { throw new IllegalStateException( "compiler-only type leaked into frontend condition fact: " + compilerOnlyType.getTypeName() diff --git a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendVariantBoundaryCompatibility.java b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendVariantBoundaryCompatibility.java index 594b88ed..9e785ce0 100644 --- a/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendVariantBoundaryCompatibility.java +++ b/src/main/java/gd/script/gdcc/frontend/sema/analyzer/support/FrontendVariantBoundaryCompatibility.java @@ -1,7 +1,7 @@ package gd.script.gdcc.frontend.sema.analyzer.support; import gd.script.gdcc.scope.ClassRegistry; -import gd.script.gdcc.type.GdccForRangeIterType; +import gd.script.gdcc.type.GdCompilerType; import gd.script.gdcc.type.GdFloatType; import gd.script.gdcc.type.GdFloatVectorType; import gd.script.gdcc.type.GdIntType; @@ -66,7 +66,7 @@ private FrontendVariantBoundaryCompatibility() { var registry = Objects.requireNonNull(classRegistry, "classRegistry must not be null"); var source = Objects.requireNonNull(sourceType, "sourceType must not be null"); var target = Objects.requireNonNull(targetType, "targetType must not be null"); - if (source instanceof GdccForRangeIterType || target instanceof GdccForRangeIterType) { + if (source instanceof GdCompilerType || target instanceof GdCompilerType) { return Decision.REJECT; } if (target instanceof GdVariantType) { diff --git a/src/main/java/gd/script/gdcc/lir/parser/DomLirSerializer.java b/src/main/java/gd/script/gdcc/lir/parser/DomLirSerializer.java index 00578187..65327b51 100644 --- a/src/main/java/gd/script/gdcc/lir/parser/DomLirSerializer.java +++ b/src/main/java/gd/script/gdcc/lir/parser/DomLirSerializer.java @@ -194,7 +194,7 @@ public void serialize(@NotNull LirModule module, @NotNull Writer out) throws Exc /// Serializer keeps compiler-only types on the dedicated `compiler::...` grammar and rejects any /// attempt to leak them into outward-facing metadata surfaces. private @NotNull String renderTypeText(@NotNull GdType type, @NotNull TypeUseSite useSite) { - if (type instanceof GdccForRangeIterType compilerOnlyType) { + if (type instanceof GdCompilerType compilerOnlyType) { if (!useSite.allowCompilerOnlyType) { throw new IllegalArgumentException( "compiler-only type leaked into " + useSite.displayName + ": " + compilerOnlyType.getLirTypeText() diff --git a/src/main/java/gd/script/gdcc/type/GdCompilerType.java b/src/main/java/gd/script/gdcc/type/GdCompilerType.java new file mode 100644 index 00000000..bbc80b2c --- /dev/null +++ b/src/main/java/gd/script/gdcc/type/GdCompilerType.java @@ -0,0 +1,47 @@ +package gd.script.gdcc.type; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/// Shared abstraction for all compiler-only storage types. +/// +/// These types represent backend/runtime internal C storage and are never part of the +/// GDScript source-facing type system. They must only appear on LIR local variables and +/// backend-owned intrinsic operands/results, never on public ABI surfaces. +/// +/// Each concrete type provides its own LIR-only text, C storage type name, and C init/destroy +/// helper names. The shared defaults encode the design invariants: no GDExtension metadata, +/// destroyable non-object lifecycle, and non-nullable value semantics. +public sealed interface GdCompilerType extends GdType + permits GdccForRangeIterType { + + /// LIR-only text grammar: `compiler::`, recognized solely by the LIR parser/serializer. + @NotNull String getLirTypeText(); + + /// C storage type name used in generated C declarations (e.g. `gdcc_for_range_iter`). + @NotNull String getCStorageTypeName(); + + /// C init helper function name for prepare-block initialization (e.g. `gdcc_for_range_iter_init`). + @NotNull String getCInitHelperName(); + + /// C destroy helper function name for lifecycle cleanup (e.g. `gdcc_for_range_iter_destroy`). + @NotNull String getCDestroyHelperName(); + + /// Compiler-only types carry no GDExtension metadata. + @Override + default @Nullable GdExtensionTypeEnum getGdExtensionType() { + return null; + } + + /// Compiler-only storage types are destroyable non-object values. + @Override + default boolean isDestroyable() { + return true; + } + + /// Compiler-only types are value-passed and non-nullable by design. + @Override + default boolean isNullable() { + return false; + } +} diff --git a/src/main/java/gd/script/gdcc/type/GdType.java b/src/main/java/gd/script/gdcc/type/GdType.java index 4e9384f4..b381809d 100644 --- a/src/main/java/gd/script/gdcc/type/GdType.java +++ b/src/main/java/gd/script/gdcc/type/GdType.java @@ -4,7 +4,7 @@ import org.jetbrains.annotations.Nullable; public sealed interface GdType - permits GdContainerType, GdMetaType, GdNilType, GdObjectType, GdPrimitiveType, GdRidType, GdStringLikeType, GdVariantType, GdVectorType, GdVoidType, GdccForRangeIterType { + permits GdContainerType, GdCompilerType, GdMetaType, GdNilType, GdObjectType, GdPrimitiveType, GdRidType, GdStringLikeType, GdVariantType, GdVectorType, GdVoidType { @NotNull String getTypeName(); boolean isNullable(); diff --git a/src/main/java/gd/script/gdcc/type/GdccForRangeIterType.java b/src/main/java/gd/script/gdcc/type/GdccForRangeIterType.java index c8354423..5c3ebcb0 100644 --- a/src/main/java/gd/script/gdcc/type/GdccForRangeIterType.java +++ b/src/main/java/gd/script/gdcc/type/GdccForRangeIterType.java @@ -1,11 +1,10 @@ package gd.script.gdcc.type; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; /// Compiler-only storage type for future lowered `for i in range(...)` iterator state. /// It is intentionally not source-facing and must only be serialized through LIR-only text. -public final class GdccForRangeIterType implements GdType { +public final class GdccForRangeIterType implements GdCompilerType { public static final @NotNull GdccForRangeIterType FOR_RANGE_ITER = new GdccForRangeIterType(); public static final @NotNull String LIR_TYPE_TEXT = "compiler::GdccForRangeIter"; @@ -18,34 +17,23 @@ public final class GdccForRangeIterType implements GdType { return "GdccForRangeIter"; } + @Override public @NotNull String getLirTypeText() { return LIR_TYPE_TEXT; } + @Override public @NotNull String getCStorageTypeName() { return C_STORAGE_TYPE_NAME; } + @Override public @NotNull String getCInitHelperName() { return C_INIT_HELPER_NAME; } + @Override public @NotNull String getCDestroyHelperName() { return C_DESTROY_HELPER_NAME; } - - @Override - public boolean isNullable() { - return false; - } - - @Override - public @Nullable GdExtensionTypeEnum getGdExtensionType() { - return null; - } - - @Override - public boolean isDestroyable() { - return true; - } } diff --git a/src/test/java/gd/script/gdcc/type/GdCompilerTypeTest.java b/src/test/java/gd/script/gdcc/type/GdCompilerTypeTest.java new file mode 100644 index 00000000..9344d9c3 --- /dev/null +++ b/src/test/java/gd/script/gdcc/type/GdCompilerTypeTest.java @@ -0,0 +1,112 @@ +package gd.script.gdcc.type; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// Contract test for the {@link GdCompilerType} sealed interface abstraction layer. +/// +/// Anchors the shared protocol that all compiler-only storage types must satisfy: +/// LIR-only text, C storage/init/destroy helper names, and the design-invariant defaults +/// (non-nullable, no GDExtension metadata, destroyable non-object lifecycle). +/// Also verifies that compiler-only types stay outside the user-facing type families. +class GdCompilerTypeTest { + + @Test + @org.junit.jupiter.api.DisplayName("GdCompilerType is a permitted subtype of GdType") + void compilerTypeIsPermittedByGdType() { + var type = GdccForRangeIterType.FOR_RANGE_ITER; + // GdCompilerType extends GdType, so every compiler-only type is also a GdType. + assertInstanceOf(GdType.class, type); + assertInstanceOf(GdCompilerType.class, type); + } + + @Test + @org.junit.jupiter.api.DisplayName("GdccForRangeIterType is the sole permitted GdCompilerType subtype") + void forRangeIterIsSolePermittedSubtype() { + // GdCompilerType is sealed and permits only GdccForRangeIterType. + // The singleton instance must be assignable to GdCompilerType. + assertTrue(GdCompilerType.class.isAssignableFrom(GdccForRangeIterType.class), + "GdccForRangeIterType must implement GdCompilerType"); + } + + @Test + @org.junit.jupiter.api.DisplayName("compiler-only type shared defaults: non-nullable, no metadata, destroyable") + void sharedDefaultsHold() { + var type = GdccForRangeIterType.FOR_RANGE_ITER; + // These are inherited from GdCompilerType default methods, not re-declared per concrete type. + assertFalse(type.isNullable(), "compiler-only types are value-passed and non-nullable by design"); + assertNull(type.getGdExtensionType(), "compiler-only types carry no GDExtension metadata"); + assertTrue(type.isDestroyable(), "compiler-only storage types are destroyable non-object values"); + } + + @Test + @org.junit.jupiter.api.DisplayName("compiler-only type protocol methods return gdcc_* helpers and LIR-only text") + void protocolMethodsReturnGdccHelpers() { + var type = (GdCompilerType) GdccForRangeIterType.FOR_RANGE_ITER; + + // LIR-only text must use the compiler:: grammar, never a bare source-facing name. + assertEquals("compiler::GdccForRangeIter", type.getLirTypeText()); + + // C storage and helper names must use the gdcc_* namespace, never godot_* defaults. + assertEquals("gdcc_for_range_iter", type.getCStorageTypeName()); + assertEquals("gdcc_for_range_iter_init", type.getCInitHelperName()); + assertEquals("gdcc_for_range_iter_destroy", type.getCDestroyHelperName()); + } + + @Test + @org.junit.jupiter.api.DisplayName("compiler-only type internal name is stable but not a source-facing declared type") + void internalNameIsStableAndNotSourceFacing() { + var type = GdccForRangeIterType.FOR_RANGE_ITER; + assertEquals("GdccForRangeIter", type.getTypeName()); + // The internal name must differ from the LIR-only text grammar. + assertFalse(type.getTypeName().startsWith("compiler::"), + "getTypeName() must not use the LIR-only compiler:: grammar"); + } + + @Test + @org.junit.jupiter.api.DisplayName("compiler-only type stays outside all user-facing type families") + void staysOutOfUserFacingTypeFamilies() { + var type = GdccForRangeIterType.FOR_RANGE_ITER; + var userFacingFamilies = List.>of( + GdPrimitiveType.class, + GdObjectType.class, + GdVariantType.class, + GdMetaType.class, + GdContainerType.class, + GdNilType.class, + GdVoidType.class, + GdRidType.class, + GdStringLikeType.class, + GdVectorType.class + ); + for (var family : userFacingFamilies) { + assertFalse(family.isAssignableFrom(type.getClass()), + family.getSimpleName() + " must not be assignable from compiler-only type"); + } + } + + @Test + @org.junit.jupiter.api.DisplayName("compiler-only type must not produce godot_* default helper names") + void mustNotProduceGodotDefaultHelpers() { + var type = GdccForRangeIterType.FOR_RANGE_ITER; + var cStorage = type.getCStorageTypeName(); + var cInit = type.getCInitHelperName(); + var cDestroy = type.getCDestroyHelperName(); + + // All helper names must use gdcc_* namespace, not godot_* generated binding defaults. + assertFalse(cStorage.startsWith("godot_"), "C storage type must not use godot_* prefix: " + cStorage); + assertFalse(cInit.startsWith("godot_"), "C init helper must not use godot_* prefix: " + cInit); + assertFalse(cDestroy.startsWith("godot_"), "C destroy helper must not use godot_* prefix: " + cDestroy); + + assertTrue(cStorage.startsWith("gdcc_"), "C storage type must use gdcc_* prefix: " + cStorage); + assertTrue(cInit.startsWith("gdcc_"), "C init helper must use gdcc_* prefix: " + cInit); + assertTrue(cDestroy.startsWith("gdcc_"), "C destroy helper must use gdcc_* prefix: " + cDestroy); + } +} diff --git a/src/test/java/gd/script/gdcc/type/GdccForRangeIterTypeTest.java b/src/test/java/gd/script/gdcc/type/GdccForRangeIterTypeTest.java index 6974b9d1..d65224c5 100644 --- a/src/test/java/gd/script/gdcc/type/GdccForRangeIterTypeTest.java +++ b/src/test/java/gd/script/gdcc/type/GdccForRangeIterTypeTest.java @@ -15,11 +15,15 @@ class GdccForRangeIterTypeTest { void exposesOnlyStableCompilerInternalProtocol() { var type = GdccForRangeIterType.FOR_RANGE_ITER; + // The concrete type must implement the GdCompilerType abstraction layer. + assertInstanceOf(GdCompilerType.class, type); + assertEquals("GdccForRangeIter", type.getTypeName()); assertEquals("compiler::GdccForRangeIter", type.getLirTypeText()); assertEquals("gdcc_for_range_iter", type.getCStorageTypeName()); assertEquals("gdcc_for_range_iter_init", type.getCInitHelperName()); assertEquals("gdcc_for_range_iter_destroy", type.getCDestroyHelperName()); + // isNullable, getGdExtensionType, and isDestroyable are inherited from GdCompilerType defaults. assertFalse(type.isNullable()); assertNull(type.getGdExtensionType()); assertTrue(type.isDestroyable()); From 20ee201d9c5b3a2d3ce3d2414eb3e4a1c6049332 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Tue, 30 Jun 2026 00:46:54 +0800 Subject: [PATCH 07/16] test(frontend): add comprehensive tests for compiler-only type leak guards - Cover property store, subscript, call arg, and return paths - Verify fail-fast behavior across all frontend boundary sources - Ensure each leak site produces the expected diagnostic context --- .../FrontendLoweringBodyInsnPassTest.java | 252 +++++++++++++++++- 1 file changed, 251 insertions(+), 1 deletion(-) diff --git a/src/test/java/gd/script/gdcc/frontend/lowering/FrontendLoweringBodyInsnPassTest.java b/src/test/java/gd/script/gdcc/frontend/lowering/FrontendLoweringBodyInsnPassTest.java index a5271548..bc769db7 100644 --- a/src/test/java/gd/script/gdcc/frontend/lowering/FrontendLoweringBodyInsnPassTest.java +++ b/src/test/java/gd/script/gdcc/frontend/lowering/FrontendLoweringBodyInsnPassTest.java @@ -75,9 +75,13 @@ import gd.script.gdcc.type.GdStringNameType; import gd.script.gdcc.type.GdStringType; import gd.script.gdcc.type.GdVariantType; +import dev.superice.gdparser.frontend.ast.AssignmentExpression; import dev.superice.gdparser.frontend.ast.AttributeExpression; -import dev.superice.gdparser.frontend.ast.ExpressionStatement; 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.ExpressionStatement; +import dev.superice.gdparser.frontend.ast.SubscriptExpression; import dev.superice.gdparser.frontend.ast.IdentifierExpression; import dev.superice.gdparser.frontend.ast.LiteralExpression; import dev.superice.gdparser.frontend.ast.Node; @@ -996,6 +1000,252 @@ func branch_on_int(count: int) -> int: ); } + @Test + void runFailsFastWhenCompilerOnlyTypeLeaksIntoPropertyStoreValue() throws Exception { + var prepared = prepareContext( + "body_insn_compiler_only_property_store.gd", + """ + class_name BodyInsnCompilerOnlyPropertyStore + extends RefCounted + + var payload_int: int + + func ping(box: Variant) -> void: + payload_int = box + """, + Map.of("BodyInsnCompilerOnlyPropertyStore", "RuntimeBodyInsnCompilerOnlyPropertyStore"), + true + ); + var functionContext = requireContext( + prepared.context().requireFunctionLoweringContexts(), + FunctionLoweringContext.Kind.EXECUTABLE_BODY, + "RuntimeBodyInsnCompilerOnlyPropertyStore", + "ping" + ); + var root = assertInstanceOf(Block.class, functionContext.loweringRoot()); + var statement = assertInstanceOf(ExpressionStatement.class, root.statements().getFirst()); + var assignment = assertInstanceOf(AssignmentExpression.class, statement.expression()); + functionContext.analysisData().expressionTypes().put( + assignment.right(), + FrontendExpressionType.resolved(GdccForRangeIterType.FOR_RANGE_ITER) + ); + + var failure = assertThrows( + IllegalStateException.class, + () -> new FrontendLoweringBodyInsnPass().run(prepared.context()) + ); + + assertAll( + () -> assertFalse(prepared.diagnostics().hasErrors()), + () -> assertTrue(failure.getMessage().contains("compiler-only type leaked into frontend boundary source")), + () -> assertTrue(failure.getMessage().contains("store_property")) + ); + } + + @Test + void runFailsFastWhenCompilerOnlyTypeLeaksIntoSubscriptStoreValue() throws Exception { + var prepared = prepareContext( + "body_insn_compiler_only_subscript_store.gd", + """ + class_name BodyInsnCompilerOnlySubscriptStore + extends RefCounted + + func ping(values: Array[int], box: Variant) -> void: + values[0] = box + """, + Map.of("BodyInsnCompilerOnlySubscriptStore", "RuntimeBodyInsnCompilerOnlySubscriptStore"), + true + ); + var functionContext = requireContext( + prepared.context().requireFunctionLoweringContexts(), + FunctionLoweringContext.Kind.EXECUTABLE_BODY, + "RuntimeBodyInsnCompilerOnlySubscriptStore", + "ping" + ); + var root = assertInstanceOf(Block.class, functionContext.loweringRoot()); + var statement = assertInstanceOf(ExpressionStatement.class, root.statements().getFirst()); + var assignment = assertInstanceOf(AssignmentExpression.class, statement.expression()); + functionContext.analysisData().expressionTypes().put( + assignment.right(), + FrontendExpressionType.resolved(GdccForRangeIterType.FOR_RANGE_ITER) + ); + + var failure = assertThrows( + IllegalStateException.class, + () -> new FrontendLoweringBodyInsnPass().run(prepared.context()) + ); + + assertAll( + () -> assertFalse(prepared.diagnostics().hasErrors()), + () -> assertTrue(failure.getMessage().contains("compiler-only type leaked into frontend boundary source")), + () -> assertTrue(failure.getMessage().contains("store_subscript")) + ); + } + + @Test + void runFailsFastWhenCompilerOnlyTypeLeaksIntoSubscriptReadKey() throws Exception { + var prepared = prepareContext( + "body_insn_compiler_only_subscript_key.gd", + """ + class_name BodyInsnCompilerOnlySubscriptKey + extends RefCounted + + func ping(values: Array[int], box: Variant) -> int: + return values[box] + """, + Map.of("BodyInsnCompilerOnlySubscriptKey", "RuntimeBodyInsnCompilerOnlySubscriptKey"), + true + ); + var functionContext = requireContext( + prepared.context().requireFunctionLoweringContexts(), + FunctionLoweringContext.Kind.EXECUTABLE_BODY, + "RuntimeBodyInsnCompilerOnlySubscriptKey", + "ping" + ); + var root = assertInstanceOf(Block.class, functionContext.loweringRoot()); + var returnStatement = assertInstanceOf(ReturnStatement.class, root.statements().getFirst()); + var subscript = assertInstanceOf(SubscriptExpression.class, returnStatement.value()); + functionContext.analysisData().expressionTypes().put( + subscript.arguments().getFirst(), + FrontendExpressionType.resolved(GdccForRangeIterType.FOR_RANGE_ITER) + ); + + var failure = assertThrows( + IllegalStateException.class, + () -> new FrontendLoweringBodyInsnPass().run(prepared.context()) + ); + + assertAll( + () -> assertFalse(prepared.diagnostics().hasErrors()), + () -> assertTrue(failure.getMessage().contains("compiler-only type leaked into frontend boundary source")), + () -> assertTrue(failure.getMessage().contains("subscript_read_key")) + ); + } + + @Test + void runFailsFastWhenCompilerOnlyTypeLeaksIntoFixedCallArgument() throws Exception { + var prepared = prepareContext( + "body_insn_compiler_only_fixed_call_arg.gd", + """ + class_name BodyInsnCompilerOnlyFixedCallArg + extends RefCounted + + func take_i(value: int) -> int: + return value + + func call_concrete(box: Variant) -> int: + return take_i(box) + """, + Map.of("BodyInsnCompilerOnlyFixedCallArg", "RuntimeBodyInsnCompilerOnlyFixedCallArg"), + true + ); + var functionContext = requireContext( + prepared.context().requireFunctionLoweringContexts(), + FunctionLoweringContext.Kind.EXECUTABLE_BODY, + "RuntimeBodyInsnCompilerOnlyFixedCallArg", + "call_concrete" + ); + var root = assertInstanceOf(Block.class, functionContext.loweringRoot()); + var returnStatement = assertInstanceOf(ReturnStatement.class, root.statements().getFirst()); + var callExpression = assertInstanceOf(CallExpression.class, returnStatement.value()); + functionContext.analysisData().expressionTypes().put( + callExpression.arguments().getFirst(), + FrontendExpressionType.resolved(GdccForRangeIterType.FOR_RANGE_ITER) + ); + + var failure = assertThrows( + IllegalStateException.class, + () -> new FrontendLoweringBodyInsnPass().run(prepared.context()) + ); + + assertAll( + () -> assertFalse(prepared.diagnostics().hasErrors()), + () -> assertTrue(failure.getMessage().contains("compiler-only type leaked into frontend boundary source")), + () -> assertTrue(failure.getMessage().contains("call_fixed_0")) + ); + } + + @Test + void runFailsFastWhenCompilerOnlyTypeLeaksIntoVarargTailArgument() throws Exception { + var prepared = prepareContext( + "body_insn_compiler_only_vararg_arg.gd", + """ + class_name BodyInsnCompilerOnlyVarargArg + extends RefCounted + + func call_vararg(seed: int, box: Variant) -> void: + print(seed, box) + """, + Map.of("BodyInsnCompilerOnlyVarargArg", "RuntimeBodyInsnCompilerOnlyVarargArg"), + true + ); + var functionContext = requireContext( + prepared.context().requireFunctionLoweringContexts(), + FunctionLoweringContext.Kind.EXECUTABLE_BODY, + "RuntimeBodyInsnCompilerOnlyVarargArg", + "call_vararg" + ); + var root = assertInstanceOf(Block.class, functionContext.loweringRoot()); + var statement = assertInstanceOf(ExpressionStatement.class, root.statements().getFirst()); + var callExpression = assertInstanceOf(CallExpression.class, statement.expression()); + functionContext.analysisData().expressionTypes().put( + callExpression.arguments().get(1), + FrontendExpressionType.resolved(GdccForRangeIterType.FOR_RANGE_ITER) + ); + + var failure = assertThrows( + IllegalStateException.class, + () -> new FrontendLoweringBodyInsnPass().run(prepared.context()) + ); + + assertAll( + () -> assertFalse(prepared.diagnostics().hasErrors()), + () -> assertTrue(failure.getMessage().contains("compiler-only type leaked into frontend boundary source")), + () -> assertTrue(failure.getMessage().contains("call_vararg_1"), "Expected 'call_vararg_1' in: " + failure.getMessage()) + ); + } + + @Test + void runFailsFastWhenCompilerOnlyTypeLeaksIntoReturnSlot() throws Exception { + var prepared = prepareContext( + "body_insn_compiler_only_return_slot.gd", + """ + class_name BodyInsnCompilerOnlyReturnSlot + extends RefCounted + + func ping(box: Variant) -> int: + return box + """, + Map.of("BodyInsnCompilerOnlyReturnSlot", "RuntimeBodyInsnCompilerOnlyReturnSlot"), + true + ); + var functionContext = requireContext( + prepared.context().requireFunctionLoweringContexts(), + FunctionLoweringContext.Kind.EXECUTABLE_BODY, + "RuntimeBodyInsnCompilerOnlyReturnSlot", + "ping" + ); + var root = assertInstanceOf(Block.class, functionContext.loweringRoot()); + var returnStatement = assertInstanceOf(ReturnStatement.class, root.statements().getFirst()); + var returnExpression = returnStatement.value(); + assertNotNull(returnExpression); + functionContext.analysisData().expressionTypes().put( + returnExpression, + FrontendExpressionType.resolved(GdccForRangeIterType.FOR_RANGE_ITER) + ); + + var failure = assertThrows( + IllegalStateException.class, + () -> new FrontendLoweringBodyInsnPass().run(prepared.context()) + ); + + assertAll( + () -> assertFalse(prepared.diagnostics().hasErrors()), + () -> assertTrue(failure.getMessage().contains("compiler-only type leaked into frontend boundary source")), + () -> assertTrue(failure.getMessage().contains("return_value")) + ); + } + @Test void runChoosesIndexedNamedAndKeyedSubscriptInstructionsFromPublishedKeyTypes() throws Exception { var prepared = prepareContext( From 9cae1aef6beae981313185c2843fbb291ee25776 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Tue, 30 Jun 2026 11:45:44 +0800 Subject: [PATCH 08/16] feat(lir): add pre-codegen validator for compiler-only type leaks on ABI surfaces - reject compiler-only type on property, signal, function param/return and lambda capture - hidden functions share the same restriction as public ones in MVP - integrate validator into CCodegen.generate() before backend synthesis - update phase-5 plan status and add tests for validator and codegen integration --- .../frontend/frontend_gdcompiler_type_plan.md | 438 +++++++++++------- .../script/gdcc/backend/c/gen/CCodegen.java | 5 + .../lir/validation/LirPublicAbiValidator.java | 93 ++++ .../gdcc/backend/c/gen/CCodegenTest.java | 69 +++ .../validation/LirPublicAbiValidatorTest.java | 113 +++++ 5 files changed, 542 insertions(+), 176 deletions(-) create mode 100644 src/main/java/gd/script/gdcc/lir/validation/LirPublicAbiValidator.java create mode 100644 src/test/java/gd/script/gdcc/lir/validation/LirPublicAbiValidatorTest.java diff --git a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md index 5963f9fd..e832e679 100644 --- a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md +++ b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md @@ -9,37 +9,40 @@ - 状态:阶段一至四已完成,后续阶段计划维护中 - 更新时间:2026-06-29 - 适用范围: - - `src/main/java/gd/script/gdcc/type/**` - - `src/main/java/gd/script/gdcc/scope/**` - - `src/main/java/gd/script/gdcc/frontend/**` - - `src/main/java/gd/script/gdcc/lir/**` - - `src/main/java/gd/script/gdcc/backend/c/**` - - `src/main/c/codegen/**` + - `src/main/java/gd/script/gdcc/type/**` + - `src/main/java/gd/script/gdcc/scope/**` + - `src/main/java/gd/script/gdcc/frontend/**` + - `src/main/java/gd/script/gdcc/lir/**` + - `src/main/java/gd/script/gdcc/backend/c/**` + - `src/main/c/codegen/**` - 关联文档: - - `doc/analysis/gdcompiler_type_design_risk_analysis.md` - - `doc/gdcc_type_system.md` - - `doc/gdcc_low_ir.md` - - `doc/gdcc_lir_intrinsic.md` - - `doc/gdcc_c_backend.md` - - `doc/gdcc_runtime_lib.md` - - `doc/gdcc_ownership_lifecycle_spec.md` - - `doc/module_impl/common_rules.md` - - `doc/module_impl/frontend/frontend_rules.md` - - `doc/module_impl/frontend/frontend_implicit_conversion_matrix.md` - - `doc/module_impl/frontend/frontend_lowering_(un)pack_implementation.md` - - `doc/module_impl/frontend/frontend_dynamic_call_lowering_implementation.md` - - Godot docs:`tutorials/scripting/gdscript/gdscript_advanced.rst` 的 `range(...)` / custom iterator 说明 + - `doc/analysis/gdcompiler_type_design_risk_analysis.md` + - `doc/gdcc_type_system.md` + - `doc/gdcc_low_ir.md` + - `doc/gdcc_lir_intrinsic.md` + - `doc/gdcc_c_backend.md` + - `doc/gdcc_runtime_lib.md` + - `doc/gdcc_ownership_lifecycle_spec.md` + - `doc/module_impl/common_rules.md` + - `doc/module_impl/frontend/frontend_rules.md` + - `doc/module_impl/frontend/frontend_implicit_conversion_matrix.md` + - `doc/module_impl/frontend/frontend_lowering_(un)pack_implementation.md` + - `doc/module_impl/frontend/frontend_dynamic_call_lowering_implementation.md` + - Godot docs:`tutorials/scripting/gdscript/gdscript_advanced.rst` 的 `range(...)` / custom iterator 说明 --- ## 0. 维护合同 - 本文档是 `GdCompilerType` 实施顺序、禁止边界和验收细则的计划事实源。 -- `frontend_implicit_conversion_matrix.md` 仍是 ordinary typed-boundary compatibility 的唯一真源;本文档不得维护第二份 source/target conversion 矩阵。 -- `gdcc_lir_intrinsic.md` 仍是 `call_intrinsic` surface、backend registry 和 intrinsic catalog 的事实源;本文档只规定 compiler-only 类型必须通过该通道操作。 +- `frontend_implicit_conversion_matrix.md` 仍是 ordinary typed-boundary compatibility 的唯一真源;本文档不得维护第二份 + source/target conversion 矩阵。 +- `gdcc_lir_intrinsic.md` 仍是 `call_intrinsic` surface、backend registry 和 intrinsic catalog 的事实源;本文档只规定 + compiler-only 类型必须通过该通道操作。 - `gdcc_low_ir.md` 仍是 LIR XML surface 的事实源;本文档的 LIR XML 计划落地后必须同步更新该文档。 - 实现时不得用 `Variant`、`DYNAMIC`、`TYPE_META` 或 Godot object metadata 伪装 compiler-only storage。 -- 若实现过程中发现某个计划项需要改变 source-facing typing、ordinary boundary、public ABI 或 runtime helper 命名,必须先更新对应事实源文档,再修改代码与测试。 +- 若实现过程中发现某个计划项需要改变 source-facing typing、ordinary boundary、public ABI 或 runtime helper + 命名,必须先更新对应事实源文档,再修改代码与测试。 --- @@ -51,9 +54,9 @@ - 允许作为 LIR 内部 local/temp variable 的类型。 - 允许作为 backend-owned intrinsic 的 operand / result type。 - 每个具体 compiler-only 类型必须提供: - - C storage type name。 - - init helper function name。 - - destroy helper function name。 + - C storage type name。 + - init helper function name。 + - destroy helper function name。 - compiler-only 类型按值传递且不可变,这是设计前提,不在实现中额外证明。 `GdCompilerType` 明确不允许进入: @@ -68,9 +71,15 @@ - engine method / utility / global / property / index / operator ABI。 - generated `call_func` wrapper metadata、argument unpack 或 cleanup surface。 -Godot upstream 的对齐依据是:`GDScriptFunctionState` 注册为 internal class,脚本 analyzer 不把它作为可声明标识符;for-loop iterator state 也停留在 bytecode / VM stack slot / opcode operand 层。GDCC 的 iterator、function state 等 C runtime storage 同样应保持在 IR / backend / runtime support 层,而不是扩展成 GDScript source-facing 类型。 +Godot upstream 的对齐依据是:`GDScriptFunctionState` 注册为 internal class,脚本 analyzer 不把它作为可声明标识符;for-loop +iterator state 也停留在 bytecode / VM stack slot / opcode operand 层。GDCC 的 iterator、function state 等 C runtime +storage 同样应保持在 IR / backend / runtime support 层,而不是扩展成 GDScript source-facing 类型。 -首个 concrete compiler-only type 固定为 `GdccForRangeIterType`,服务未来 GDScript `for i in range(...)` lowering。Godot 文档说明 `range` 支持 `range(n)`、`range(b, n)`、`range(b, n, s)` 三种形态,起点 inclusive、终点 exclusive,默认起点为 `0`、默认步长为 `1`,负步长用于反向迭代;custom iterator 合同也把初始化、推进、取值拆成 `_iter_init()`、`_iter_next()`、`_iter_get()` 三类动作。当前阶段不实现 `for` lowering,只用 `GdccForRangeIterType` 与对应 intrinsics 验证 compiler-only type 的 LIR/backend 内部通路。 +首个 concrete compiler-only type 固定为 `GdccForRangeIterType`,服务未来 GDScript `for i in range(...)` lowering。Godot +文档说明 `range` 支持 `range(n)`、`range(b, n)`、`range(b, n, s)` 三种形态,起点 inclusive、终点 exclusive,默认起点为 `0` +、默认步长为 `1`,负步长用于反向迭代;custom iterator 合同也把初始化、推进、取值拆成 `_iter_init()`、`_iter_next()`、 +`_iter_get()` 三类动作。当前阶段不实现 `for` lowering,只用 `GdccForRangeIterType` 与对应 intrinsics 验证 compiler-only +type 的 LIR/backend 内部通路。 --- @@ -79,33 +88,45 @@ Godot upstream 的对齐依据是:`GDScriptFunctionState` 注册为 internal c 当前 `GdType` 被多个层面共同消费。新增 compiler-only 分支时,以下默认路径会静默误处理: - `GdType` 当前 sealed permits 不包含 compiler-only 分支。 -- `ClassRegistry.tryParseStrictTextType(...)` 和 `ScopeTypeResolver.tryResolveDeclaredType(...)` 是 source-facing declared type 入口,不能认识 compiler-only 名称。 -- `ClassRegistry.findType(...)` 当前被 `DomLirParser` 用于 LIR XML 类型文本解析;若 LIR XML 需要 compiler-only 类型,不能把它塞进普通 declared type parser。 -- `ClassRegistry.checkAssignable(...)` 第一条规则是 `getTypeName()` 同名即 assignable,会让 compiler-only 类型穿过 ordinary typed boundary。 -- `FrontendVariantBoundaryCompatibility` 当前会允许 stable type -> `Variant` pack、`Variant` -> concrete unpack,并在 default 分支回退 `checkAssignable(...)`。 -- `FrontendBodyLoweringSession.materializeFrontendBoundaryValue(...)` 会把 accepted boundary 物化为 `PackVariantInsn`、`UnpackVariantInsn`、`CallIntrinsicInsn` 或 `ConstructBuiltinInsn`。 +- `ClassRegistry.tryParseStrictTextType(...)` 和 `ScopeTypeResolver.tryResolveDeclaredType(...)` 是 source-facing + declared type 入口,不能认识 compiler-only 名称。 +- `ClassRegistry.findType(...)` 当前被 `DomLirParser` 用于 LIR XML 类型文本解析;若 LIR XML 需要 compiler-only + 类型,不能把它塞进普通 declared type parser。 +- `ClassRegistry.checkAssignable(...)` 第一条规则是 `getTypeName()` 同名即 assignable,会让 compiler-only 类型穿过 + ordinary typed boundary。 +- `FrontendVariantBoundaryCompatibility` 当前会允许 stable type -> `Variant` pack、`Variant` -> concrete unpack,并在 + default 分支回退 `checkAssignable(...)`。 +- `FrontendBodyLoweringSession.materializeFrontendBoundaryValue(...)` 会把 accepted boundary 物化为 `PackVariantInsn`、 + `UnpackVariantInsn`、`CallIntrinsicInsn` 或 `ConstructBuiltinInsn`。 - condition lowering 对非 `bool` / `Variant` stable type 当前可能走 `pack_variant -> unpack_variant(bool)`。 - dynamic call backend path 会把非 `Variant` 参数 pack 成 `Variant`,也可能把 dynamic result unpack 到 target type。 - `DomLirSerializer` 直接写 `GdType.getTypeName()`;`DomLirParser` 直接用 `ClassRegistry.findType(...)` 读回类型文本。 - `CGenHelper.renderGdTypeInC(...)` / `renderGdTypeRefInC(...)` default 会生成 `godot_` / `godot_*`。 -- `CGenHelper.renderPackFunctionName(...)` / `renderUnpackFunctionName(...)` default 会生成 `godot_new_Variant_with_` / `godot_new__with_Variant`。 -- `CGenHelper.renderCopyAssignFunctionName(...)` / `renderDestroyFunctionName(...)` default 会生成 `godot_new__with_` / `godot__destroy`。 +- `CGenHelper.renderPackFunctionName(...)` / `renderUnpackFunctionName(...)` default 会生成 + `godot_new_Variant_with_` / `godot_new__with_Variant`。 +- `CGenHelper.renderCopyAssignFunctionName(...)` / `renderDestroyFunctionName(...)` default 会生成 + `godot_new__with_` / `godot__destroy`。 - `CCodegen.generateFunctionPrepareBlock(...)` 对未知非 void local default 到 `ConstructBuiltinInsn`。 - `CBodyBuilder.renderDefaultValueExpr(...)` 对未知 type default 到 `godot_new_()`。 -- `DestructInsnGen` 当前只显式处理 Godot value/object/meta/container family;compiler-only destroyable type 不能落到 no-op。 +- `DestructInsnGen` 当前只显式处理 Godot value/object/meta/container family;compiler-only destroyable type 不能落到 + no-op。 - `CGenHelper.renderBoundMetadata(...)` 会通过 `getGdExtensionType() == null` late fail,但这不是足够早的 public ABI 边界。 -- `func.ftl` 对函数返回和参数直接调用 `renderGdTypeInC(...)` / `renderGdTypeRefInC(...)`,public 函数签名若混入 compiler-only type 会生成错误 C ABI。 +- `func.ftl` 对函数返回和参数直接调用 `renderGdTypeInC(...)` / `renderGdTypeRefInC(...)`,public 函数签名若混入 + compiler-only type 会生成错误 C ABI。 --- ## 3. 实施原则 - 先定义边界,再放开路径。任何默认兼容、默认 C helper 命名、默认 metadata 生成都必须对 compiler-only type 显式处理。 -- 实现应从 `GdccForRangeIterType` 和一组 range iterator intrinsic 闭环开始;随后补入 `GdCompilerType` 作为 compiler-only 类型的共同抽象层,并把已实现的具体类型迁移到该层下。 -- compiler-only 类型不参与 ordinary frontend typed-boundary matrix。内部 LIR slot 的 direct assignment 由 backend/LIR 合同处理,不由 source-facing semantic compatibility 扩面。 +- 实现应从 `GdccForRangeIterType` 和一组 range iterator intrinsic 闭环开始;随后补入 `GdCompilerType` 作为 compiler-only + 类型的共同抽象层,并把已实现的具体类型迁移到该层下。 +- compiler-only 类型不参与 ordinary frontend typed-boundary matrix。内部 LIR slot 的 direct assignment 由 backend/LIR + 合同处理,不由 source-facing semantic compatibility 扩面。 - helper 命名使用 `gdcc_*`,不得伪造 `godot_*` generated binding helper。 - lifecycle 走非对象 destroyable value 路径,不走 object ownership。 -- parser、serializer、backend 和 frontend boundary 的错误信息应明确说明 `compiler-only type leaked into ...`,避免让使用者看到晚期 `getGdExtensionType()==null` 或 C symbol 缺失。 +- parser、serializer、backend 和 frontend boundary 的错误信息应明确说明 `compiler-only type leaked into ...`,避免让使用者看到晚期 + `getGdExtensionType()==null` 或 C symbol 缺失。 --- @@ -115,25 +136,27 @@ MVP 采用以下策略: - 允许 compiler-only 类型出现在 LIR XML 的 function `` 中。 - 类型文本使用 LIR-only grammar:`compiler::`;本阶段唯一合法实例是 `compiler::GdccForRangeIter`。 -- 该 grammar 只由 LIR parser / serializer 识别,不进入 `ScopeTypeResolver`、`ClassRegistry.tryParseStrictTextType(...)` 或 source-facing type-meta namespace。 +- 该 grammar 只由 LIR parser / serializer 识别,不进入 `ScopeTypeResolver`、`ClassRegistry.tryParseStrictTextType(...)` 或 + source-facing type-meta namespace。 - compiler-only 类型禁止出现在以下 LIR XML surface: - - function ``。 - - function ``。 - - ``。 - - `` parameter。 - - lambda ``。 -- `is_hidden=true` 函数在 MVP 中也不允许 compiler-only parameter / return type。若未来 backend-owned hidden helper 需要 compiler-only ABI,必须先单独更新本文档和 `gdcc_low_ir.md`,并证明不会生成 outward binding metadata 或 call wrapper。 + - function ``。 + - function ``。 + - ``。 + - `` parameter。 + - lambda ``。 +- `is_hidden=true` 函数在 MVP 中也不允许 compiler-only parameter / return type。若未来 backend-owned hidden helper 需要 + compiler-only ABI,必须先单独更新本文档和 `gdcc_low_ir.md`,并证明不会生成 outward binding metadata 或 call wrapper。 验收细则: - happy path: - - `` 可解析为 `GdccForRangeIterType`。 - - `DomLirSerializer` 对该 compiler-only local 输出稳定的 `compiler::GdccForRangeIter`。 - - parser / serializer round-trip 不通过 ordinary source-facing resolver。 + - `` 可解析为 `GdccForRangeIterType`。 + - `DomLirSerializer` 对该 compiler-only local 输出稳定的 `compiler::GdccForRangeIter`。 + - parser / serializer round-trip 不通过 ordinary source-facing resolver。 - negative path: - - 用户 declared type resolver 不能解析 `compiler::GdccForRangeIter` 或 bare `GdccForRangeIter`。 - - property / signal / parameter / capture / return type 使用 `compiler::GdccForRangeIter` 时 fail-fast。 - - malformed `compiler::` grammar 报错清晰,不退化成 `GdObjectType("compiler::...")`。 + - 用户 declared type resolver 不能解析 `compiler::GdccForRangeIter` 或 bare `GdccForRangeIter`。 + - property / signal / parameter / capture / return type 使用 `compiler::GdccForRangeIter` 时 fail-fast。 + - malformed `compiler::` grammar 报错清晰,不退化成 `GdObjectType("compiler::...")`。 --- @@ -146,8 +169,10 @@ MVP 采用以下策略: 产出: - `GdType` sealed hierarchy 最初直接允许 `GdccForRangeIterType`,阶段四已补入 `GdCompilerType` 抽象层并完成回迁。 -- `GdccForRangeIterType` 固定内部名、LIR-only text、C storage/init/destroy helper、不可空、无 GDExtension metadata、destroyable 协议。 -- C helper 对该类型使用 `gdcc_for_range_iter` / `gdcc_for_range_iter_destroy`,Variant pack/unpack 和 default-value 路径 fail-fast,copy assignment 不走 `godot_new_*_with_*`。 +- `GdccForRangeIterType` 固定内部名、LIR-only text、C storage/init/destroy helper、不可空、无 GDExtension + metadata、destroyable 协议。 +- C helper 对该类型使用 `gdcc_for_range_iter` / `gdcc_for_range_iter_destroy`,Variant pack/unpack 和 default-value 路径 + fail-fast,copy assignment 不走 `godot_new_*_with_*`。 - Frontend ordinary boundary/writeback helper 对该类型显式拒绝,避免阶段一后由默认兼容路径泄漏到用户语义。 - 本阶段未新增任何 `for` parser / analyzer / lowering 行为;prepare 初始化仍按计划留给阶段五专用 init 路径。 @@ -160,28 +185,31 @@ MVP 采用以下策略: 建议实施内容: - 更新 `GdType` sealed permits。 -- 新增首个具体 compiler-only type:`GdccForRangeIterType`。先完成首个 concrete type 再在阶段四补入 `GdCompilerType` 抽象层并回迁,避免为一个实现过早制造抽象层。 +- 新增首个具体 compiler-only type:`GdccForRangeIterType`。先完成首个 concrete type 再在阶段四补入 `GdCompilerType` + 抽象层并回迁,避免为一个实现过早制造抽象层。 - 类型协议至少覆盖: - - `getTypeName()`:建议稳定为 `GdccForRangeIter`,用于内部 identity,不作为 source-facing declared type text。 - - LIR-only text:`compiler::GdccForRangeIter`。 - - C storage type name:建议为 `gdcc_for_range_iter`。 - - init helper function name:建议为 `gdcc_for_range_iter_init`。 - - destroy helper function name:建议为 `gdcc_for_range_iter_destroy`。 - - `isNullable() == false`。 - - `getGdExtensionType() == null`。 - - `isDestroyable()` 根据 destroy helper 策略返回 true。 + - `getTypeName()`:建议稳定为 `GdccForRangeIter`,用于内部 identity,不作为 source-facing declared type text。 + - LIR-only text:`compiler::GdccForRangeIter`。 + - C storage type name:建议为 `gdcc_for_range_iter`。 + - init helper function name:建议为 `gdcc_for_range_iter_init`。 + - destroy helper function name:建议为 `gdcc_for_range_iter_destroy`。 + - `isNullable() == false`。 + - `getGdExtensionType() == null`。 + - `isDestroyable()` 根据 destroy helper 策略返回 true。 - `GdccForRangeIterType` 代表按值保存的 range iterator state,不表示 GDScript `range(...)` 调用返回的 `Array`。 - 如需 direct C struct assignment,明确该类型不使用 `renderCopyAssignFunctionName(...)` 的 `godot_new_*_with_*` 路径。 验收细则: - happy path: - - type unit test 覆盖 `GdccForRangeIterType` 的 stable internal name、LIR-only text、C type name、init helper、destroy helper、nullability、extension metadata。 - - Java sealed switch 编译强制覆盖新增分支。 + - type unit test 覆盖 `GdccForRangeIterType` 的 stable internal name、LIR-only text、C type name、init helper、destroy + helper、nullability、extension metadata。 + - Java sealed switch 编译强制覆盖新增分支。 - negative path: - - `GdccForRangeIterType` 不被当作 `GdPrimitiveType`、`GdObjectType`、`GdVariantType` 或 `GdMetaType`。 - - 不出现 `godot_GdccForRangeIter`、`godot_new_GdccForRangeIter...`、`godot_GdccForRangeIter_destroy` 形式的默认 helper。 - - 不新增任何 `for` parser / analyzer / lowering 行为。 + - `GdccForRangeIterType` 不被当作 `GdPrimitiveType`、`GdObjectType`、`GdVariantType` 或 `GdMetaType`。 + - 不出现 `godot_GdccForRangeIter`、`godot_new_GdccForRangeIter...`、`godot_GdccForRangeIter_destroy` 形式的默认 + helper。 + - 不新增任何 `for` parser / analyzer / lowering 行为。 测试锚点: @@ -194,11 +222,17 @@ MVP 采用以下策略: 产出: -- `ClassRegistry.tryParseStrictTextType(...)`、`ScopeTypeResolver.tryResolveDeclaredType(...)` 与 `ClassRegistry.resolveTypeMetaHere(...)` 继续保持 source-facing strict namespace,不识别 `compiler::GdccForRangeIter` 或 bare `GdccForRangeIter`。 -- `DomLirParser` 新增按 use-site 区分的 LIR type parser:只有 function `` 可解析 `compiler::GdccForRangeIter`,其余 public ABI-like surface 统一以 `compiler-only type leaked into ...` fail-fast。 -- `DomLirSerializer` 新增对应的 compiler-only type text renderer:function local variable 稳定输出 `compiler::GdccForRangeIter`,同时拒绝把 compiler-only 类型序列化到 parameter / return / property / signal surface。 -- ordinary builtin / object / container 的 XML 解析与序列化继续复用既有 `ClassRegistry.findType(...)` / `getTypeName()` 路径,阶段二不改变非 compiler-only 类型行为。 -- 针对 source-facing declared type、registry type-meta、frontend declared type fallback、LIR parser/serializer round-trip 与 leak negative path 的测试已补齐,明确锚定“变量面可用、ABI 面禁止、未知 `compiler::` grammar 不退化为 object guess”。 +- `ClassRegistry.tryParseStrictTextType(...)`、`ScopeTypeResolver.tryResolveDeclaredType(...)` 与 + `ClassRegistry.resolveTypeMetaHere(...)` 继续保持 source-facing strict namespace,不识别 `compiler::GdccForRangeIter` 或 + bare `GdccForRangeIter`。 +- `DomLirParser` 新增按 use-site 区分的 LIR type parser:只有 function `` 可解析 `compiler::GdccForRangeIter` + ,其余 public ABI-like surface 统一以 `compiler-only type leaked into ...` fail-fast。 +- `DomLirSerializer` 新增对应的 compiler-only type text renderer:function local variable 稳定输出 + `compiler::GdccForRangeIter`,同时拒绝把 compiler-only 类型序列化到 parameter / return / property / signal surface。 +- ordinary builtin / object / container 的 XML 解析与序列化继续复用既有 `ClassRegistry.findType(...)` / `getTypeName()` + 路径,阶段二不改变非 compiler-only 类型行为。 +- 针对 source-facing declared type、registry type-meta、frontend declared type fallback、LIR parser/serializer round-trip 与 + leak negative path 的测试已补齐,明确锚定“变量面可用、ABI 面禁止、未知 `compiler::` grammar 不退化为 object guess”。 目标: @@ -217,13 +251,13 @@ MVP 采用以下策略: 验收细则: - happy path: - - local variable 的 `compiler::` XML round-trip 稳定。 - - ordinary builtin / object / container type XML 行为保持不变。 + - local variable 的 `compiler::` XML round-trip 稳定。 + - ordinary builtin / object / container type XML 行为保持不变。 - negative path: - - `ScopeTypeResolverTest` 证明 declared type 文本无法解析 compiler-only type。 - - `ClassRegistryTypeMetaTest` 证明 registry 不发布 compiler-only type-meta。 - - `DomLirParserTest` 证明 parameter / return / property / signal / capture surface 拒绝 compiler-only type。 - - `ClassRegistry.findType(...)` 不把 `compiler::` 猜成 object。 + - `ScopeTypeResolverTest` 证明 declared type 文本无法解析 compiler-only type。 + - `ClassRegistryTypeMetaTest` 证明 registry 不发布 compiler-only type-meta。 + - `DomLirParserTest` 证明 parameter / return / property / signal / capture surface 拒绝 compiler-only type。 + - `ClassRegistry.findType(...)` 不把 `compiler::` 猜成 object。 测试锚点: @@ -240,12 +274,19 @@ MVP 采用以下策略: 产出: -- `FrontendExprTypeAnalyzer` 在 `expressionTypes()` 发布入口与 inferred-local backfill 路径上对 compiler-only type fail-fast,避免 compiler-only published fact 进入用户 expression/local semantic facts。 -- `FrontendLocalTypeStabilizationAnalyzer` 在 local slot 写回前显式拒绝 compiler-only resolved initializer,保持 ordinary local `:=` 只接受用户语义类型。 -- `FrontendTypeCheckAnalyzer` 对 condition published fact 新增 compiler-only guard,防止 leaked compiler-only condition 静默通过 shared type-check 消费。 -- `FrontendBodyLoweringSession.materializeFrontendBoundaryValue(...)` 新增 compiler-only source/target 二次防线;即使 shared boundary helper 未来回归或 synthetic CFG/value fact 被篡改,也不会生成 `PackVariantInsn`、`UnpackVariantInsn`、`ConstructBuiltinInsn` 或 intrinsic-cast boundary。 -- `FrontendCfgNodeInsnLoweringProcessors` 对 condition normalization 新增 compiler-only fail-fast,阻止 compiler-only condition 走 `pack_variant -> unpack_variant(bool)` lowering。 -- 针对 shared boundary、semantic facts、local stabilization、condition type-check、boundary materialization 与 condition lowering 的正反测试已补齐,并继续保留既有 ordinary boundary happy path。 +- `FrontendExprTypeAnalyzer` 在 `expressionTypes()` 发布入口与 inferred-local backfill 路径上对 compiler-only type + fail-fast,避免 compiler-only published fact 进入用户 expression/local semantic facts。 +- `FrontendLocalTypeStabilizationAnalyzer` 在 local slot 写回前显式拒绝 compiler-only resolved initializer,保持 ordinary + local `:=` 只接受用户语义类型。 +- `FrontendTypeCheckAnalyzer` 对 condition published fact 新增 compiler-only guard,防止 leaked compiler-only condition + 静默通过 shared type-check 消费。 +- `FrontendBodyLoweringSession.materializeFrontendBoundaryValue(...)` 新增 compiler-only source/target 二次防线;即使 + shared boundary helper 未来回归或 synthetic CFG/value fact 被篡改,也不会生成 `PackVariantInsn`、`UnpackVariantInsn`、 + `ConstructBuiltinInsn` 或 intrinsic-cast boundary。 +- `FrontendCfgNodeInsnLoweringProcessors` 对 condition normalization 新增 compiler-only fail-fast,阻止 compiler-only + condition 走 `pack_variant -> unpack_variant(bool)` lowering。 +- 针对 shared boundary、semantic facts、local stabilization、condition type-check、boundary materialization 与 condition + lowering 的正反测试已补齐,并继续保留既有 ordinary boundary happy path。 目标: @@ -255,9 +296,12 @@ MVP 采用以下策略: 建议实施内容: -- 在 `FrontendVariantBoundaryCompatibility.determineFrontendBoundaryDecision(...)` 最前面拒绝 compiler-only source 或 target。 -- 明确 `ClassRegistry.checkAssignable(...)` 的 source-facing consumer 不得把 compiler-only 同名视作 ordinary boundary success;可以通过 frontend helper 前置拒绝实现,不必污染 strict backend assignability 基线。 -- 在 `FrontendLocalTypeStabilizationAnalyzer` 和 `FrontendExprTypeAnalyzer` 的 local backfill 风险点增加 fail-closed / fail-fast 策略,防止 compiler-only published expression type 写回 ordinary local slot。 +- 在 `FrontendVariantBoundaryCompatibility.determineFrontendBoundaryDecision(...)` 最前面拒绝 compiler-only source 或 + target。 +- 明确 `ClassRegistry.checkAssignable(...)` 的 source-facing consumer 不得把 compiler-only 同名视作 ordinary boundary + success;可以通过 frontend helper 前置拒绝实现,不必污染 strict backend assignability 基线。 +- 在 `FrontendLocalTypeStabilizationAnalyzer` 和 `FrontendExprTypeAnalyzer` 的 local backfill 风险点增加 fail-closed / + fail-fast 策略,防止 compiler-only published expression type 写回 ordinary local slot。 - 在 condition lowering 的非 bool / non Variant pack path 前显式拒绝 compiler-only type。 - 在 `FrontendBodyLoweringSession.materializeFrontendBoundaryValue(...)` 增加 invariant guard,作为 shared helper 的二次防线。 - 在 call materialization 中,fixed args、vararg tail、dynamic route 均不得接受 compiler-only value。 @@ -265,14 +309,15 @@ MVP 采用以下策略: 验收细则: - happy path: - - 现有 ordinary boundary:`Variant` pack/unpack、`int -> float`、`Vector*i -> Vector*`、`String <-> StringName` 测试保持通过。 + - 现有 ordinary boundary:`Variant` pack/unpack、`int -> float`、`Vector*i -> Vector*`、`String <-> StringName` 测试保持通过。 - negative path: - - compiler-only -> `Variant` reject,不生成 `PackVariantInsn`。 - - `Variant` -> compiler-only reject,不生成 `UnpackVariantInsn`。 - - compiler-only -> compiler-only 不通过 ordinary user boundary。 - - compiler-only condition 不生成 `pack_variant -> unpack_variant(bool)`。 - - fixed call argument、vararg tail、return slot、property store、subscript key/index 都不能 materialize compiler-only boundary。 - - artificial published compiler-only expression type 不能写回 user local slot。 + - compiler-only -> `Variant` reject,不生成 `PackVariantInsn`。 + - `Variant` -> compiler-only reject,不生成 `UnpackVariantInsn`。 + - compiler-only -> compiler-only 不通过 ordinary user boundary。 + - compiler-only condition 不生成 `pack_variant -> unpack_variant(bool)`。 + - fixed call argument、vararg tail、return slot、property store、subscript key/index 都不能 materialize compiler-only + boundary。 + - artificial published compiler-only expression type 不能写回 user local slot。 测试锚点: @@ -289,12 +334,21 @@ MVP 采用以下策略: 产出: -- 新增 `GdCompilerType` sealed interface,承载所有 compiler-only 类型的共同协议:`getLirTypeText()`、`getCStorageTypeName()`、`getCInitHelperName()`、`getCDestroyHelperName()`,以及共享默认实现 `isNullable() == false`、`getGdExtensionType() == null`、`isDestroyable() == true`。 -- `GdccForRangeIterType` 从直接 `implements GdType` 迁移为 `implements GdCompilerType`,移除了与抽象层默认实现重复的 `isNullable()`、`getGdExtensionType()`、`isDestroyable()` 覆写,保留其 internal name、LIR-only text、C helper 协议不变。 -- `GdType` permits 从直接包含 `GdccForRangeIterType` 改为包含 `GdCompilerType`,使 compiler-only 类型通过统一抽象层挂入 sealed hierarchy。 -- 所有 consumer 侧的 `instanceof GdccForRangeIterType` / `case GdccForRangeIterType` 判断已迁移为面向 `GdCompilerType` 的判断,覆盖 frontend leak guards(`FrontendVariantBoundaryCompatibility`、`FrontendExprTypeAnalyzer`、`FrontendLocalTypeStabilizationAnalyzer`、`FrontendTypeCheckAnalyzer`、`FrontendBodyLoweringSession`、`FrontendCfgNodeInsnLoweringProcessors`、`FrontendWritableTypeWritebackSupport`)、C backend guards(`CGenHelper`、`CCodegen`、`CBodyBuilder`、`DestructInsnGen`、`EngineMethodAbiCodec`)和 LIR serializer(`DomLirSerializer`)。 +- 新增 `GdCompilerType` sealed interface,承载所有 compiler-only 类型的共同协议:`getLirTypeText()`、 + `getCStorageTypeName()`、`getCInitHelperName()`、`getCDestroyHelperName()`,以及共享默认实现 `isNullable() == false`、 + `getGdExtensionType() == null`、`isDestroyable() == true`。 +- `GdccForRangeIterType` 从直接 `implements GdType` 迁移为 `implements GdCompilerType`,移除了与抽象层默认实现重复的 + `isNullable()`、`getGdExtensionType()`、`isDestroyable()` 覆写,保留其 internal name、LIR-only text、C helper 协议不变。 +- `GdType` permits 从直接包含 `GdccForRangeIterType` 改为包含 `GdCompilerType`,使 compiler-only 类型通过统一抽象层挂入 + sealed hierarchy。 +- 所有 consumer 侧的 `instanceof GdccForRangeIterType` / `case GdccForRangeIterType` 判断已迁移为面向 `GdCompilerType` + 的判断,覆盖 frontend leak guards(`FrontendVariantBoundaryCompatibility`、`FrontendExprTypeAnalyzer`、 + `FrontendLocalTypeStabilizationAnalyzer`、`FrontendTypeCheckAnalyzer`、`FrontendBodyLoweringSession`、 + `FrontendCfgNodeInsnLoweringProcessors`、`FrontendWritableTypeWritebackSupport`)、C backend guards(`CGenHelper`、 + `CCodegen`、`CBodyBuilder`、`DestructInsnGen`、`EngineMethodAbiCodec`)和 LIR serializer(`DomLirSerializer`)。 - `DomLirParser` 的 text-to-instance dispatch 保留对 `GdccForRangeIterType.LIR_TYPE_TEXT` 的引用,因为该路径是文本到具体实例的映射,不是类型判断。 -- 新增 `GdCompilerTypeTest` 契约测试,覆盖抽象层协议方法、共享默认值、sealed hierarchy 归属、user-facing family 排除和 `gdcc_*` helper 命名约束的正反两面。 +- 新增 `GdCompilerTypeTest` 契约测试,覆盖抽象层协议方法、共享默认值、sealed hierarchy 归属、user-facing family 排除和 + `gdcc_*` helper 命名约束的正反两面。 - `GdccForRangeIterTypeTest` 已更新,增加 `assertInstanceOf(GdCompilerType.class, type)` 断言以锚定抽象层归属。 目标: @@ -314,11 +368,11 @@ MVP 采用以下策略: 验收细则: - happy path: - - `GdccForRangeIterType` 继续通过所有现有 type/backend/serialization 边界测试。 - - 新增的抽象层测试覆盖 `GdCompilerType` 的共同契约。 + - `GdccForRangeIterType` 继续通过所有现有 type/backend/serialization 边界测试。 + - 新增的抽象层测试覆盖 `GdCompilerType` 的共同契约。 - negative path: - - 不再出现“所有 compiler-only 类型都必须直接挂到 `GdType` 根层”的实现假设。 - - `GdccForRangeIterType` 仍不进入 source-facing type namespace、public ABI 或 ordinary boundary。 + - 不再出现“所有 compiler-only 类型都必须直接挂到 `GdType` 根层”的实现假设。 + - `GdccForRangeIterType` 仍不进入 source-facing type namespace、public ABI 或 ordinary boundary。 测试锚点: @@ -327,6 +381,24 @@ MVP 采用以下策略: ### 5.5 阶段五:LIR public ABI validator +状态:已完成(2026-06-30)。 + +产出: + +- 新增 `gd.script.gdcc.lir.validation.LirPublicAbiValidator`,在 backend codegen 前统一扫描 property、signal + parameter、function parameter、function return 与 lambda capture,拒绝任何 `GdCompilerType` 泄漏。 +- `CCodegen.generate()` 现在在 helper 合成、prepare/finally 注入与模板渲染之前执行该 validator,确保失败早于 property init + synthesize、bound metadata 组装和 `getGdExtensionType()==null` 等晚期错误。 +- validator 面向 `GdCompilerType` 抽象层工作,不把 `GdccForRangeIterType` 写死在规则里,为后续 compiler-only concrete type + 复用同一 ABI 边界。 +- hidden function 在 MVP 中继续与 public function 共用 parameter/return 限制;`is_hidden=true` 不再被视作可绕过 ABI + 校验的特殊通道。 +- 新增 `LirPublicAbiValidatorTest`,覆盖 happy path(compiler-only local variable 合法)以及 property、signal + parameter、function parameter、hidden function return、lambda capture 的 negative path,错误信息统一锚定 + `compiler-only type leaked into ...`。 +- 本阶段只负责 ABI-like surface fail-fast,不改变 function `` 的 compiler-only 合法性,也不替代后续阶段在具体 + codegen/helper 路径上的二次封堵。 + 目标: - compiler-only type 即使由手写 LIR 或 parser 注入,也不能流入 public ABI。 @@ -335,23 +407,23 @@ MVP 采用以下策略: - 增加 LIR validator 或在现有 validation/codegen 前置流程中增加 pass。 - 校验范围至少覆盖: - - class property type。 - - signal parameter type。 - - public and hidden function parameter type。 - - public and hidden function return type。 - - lambda capture type。 - - generated call wrapper / binding data collection surface。 + - class property type。 + - signal parameter type。 + - public and hidden function parameter type。 + - public and hidden function return type。 + - lambda capture type。 + - generated call wrapper / binding data collection surface。 - MVP 允许 compiler-only type 只出现在 function variables。 - 错误信息使用 `compiler-only type leaked into public ABI` 或具体 surface 名称。 验收细则: - happy path: - - 含 compiler-only local variable 和 intrinsic 的 LIR module 可以进入 backend codegen。 + - 含 compiler-only local variable 和 intrinsic 的 LIR module 可以进入 backend codegen。 - negative path: - - function parameter / return / property / signal / capture 使用 compiler-only type fail-fast。 - - hidden function parameter / return 在 MVP 中同样 fail-fast。 - - failure 早于 `CGenHelper.renderBoundMetadata(...)` 的 `getGdExtensionType()==null`。 + - function parameter / return / property / signal / capture 使用 compiler-only type fail-fast。 + - hidden function parameter / return 在 MVP 中同样 fail-fast。 + - failure 早于 `CGenHelper.renderBoundMetadata(...)` 的 `getGdExtensionType()==null`。 测试锚点: @@ -370,29 +442,33 @@ MVP 采用以下策略: 建议实施内容: - `CGenHelper.renderGdTypeInC(...)` 对 compiler-only type 使用其 C storage type name。 -- `CGenHelper.renderGdTypeRefInC(...)` 仅在允许的 internal function / intrinsic helper surface 使用明确策略;public ABI 已由 validator 禁止。 +- `CGenHelper.renderGdTypeRefInC(...)` 仅在允许的 internal function / intrinsic helper surface 使用明确策略;public ABI + 已由 validator 禁止。 - `renderPackFunctionName(...)` / `renderUnpackFunctionName(...)` 对 compiler-only type fail-fast。 -- `renderCopyAssignFunctionName(...)` 对 compiler-only type 不返回 `godot_new_*_with_*`。若所有 compiler-only type 允许 C struct assignment,返回空字符串并让 assignment 走 direct path;若未来需要 deep copy,先扩展 copy helper 协议。 +- `renderCopyAssignFunctionName(...)` 对 compiler-only type 不返回 `godot_new_*_with_*`。若所有 compiler-only type 允许 C + struct assignment,返回空字符串并让 assignment 走 direct path;若未来需要 deep copy,先扩展 copy helper 协议。 - `renderDestroyFunctionName(...)` 对 compiler-only type 返回其 `gdcc_*_destroy` helper。 - `CCodegen.generateFunctionPrepareBlock(...)` 对 compiler-only local 生成专用初始化路径,不生成 `ConstructBuiltinInsn`。 -- `CBodyBuilder.renderDefaultValueExpr(...)` 对 compiler-only type fail-fast,除非该 use-site 明确是 internal helper 且能调用 init helper。 -- `CBodyBuilder.needsAddressOf(...)`、`prepareRhsValue(...)`、`prepareReturnValue(...)`、`emitDestroy(...)` 明确处理 compiler-only direct assignment 与 destroy。 +- `CBodyBuilder.renderDefaultValueExpr(...)` 对 compiler-only type fail-fast,除非该 use-site 明确是 internal helper 且能调用 + init helper。 +- `CBodyBuilder.needsAddressOf(...)`、`prepareRhsValue(...)`、`prepareReturnValue(...)`、`emitDestroy(...)` 明确处理 + compiler-only direct assignment 与 destroy。 - `DestructInsnGen` 对 compiler-only destroyable type 调用 `gdcc_*_destroy`,不能 default no-op。 验收细则: - happy path: - - compiler-only local declaration 使用 C storage type name。 - - prepare block 调用 `gdcc_*_init` 或等价专用 init instruction/code path。 - - overwrite、scope cleanup、discarded destroyable value 调用 `gdcc_*_destroy`。 - - direct assignment 不调用 `godot_new_*_with_*`。 + - compiler-only local declaration 使用 C storage type name。 + - prepare block 调用 `gdcc_*_init` 或等价专用 init instruction/code path。 + - overwrite、scope cleanup、discarded destroyable value 调用 `gdcc_*_destroy`。 + - direct assignment 不调用 `godot_new_*_with_*`。 - negative path: - - 生成结果中不出现 `godot_`。 - - 不出现 `godot_new_()`。 - - 不出现 `godot_new_Variant_with_`。 - - 不出现 `godot_new__with_Variant`。 - - 不出现 `godot_new__with_`。 - - 不出现 `godot__destroy`。 + - 生成结果中不出现 `godot_`。 + - 不出现 `godot_new_()`。 + - 不出现 `godot_new_Variant_with_`。 + - 不出现 `godot_new__with_Variant`。 + - 不出现 `godot_new__with_`。 + - 不出现 `godot__destroy`。 测试锚点: @@ -414,27 +490,31 @@ MVP 采用以下策略: 建议实施内容: - 为 `GdccForRangeIterType` 新增一组最小 intrinsic。推荐命名如下,最终名称可按现有 intrinsic 命名风格调整,但语义必须保持: - - `gdcc.for_range_iter.init`:根据 `start`、`end`、`step` 初始化 iterator state。 - - `gdcc.for_range_iter.should_continue`:读取当前 state,返回是否还有当前值可用。 - - `gdcc.for_range_iter.next`:推进到下一个值,并返回推进后的新 iterator state。 - - `gdcc.for_range_iter.get`:读取当前迭代值。 + - `gdcc.for_range_iter.init`:根据 `start`、`end`、`step` 初始化 iterator state。 + - `gdcc.for_range_iter.should_continue`:读取当前 state,返回是否还有当前值可用。 + - `gdcc.for_range_iter.next`:推进到下一个值,并返回推进后的新 iterator state。 + - `gdcc.for_range_iter.get`:读取当前迭代值。 - intrinsic 类型合同: - - `init` 的 result 必须是非 ref 的 `compiler::GdccForRangeIter`;arguments 为三个 `int` 值,对应 normalized `start`、`end`、`step`。 - - `should_continue` 的 result 必须是非 ref 的 `bool`;argument 为一个 `compiler::GdccForRangeIter`。 - - `next` 的 result 必须是非 ref 的 `compiler::GdccForRangeIter`;argument 为一个 `compiler::GdccForRangeIter`,返回推进后的新 iterator state。 - - `get` 的 result 必须是非 ref 的 `int`;argument 为一个 `compiler::GdccForRangeIter`。 -- 未来 lowering 的基本形状应为:`iter = init(start,end,step)`,每轮先 `should_continue(iter)`,再 `value = get(iter)`,循环体结束后 `iter = next(iter)`。这保证 `GdccForRangeIterType` 仍按不可变值语义传递,不需要 ref mutation。 -- `range(n)` / `range(b, n)` / `range(b, n, s)` 的参数归一化属于未来 frontend lowering:本阶段只要求 intrinsic 能接受已归一化的三个 `int` 参数。 + - `init` 的 result 必须是非 ref 的 `compiler::GdccForRangeIter`;arguments 为三个 `int` 值,对应 normalized `start`、 + `end`、`step`。 + - `should_continue` 的 result 必须是非 ref 的 `bool`;argument 为一个 `compiler::GdccForRangeIter`。 + - `next` 的 result 必须是非 ref 的 `compiler::GdccForRangeIter`;argument 为一个 `compiler::GdccForRangeIter`,返回推进后的新 + iterator state。 + - `get` 的 result 必须是非 ref 的 `int`;argument 为一个 `compiler::GdccForRangeIter`。 +- 未来 lowering 的基本形状应为:`iter = init(start,end,step)`,每轮先 `should_continue(iter)`,再 `value = get(iter)` + ,循环体结束后 `iter = next(iter)`。这保证 `GdccForRangeIterType` 仍按不可变值语义传递,不需要 ref mutation。 +- `range(n)` / `range(b, n)` / `range(b, n, s)` 的参数归一化属于未来 frontend lowering:本阶段只要求 intrinsic 能接受已归一化的三个 + `int` 参数。 - `step == 0` 的处理策略必须在 intrinsic catalog 中写清。建议 fail-fast 或 runtime error helper,不允许生成无限循环语义。 - 负步长必须作为合法输入进入 helper 语义:`should_continue` / `next` 对正 step 使用 `< end`,对负 step 使用 `> end`。 - 更新 `doc/gdcc_lir_intrinsic.md` catalog,记录: - - intrinsic name。 - - LIR textual shape。 - - result 是否必须存在。 - - result 是否允许 ref。 - - argument 数量与类型。 - - C backend 语义。 - - lifecycle / ownership 说明。 + - intrinsic name。 + - LIR textual shape。 + - result 是否必须存在。 + - result 是否允许 ref。 + - argument 数量与类型。 + - C backend 语义。 + - lifecycle / ownership 说明。 - 在 `CIntrinsicManager` 注册白名单。 - 每个 `CIntrinsicFunction` 自己校验 result / arg / arity / ref。 - 成功路径优先复用 `CBodyBuilder.assignVar(...)` 或 `callAssign(...)`,除非该 compiler-only type 需要更窄的写入策略。 @@ -442,20 +522,23 @@ MVP 采用以下策略: 验收细则: - happy path: - - parser / serializer 保留 `call_intrinsic` textual shape。 - - backend registry 能找到 `gdcc.for_range_iter.init`、`gdcc.for_range_iter.should_continue`、`gdcc.for_range_iter.next`、`gdcc.for_range_iter.get`。 - - 成功 codegen 使用 `gdcc_*` helper,不绕过 slot lifecycle。 - - `range(10)` 归一化后的 `start=0,end=10,step=1`、`range(5,10)` 归一化后的 `start=5,end=10,step=1`、`range(10,0,-1)` 归一化后的 `start=10,end=0,step=-1` 都能用手写 LIR intrinsic 序列生成预期 C helper 调用形状。 + - parser / serializer 保留 `call_intrinsic` textual shape。 + - backend registry 能找到 `gdcc.for_range_iter.init`、`gdcc.for_range_iter.should_continue`、 + `gdcc.for_range_iter.next`、`gdcc.for_range_iter.get`。 + - 成功 codegen 使用 `gdcc_*` helper,不绕过 slot lifecycle。 + - `range(10)` 归一化后的 `start=0,end=10,step=1`、`range(5,10)` 归一化后的 `start=5,end=10,step=1`、`range(10,0,-1)` + 归一化后的 `start=10,end=0,step=-1` 都能用手写 LIR intrinsic 序列生成预期 C helper 调用形状。 - negative path: - - unknown intrinsic fail-fast。 - - bad arity fail-fast。 - - missing result / unexpected result fail-fast。 - - result ref 不符合合同 fail-fast。 - - result type 错误 fail-fast。 - - argument type 错误 fail-fast。 - - literal operand fail-fast。 - - `step == 0` 的手写 LIR 用例按 catalog 约定 fail-fast 或进入明确 runtime error helper,不允许静默生成无限循环 helper 调用。 - - `GdccForRangeIterType` 不能被传给非上述四个 range iterator intrinsic。 + - unknown intrinsic fail-fast。 + - bad arity fail-fast。 + - missing result / unexpected result fail-fast。 + - result ref 不符合合同 fail-fast。 + - result type 错误 fail-fast。 + - argument type 错误 fail-fast。 + - literal operand fail-fast。 + - `step == 0` 的手写 LIR 用例按 catalog 约定 fail-fast 或进入明确 runtime error helper,不允许静默生成无限循环 helper + 调用。 + - `GdccForRangeIterType` 不能被传给非上述四个 range iterator intrinsic。 测试锚点: @@ -474,7 +557,8 @@ MVP 采用以下策略: 建议实施内容: - `PackUnpackVariantInsnGen` 显式拒绝 compiler-only pack / unpack。 -- `CallMethodInsnGen`、`BackendMethodCallResolver`、`CallGlobalInsnGen`、static call path 显式拒绝 compiler-only receiver / argument / return target。 +- `CallMethodInsnGen`、`BackendMethodCallResolver`、`CallGlobalInsnGen`、static call path 显式拒绝 compiler-only receiver / + argument / return target。 - operator / index / property instruction generators 显式拒绝 compiler-only operand / receiver / value。 - typed array / dictionary metadata 和 runtime guard leaf 渲染拒绝 compiler-only leaf。 - generated `call_func` wrapper argument gate、unpack expression、destroy stmt 不接受 compiler-only type。 @@ -482,12 +566,12 @@ MVP 采用以下策略: 验收细则: - negative path: - - `PACK_VARIANT` source 是 compiler-only type fail-fast。 - - `UNPACK_VARIANT` result 是 compiler-only type fail-fast。 - - dynamic call argument 是 compiler-only type 不被 pack 成 `Variant`。 - - dynamic result 不可 unpack 到 compiler-only type。 - - method/global/static/operator/index/property path 遇到 compiler-only type fail-fast。 - - `Array[compiler::]` / `Dictionary[String, compiler::]` 不能生成 outward metadata。 + - `PACK_VARIANT` source 是 compiler-only type fail-fast。 + - `UNPACK_VARIANT` result 是 compiler-only type fail-fast。 + - dynamic call argument 是 compiler-only type 不被 pack 成 `Variant`。 + - dynamic result 不可 unpack 到 compiler-only type。 + - method/global/static/operator/index/property path 遇到 compiler-only type fail-fast。 + - `Array[compiler::]` / `Dictionary[String, compiler::]` 不能生成 outward metadata。 测试锚点: @@ -511,19 +595,19 @@ MVP 采用以下策略: 建议实施内容: - 更新 `doc/gdcc_type_system.md`: - - 增加 compiler-only type 的定位。 - - 明确它不属于 GDScript source-facing type set。 - - 明确 `GdCompilerType` 是 compiler-only 共同抽象层,`GdccForRangeIterType` 是其首个具体实现。 - - 明确 ordinary compatibility matrix 不接受它。 + - 增加 compiler-only type 的定位。 + - 明确它不属于 GDScript source-facing type set。 + - 明确 `GdCompilerType` 是 compiler-only 共同抽象层,`GdccForRangeIterType` 是其首个具体实现。 + - 明确 ordinary compatibility matrix 不接受它。 - 更新 `doc/gdcc_low_ir.md`: - - 记录 `compiler::` LIR-only type grammar。 - - 记录只允许 function variables 的 MVP 约束。 + - 记录 `compiler::` LIR-only type grammar。 + - 记录只允许 function variables 的 MVP 约束。 - 更新 `doc/gdcc_lir_intrinsic.md`: - - 增加 `GdccForRangeIterType` 的四个 intrinsic catalog:init、should_continue、next、get。 + - 增加 `GdccForRangeIterType` 的四个 intrinsic catalog:init、should_continue、next、get。 - 更新 `doc/gdcc_c_backend.md`: - - 记录 `gdcc_for_range_iter` C storage type、init/destroy helper、禁止 `godot_*` default helper。 + - 记录 `gdcc_for_range_iter` C storage type、init/destroy helper、禁止 `godot_*` default helper。 - 更新 `doc/gdcc_runtime_lib.md`: - - 记录新增 `gdcc_for_range_iter_*` helper 声明/实现边界。 + - 记录新增 `gdcc_for_range_iter_*` helper 声明/实现边界。 - 如 lifecycle 行为扩面,更新 `doc/gdcc_ownership_lifecycle_spec.md` 或 backend lifecycle contract。 验收细则: @@ -546,7 +630,8 @@ MVP 采用以下策略: - 支持 public 或 hidden function 使用 compiler-only parameter / return type。 - 把 Godot internal class,例如 `GDScriptFunctionState`,建模为 source-facing GDCC type。 - 一次性实现完整 iterator / async lowering。 -- 在本阶段实现 `for` parser / analyzer / lowering,或把 GDScript `range(...)` ordinary call 改写为内部 iterator;当前只实现 `GdccForRangeIterType` 与四个 range iterator intrinsics,作为未来任务的实现与验收锚点。 +- 在本阶段实现 `for` parser / analyzer / lowering,或把 GDScript `range(...)` ordinary call 改写为内部 iterator;当前只实现 + `GdccForRangeIterType` 与四个 range iterator intrinsics,作为未来任务的实现与验收锚点。 --- @@ -616,4 +701,5 @@ script/run-gradle-targeted-tests.sh --tests "gd.script.gdcc.frontend.sema.analyz 8. 普通 method / global / operator / index / property / wrapper path 负例补齐。 9. 文档同步和 targeted regression。 -这条顺序的核心约束是:先让 compiler-only type 不能从用户世界进入,再允许它在 LIR/backend 内部出现;先封掉默认 `Variant` / `godot_*` 路径,再接入具体 intrinsic 成功路径。 +这条顺序的核心约束是:先让 compiler-only type 不能从用户世界进入,再允许它在 LIR/backend 内部出现;先封掉默认 `Variant` / +`godot_*` 路径,再接入具体 intrinsic 成功路径。 diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/CCodegen.java b/src/main/java/gd/script/gdcc/backend/c/gen/CCodegen.java index 44e3f220..273b180d 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/CCodegen.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/CCodegen.java @@ -13,6 +13,7 @@ import gd.script.gdcc.lir.*; import gd.script.gdcc.lir.insn.*; import gd.script.gdcc.lir.validation.ControlFlowIntegrityValidator; +import gd.script.gdcc.lir.validation.LirPublicAbiValidator; import gd.script.gdcc.lir.validation.LifecycleInstructionRestrictionValidator; import gd.script.gdcc.scope.ParameterDef; import gd.script.gdcc.scope.RefCountedStatus; @@ -62,6 +63,8 @@ public class CCodegen implements Codegen { private CGenHelper helper; /// Validator for block layout and successor integrity. private final ControlFlowIntegrityValidator controlFlowValidator = new ControlFlowIntegrityValidator(); + /// Validator for compiler-only type leaks on ABI-like LIR surfaces. + private final LirPublicAbiValidator publicAbiValidator = new LirPublicAbiValidator(); /// Validator for lifecycle instruction usage restrictions. private final LifecycleInstructionRestrictionValidator lifecycleValidator = new LifecycleInstructionRestrictionValidator(); @@ -533,6 +536,8 @@ public List generate() { if (ctx == null || module == null) { throw new IllegalStateException("CCodegen not prepared. Call prepare() before generate()."); } + // Validate ABI surfaces before backend synthesizes helpers or touches outward metadata. + publicAbiValidator.validateModule(module); this.generateDefaultGetterSetterInitialization(); this.validatePropertyInitFunctionsReadyForCodegen(); this.generateFunctionPrepareBlock(); diff --git a/src/main/java/gd/script/gdcc/lir/validation/LirPublicAbiValidator.java b/src/main/java/gd/script/gdcc/lir/validation/LirPublicAbiValidator.java new file mode 100644 index 00000000..6e14fd6f --- /dev/null +++ b/src/main/java/gd/script/gdcc/lir/validation/LirPublicAbiValidator.java @@ -0,0 +1,93 @@ +package gd.script.gdcc.lir.validation; + +import gd.script.gdcc.lir.LirClassDef; +import gd.script.gdcc.lir.LirFunctionDef; +import gd.script.gdcc.lir.LirModule; +import gd.script.gdcc.lir.LirPropertyDef; +import gd.script.gdcc.lir.LirSignalDef; +import gd.script.gdcc.type.GdCompilerType; +import gd.script.gdcc.type.GdType; +import org.jetbrains.annotations.NotNull; + +/// Validates that compiler-only types never leak into public ABI-like LIR surfaces. +/// +/// The current MVP contract only allows compiler-only types on function-local variables. +/// Hidden functions are validated the same way as public ones so backend/template code never +/// has to guess whether a hidden signature is safe to expose through later wiring. +public final class LirPublicAbiValidator { + + public void validateModule(@NotNull LirModule module) { + for (var classDef : module.getClassDefs()) { + validateClass(classDef); + } + } + + public void validateClass(@NotNull LirClassDef classDef) { + for (var propertyDef : classDef.getProperties()) { + validateProperty(classDef, propertyDef); + } + for (var signalDef : classDef.getSignals()) { + validateSignal(classDef, signalDef); + } + for (var functionDef : classDef.getFunctions()) { + validateFunction(classDef, functionDef); + } + } + + public void validateFunction(@NotNull LirClassDef classDef, @NotNull LirFunctionDef functionDef) { + for (var parameterDef : functionDef.getParameters()) { + requireNoCompilerOnlyType( + parameterDef.getType(), + "function parameter", + describeFunction(classDef, functionDef) + "(" + parameterDef.getName() + ")" + ); + } + for (var captureDef : functionDef.getCaptures().values()) { + requireNoCompilerOnlyType( + captureDef.getType(), + "function capture", + describeFunction(classDef, functionDef) + "(" + captureDef.getName() + ")" + ); + } + requireNoCompilerOnlyType( + functionDef.getReturnType(), + "function return", + describeFunction(classDef, functionDef) + ); + } + + private void validateProperty(@NotNull LirClassDef classDef, @NotNull LirPropertyDef propertyDef) { + requireNoCompilerOnlyType( + propertyDef.getType(), + "property", + classDef.getName() + "." + propertyDef.getName() + ); + } + + private void validateSignal(@NotNull LirClassDef classDef, @NotNull LirSignalDef signalDef) { + for (var parameterDef : signalDef.getParameters()) { + requireNoCompilerOnlyType( + parameterDef.getType(), + "signal parameter", + classDef.getName() + "." + signalDef.getName() + "(" + parameterDef.getName() + ")" + ); + } + } + + private void requireNoCompilerOnlyType(@NotNull GdType type, + @NotNull String surfaceName, + @NotNull String owner) { + if (type instanceof GdCompilerType compilerType) { + throw new IllegalArgumentException( + "compiler-only type leaked into " + surfaceName + ": " + + compilerType.getTypeName() + + " at " + + owner + ); + } + } + + private @NotNull String describeFunction(@NotNull LirClassDef classDef, @NotNull LirFunctionDef functionDef) { + return classDef.getName() + "." + functionDef.getName(); + } +} diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/CCodegenTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CCodegenTest.java index 911b0f6f..d7f48c24 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/CCodegenTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/CCodegenTest.java @@ -35,6 +35,7 @@ import gd.script.gdcc.type.GdArrayType; import gd.script.gdcc.type.GdBoolType; import gd.script.gdcc.type.GdDictionaryType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdFloatVectorType; import gd.script.gdcc.type.GdFloatType; import gd.script.gdcc.type.GdIntVectorType; @@ -210,6 +211,74 @@ public void generatesEntryFiles() throws Exception { assertTrue(hCode.contains("#include \"engine_method_binds.h\"")); } + @Test + public void generateShouldRejectCompilerOnlyTypeOnHiddenFunctionReturnBeforeBackendSynthesis() { + var workerClass = new LirClassDef("Worker", "RefCounted"); + var helper = new LirFunctionDef("helper"); + helper.setHidden(true); + helper.setReturnType(GdccForRangeIterType.FOR_RANGE_ITER); + var entry = new LirBasicBlock("entry"); + entry.setTerminator(new ReturnInsn(null)); + helper.addBasicBlock(entry); + helper.setEntryBlockId("entry"); + workerClass.addFunction(helper); + + var module = new LirModule("compiler_only_hidden_return", List.of(workerClass)); + var classRegistry = new ClassRegistry(new ExtensionAPI( + null, + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of() + )); + ProjectInfo projectInfo = new ProjectInfo("test", GodotVersion.V451, Path.of(".")) { + }; + var ctx = new CodegenContext(projectInfo, classRegistry); + var codegen = new CCodegen(); + codegen.prepare(ctx, module); + + var exception = assertThrows(IllegalArgumentException.class, codegen::generate); + + assertTrue(exception.getMessage().contains("compiler-only type leaked into function return"), exception.getMessage()); + assertFalse(exception.getMessage().contains("dedicated prepare initialization"), exception.getMessage()); + assertFalse(exception.getMessage().contains("property initializer"), exception.getMessage()); + assertFalse(exception.getMessage().contains("outward GDExtension metadata"), exception.getMessage()); + } + + @Test + public void generateShouldRejectCompilerOnlyPropertyBeforeDefaultInitSynthesis() { + var workerClass = new LirClassDef("Worker", "RefCounted"); + workerClass.addProperty(new LirPropertyDef("iter", GdccForRangeIterType.FOR_RANGE_ITER)); + + var module = new LirModule("compiler_only_property", List.of(workerClass)); + var classRegistry = new ClassRegistry(new ExtensionAPI( + null, + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of() + )); + ProjectInfo projectInfo = new ProjectInfo("test", GodotVersion.V451, Path.of(".")) { + }; + var ctx = new CodegenContext(projectInfo, classRegistry); + var codegen = new CCodegen(); + codegen.prepare(ctx, module); + + var exception = assertThrows(IllegalArgumentException.class, codegen::generate); + + assertTrue(exception.getMessage().contains("compiler-only type leaked into property"), exception.getMessage()); + assertFalse(exception.getMessage().contains("property initializer"), exception.getMessage()); + assertFalse(exception.getMessage().contains("outward GDExtension metadata"), exception.getMessage()); + } + @Test public void entryTemplateShouldSetMinimumInitializationLevelAndGuardLifecycleLevels() throws Exception { var workerClass = new LirClassDef("GDEntryWorker", "RefCounted"); diff --git a/src/test/java/gd/script/gdcc/lir/validation/LirPublicAbiValidatorTest.java b/src/test/java/gd/script/gdcc/lir/validation/LirPublicAbiValidatorTest.java new file mode 100644 index 00000000..94b188e0 --- /dev/null +++ b/src/test/java/gd/script/gdcc/lir/validation/LirPublicAbiValidatorTest.java @@ -0,0 +1,113 @@ +package gd.script.gdcc.lir.validation; + +import gd.script.gdcc.lir.LirBasicBlock; +import gd.script.gdcc.lir.LirCaptureDef; +import gd.script.gdcc.lir.LirClassDef; +import gd.script.gdcc.lir.LirFunctionDef; +import gd.script.gdcc.lir.LirModule; +import gd.script.gdcc.lir.LirParameterDef; +import gd.script.gdcc.lir.LirPropertyDef; +import gd.script.gdcc.lir.LirSignalDef; +import gd.script.gdcc.lir.insn.ReturnInsn; +import gd.script.gdcc.type.GdccForRangeIterType; +import gd.script.gdcc.type.GdVoidType; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class LirPublicAbiValidatorTest { + private final LirPublicAbiValidator validator = new LirPublicAbiValidator(); + + @Test + @DisplayName("compiler-only local variable should stay valid for backend-only LIR") + void compilerOnlyLocalVariableShouldStayValid() { + var function = newFunction("iter_helper"); + function.createAndAddVariable("iter", GdccForRangeIterType.FOR_RANGE_ITER); + + var module = new LirModule("m", List.of(newClassWith(function))); + + assertDoesNotThrow(() -> validator.validateModule(module)); + } + + @Test + @DisplayName("validator should reject compiler-only ABI surfaces including hidden function return") + void validatorShouldRejectCompilerOnlyAbiSurfaces() { + assertSurfaceRejected("property", classWithCompilerOnlyProperty()); + assertSurfaceRejected("signal parameter", classWithCompilerOnlySignalParameter()); + assertSurfaceRejected("function parameter", classWithCompilerOnlyFunctionParameter()); + assertSurfaceRejected("function parameter", classWithCompilerOnlyHiddenFunctionParameter()); + assertSurfaceRejected("function return", classWithCompilerOnlyHiddenFunctionReturn()); + assertSurfaceRejected("function capture", classWithCompilerOnlyFunctionCapture()); + } + + private void assertSurfaceRejected(@NotNull String surfaceName, @NotNull LirClassDef classDef) { + var exception = assertThrows( + IllegalArgumentException.class, + () -> validator.validateModule(new LirModule("m", List.of(classDef))), + surfaceName + ); + + assertTrue(exception.getMessage().contains("compiler-only type leaked into " + surfaceName), exception.getMessage()); + } + + private static @NotNull LirClassDef classWithCompilerOnlyProperty() { + var classDef = new LirClassDef("Worker", "RefCounted"); + classDef.addProperty(new LirPropertyDef("iter", GdccForRangeIterType.FOR_RANGE_ITER)); + return classDef; + } + + private static @NotNull LirClassDef classWithCompilerOnlySignalParameter() { + var classDef = new LirClassDef("Worker", "RefCounted"); + var signal = new LirSignalDef("changed"); + signal.addParameter(new LirParameterDef("iter", GdccForRangeIterType.FOR_RANGE_ITER, null, signal)); + classDef.addSignal(signal); + return classDef; + } + + private static @NotNull LirClassDef classWithCompilerOnlyFunctionParameter() { + var function = newFunction("consume_iter"); + function.addParameter(new LirParameterDef("iter", GdccForRangeIterType.FOR_RANGE_ITER, null, function)); + return newClassWith(function); + } + + private static @NotNull LirClassDef classWithCompilerOnlyHiddenFunctionParameter() { + var function = newFunction("consume_hidden_iter"); + function.setHidden(true); + function.addParameter(new LirParameterDef("iter", GdccForRangeIterType.FOR_RANGE_ITER, null, function)); + return newClassWith(function); + } + + private static @NotNull LirClassDef classWithCompilerOnlyHiddenFunctionReturn() { + var function = newFunction("produce_iter"); + function.setHidden(true); + function.setReturnType(GdccForRangeIterType.FOR_RANGE_ITER); + return newClassWith(function); + } + + private static @NotNull LirClassDef classWithCompilerOnlyFunctionCapture() { + var function = newFunction("lambda0"); + function.setLambda(true); + function.addCapture(new LirCaptureDef("iter", GdccForRangeIterType.FOR_RANGE_ITER, function)); + return newClassWith(function); + } + + private static @NotNull LirFunctionDef newFunction(@NotNull String name) { + var function = new LirFunctionDef(name); + function.setReturnType(GdVoidType.VOID); + function.addBasicBlock(new LirBasicBlock("entry", List.of(new ReturnInsn(null)))); + function.setEntryBlockId("entry"); + return function; + } + + private static @NotNull LirClassDef newClassWith(@NotNull LirFunctionDef function) { + var classDef = new LirClassDef("Worker", "RefCounted"); + classDef.addFunction(function); + return classDef; + } +} From 51432a46be7d6eb866da9fc652771d21b5805e03 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Tue, 30 Jun 2026 13:02:38 +0800 Subject: [PATCH 09/16] feat(backend): implement compiler-only storage init intrinsic and prepare-block scaffolding - Replace IllegalStateException with CallIntrinsicInsn for compiler-only local initialization in prepare block - Add intrinsic that emits gdcc_*_init(&var) with type validation for range iterator storage - Register intrinsic in manager so compiler-only init helpers are routed through intrinsic dispatch - Add tests covering init, assign, destruct and CALL_GLOBAL rejection for compiler-only types - Update phase-6 plan status and document completed scope --- .../frontend/frontend_gdcompiler_type_plan.md | 8 ++++ .../script/gdcc/backend/c/gen/CCodegen.java | 6 ++- .../gdcc/backend/c/gen/CIntrinsicManager.java | 3 ++ .../intrinsic/CRangeIterInitIntrinsic.java | 41 +++++++++++++++++++ .../backend/c/gen/CAssignInsnGenTest.java | 20 +++++++++ .../gdcc/backend/c/gen/CCodegenTest.java | 38 +++++++++++++++++ .../backend/c/gen/CDestructInsnGenTest.java | 23 +++++++++++ .../backend/c/gen/CallGlobalInsnGenTest.java | 21 ++++++++++ 8 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 src/main/java/gd/script/gdcc/backend/c/gen/intrinsic/CRangeIterInitIntrinsic.java diff --git a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md index e832e679..f977b40d 100644 --- a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md +++ b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md @@ -434,6 +434,14 @@ MVP 采用以下策略: ### 5.6 阶段六:C 后端类型渲染、初始化、赋值与销毁 +状态:已完成(2026-06-30,脚手架版;不包含具体 compiler-only runtime helper 实现)。 + +产出: + +- `CCodegen.generateFunctionPrepareBlock(...)` 对 compiler-only local 生成 `CALL_INTRINSIC` 初始化指令,禁止走 `CALL_GLOBAL`。 +- 新增 compiler-only range iterator init intrinsic 脚手架,只负责发出 `gdcc_*_init` 调用形状与窄类型校验,不实现具体运行时语义。 +- 赋值、显式销毁与 generated C shape 测试覆盖 direct struct assignment、`gdcc_*_destroy`、`gdcc_*_init` 以及 `godot_*` default helper negative path。 + 目标: - compiler-only storage 在 C 中使用显式 `gdcc_*` helper 和 C type。 diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/CCodegen.java b/src/main/java/gd/script/gdcc/backend/c/gen/CCodegen.java index 273b180d..dcc85b88 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/CCodegen.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/CCodegen.java @@ -215,8 +215,10 @@ private void generateFunctionPrepareBlock() { continue; } var initInsn = switch (variable.type()) { - case GdCompilerType _ -> throw new IllegalStateException( - "compiler-only type requires dedicated prepare initialization: " + variable.type().getTypeName() + case GdCompilerType compilerType -> new CallIntrinsicInsn( + variable.id(), + compilerType.getCInitHelperName(), + List.of() ); case GdObjectType _ -> new LiteralNullInsn(variable.id()); case GdVariantType _, GdNilType _ -> new LiteralNilInsn(variable.id()); diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/CIntrinsicManager.java b/src/main/java/gd/script/gdcc/backend/c/gen/CIntrinsicManager.java index b9b59a93..2d745378 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/CIntrinsicManager.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/CIntrinsicManager.java @@ -1,6 +1,7 @@ package gd.script.gdcc.backend.c.gen; import gd.script.gdcc.backend.c.gen.intrinsic.CIntToFloatIntrinsic; +import gd.script.gdcc.backend.c.gen.intrinsic.CRangeIterInitIntrinsic; import gd.script.gdcc.backend.c.gen.intrinsic.CVectorIToVectorIntrinsic; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -16,11 +17,13 @@ public final class CIntrinsicManager { public CIntrinsicManager() { var intToFloat = new CIntToFloatIntrinsic(); + var rangeIterInit = new CRangeIterInitIntrinsic(); var vector2iToVector2 = CVectorIToVectorIntrinsic.vector2(); var vector3iToVector3 = CVectorIToVectorIntrinsic.vector3(); var vector4iToVector4 = CVectorIToVectorIntrinsic.vector4(); this.functions = Map.of( intToFloat.name(), intToFloat, + rangeIterInit.name(), rangeIterInit, vector2iToVector2.name(), vector2iToVector2, vector3iToVector3.name(), vector3iToVector3, vector4iToVector4.name(), vector4iToVector4 diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/intrinsic/CRangeIterInitIntrinsic.java b/src/main/java/gd/script/gdcc/backend/c/gen/intrinsic/CRangeIterInitIntrinsic.java new file mode 100644 index 00000000..f7436111 --- /dev/null +++ b/src/main/java/gd/script/gdcc/backend/c/gen/intrinsic/CRangeIterInitIntrinsic.java @@ -0,0 +1,41 @@ +package gd.script.gdcc.backend.c.gen.intrinsic; + +import gd.script.gdcc.backend.c.gen.CBodyBuilder; +import gd.script.gdcc.backend.c.gen.CIntrinsicFunction; +import gd.script.gdcc.lir.LirVariable; +import gd.script.gdcc.type.GdccForRangeIterType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/// Skeleton intrinsic for compiler-only range iterator storage initialization. +/// +/// The phase-6 scaffold only wires the intrinsic shape into backend codegen and tests. +/// It intentionally avoids implementing any concrete runtime iterator semantics. +public final class CRangeIterInitIntrinsic implements CIntrinsicFunction { + public static final @NotNull String NAME = GdccForRangeIterType.C_INIT_HELPER_NAME; + + @Override + public @NotNull String name() { + return NAME; + } + + @Override + public void generateCCode(@NotNull CBodyBuilder bodyBuilder, + @Nullable LirVariable resultVar, + @NotNull List argVars) { + if (resultVar != null) { + throw bodyBuilder.invalidInsn("'" + NAME + "' does not produce a result variable"); + } + if (argVars.size() != 1) { + throw bodyBuilder.invalidInsn("'" + NAME + "' requires exactly one argument, got " + argVars.size()); + } + var targetVar = argVars.getFirst(); + if (!(targetVar.type() instanceof GdccForRangeIterType)) { + throw bodyBuilder.invalidInsn("'" + NAME + "' argument variable '" + targetVar.id() + + "' must be compiler-only range iterator storage, got '" + targetVar.type().getTypeName() + "'"); + } + bodyBuilder.callVoid(NAME, List.of(bodyBuilder.valueOfVar(targetVar))); + } +} diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/CAssignInsnGenTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CAssignInsnGenTest.java index 2ed74bab..4840bd64 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/CAssignInsnGenTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/CAssignInsnGenTest.java @@ -18,6 +18,7 @@ import gd.script.gdcc.type.GdFloatType; import gd.script.gdcc.type.GdIntType; import gd.script.gdcc.type.GdObjectType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdStringType; import gd.script.gdcc.type.GdStringNameType; import gd.script.gdcc.type.GdVariantType; @@ -74,6 +75,25 @@ void assignStringShouldCopyAndDestroyOldValue() { assertFalse(body.contains("__gdcc_tmp_string_"), body); } + @Test + @DisplayName("assign compiler-only value should use direct struct assignment without copy helper") + void assignCompilerOnlyShouldUseDirectAssignment() { + var workerClass = new LirClassDef("Worker", "RefCounted", false, false, Map.of(), List.of(), List.of(), List.of()); + var func = new LirFunctionDef("assign_compiler_only"); + func.setReturnType(GdVoidType.VOID); + func.createAndAddVariable("dst", GdccForRangeIterType.FOR_RANGE_ITER); + func.createAndAddVariable("src", GdccForRangeIterType.FOR_RANGE_ITER); + addEntryAssignAndReturn(func, new AssignInsn("dst", "src")); + workerClass.addFunction(func); + + var module = new LirModule("test_module", List.of(workerClass)); + var codegen = newCodegen(module, emptyApi(), List.of(workerClass)); + + var body = codegen.generateFuncBody(workerClass, func); + assertTrue(body.contains("$dst = $src;"), body); + assertFalse(body.contains("godot_new_GdccForRangeIter_with_GdccForRangeIter"), body); + } + @Test @DisplayName("assign self String should stage a stable carrier before destroy and consume it into the slot") void assignSelfStringShouldUseStableCarrier() { diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/CCodegenTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CCodegenTest.java index d7f48c24..6ffb1498 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/CCodegenTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/CCodegenTest.java @@ -279,6 +279,44 @@ public void generateShouldRejectCompilerOnlyPropertyBeforeDefaultInitSynthesis() assertFalse(exception.getMessage().contains("outward GDExtension metadata"), exception.getMessage()); } + @Test + public void generateShouldEmitCompilerOnlyPrepareInitCallForLocalVariables() { + var workerClass = new LirClassDef("Worker", "RefCounted"); + var func = new LirFunctionDef("prepare_compiler_only_local"); + func.setReturnType(GdVoidType.VOID); + func.createAndAddVariable("iter", GdccForRangeIterType.FOR_RANGE_ITER); + var entryBlock = new LirBasicBlock("entry"); + entryBlock.appendInstruction(new ReturnInsn(null)); + func.addBasicBlock(entryBlock); + func.setEntryBlockId("entry"); + workerClass.addFunction(func); + + var module = new LirModule("compiler_only_prepare", List.of(workerClass)); + var classRegistry = new ClassRegistry(new ExtensionAPI( + null, + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of() + )); + ProjectInfo projectInfo = new ProjectInfo("test", GodotVersion.V451, Path.of(".")) { + }; + var ctx = new CodegenContext(projectInfo, classRegistry); + var codegen = new CCodegen(); + codegen.prepare(ctx, module); + + var files = codegen.generate(); + var cCode = generatedFileText(files, "entry.c"); + + assertTrue(cCode.contains("gdcc_for_range_iter $iter;"), cCode); + assertTrue(cCode.contains("gdcc_for_range_iter_init(&$iter);"), cCode); + assertFalse(cCode.contains("godot_new_GdccForRangeIter"), cCode); + } + @Test public void entryTemplateShouldSetMinimumInitializationLevelAndGuardLifecycleLevels() throws Exception { var workerClass = new LirClassDef("GDEntryWorker", "RefCounted"); diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/CDestructInsnGenTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CDestructInsnGenTest.java index 0d38b661..a6465798 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/CDestructInsnGenTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/CDestructInsnGenTest.java @@ -15,6 +15,7 @@ import gd.script.gdcc.lir.insn.ReturnInsn; import gd.script.gdcc.scope.ClassRegistry; import gd.script.gdcc.type.GdObjectType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdStringType; import gd.script.gdcc.type.GdVoidType; import org.junit.jupiter.api.DisplayName; @@ -51,6 +52,28 @@ void destructStringShouldCallDestroyFunction() { assertTrue(body.contains("godot_String_destroy(&$s);")); } + @Test + @DisplayName("destruct compiler-only value should call gdcc destroy helper") + void destructCompilerOnlyShouldCallDestroyHelper() { + var workerClass = new LirClassDef("Worker", "RefCounted", false, false, Map.of(), List.of(), List.of(), List.of()); + var func = new LirFunctionDef("destruct_compiler_only"); + func.setReturnType(GdVoidType.VOID); + func.createAndAddVariable("iter", GdccForRangeIterType.FOR_RANGE_ITER); + + var entry = new LirBasicBlock("entry"); + entry.appendInstruction(new DestructInsn("iter", LifecycleProvenance.USER_EXPLICIT)); + func.addBasicBlock(entry); + func.setEntryBlockId("entry"); + workerClass.addFunction(func); + + var module = new LirModule("test_module", List.of(workerClass)); + var codegen = newCodegen(module, List.of(workerClass), emptyApi()); + + var body = codegen.generateFuncBody(workerClass, func); + assertTrue(body.contains("gdcc_for_range_iter_destroy(&$iter);"), body); + assertFalse(body.contains("godot_GdccForRangeIter_destroy"), body); + } + @Test @DisplayName("destruct refcounted GDCC object should use release_object") void destructRefCountedGdccObjectShouldUseReleaseObject() { diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/CallGlobalInsnGenTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CallGlobalInsnGenTest.java index 530e78dd..8fd34453 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/CallGlobalInsnGenTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/CallGlobalInsnGenTest.java @@ -17,6 +17,7 @@ import gd.script.gdcc.scope.ClassRegistry; import gd.script.gdcc.type.GdBoolType; import gd.script.gdcc.type.GdFloatType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdStringType; import gd.script.gdcc.type.GdVariantType; import gd.script.gdcc.type.GdVoidType; @@ -263,6 +264,26 @@ void callGlobalUnknownUtility() { assertTrue(ex.getMessage().contains("not found in registry")); } + @Test + @DisplayName("CALL_GLOBAL should reject compiler-only init helper because it is intrinsic-only") + void callGlobalShouldRejectCompilerOnlyInitHelper() { + var clazz = newTestClass(); + var func = newFunction("call_compiler_only_init_global"); + func.createAndAddVariable("iter", GdccForRangeIterType.FOR_RANGE_ITER); + + entry(func).appendInstruction(new CallGlobalInsn( + null, + GdccForRangeIterType.C_INIT_HELPER_NAME, + List.of(new LirInstruction.VariableOperand("iter")) + )); + clazz.addFunction(func); + + var ex = assertThrows(InvalidInsnException.class, () -> generateBody(clazz, func, utilityApi())); + assertInstanceOf(InvalidInsnException.class, ex); + assertTrue(ex.getMessage().contains("not found in registry"), ex.getMessage()); + assertTrue(ex.getMessage().contains(GdccForRangeIterType.C_INIT_HELPER_NAME), ex.getMessage()); + } + @Test @DisplayName("CALL_GLOBAL should reject resultId for void utility") void callGlobalVoidUtilityWithResultId() { From f2909f72ef678bfb26a2bfb9d9a5a334185fa0f5 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Tue, 30 Jun 2026 15:39:25 +0800 Subject: [PATCH 10/16] feat(backend): implement compiler-only for-range-iterator intrinsics and refactor prepare-block init - add gdcc.for_range_iter.init/should_continue/next/get with C runtime helpers and contract validation - rework default storage init to callAssign-based no-arg intrinsic with result-type enforcement - register all range-iterator intrinsics in manager and reject compiler-only type on non-range calls - document intrinsic contracts in LIR spec and mark phase-7 closure in implementation plan - add tests covering parser/serializer roundtrip, registry dispatch, codegen output and boundary rejection --- doc/gdcc_lir_intrinsic.md | 135 ++++++++ .../frontend/frontend_gdcompiler_type_plan.md | 17 +- .../codegen/include_451/gdcc/gdcc_intrinsic.h | 45 +++ .../gdcc/backend/c/gen/CIntrinsicManager.java | 13 +- .../gen/intrinsic/CForRangeIterIntrinsic.java | 136 ++++++++ .../CForRangeIterRawInitIntrinsic.java | 43 +++ .../intrinsic/CRangeIterInitIntrinsic.java | 41 --- .../gdcc/backend/c/gen/CCodegenTest.java | 2 +- .../gdcc/backend/c/gen/CGenHelperTest.java | 7 + .../backend/c/gen/CIntrinsicManagerTest.java | 9 + .../c/gen/CallIntrinsicInsnGenTest.java | 38 +++ .../c/gen/GdccForRangeIterIntrinsicTest.java | 297 ++++++++++++++++++ .../parser/SimpleLirBlockInsnParserTest.java | 33 ++ .../SimpleLirBlockInsnSerializerTest.java | 17 + 14 files changed, 788 insertions(+), 45 deletions(-) create mode 100644 src/main/java/gd/script/gdcc/backend/c/gen/intrinsic/CForRangeIterIntrinsic.java create mode 100644 src/main/java/gd/script/gdcc/backend/c/gen/intrinsic/CForRangeIterRawInitIntrinsic.java delete mode 100644 src/main/java/gd/script/gdcc/backend/c/gen/intrinsic/CRangeIterInitIntrinsic.java create mode 100644 src/test/java/gd/script/gdcc/backend/c/gen/GdccForRangeIterIntrinsicTest.java diff --git a/doc/gdcc_lir_intrinsic.md b/doc/gdcc_lir_intrinsic.md index e36edfd1..53aa379c 100644 --- a/doc/gdcc_lir_intrinsic.md +++ b/doc/gdcc_lir_intrinsic.md @@ -170,6 +170,141 @@ $target = godot_new_Vector4_with_Vector4i(&$source); - `doc/module_impl/backend/implicit_conversion_implementation.md` +### `gdcc.for_range_iter.init` + +状态:Implemented + +形态: + +```text +$ = call_intrinsic "gdcc.for_range_iter.init" $ $ $; +``` + +合同: + +- result 必须存在。 +- result 必须是非 `ref` 的 `compiler::GdccForRangeIter` slot。 +- exactly three arguments。 +- arguments 必须依次是 `int`、`int`、`int` slot,分别表示已归一化的 `start`、`end`、`step`。 + +C backend 语义: + +```c +$target = gdcc_for_range_iter_from_bounds($start, $end, $step); +``` + +`step == 0` 策略: + +- `gdcc_for_range_iter_from_bounds(...)` 显式调用 runtime error helper(`godot_print_error(...)`)并返回不可无限循环的 fallback state。 +- 前端未来 lowering 仍应避免生成零步长;手写 LIR 不会静默获得无限循环语义。 + +Lifecycle / ownership: + +- `compiler::GdccForRangeIter` 是 destroyable non-object value。 +- helper 返回按值 iterator state;slot 写入仍通过 `CBodyBuilder.callAssign(...)` 处理旧值 destroy 与 direct struct assignment。 +- 不产生 Godot object ownership,也不参与 Variant pack / unpack。 + +长期事实源: + +- `doc/module_impl/frontend/frontend_gdcompiler_type_plan.md` + +### `gdcc.for_range_iter.should_continue` + +状态:Implemented + +形态: + +```text +$ = call_intrinsic "gdcc.for_range_iter.should_continue" $; +``` + +合同: + +- result 必须存在。 +- result 必须是非 `ref` 的 `bool` slot。 +- exactly one argument。 +- argument 必须是 `compiler::GdccForRangeIter` slot。 + +C backend 语义: + +```c +$target = gdcc_for_range_iter_should_continue(&$iter); +``` + +Lifecycle / ownership: + +- 只读 iterator state,不销毁或转移 iterator ownership。 +- 正步长使用 `current < end`,负步长使用 `current > end`,end 为排他边界。 + +长期事实源: + +- `doc/module_impl/frontend/frontend_gdcompiler_type_plan.md` + +### `gdcc.for_range_iter.next` + +状态:Implemented + +形态: + +```text +$ = call_intrinsic "gdcc.for_range_iter.next" $; +``` + +合同: + +- result 必须存在。 +- result 必须是非 `ref` 的 `compiler::GdccForRangeIter` slot。 +- exactly one argument。 +- argument 必须是 `compiler::GdccForRangeIter` slot。 + +C backend 语义: + +```c +$target = gdcc_for_range_iter_next(&$iter); +``` + +Lifecycle / ownership: + +- 输入 iterator 只读。 +- helper 返回新的按值 iterator state,语义为 `current + step`,保留原 `end` 与 `step`。 +- slot 写入仍通过 `CBodyBuilder.callAssign(...)` 处理旧值 destroy 与 direct struct assignment。 + +长期事实源: + +- `doc/module_impl/frontend/frontend_gdcompiler_type_plan.md` + +### `gdcc.for_range_iter.get` + +状态:Implemented + +形态: + +```text +$ = call_intrinsic "gdcc.for_range_iter.get" $; +``` + +合同: + +- result 必须存在。 +- result 必须是非 `ref` 的 `int` slot。 +- exactly one argument。 +- argument 必须是 `compiler::GdccForRangeIter` slot。 + +C backend 语义: + +```c +$target = gdcc_for_range_iter_get(&$iter); +``` + +Lifecycle / ownership: + +- 只读 iterator state,不销毁或转移 iterator ownership。 +- 返回当前迭代值,不推进 state。 + +长期事实源: + +- `doc/module_impl/frontend/frontend_gdcompiler_type_plan.md` + ## 新增 Intrinsic Checklist 新增 intrinsic 时按以下顺序维护: diff --git a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md index f977b40d..4b89ff4f 100644 --- a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md +++ b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md @@ -34,7 +34,7 @@ ## 0. 维护合同 -- 本文档是 `GdCompilerType` 实施顺序、禁止边界和验收细则的计划事实源。 +- 本文档是 `GdccCompilerType` 实施顺序、禁止边界和验收细则的计划事实源。 - `frontend_implicit_conversion_matrix.md` 仍是 ordinary typed-boundary compatibility 的唯一真源;本文档不得维护第二份 source/target conversion 矩阵。 - `gdcc_lir_intrinsic.md` 仍是 `call_intrinsic` surface、backend registry 和 intrinsic catalog 的事实源;本文档只规定 @@ -43,6 +43,7 @@ - 实现时不得用 `Variant`、`DYNAMIC`、`TYPE_META` 或 Godot object metadata 伪装 compiler-only storage。 - 若实现过程中发现某个计划项需要改变 source-facing typing、ordinary boundary、public ABI 或 runtime helper 命名,必须先更新对应事实源文档,再修改代码与测试。 +- 参考`gdcompiler_type_design_risk_analysis.md`中的内容进行实施,`GdccCompilerType`是值语义类型,使用复制语义传参,必须使用intrinsic进行操作。 --- @@ -489,6 +490,20 @@ MVP 采用以下策略: ### 5.7 阶段七:`GdccForRangeIterType` intrinsic 最小闭环 +状态:已完成(2026-06-30)。 + +产出: + +- 新增 `gdcc.for_range_iter.init`、`gdcc.for_range_iter.should_continue`、`gdcc.for_range_iter.next`、`gdcc.for_range_iter.get` + 四个 backend-owned intrinsic,并在 `CIntrinsicManager` 注册白名单。 +- `init` 接受已归一化的 `start/end/step` 三个 `int` slot,返回非 ref `compiler::GdccForRangeIter`;`should_continue` 返回 `bool`; + `next` 返回推进后的新 iterator state;`get` 返回当前 `int` 值。 +- Runtime helper 使用 `gdcc_for_range_iter_from_bounds` / `gdcc_for_range_iter_should_continue` / + `gdcc_for_range_iter_next` / `gdcc_for_range_iter_get`,保留 `gdcc_for_range_iter_init()` 作为 prepare-block 默认初始化 helper。 +- `step == 0` 采用明确 runtime error helper 策略:`gdcc_for_range_iter_from_bounds(...)` 调用 `godot_print_error(...)` 后返回不可无限循环的 fallback state。 +- 测试覆盖 parser / serializer 文本保留、registry dispatch、完整 `CallIntrinsicInsnGen` codegen、正负合同错误、literal operand 拒绝、零步长策略和 + `GdccForRangeIterType` 不能传给非 range intrinsic。 + 目标: - compiler-only type 只通过 backend-owned intrinsic 操作。 diff --git a/src/main/c/codegen/include_451/gdcc/gdcc_intrinsic.h b/src/main/c/codegen/include_451/gdcc/gdcc_intrinsic.h index 123a7831..681d0615 100644 --- a/src/main/c/codegen/include_451/gdcc/gdcc_intrinsic.h +++ b/src/main/c/codegen/include_451/gdcc/gdcc_intrinsic.h @@ -3,6 +3,51 @@ #include +typedef struct gdcc_for_range_iter { + godot_int current; + godot_int end; + godot_int step; +} gdcc_for_range_iter; + +static inline gdcc_for_range_iter gdcc_for_range_iter_init(void) { + return (gdcc_for_range_iter){ .current = 0, .end = 0, .step = 1 }; +} + +static inline void gdcc_for_range_iter_destroy(gdcc_for_range_iter *iter) { + (void)iter; +} + +static inline gdcc_for_range_iter gdcc_for_range_iter_from_bounds( + godot_int start, + godot_int end, + godot_int step +) { + if (step == 0) { + godot_print_error("range step argument is zero", "gdcc_for_range_iter_from_bounds", "", 0, true); + return (gdcc_for_range_iter){ .current = start, .end = end, .step = 1 }; + } + return (gdcc_for_range_iter){ .current = start, .end = end, .step = step }; +} + +static inline godot_bool gdcc_for_range_iter_should_continue(const gdcc_for_range_iter *iter) { + if (iter->step > 0) { + return iter->current < iter->end; + } + return iter->current > iter->end; +} + +static inline gdcc_for_range_iter gdcc_for_range_iter_next(const gdcc_for_range_iter *iter) { + return (gdcc_for_range_iter){ + .current = iter->current + iter->step, + .end = iter->end, + .step = iter->step, + }; +} + +static inline godot_int gdcc_for_range_iter_get(const gdcc_for_range_iter *iter) { + return iter->current; +} + /// Wrapper-only inbound constructors for call_func arguments whose accepted Variant payload /// runtime type differs from the published method metadata. /// The generated wrapper must run its runtime type gate first; these helpers materialize the diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/CIntrinsicManager.java b/src/main/java/gd/script/gdcc/backend/c/gen/CIntrinsicManager.java index 2d745378..5a81b1ff 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/CIntrinsicManager.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/CIntrinsicManager.java @@ -1,7 +1,8 @@ package gd.script.gdcc.backend.c.gen; import gd.script.gdcc.backend.c.gen.intrinsic.CIntToFloatIntrinsic; -import gd.script.gdcc.backend.c.gen.intrinsic.CRangeIterInitIntrinsic; +import gd.script.gdcc.backend.c.gen.intrinsic.CForRangeIterIntrinsic; +import gd.script.gdcc.backend.c.gen.intrinsic.CForRangeIterRawInitIntrinsic; import gd.script.gdcc.backend.c.gen.intrinsic.CVectorIToVectorIntrinsic; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -17,13 +18,21 @@ public final class CIntrinsicManager { public CIntrinsicManager() { var intToFloat = new CIntToFloatIntrinsic(); - var rangeIterInit = new CRangeIterInitIntrinsic(); + var rangeIterInit = new CForRangeIterRawInitIntrinsic(); + var forRangeIterInit = CForRangeIterIntrinsic.init(); + var forRangeIterShouldContinue = CForRangeIterIntrinsic.shouldContinue(); + var forRangeIterNext = CForRangeIterIntrinsic.next(); + var forRangeIterGet = CForRangeIterIntrinsic.get(); var vector2iToVector2 = CVectorIToVectorIntrinsic.vector2(); var vector3iToVector3 = CVectorIToVectorIntrinsic.vector3(); var vector4iToVector4 = CVectorIToVectorIntrinsic.vector4(); this.functions = Map.of( intToFloat.name(), intToFloat, rangeIterInit.name(), rangeIterInit, + forRangeIterInit.name(), forRangeIterInit, + forRangeIterShouldContinue.name(), forRangeIterShouldContinue, + forRangeIterNext.name(), forRangeIterNext, + forRangeIterGet.name(), forRangeIterGet, vector2iToVector2.name(), vector2iToVector2, vector3iToVector3.name(), vector3iToVector3, vector4iToVector4.name(), vector4iToVector4 diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/intrinsic/CForRangeIterIntrinsic.java b/src/main/java/gd/script/gdcc/backend/c/gen/intrinsic/CForRangeIterIntrinsic.java new file mode 100644 index 00000000..fa46f312 --- /dev/null +++ b/src/main/java/gd/script/gdcc/backend/c/gen/intrinsic/CForRangeIterIntrinsic.java @@ -0,0 +1,136 @@ +package gd.script.gdcc.backend.c.gen.intrinsic; + +import gd.script.gdcc.backend.c.gen.CBodyBuilder; +import gd.script.gdcc.backend.c.gen.CIntrinsicFunction; +import gd.script.gdcc.lir.LirVariable; +import gd.script.gdcc.type.GdBoolType; +import gd.script.gdcc.type.GdIntType; +import gd.script.gdcc.type.GdType; +import gd.script.gdcc.type.GdccForRangeIterType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/// Intrinsics for immutable `for range(...)` iterator state operations. +/// +/// The LIR names stay backend-owned (`gdcc.for_range_iter.*`) while the C helper symbols use +/// `gdcc_*` names. That keeps handwritten LIR from escaping into arbitrary C calls and avoids +/// colliding with the prepare-block storage initializer `gdcc_for_range_iter_init`. +public final class CForRangeIterIntrinsic implements CIntrinsicFunction { + public static final @NotNull String INIT_NAME = "gdcc.for_range_iter.init"; + public static final @NotNull String SHOULD_CONTINUE_NAME = "gdcc.for_range_iter.should_continue"; + public static final @NotNull String NEXT_NAME = "gdcc.for_range_iter.next"; + public static final @NotNull String GET_NAME = "gdcc.for_range_iter.get"; + + static final @NotNull String INIT_HELPER_NAME = "gdcc_for_range_iter_from_bounds"; + static final @NotNull String SHOULD_CONTINUE_HELPER_NAME = "gdcc_for_range_iter_should_continue"; + static final @NotNull String NEXT_HELPER_NAME = "gdcc_for_range_iter_next"; + static final @NotNull String GET_HELPER_NAME = "gdcc_for_range_iter_get"; + + private final @NotNull Spec spec; + + private CForRangeIterIntrinsic(@NotNull Spec spec) { + this.spec = spec; + } + + public static @NotNull CForRangeIterIntrinsic init() { + return new CForRangeIterIntrinsic(new Spec( + INIT_NAME, + INIT_HELPER_NAME, + GdccForRangeIterType.FOR_RANGE_ITER, + List.of(GdIntType.INT, GdIntType.INT, GdIntType.INT) + )); + } + + public static @NotNull CForRangeIterIntrinsic shouldContinue() { + return new CForRangeIterIntrinsic(new Spec( + SHOULD_CONTINUE_NAME, + SHOULD_CONTINUE_HELPER_NAME, + GdBoolType.BOOL, + List.of(GdccForRangeIterType.FOR_RANGE_ITER) + )); + } + + public static @NotNull CForRangeIterIntrinsic next() { + return new CForRangeIterIntrinsic(new Spec( + NEXT_NAME, + NEXT_HELPER_NAME, + GdccForRangeIterType.FOR_RANGE_ITER, + List.of(GdccForRangeIterType.FOR_RANGE_ITER) + )); + } + + public static @NotNull CForRangeIterIntrinsic get() { + return new CForRangeIterIntrinsic(new Spec( + GET_NAME, + GET_HELPER_NAME, + GdIntType.INT, + List.of(GdccForRangeIterType.FOR_RANGE_ITER) + )); + } + + @Override + public @NotNull String name() { + return spec.name(); + } + + @Override + public void generateCCode(@NotNull CBodyBuilder bodyBuilder, + @Nullable LirVariable resultVar, + @NotNull List argVars) { + checkResult(bodyBuilder, resultVar); + checkArgs(bodyBuilder, argVars); + + var args = argVars.stream() + .map(bodyBuilder::valueOfVar) + .toList(); + bodyBuilder.callAssign( + bodyBuilder.targetOfVar(resultVar), + spec.helperName(), + spec.resultType(), + args + ); + } + + private void checkResult(@NotNull CBodyBuilder bodyBuilder, + @Nullable LirVariable resultVar) { + if (resultVar == null) { + throw bodyBuilder.invalidInsn("'" + name() + "' requires a result variable"); + } + if (resultVar.ref()) { + throw bodyBuilder.invalidInsn("'" + name() + "' result variable '" + resultVar.id() + "' cannot be a reference"); + } + checkType(bodyBuilder, "result", resultVar, spec.resultType()); + } + + private void checkArgs(@NotNull CBodyBuilder bodyBuilder, + @NotNull List argVars) { + if (argVars.size() != spec.argumentTypes().size()) { + throw bodyBuilder.invalidInsn("'" + name() + "' requires exactly " + spec.argumentTypes().size() + + " argument" + (spec.argumentTypes().size() == 1 ? "" : "s") + ", got " + argVars.size()); + } + for (var i = 0; i < argVars.size(); i++) { + checkType(bodyBuilder, "argument #" + (i + 1), argVars.get(i), spec.argumentTypes().get(i)); + } + } + + private void checkType(@NotNull CBodyBuilder bodyBuilder, + @NotNull String role, + @NotNull LirVariable variable, + @NotNull GdType expectedType) { + if (!variable.type().equals(expectedType)) { + throw bodyBuilder.invalidInsn("'" + name() + "' " + role + " variable '" + variable.id() + + "' must be " + expectedType.getTypeName() + ", got '" + variable.type().getTypeName() + "'"); + } + } + + private record Spec(@NotNull String name, + @NotNull String helperName, + @NotNull GdType resultType, + @NotNull List argumentTypes) { + private Spec { + argumentTypes = List.copyOf(argumentTypes); + } + } +} diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/intrinsic/CForRangeIterRawInitIntrinsic.java b/src/main/java/gd/script/gdcc/backend/c/gen/intrinsic/CForRangeIterRawInitIntrinsic.java new file mode 100644 index 00000000..8e3150ec --- /dev/null +++ b/src/main/java/gd/script/gdcc/backend/c/gen/intrinsic/CForRangeIterRawInitIntrinsic.java @@ -0,0 +1,43 @@ +package gd.script.gdcc.backend.c.gen.intrinsic; + +import gd.script.gdcc.backend.c.gen.CBodyBuilder; +import gd.script.gdcc.backend.c.gen.CIntrinsicFunction; +import gd.script.gdcc.lir.LirVariable; +import gd.script.gdcc.type.GdccForRangeIterType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/// Prepare-block intrinsic for default range iterator storage initialization. +/// +/// This initializes compiler-only storage before ordinary control flow starts. It is deliberately +/// separate from `gdcc.for_range_iter.init`, which builds a runtime range state from start/end/step. +public final class CForRangeIterRawInitIntrinsic implements CIntrinsicFunction { + public static final @NotNull String NAME = GdccForRangeIterType.C_INIT_HELPER_NAME; + + @Override + public @NotNull String name() { + return NAME; + } + + @Override + public void generateCCode(@NotNull CBodyBuilder bodyBuilder, + @Nullable LirVariable resultVar, + @NotNull List argVars) { + if (resultVar == null) { + throw bodyBuilder.invalidInsn("'" + NAME + "' requires a result variable"); + } + if (resultVar.ref()) { + throw bodyBuilder.invalidInsn("'" + NAME + "' result variable '" + resultVar.id() + "' cannot be a reference"); + } + if (!(resultVar.type() instanceof GdccForRangeIterType)) { + throw bodyBuilder.invalidInsn("'" + NAME + "' result variable '" + resultVar.id() + + "' must be compiler-only range iterator storage, got '" + resultVar.type().getTypeName() + "'"); + } + if (!argVars.isEmpty()) { + throw bodyBuilder.invalidInsn("'" + NAME + "' requires no arguments, got " + argVars.size()); + } + bodyBuilder.callAssign(bodyBuilder.targetOfVar(resultVar), NAME, GdccForRangeIterType.FOR_RANGE_ITER, List.of()); + } +} diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/intrinsic/CRangeIterInitIntrinsic.java b/src/main/java/gd/script/gdcc/backend/c/gen/intrinsic/CRangeIterInitIntrinsic.java deleted file mode 100644 index f7436111..00000000 --- a/src/main/java/gd/script/gdcc/backend/c/gen/intrinsic/CRangeIterInitIntrinsic.java +++ /dev/null @@ -1,41 +0,0 @@ -package gd.script.gdcc.backend.c.gen.intrinsic; - -import gd.script.gdcc.backend.c.gen.CBodyBuilder; -import gd.script.gdcc.backend.c.gen.CIntrinsicFunction; -import gd.script.gdcc.lir.LirVariable; -import gd.script.gdcc.type.GdccForRangeIterType; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.List; - -/// Skeleton intrinsic for compiler-only range iterator storage initialization. -/// -/// The phase-6 scaffold only wires the intrinsic shape into backend codegen and tests. -/// It intentionally avoids implementing any concrete runtime iterator semantics. -public final class CRangeIterInitIntrinsic implements CIntrinsicFunction { - public static final @NotNull String NAME = GdccForRangeIterType.C_INIT_HELPER_NAME; - - @Override - public @NotNull String name() { - return NAME; - } - - @Override - public void generateCCode(@NotNull CBodyBuilder bodyBuilder, - @Nullable LirVariable resultVar, - @NotNull List argVars) { - if (resultVar != null) { - throw bodyBuilder.invalidInsn("'" + NAME + "' does not produce a result variable"); - } - if (argVars.size() != 1) { - throw bodyBuilder.invalidInsn("'" + NAME + "' requires exactly one argument, got " + argVars.size()); - } - var targetVar = argVars.getFirst(); - if (!(targetVar.type() instanceof GdccForRangeIterType)) { - throw bodyBuilder.invalidInsn("'" + NAME + "' argument variable '" + targetVar.id() + - "' must be compiler-only range iterator storage, got '" + targetVar.type().getTypeName() + "'"); - } - bodyBuilder.callVoid(NAME, List.of(bodyBuilder.valueOfVar(targetVar))); - } -} diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/CCodegenTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CCodegenTest.java index 6ffb1498..d7ede7d4 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/CCodegenTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/CCodegenTest.java @@ -313,7 +313,7 @@ public void generateShouldEmitCompilerOnlyPrepareInitCallForLocalVariables() { var cCode = generatedFileText(files, "entry.c"); assertTrue(cCode.contains("gdcc_for_range_iter $iter;"), cCode); - assertTrue(cCode.contains("gdcc_for_range_iter_init(&$iter);"), cCode); + assertTrue(cCode.contains("$iter = gdcc_for_range_iter_init();"), cCode); assertFalse(cCode.contains("godot_new_GdccForRangeIter"), cCode); } diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/CGenHelperTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CGenHelperTest.java index c9389ad4..358751b3 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/CGenHelperTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/CGenHelperTest.java @@ -797,6 +797,13 @@ void callWrapperStringMaterializersShouldConvertCrossCasePayloadsAndDestroyInter assertTrue(source.contains("godot_StringName source = godot_new_StringName_with_Variant(value);"), source); assertTrue(source.contains("godot_String result = godot_new_String_with_StringName(&source);"), source); assertTrue(source.contains("godot_StringName_destroy(&source);"), source); + + assertTrue(source.contains("typedef struct gdcc_for_range_iter"), source); + assertTrue(source.contains("gdcc_for_range_iter_from_bounds"), source); + assertTrue(source.contains("gdcc_for_range_iter_should_continue"), source); + assertTrue(source.contains("gdcc_for_range_iter_next"), source); + assertTrue(source.contains("gdcc_for_range_iter_get"), source); + assertTrue(source.contains("godot_print_error(\"range step argument is zero\""), source); } @Test diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/CIntrinsicManagerTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CIntrinsicManagerTest.java index 1dc88742..37904f99 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/CIntrinsicManagerTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/CIntrinsicManagerTest.java @@ -2,7 +2,9 @@ import gd.script.gdcc.backend.CodegenContext; import gd.script.gdcc.backend.ProjectInfo; +import gd.script.gdcc.backend.c.gen.intrinsic.CForRangeIterIntrinsic; import gd.script.gdcc.backend.c.gen.intrinsic.CIntToFloatIntrinsic; +import gd.script.gdcc.backend.c.gen.intrinsic.CForRangeIterRawInitIntrinsic; import gd.script.gdcc.backend.c.gen.intrinsic.CVectorIToVectorIntrinsic; import gd.script.gdcc.enums.GodotVersion; import gd.script.gdcc.gdextension.ExtensionAPI; @@ -25,6 +27,11 @@ void managerShouldExposeOnlyRegisteredBackendIntrinsics() { var manager = new CIntrinsicManager(); assertInstanceOf(CIntToFloatIntrinsic.class, manager.find(CIntToFloatIntrinsic.NAME)); + assertInstanceOf(CForRangeIterRawInitIntrinsic.class, manager.find(CForRangeIterRawInitIntrinsic.NAME)); + assertInstanceOf(CForRangeIterIntrinsic.class, manager.find(CForRangeIterIntrinsic.INIT_NAME)); + assertInstanceOf(CForRangeIterIntrinsic.class, manager.find(CForRangeIterIntrinsic.SHOULD_CONTINUE_NAME)); + assertInstanceOf(CForRangeIterIntrinsic.class, manager.find(CForRangeIterIntrinsic.NEXT_NAME)); + assertInstanceOf(CForRangeIterIntrinsic.class, manager.find(CForRangeIterIntrinsic.GET_NAME)); assertInstanceOf(CVectorIToVectorIntrinsic.class, manager.find(CVectorIToVectorIntrinsic.VECTOR2I_TO_VECTOR2_NAME)); assertInstanceOf(CVectorIToVectorIntrinsic.class, @@ -48,6 +55,8 @@ void cGenHelperShouldExposeIntrinsicManager() { var helper = new CGenHelper(new CodegenContext(projectInfo, classRegistry), List.of(workerClass)); assertInstanceOf(CIntToFloatIntrinsic.class, helper.intrinsicManager().find(CIntToFloatIntrinsic.NAME)); + assertInstanceOf(CForRangeIterIntrinsic.class, + helper.intrinsicManager().find(CForRangeIterIntrinsic.SHOULD_CONTINUE_NAME)); assertInstanceOf(CVectorIToVectorIntrinsic.class, helper.intrinsicManager().find(CVectorIToVectorIntrinsic.VECTOR3I_TO_VECTOR3_NAME)); } diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/CallIntrinsicInsnGenTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CallIntrinsicInsnGenTest.java index a19a39df..2e56a0d1 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/CallIntrinsicInsnGenTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/CallIntrinsicInsnGenTest.java @@ -2,6 +2,7 @@ import gd.script.gdcc.backend.CodegenContext; import gd.script.gdcc.backend.ProjectInfo; +import gd.script.gdcc.backend.c.gen.intrinsic.CForRangeIterIntrinsic; import gd.script.gdcc.backend.c.gen.intrinsic.CIntToFloatIntrinsic; import gd.script.gdcc.backend.c.gen.intrinsic.CVectorIToVectorIntrinsic; import gd.script.gdcc.enums.GodotVersion; @@ -22,6 +23,7 @@ import gd.script.gdcc.type.GdIntType; import gd.script.gdcc.type.GdType; import gd.script.gdcc.type.GdVoidType; +import gd.script.gdcc.type.GdccForRangeIterType; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -73,6 +75,42 @@ void callIntrinsicShouldDispatchVectorWideningThroughCodegenRegistry() { assertTrue(body.contains("godot_new_Vector3_with_Vector3i"), body); } + @Test + @DisplayName("CALL_INTRINSIC should dispatch range iterator closed-loop helpers") + void callIntrinsicShouldDispatchRangeIteratorClosedLoopHelpers() { + var initBody = generateBody( + new CallIntrinsicInsn( + "iter", + CForRangeIterIntrinsic.INIT_NAME, + List.of( + new LirInstruction.VariableOperand("start"), + new LirInstruction.VariableOperand("end"), + new LirInstruction.VariableOperand("step") + ) + ), + List.of( + new VariableSpec("start", GdIntType.INT, false), + new VariableSpec("end", GdIntType.INT, false), + new VariableSpec("step", GdIntType.INT, false), + new VariableSpec("iter", GdccForRangeIterType.FOR_RANGE_ITER, false) + ) + ); + assertTrue(initBody.contains("$iter = gdcc_for_range_iter_from_bounds($start, $end, $step);"), initBody); + + var body = generateBody( + new CallIntrinsicInsn( + "cond", + CForRangeIterIntrinsic.SHOULD_CONTINUE_NAME, + List.of(new LirInstruction.VariableOperand("iter")) + ), + List.of( + new VariableSpec("iter", GdccForRangeIterType.FOR_RANGE_ITER, false), + new VariableSpec("cond", GdBoolType.BOOL, false) + ) + ); + assertTrue(body.contains("$cond = gdcc_for_range_iter_should_continue(&$iter);"), body); + } + @Test @DisplayName("CALL_INTRINSIC should reject unknown intrinsic names") void callIntrinsicShouldRejectUnknownIntrinsicNames() { diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/GdccForRangeIterIntrinsicTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/GdccForRangeIterIntrinsicTest.java new file mode 100644 index 00000000..8f0d537c --- /dev/null +++ b/src/test/java/gd/script/gdcc/backend/c/gen/GdccForRangeIterIntrinsicTest.java @@ -0,0 +1,297 @@ +package gd.script.gdcc.backend.c.gen; + +import gd.script.gdcc.backend.CodegenContext; +import gd.script.gdcc.backend.ProjectInfo; +import gd.script.gdcc.backend.c.gen.intrinsic.CForRangeIterIntrinsic; +import gd.script.gdcc.backend.c.gen.intrinsic.CIntToFloatIntrinsic; +import gd.script.gdcc.backend.c.gen.intrinsic.CForRangeIterRawInitIntrinsic; +import gd.script.gdcc.enums.GodotVersion; +import gd.script.gdcc.exception.InvalidInsnException; +import gd.script.gdcc.gdextension.ExtensionAPI; +import gd.script.gdcc.lir.LirBasicBlock; +import gd.script.gdcc.lir.LirClassDef; +import gd.script.gdcc.lir.LirFunctionDef; +import gd.script.gdcc.lir.LirInstruction; +import gd.script.gdcc.lir.LirVariable; +import gd.script.gdcc.lir.insn.CallIntrinsicInsn; +import gd.script.gdcc.scope.ClassRegistry; +import gd.script.gdcc.type.GdBoolType; +import gd.script.gdcc.type.GdFloatType; +import gd.script.gdcc.type.GdIntType; +import gd.script.gdcc.type.GdType; +import gd.script.gdcc.type.GdccForRangeIterType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class GdccForRangeIterIntrinsicTest { + @Test + @DisplayName("prepare init should produce default compiler-only iterator storage") + void prepareInitShouldProduceDefaultCompilerOnlyIteratorStorage() { + var fixture = new Fixture(List.of( + new VariableSpec("iter", GdccForRangeIterType.FOR_RANGE_ITER, false) + ), CForRangeIterRawInitIntrinsic.NAME, "__prepare__"); + var intrinsic = new CForRangeIterRawInitIntrinsic(); + + intrinsic.generateCCode(fixture.builder(), fixture.variable("iter"), List.of()); + + assertEquals("$iter = gdcc_for_range_iter_init();\n", fixture.builder().build()); + } + + @Test + @DisplayName("range init should emit normalized bounds helper through slot write path") + void rangeInitShouldEmitNormalizedBoundsHelperThroughSlotWritePath() { + var fixture = new Fixture(List.of( + new VariableSpec("start", GdIntType.INT, false), + new VariableSpec("end", GdIntType.INT, false), + new VariableSpec("step", GdIntType.INT, false), + new VariableSpec("iter", GdccForRangeIterType.FOR_RANGE_ITER, false) + ), CForRangeIterIntrinsic.INIT_NAME); + + CForRangeIterIntrinsic.init().generateCCode( + fixture.builder(), + fixture.variable("iter"), + List.of(fixture.variable("start"), fixture.variable("end"), fixture.variable("step")) + ); + + assertEquals(""" + gdcc_for_range_iter_destroy(&$iter); + $iter = gdcc_for_range_iter_from_bounds($start, $end, $step); + """, fixture.builder().build()); + } + + @Test + @DisplayName("range iteration intrinsics should emit continue next and get helpers") + void rangeIterationIntrinsicsShouldEmitContinueNextAndGetHelpers() { + var fixture = new Fixture(List.of( + new VariableSpec("iter", GdccForRangeIterType.FOR_RANGE_ITER, false), + new VariableSpec("next_iter", GdccForRangeIterType.FOR_RANGE_ITER, false), + new VariableSpec("cond", GdBoolType.BOOL, false), + new VariableSpec("value", GdIntType.INT, false) + ), CForRangeIterIntrinsic.SHOULD_CONTINUE_NAME); + + CForRangeIterIntrinsic.shouldContinue().generateCCode( + fixture.builder(), fixture.variable("cond"), List.of(fixture.variable("iter"))); + CForRangeIterIntrinsic.get().generateCCode( + fixture.builder(), fixture.variable("value"), List.of(fixture.variable("iter"))); + CForRangeIterIntrinsic.next().generateCCode( + fixture.builder(), fixture.variable("next_iter"), List.of(fixture.variable("iter"))); + + assertEquals(""" + $cond = gdcc_for_range_iter_should_continue(&$iter); + $value = gdcc_for_range_iter_get(&$iter); + gdcc_for_range_iter_destroy(&$next_iter); + $next_iter = gdcc_for_range_iter_next(&$iter); + """, fixture.builder().build()); + } + + @Test + @DisplayName("range iterator runtime helpers should anchor exclusive end negative step and zero step policy") + void runtimeHelpersShouldAnchorRangeIteratorSemantics() throws IOException { + var source = Files.readString(Path.of("src/main/c/codegen/include_451/gdcc/gdcc_intrinsic.h")); + + assertTrue(source.contains("typedef struct gdcc_for_range_iter"), source); + assertTrue(source.contains("godot_print_error(\"range step argument is zero\""), source); + assertTrue(source.contains("if (iter->step > 0)"), source); + assertTrue(source.contains("return iter->current < iter->end;"), source); + assertTrue(source.contains("return iter->current > iter->end;"), source); + assertTrue(source.contains(".current = iter->current + iter->step"), source); + assertTrue(source.contains("return iter->current;"), source); + } + + @Test + @DisplayName("range intrinsics should reject missing ref or wrong result slots") + void rangeIntrinsicsShouldRejectBadResultSlots() { + assertInvalidSignature( + CForRangeIterIntrinsic.init(), + null, + List.of( + new VariableSpec("start", GdIntType.INT, false), + new VariableSpec("end", GdIntType.INT, false), + new VariableSpec("step", GdIntType.INT, false) + ), + List.of("start", "end", "step"), + "requires a result variable" + ); + assertInvalidSignature( + CForRangeIterIntrinsic.init(), + "iter", + List.of( + new VariableSpec("start", GdIntType.INT, false), + new VariableSpec("end", GdIntType.INT, false), + new VariableSpec("step", GdIntType.INT, false), + new VariableSpec("iter", GdccForRangeIterType.FOR_RANGE_ITER, true) + ), + List.of("start", "end", "step"), + "cannot be a reference" + ); + assertInvalidSignature( + CForRangeIterIntrinsic.shouldContinue(), + "cond", + List.of( + new VariableSpec("iter", GdccForRangeIterType.FOR_RANGE_ITER, false), + new VariableSpec("cond", GdIntType.INT, false) + ), + List.of("iter"), + "must be bool" + ); + } + + @Test + @DisplayName("range intrinsics should reject wrong arity and argument types") + void rangeIntrinsicsShouldRejectWrongArityAndArgumentTypes() { + assertInvalidSignature( + CForRangeIterIntrinsic.init(), + "iter", + List.of(new VariableSpec("iter", GdccForRangeIterType.FOR_RANGE_ITER, false)), + List.of(), + "requires exactly 3 arguments" + ); + assertInvalidSignature( + CForRangeIterIntrinsic.init(), + "iter", + List.of( + new VariableSpec("start", GdBoolType.BOOL, false), + new VariableSpec("end", GdIntType.INT, false), + new VariableSpec("step", GdIntType.INT, false), + new VariableSpec("iter", GdccForRangeIterType.FOR_RANGE_ITER, false) + ), + List.of("start", "end", "step"), + "argument #1 variable 'start' must be int" + ); + assertInvalidSignature( + CForRangeIterIntrinsic.get(), + "value", + List.of( + new VariableSpec("not_iter", GdIntType.INT, false), + new VariableSpec("value", GdIntType.INT, false) + ), + List.of("not_iter"), + "argument #1 variable 'not_iter' must be GdccForRangeIter" + ); + } + + @Test + @DisplayName("non range intrinsics should reject compiler-only iterator operands") + void nonRangeIntrinsicsShouldRejectCompilerOnlyIteratorOperands() { + var fixture = new Fixture(List.of( + new VariableSpec("iter", GdccForRangeIterType.FOR_RANGE_ITER, false), + new VariableSpec("f", GdFloatType.FLOAT, false) + ), CIntToFloatIntrinsic.NAME); + var intrinsic = new CIntToFloatIntrinsic(); + + var ex = assertThrows(InvalidInsnException.class, () -> intrinsic.generateCCode( + fixture.builder(), fixture.variable("f"), List.of(fixture.variable("iter")))); + + assertTrue(ex.getMessage().contains(CIntToFloatIntrinsic.NAME), ex.getMessage()); + assertTrue(ex.getMessage().contains("must be int"), ex.getMessage()); + } + + private static void assertInvalidSignature(@NotNull CForRangeIterIntrinsic intrinsic, + @Nullable String resultId, + @NotNull List variableSpecs, + @NotNull List argIds, + @NotNull String expectedMessage) { + var fixture = new Fixture(variableSpecs, intrinsic.name()); + var resultVar = resultId == null ? null : fixture.variable(resultId); + var argVars = argIds.stream() + .map(fixture::variable) + .toList(); + + var ex = assertThrows(InvalidInsnException.class, () -> + intrinsic.generateCCode(fixture.builder(), resultVar, argVars) + ); + + assertInstanceOf(InvalidInsnException.class, ex); + assertTrue(ex.getMessage().contains(expectedMessage), ex.getMessage()); + assertTrue(ex.getMessage().contains(intrinsic.name()), ex.getMessage()); + } + + private static @NotNull CGenHelper newHelper(@NotNull LirClassDef workerClass) { + var api = new ExtensionAPI(null, List.of(), List.of(), List.of(), List.of(), List.of(), + List.of(), List.of(), List.of()); + var classRegistry = new ClassRegistry(api); + classRegistry.addGdccClass(workerClass); + ProjectInfo projectInfo = new ProjectInfo("TestProject", GodotVersion.V451, Path.of(".")) { + }; + return new CGenHelper(new CodegenContext(projectInfo, classRegistry), List.of(workerClass)); + } + + private record Fixture(@NotNull CBodyBuilder builder, + @NotNull LirFunctionDef func) { + Fixture(@NotNull List variableSpecs, + @NotNull String intrinsicName) { + this(variableSpecs, intrinsicName, "entry"); + } + + Fixture(@NotNull List variableSpecs, + @NotNull String intrinsicName, + @NotNull String blockId) { + this(newWorkerClass(), new LirFunctionDef("intrinsic_test"), variableSpecs, intrinsicName, blockId); + } + + private Fixture(@NotNull LirClassDef workerClass, + @NotNull LirFunctionDef func, + @NotNull List variableSpecs, + @NotNull String intrinsicName, + @NotNull String blockId) { + this(newBuilder(workerClass, func, variableSpecs, intrinsicName, blockId), func); + } + + private static @NotNull LirClassDef newWorkerClass() { + return new LirClassDef("Worker", "RefCounted", false, false, + Map.of(), List.of(), List.of(), List.of()); + } + + private static @NotNull CBodyBuilder newBuilder(@NotNull LirClassDef workerClass, + @NotNull LirFunctionDef func, + @NotNull List variableSpecs, + @NotNull String intrinsicName, + @NotNull String blockId) { + func.setReturnType(GdIntType.INT); + for (var variableSpec : variableSpecs) { + if (variableSpec.ref()) { + func.createAndAddRefVariable(variableSpec.id(), variableSpec.type()); + } else { + func.createAndAddVariable(variableSpec.id(), variableSpec.type()); + } + } + var block = new LirBasicBlock(blockId); + var insn = currentInsn(intrinsicName); + block.appendNonTerminatorInstruction(insn); + func.addBasicBlock(block); + func.setEntryBlockId(blockId); + workerClass.addFunction(func); + + return new CBodyBuilder(newHelper(workerClass), workerClass, func) + .setCurrentPosition(block, 0, insn); + } + + private static @NotNull LirInstruction currentInsn(@NotNull String intrinsicName) { + return new CallIntrinsicInsn("result", intrinsicName, List.of()); + } + + @NotNull LirVariable variable(@NotNull String id) { + var variable = func.getVariableById(id); + if (variable == null) { + throw new IllegalArgumentException("Unknown fixture variable: " + id); + } + return variable; + } + } + + private record VariableSpec(@NotNull String id, @NotNull GdType type, boolean ref) { + } +} diff --git a/src/test/java/gd/script/gdcc/lir/parser/SimpleLirBlockInsnParserTest.java b/src/test/java/gd/script/gdcc/lir/parser/SimpleLirBlockInsnParserTest.java index 6720e947..d032945a 100644 --- a/src/test/java/gd/script/gdcc/lir/parser/SimpleLirBlockInsnParserTest.java +++ b/src/test/java/gd/script/gdcc/lir/parser/SimpleLirBlockInsnParserTest.java @@ -222,6 +222,32 @@ public void parse_callIntrinsicVectorInstructionRoundTripsThroughSerializer() th ); } + @Test + public void parse_callIntrinsicRangeIteratorInstructionsPreservesNamesAndVariableArgs() throws Exception { + var input = """ + $iter = call_intrinsic "gdcc.for_range_iter.init" $start $end $step; + $cond = call_intrinsic "gdcc.for_range_iter.should_continue" $iter; + $value = call_intrinsic "gdcc.for_range_iter.get" $iter; + $iter2 = call_intrinsic "gdcc.for_range_iter.next" $iter; + """; + var parsed = parse(input); + var serializer = new SimpleLirBlockInsnSerializer(); + var out = new StringWriter(); + + serializer.serialize(parsed, out); + var reparsed = parse(out.toString()); + + assertEquals(input, out.toString()); + assertEquals(4, reparsed.size()); + var init = assertInstanceOf(CallIntrinsicInsn.class, reparsed.getFirst()); + assertEquals("iter", init.resultId()); + assertEquals("gdcc.for_range_iter.init", init.intrinsicName()); + assertEquals(3, init.args().size()); + assertCallIntrinsic(reparsed.get(1), "cond", "gdcc.for_range_iter.should_continue", "iter"); + assertCallIntrinsic(reparsed.get(2), "value", "gdcc.for_range_iter.get", "iter"); + assertCallIntrinsic(reparsed.get(3), "iter2", "gdcc.for_range_iter.next", "iter"); + } + @Test public void parse_loadStaticGlobalScopeSingletonSurfaceRoundTripsThroughSerializer() throws Exception { var input = "$engine = load_static \"@GlobalScope\" \"Engine\";\n"; @@ -257,6 +283,13 @@ public void parse_callIntrinsicVarargsRequireVariables() { assertParseError(line, 1, col, "Expected variable operand"); } + @Test + public void parse_callIntrinsicRangeIteratorRejectsLiteralBounds() { + var line = "$iter = call_intrinsic \"gdcc.for_range_iter.init\" 0 $end $step;"; + var col = line.indexOf("0") + 1; + assertParseError(line, 1, col, "Expected variable operand"); + } + @Test public void parse_assignInstruction() { var input = "$a = assign $b;"; diff --git a/src/test/java/gd/script/gdcc/lir/parser/SimpleLirBlockInsnSerializerTest.java b/src/test/java/gd/script/gdcc/lir/parser/SimpleLirBlockInsnSerializerTest.java index 8ff088c6..9ae45559 100644 --- a/src/test/java/gd/script/gdcc/lir/parser/SimpleLirBlockInsnSerializerTest.java +++ b/src/test/java/gd/script/gdcc/lir/parser/SimpleLirBlockInsnSerializerTest.java @@ -118,6 +118,23 @@ public void serialize_callIntrinsicVectorInstructionMatchesTextShape() throws Ex assertEquals("$v = call_intrinsic \"c_vector3i_to_vector3\" $vi;\n", sw.toString()); } + @Test + public void serialize_callIntrinsicRangeIteratorInstructionMatchesTextShape() throws Exception { + var insnList = List.of( + new CallIntrinsicInsn("iter", "gdcc.for_range_iter.init", List.of( + new LirInstruction.VariableOperand("start"), + new LirInstruction.VariableOperand("end"), + new LirInstruction.VariableOperand("step") + )) + ); + + var serializer = new SimpleLirBlockInsnSerializer(); + var sw = new StringWriter(); + serializer.serialize(insnList, sw); + + assertEquals("$iter = call_intrinsic \"gdcc.for_range_iter.init\" $start $end $step;\n", sw.toString()); + } + @Test public void serialize_assignInstruction() throws Exception { var insnList = List.of(new AssignInsn("a", "b")); From be8b5493db25a9e8a2ef5b2acbe1cf405de1a52f Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Tue, 30 Jun 2026 17:19:42 +0800 Subject: [PATCH 11/16] feat(backend): seal compiler-only type leaks in ordinary codegen paths and consolidate validation utilities - Add centralized rejectCompilerOnly guards in InsnGenSupport and wire them into all instruction generators for receiver, operand, argument, result target and Variant boundary checks - Unify outward metadata, typed container leaf and call wrapper paths in CGenHelper under explicit compiler-only rejection - Extract shared LirTypeUseSite enum from duplicated parser/serializer definitions and consolidate requireNonCompilerOnly logic into TypeCheckUtil utility - Delete private redundant reject/require methods across BackendMethodCallResolver, LirPublicAbiValidator and CGenHelper - Document phase-8 completion in compiler-type plan and add targeted negative tests covering every guarded code path --- .../frontend/frontend_gdcompiler_type_plan.md | 9 ++++ .../script/gdcc/backend/c/gen/CGenHelper.java | 20 +++++-- .../c/gen/insn/BackendMethodCallResolver.java | 5 ++ .../backend/c/gen/insn/CallGlobalInsnGen.java | 4 ++ .../backend/c/gen/insn/CallMethodInsnGen.java | 12 +++-- .../backend/c/gen/insn/IndexLoadInsnGen.java | 10 ++-- .../backend/c/gen/insn/IndexStoreInsnGen.java | 10 ++-- .../backend/c/gen/insn/InsnGenSupport.java | 34 ++++++++++++ .../c/gen/insn/LoadPropertyInsnGen.java | 12 +++-- .../backend/c/gen/insn/LoadStaticInsnGen.java | 1 + .../backend/c/gen/insn/OperatorInsnGen.java | 6 +++ .../c/gen/insn/PackUnpackVariantInsnGen.java | 12 +++-- .../c/gen/insn/StorePropertyInsnGen.java | 2 + .../gd/script/gdcc/lir/LirTypeUseSite.java | 30 +++++++++++ .../script/gdcc/lir/parser/DomLirParser.java | 41 +++++---------- .../gdcc/lir/parser/DomLirSerializer.java | 32 +++--------- .../lir/validation/LirPublicAbiValidator.java | 41 ++++----------- .../gd/script/gdcc/util/TypeCheckUtil.java | 25 +++++++++ .../gdcc/backend/c/gen/CGenHelperTest.java | 45 ++++++++++++++++ .../c/gen/CLoadPropertyInsnGenTest.java | 31 +++++++++++ .../backend/c/gen/CLoadStaticInsnGenTest.java | 15 ++++++ .../backend/c/gen/COperatorInsnGenTest.java | 21 ++++++++ .../c/gen/CPackUnpackVariantInsnGenTest.java | 52 +++++++++++++++++++ .../c/gen/CStorePropertyInsnGenTest.java | 25 +++++++++ .../backend/c/gen/CallGlobalInsnGenTest.java | 20 +++++++ .../backend/c/gen/CallMethodInsnGenTest.java | 49 +++++++++++++++++ .../backend/c/gen/IndexLoadInsnGenTest.java | 20 +++++++ .../backend/c/gen/IndexStoreInsnGenTest.java | 20 +++++++ 28 files changed, 496 insertions(+), 108 deletions(-) create mode 100644 src/main/java/gd/script/gdcc/lir/LirTypeUseSite.java create mode 100644 src/main/java/gd/script/gdcc/util/TypeCheckUtil.java diff --git a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md index 4b89ff4f..215a7101 100644 --- a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md +++ b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md @@ -573,6 +573,15 @@ MVP 采用以下策略: ### 5.8 阶段八:普通 Godot / Variant / engine 路径封堵 +状态:已完成(2026-06-30)。 + +产出: + +- `InsnGenSupport` 新增统一 compiler-only 泄漏检测与 Variant unpack 辅助,普通 `Variant` pack/unpack、dynamic call result unpack、index/property writeback 统一走显式 fail-fast 边界。 +- `PackUnpackVariantInsnGen`、`CallMethodInsnGen`、`BackendMethodCallResolver`、`CallGlobalInsnGen`、`OperatorInsnGen`、`IndexLoadInsnGen`、`IndexStoreInsnGen`、property/static path 补齐 compiler-only receiver / operand / argument / result target 拒绝。 +- `CGenHelper` 对 outward metadata、typed container leaf、`call_func` wrapper type gate / unpack expression / destroy stmt 增加 compiler-only 显式拒绝,避免错误下沉成模糊 helper 失败。 +- targeted 单元测试覆盖 pack/unpack、dynamic call、utility call、operator、index、property、static load 与 metadata/wrapper negative path,并保留关键 happy path 回归锚点。 + 目标: - 即使手写 LIR 绕过 frontend,compiler-only type 也不能进入普通 Godot runtime ABI。 diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/CGenHelper.java b/src/main/java/gd/script/gdcc/backend/c/gen/CGenHelper.java index 734ac395..6b84ad2f 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/CGenHelper.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/CGenHelper.java @@ -19,6 +19,7 @@ import gd.script.gdcc.scope.resolver.ScopeTypeParsers; import gd.script.gdcc.type.*; import gd.script.gdcc.type.*; +import gd.script.gdcc.util.TypeCheckUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -540,6 +541,7 @@ public boolean checkEngineMethodHelperRequiresLocalValueSlot(@NotNull EngineMeth /// materialization in `renderCallWrapperUnpackExpr(...)`. public @NotNull String renderCallWrapperVariantTypeGate(@NotNull GdType paramType, @NotNull String typeExpr) { + TypeCheckUtil.requireNonCompilerOnly(paramType, "call wrapper type gate"); switch (paramType) { case GdFloatType _ -> { return "(" + typeExpr + " == GDEXTENSION_VARIANT_TYPE_FLOAT || " @@ -547,7 +549,7 @@ public boolean checkEngineMethodHelperRequiresLocalValueSlot(@NotNull EngineMeth } case GdFloatVectorType vectorType -> { vectorType.ensureValidBuiltinDim(); - var targetType = requireBoundMetadataType(paramType); + var targetType = requireBoundMetadataType(paramType, "call wrapper type gate"); var vectorSourceType = new GdIntVectorType(vectorType.getDimension()).getGdExtensionType(); return "(" + typeExpr + " == GDEXTENSION_VARIANT_TYPE_" + targetType.name() + " || " + typeExpr + " == GDEXTENSION_VARIANT_TYPE_" + vectorSourceType.name() + ")"; @@ -561,7 +563,7 @@ public boolean checkEngineMethodHelperRequiresLocalValueSlot(@NotNull EngineMeth + typeExpr + " == GDEXTENSION_VARIANT_TYPE_STRING_NAME)"; } default -> { - var targetType = requireBoundMetadataType(paramType); + var targetType = requireBoundMetadataType(paramType, "call wrapper type gate"); return "(" + typeExpr + " == GDEXTENSION_VARIANT_TYPE_" + targetType.name() + ")"; } } @@ -579,6 +581,7 @@ public boolean checkEngineMethodHelperRequiresLocalValueSlot(@NotNull EngineMeth public @NotNull String renderCallWrapperUnpackExpr(@NotNull GdType paramType, @NotNull String variantPtrExpr, @Nullable String typeExpr) { + TypeCheckUtil.requireNonCompilerOnly(paramType, "call wrapper unpack expression"); switch (paramType) { case GdFloatType _ -> { var actualTypeExpr = typeExpr != null ? typeExpr : "godot_variant_get_type(" + variantPtrExpr + ")"; @@ -659,6 +662,7 @@ public boolean checkEngineMethodHelperRequiresLocalValueSlot(@NotNull EngineMeth /// - only destroyable non-object wrappers materialize an addressable local slot that the wrapper must destroy /// - object locals stay as plain pointers here, so they must not be blanket destroy/release'd at wrapper exit public @NotNull String renderCallWrapperDestroyStmt(@NotNull GdType type, @NotNull String varName) { + TypeCheckUtil.requireNonCompilerOnly(type, "call wrapper destroy stmt"); if (type instanceof GdObjectType || !type.isDestroyable()) { return ""; } @@ -732,7 +736,7 @@ public boolean isTypedDictionaryGuardObjectLeaf(@NotNull GdType type, @NotNull S public @NotNull BoundMetadata renderBoundMetadata(@NotNull GdType type, @NotNull String baseUsageExpr, @NotNull String useSite) { - var extensionType = requireBoundMetadataType(type); + var extensionType = requireBoundMetadataType(type, useSite + " metadata"); var usageExpr = type instanceof GdVariantType ? baseUsageExpr + " | godot_PROPERTY_USAGE_NIL_IS_VARIANT" : baseUsageExpr; @@ -769,7 +773,9 @@ public boolean isTypedDictionaryGuardObjectLeaf(@NotNull GdType type, @NotNull S return "godot_PROPERTY_USAGE_NO_EDITOR"; } - private @NotNull GdExtensionTypeEnum requireBoundMetadataType(@NotNull GdType type) { + private @NotNull GdExtensionTypeEnum requireBoundMetadataType(@NotNull GdType type, + @NotNull String useSite) { + TypeCheckUtil.requireNonCompilerOnly(type, useSite); var extensionType = type.getGdExtensionType(); if (extensionType == null) { throw new IllegalArgumentException("Type " + type.getTypeName() + " does not have outward GDExtension metadata"); @@ -794,6 +800,9 @@ public boolean isTypedDictionaryGuardObjectLeaf(@NotNull GdType type, @NotNull S @NotNull String containerKind, boolean allowVariantLeaf) { return switch (type) { + case GdCompilerType _ -> throw new IllegalArgumentException( + "compiler-only type leaked into " + containerKind + " outward hint leaf at " + useSite + ": " + type.getTypeName() + ); case GdVariantType _ -> { if (allowVariantLeaf) { yield type.getTypeName(); @@ -900,6 +909,9 @@ public boolean isTypedDictionaryGuardObjectLeaf(@NotNull GdType type, @NotNull S @NotNull String containerKind, boolean allowVariantLeaf) { return switch (type) { + case GdCompilerType _ -> throw new IllegalArgumentException( + "compiler-only type leaked into " + containerKind + " runtime leaf at " + useSite + ": " + type.getTypeName() + ); case GdVariantType _ -> { if (allowVariantLeaf) { yield GdExtensionTypeEnum.NIL; diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/insn/BackendMethodCallResolver.java b/src/main/java/gd/script/gdcc/backend/c/gen/insn/BackendMethodCallResolver.java index 3a9b5cac..aa77f634 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/insn/BackendMethodCallResolver.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/insn/BackendMethodCallResolver.java @@ -163,11 +163,16 @@ public record ResolvedMethodCall(@NotNull DispatchMode mode, @NotNull String methodName, @NotNull List argVars) { var receiverType = receiverVar.type(); + InsnGenSupport.rejectCompilerOnlyType(bodyBuilder, receiverType, "call_method receiver"); if (receiverType instanceof GdVoidType || receiverType instanceof GdNilType) { throw bodyBuilder.invalidInsn("Receiver variable '" + receiverVar.id() + "' has invalid type '" + receiverType.getTypeName() + "' for call_method"); } + for (var i = 0; i < argVars.size(); i++) { + InsnGenSupport.rejectCompilerOnlyType(bodyBuilder, argVars.get(i).type(), "call_method argument #" + (i + 1)); + } + var argTypes = argVars.stream().map(LirVariable::type).toList(); var result = ScopeMethodResolver.resolveInstanceMethod( bodyBuilder.classRegistry(), diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/insn/CallGlobalInsnGen.java b/src/main/java/gd/script/gdcc/backend/c/gen/insn/CallGlobalInsnGen.java index fec6109f..a4665910 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/insn/CallGlobalInsnGen.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/insn/CallGlobalInsnGen.java @@ -51,6 +51,7 @@ public void generateCCode(@NotNull CBodyBuilder bodyBuilder) { "' has unresolved type metadata"); } var argVar = argVars.get(i); + InsnGenSupport.rejectCompilerOnlyVariable(bodyBuilder, argVar, "call_global argument #" + (i + 1)); if (!bodyBuilder.classRegistry().checkAssignable(argVar.type(), paramType)) { throw bodyBuilder.invalidInsn("Cannot assign value of type '" + argVar.type().getTypeName() + "' to utility parameter #" + (i + 1) + " of type '" + paramType.getTypeName() + "'"); @@ -83,6 +84,7 @@ public void generateCCode(@NotNull CBodyBuilder bodyBuilder) { } for (var i = fixedCount; i < argVars.size(); i++) { + InsnGenSupport.rejectCompilerOnlyVariable(bodyBuilder, argVars.get(i), "call_global vararg #" + (i + 1)); varargs.add(bodyBuilder.valueOfVar(argVars.get(i))); } if (signature.isVararg()) { @@ -122,6 +124,7 @@ public void generateCCode(@NotNull CBodyBuilder bodyBuilder) { if (resultVar.ref()) { throw bodyBuilder.invalidInsn("Result variable ID '" + resultId + "' cannot be a reference"); } + InsnGenSupport.rejectCompilerOnlyVariable(bodyBuilder, resultVar, "call_global result target"); if (!bodyBuilder.classRegistry().checkAssignable(returnType, resultVar.type())) { throw bodyBuilder.invalidInsn("Utility function '" + utilityLookupName + "' returns '" + returnType.getTypeName() + "', but result variable '" + resultId + @@ -156,6 +159,7 @@ public void generateCCode(@NotNull CBodyBuilder bodyBuilder) { if (argVar == null) { throw bodyBuilder.invalidInsn("Argument variable ID '" + argId + "' not found in function"); } + InsnGenSupport.rejectCompilerOnlyVariable(bodyBuilder, argVar, "call_global argument #" + (i + 1)); argVars.add(argVar); } return argVars; diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/insn/CallMethodInsnGen.java b/src/main/java/gd/script/gdcc/backend/c/gen/insn/CallMethodInsnGen.java index b2e70aea..e85446a0 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/insn/CallMethodInsnGen.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/insn/CallMethodInsnGen.java @@ -158,12 +158,12 @@ private void emitDynamicCallResult(@NotNull CBodyBuilder bodyBuilder, var dynamicResultTemp = bodyBuilder.newTempVariable("dynamic_result", GdVariantType.VARIANT); bodyBuilder.declareTempVar(dynamicResultTemp); bodyBuilder.callAssign(dynamicResultTemp, cFunctionName, GdVariantType.VARIANT, fixedArgs, varargs); - var unpackFunctionName = bodyBuilder.helper().renderUnpackFunctionName(resultVar.type()); - bodyBuilder.callAssign( + InsnGenSupport.unpackVariantAssign( + bodyBuilder, bodyBuilder.targetOfVar(resultVar), - unpackFunctionName, resultVar.type(), - List.of(dynamicResultTemp) + dynamicResultTemp, + "dynamic call result target" ); bodyBuilder.destroyTempVar(dynamicResultTemp); destroyDefaultTemps(bodyBuilder, dynamicArgTemps); @@ -463,6 +463,7 @@ private void validateVarargs(@NotNull CBodyBuilder bodyBuilder, if (receiverVar.type() instanceof GdVoidType) { throw bodyBuilder.invalidInsn("Receiver variable '" + receiverId + "' cannot be void"); } + InsnGenSupport.rejectCompilerOnlyVariable(bodyBuilder, receiverVar, "call_method receiver"); return receiverVar; } @@ -480,6 +481,7 @@ private void validateVarargs(@NotNull CBodyBuilder bodyBuilder, if (argVar == null) { throw bodyBuilder.invalidInsn("Argument variable ID '" + argId + "' not found in function"); } + InsnGenSupport.rejectCompilerOnlyVariable(bodyBuilder, argVar, "call_method argument #" + (i + 1)); out.add(argVar); } return out; @@ -499,6 +501,7 @@ private void validateVarargs(@NotNull CBodyBuilder bodyBuilder, if (resultVar.ref()) { throw bodyBuilder.invalidInsn("Result variable ID '" + resultId + "' cannot be a reference"); } + InsnGenSupport.rejectCompilerOnlyVariable(bodyBuilder, resultVar, "method call result target"); if (!bodyBuilder.classRegistry().checkAssignable(resolved.returnType(), resultVar.type())) { throw bodyBuilder.invalidInsn("Method '" + resolved.ownerClassName() + "." + resolved.methodName() + "' returns '" + resolved.returnType().getTypeName() + "', but result variable '" + resultId + @@ -520,6 +523,7 @@ private void validateVarargs(@NotNull CBodyBuilder bodyBuilder, if (resultVar.ref()) { throw bodyBuilder.invalidInsn("Result variable ID '" + resultId + "' cannot be a reference"); } + InsnGenSupport.rejectCompilerOnlyVariable(bodyBuilder, resultVar, "dynamic call result target"); return resultVar; } } diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/insn/IndexLoadInsnGen.java b/src/main/java/gd/script/gdcc/backend/c/gen/insn/IndexLoadInsnGen.java index 29a4aa9b..8e202334 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/insn/IndexLoadInsnGen.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/insn/IndexLoadInsnGen.java @@ -239,12 +239,12 @@ private void emitAssignResultFromVariant(@NotNull CBodyBuilder bodyBuilder, return; } - var unpackFunctionName = bodyBuilder.helper().renderUnpackFunctionName(resultVar.type()); - bodyBuilder.callAssign( + InsnGenSupport.unpackVariantAssign( + bodyBuilder, bodyBuilder.targetOfVar(resultVar), - unpackFunctionName, resultVar.type(), - List.of(retTemp) + retTemp, + "index load result target" ); } @@ -285,6 +285,7 @@ private void emitOobReturn(@NotNull CBodyBuilder bodyBuilder, if (resultVar.ref()) { throw bodyBuilder.invalidInsn("Result variable ID '" + resultId + "' cannot be a reference"); } + InsnGenSupport.rejectCompilerOnlyVariable(bodyBuilder, resultVar, "index load result target"); return resultVar; } @@ -296,6 +297,7 @@ private void emitOobReturn(@NotNull CBodyBuilder bodyBuilder, throw bodyBuilder.invalidInsn("Index load " + role + " operand variable ID '" + variableId + "' not found in function"); } + InsnGenSupport.rejectCompilerOnlyVariable(bodyBuilder, variable, "index load " + role + " operand"); return variable; } diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/insn/IndexStoreInsnGen.java b/src/main/java/gd/script/gdcc/backend/c/gen/insn/IndexStoreInsnGen.java index 4525ce07..abf255d9 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/insn/IndexStoreInsnGen.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/insn/IndexStoreInsnGen.java @@ -30,7 +30,6 @@ import org.jetbrains.annotations.Nullable; import java.util.EnumSet; -import java.util.List; import java.util.Objects; /// C code generator for indexing store instructions: @@ -263,12 +262,12 @@ private void emitSelfWritebackIfNeeded(@NotNull CBodyBuilder bodyBuilder, if (selfOperand.tempVar() == null) { throw bodyBuilder.invalidInsn("Internal error: missing self temp variant for writeback"); } - var unpackFunctionName = bodyBuilder.helper().renderUnpackFunctionName(selfVar.type()); - bodyBuilder.callAssign( + InsnGenSupport.unpackVariantAssign( + bodyBuilder, bodyBuilder.targetOfVar(selfVar), - unpackFunctionName, selfVar.type(), - List.of(selfOperand.tempVar()) + selfOperand.tempVar(), + "index store self writeback" ); } @@ -407,6 +406,7 @@ private void ensureNoResultVariable(@NotNull CBodyBuilder bodyBuilder, throw bodyBuilder.invalidInsn("Index store " + role + " operand variable ID '" + variableId + "' not found in function"); } + InsnGenSupport.rejectCompilerOnlyVariable(bodyBuilder, variable, "index store " + role + " operand"); return variable; } diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/insn/InsnGenSupport.java b/src/main/java/gd/script/gdcc/backend/c/gen/insn/InsnGenSupport.java index 851163e0..5c570365 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/insn/InsnGenSupport.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/insn/InsnGenSupport.java @@ -2,7 +2,9 @@ import gd.script.gdcc.backend.c.gen.CBodyBuilder; import gd.script.gdcc.lir.LirVariable; +import gd.script.gdcc.type.GdCompilerType; import gd.script.gdcc.type.GdNilType; +import gd.script.gdcc.type.GdType; import gd.script.gdcc.type.GdVariantType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -20,10 +22,32 @@ final class InsnGenSupport { private InsnGenSupport() { } + static void rejectCompilerOnlyVariable(@NotNull CBodyBuilder bodyBuilder, + @NotNull LirVariable variable, + @NotNull String useSite) { + if (variable.type() instanceof GdCompilerType compilerOnlyType) { + throw bodyBuilder.invalidInsn( + "compiler-only type leaked into " + useSite + " variable '" + variable.id() + + "': " + compilerOnlyType.getTypeName() + ); + } + } + + static void rejectCompilerOnlyType(@NotNull CBodyBuilder bodyBuilder, + @NotNull GdType type, + @NotNull String useSite) { + if (type instanceof GdCompilerType compilerOnlyType) { + throw bodyBuilder.invalidInsn( + "compiler-only type leaked into " + useSite + ": " + compilerOnlyType.getTypeName() + ); + } + } + /// `Nil` does not belong to the unary `godot_new_Variant_with_` family. static void packVariantAssign(@NotNull CBodyBuilder bodyBuilder, @NotNull CBodyBuilder.TargetRef target, @NotNull LirVariable valueVar) { + rejectCompilerOnlyVariable(bodyBuilder, valueVar, "Variant pack source"); if (valueVar.type() instanceof GdNilType) { bodyBuilder.callAssign(target, "godot_new_Variant_nil", GdVariantType.VARIANT, List.of()); return; @@ -33,6 +57,16 @@ static void packVariantAssign(@NotNull CBodyBuilder bodyBuilder, bodyBuilder.callAssign(target, packFunctionName, GdVariantType.VARIANT, List.of(bodyBuilder.valueOfVar(valueVar))); } + static void unpackVariantAssign(@NotNull CBodyBuilder bodyBuilder, + @NotNull CBodyBuilder.TargetRef target, + @NotNull GdType targetType, + @NotNull CBodyBuilder.ValueRef variantValue, + @NotNull String useSite) { + rejectCompilerOnlyType(bodyBuilder, targetType, useSite); + var unpackFunctionName = bodyBuilder.helper().renderUnpackFunctionName(targetType); + bodyBuilder.callAssign(target, unpackFunctionName, targetType, List.of(variantValue)); + } + static @NotNull VariantOperand materializeVariantOperand(@NotNull CBodyBuilder bodyBuilder, @NotNull LirVariable operandVar, @NotNull String tempPrefix) { diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/insn/LoadPropertyInsnGen.java b/src/main/java/gd/script/gdcc/backend/c/gen/insn/LoadPropertyInsnGen.java index ef56697b..d0c46043 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/insn/LoadPropertyInsnGen.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/insn/LoadPropertyInsnGen.java @@ -37,7 +37,6 @@ private record PropertyReadResolution(@NotNull GdType propertyType, public void generateCCode(@NotNull CBodyBuilder bodyBuilder) { var insn = bodyBuilder.getCurrentInsn(this); var func = bodyBuilder.func(); - var helper = bodyBuilder.helper(); if (insn.resultId() == null) { throw bodyBuilder.invalidInsn("Load property instruction missing result variable ID"); @@ -47,6 +46,7 @@ public void generateCCode(@NotNull CBodyBuilder bodyBuilder) { if (objectVar == null) { throw bodyBuilder.invalidInsn("Object variable ID " + insn.objectId() + " does not exist"); } + InsnGenSupport.rejectCompilerOnlyVariable(bodyBuilder, objectVar, "property load receiver"); var resultVar = func.getVariableById(insn.resultId()); if (resultVar == null) { throw bodyBuilder.invalidInsn("Result variable ID " + insn.resultId() + " does not exist"); @@ -54,6 +54,7 @@ public void generateCCode(@NotNull CBodyBuilder bodyBuilder) { if (resultVar.ref()) { throw bodyBuilder.invalidInsn("Result variable ID " + insn.resultId() + " cannot be a reference that cannot be written into"); } + InsnGenSupport.rejectCompilerOnlyVariable(bodyBuilder, resultVar, "property load result target"); if (objectVar.type() instanceof GdVoidType || objectVar.type() instanceof GdNilType) { throw bodyBuilder.invalidInsn("Object variable ID " + insn.objectId() + " is not a valid property target type, but " + objectVar.type().getTypeName()); } @@ -116,8 +117,13 @@ public void generateCCode(@NotNull CBodyBuilder bodyBuilder) { bodyBuilder.declareTempVar(tempVar); bodyBuilder.callAssign(tempVar, "godot_Object_get", GdVariantType.VARIANT, List.of(objectValue, propertyName)); var resultType = resultVar.type(); - var unpackFunc = helper.renderUnpackFunctionName(resultType); - bodyBuilder.callAssign(target, unpackFunc, resultType, List.of(tempVar)); + InsnGenSupport.unpackVariantAssign( + bodyBuilder, + target, + resultType, + tempVar, + "property load result target" + ); bodyBuilder.destroyTempVar(tempVar); } } else { diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/insn/LoadStaticInsnGen.java b/src/main/java/gd/script/gdcc/backend/c/gen/insn/LoadStaticInsnGen.java index 034dee7f..38846e87 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/insn/LoadStaticInsnGen.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/insn/LoadStaticInsnGen.java @@ -39,6 +39,7 @@ public void generateCCode(@NotNull CBodyBuilder bodyBuilder) { if (resultVar.ref()) { throw bodyBuilder.invalidInsn("Result variable ID '" + resultId + "' cannot be a reference"); } + InsnGenSupport.rejectCompilerOnlyVariable(bodyBuilder, resultVar, "static load result target"); var target = bodyBuilder.targetOfVar(resultVar); var classRegistry = bodyBuilder.classRegistry(); diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/insn/OperatorInsnGen.java b/src/main/java/gd/script/gdcc/backend/c/gen/insn/OperatorInsnGen.java index f8d7e6fa..a5eaa4be 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/insn/OperatorInsnGen.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/insn/OperatorInsnGen.java @@ -49,6 +49,7 @@ private void emitUnary(@NotNull CBodyBuilder bodyBuilder, @NotNull UnaryOpInsn instruction) { var resultVar = resolveRequiredResultVariable(bodyBuilder, instruction.resultId()); var operandVar = resolveOperandVariable(bodyBuilder, instruction.operandId(), "operand"); + InsnGenSupport.rejectCompilerOnlyVariable(bodyBuilder, operandVar, "operator operand"); var decision = resolver.resolveUnaryPath(bodyBuilder, instruction.op(), operandVar.type()); if (decision.path() == OperatorResolver.OperatorPath.UNIMPLEMENTED) { @@ -82,6 +83,8 @@ private void emitBinary(@NotNull CBodyBuilder bodyBuilder, var resultVar = resolveRequiredResultVariable(bodyBuilder, instruction.resultId()); var leftVar = resolveOperandVariable(bodyBuilder, instruction.leftId(), "left"); var rightVar = resolveOperandVariable(bodyBuilder, instruction.rightId(), "right"); + InsnGenSupport.rejectCompilerOnlyVariable(bodyBuilder, leftVar, "operator left operand"); + InsnGenSupport.rejectCompilerOnlyVariable(bodyBuilder, rightVar, "operator right operand"); var decision = resolver.resolveBinaryPath(bodyBuilder, instruction.op(), leftVar.type(), rightVar.type()); if (decision.path() == OperatorResolver.OperatorPath.UNIMPLEMENTED) { @@ -521,6 +524,7 @@ private void validateVariantEvaluateResultTarget(@NotNull CBodyBuilder bodyBuild if (resultVar.type() instanceof GdVoidType) { throw bodyBuilder.invalidInsn("variant_evaluate result variable cannot be void"); } + InsnGenSupport.rejectCompilerOnlyVariable(bodyBuilder, resultVar, "operator result target"); if (resultVar.type() instanceof GdVariantType) { return; } @@ -540,6 +544,7 @@ private void validateVariantEvaluateResultTarget(@NotNull CBodyBuilder bodyBuild if (resultVar.ref()) { throw bodyBuilder.invalidInsn("Result variable ID '" + resultId + "' cannot be a reference"); } + InsnGenSupport.rejectCompilerOnlyVariable(bodyBuilder, resultVar, "operator result target"); return resultVar; } @@ -550,6 +555,7 @@ private void validateVariantEvaluateResultTarget(@NotNull CBodyBuilder bodyBuild if (variable == null) { throw bodyBuilder.invalidInsn("Operator " + role + " operand variable ID '" + variableId + "' not found in function"); } + InsnGenSupport.rejectCompilerOnlyVariable(bodyBuilder, variable, "operator " + role + " operand"); return variable; } diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/insn/PackUnpackVariantInsnGen.java b/src/main/java/gd/script/gdcc/backend/c/gen/insn/PackUnpackVariantInsnGen.java index 6eca4141..03b970bc 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/insn/PackUnpackVariantInsnGen.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/insn/PackUnpackVariantInsnGen.java @@ -10,7 +10,6 @@ import org.jetbrains.annotations.NotNull; import java.util.EnumSet; -import java.util.List; import java.util.Objects; public final class PackUnpackVariantInsnGen implements CInsnGen { @@ -23,8 +22,6 @@ public final class PackUnpackVariantInsnGen implements CInsnGen public void generateCCode(@NotNull CBodyBuilder bodyBuilder) { var instruction = bodyBuilder.getCurrentInsn(this); var func = bodyBuilder.func(); - var helper = bodyBuilder.helper(); - if (instruction.resultId() == null) { throw bodyBuilder.invalidInsn("Instruction does not have a result variable ID"); } @@ -44,8 +41,13 @@ public void generateCCode(@NotNull CBodyBuilder bodyBuilder) { "' is not of variant type, but " + variantVar.type().getTypeName()); } - var unpackFunc = helper.renderUnpackFunctionName(resultVar.type()); - bodyBuilder.callAssign(target, unpackFunc, resultVar.type(), List.of(bodyBuilder.valueOfVar(variantVar))); + InsnGenSupport.unpackVariantAssign( + bodyBuilder, + target, + resultVar.type(), + bodyBuilder.valueOfVar(variantVar), + "Variant unpack target" + ); return; } diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/insn/StorePropertyInsnGen.java b/src/main/java/gd/script/gdcc/backend/c/gen/insn/StorePropertyInsnGen.java index 90824c4d..8dcd7659 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/insn/StorePropertyInsnGen.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/insn/StorePropertyInsnGen.java @@ -34,10 +34,12 @@ public void generateCCode(@NotNull CBodyBuilder bodyBuilder) { if (objectVar == null) { throw bodyBuilder.invalidInsn("Object variable ID " + insn.objectId() + " does not exist"); } + InsnGenSupport.rejectCompilerOnlyVariable(bodyBuilder, objectVar, "property store receiver"); var valueVar = func.getVariableById(insn.valueId()); if (valueVar == null) { throw bodyBuilder.invalidInsn("Value variable ID " + insn.valueId() + " does not exist"); } + InsnGenSupport.rejectCompilerOnlyVariable(bodyBuilder, valueVar, "property store value"); if (objectVar.type() instanceof GdVoidType || objectVar.type() instanceof GdNilType) { throw bodyBuilder.invalidInsn("Object variable ID " + insn.objectId() + " is not a valid property target type, but " + objectVar.type().getTypeName()); diff --git a/src/main/java/gd/script/gdcc/lir/LirTypeUseSite.java b/src/main/java/gd/script/gdcc/lir/LirTypeUseSite.java new file mode 100644 index 00000000..742dae0d --- /dev/null +++ b/src/main/java/gd/script/gdcc/lir/LirTypeUseSite.java @@ -0,0 +1,30 @@ +package gd.script.gdcc.lir; + +import org.jetbrains.annotations.NotNull; + +/// Defines the context in which a type text appears in LIR XML, +/// controlling whether compiler-only types are allowed. +public enum LirTypeUseSite { + SIGNAL_PARAMETER("signal parameter", false), + PROPERTY("property", false), + FUNCTION_PARAMETER("function parameter", false), + FUNCTION_CAPTURE("function capture", false), + FUNCTION_RETURN("function return", false), + FUNCTION_VARIABLE("function variable", true); + + private final @NotNull String displayName; + private final boolean allowCompilerOnlyType; + + LirTypeUseSite(@NotNull String displayName, boolean allowCompilerOnlyType) { + this.displayName = displayName; + this.allowCompilerOnlyType = allowCompilerOnlyType; + } + + public @NotNull String displayName() { + return displayName; + } + + public boolean allowCompilerOnlyType() { + return allowCompilerOnlyType; + } +} diff --git a/src/main/java/gd/script/gdcc/lir/parser/DomLirParser.java b/src/main/java/gd/script/gdcc/lir/parser/DomLirParser.java index babc00a5..f727c86b 100644 --- a/src/main/java/gd/script/gdcc/lir/parser/DomLirParser.java +++ b/src/main/java/gd/script/gdcc/lir/parser/DomLirParser.java @@ -16,23 +16,6 @@ /// DOM-based implementation of LirParser. Parses the XML structure into LIR entities. public final class DomLirParser implements LirParser { - private enum TypeUseSite { - SIGNAL_PARAMETER("signal parameter", false), - PROPERTY("property", false), - FUNCTION_PARAMETER("function parameter", false), - FUNCTION_CAPTURE("function capture", false), - FUNCTION_RETURN("function return", false), - FUNCTION_VARIABLE("function variable", true); - - private final @NotNull String displayName; - private final boolean allowCompilerOnlyType; - - TypeUseSite(@NotNull String displayName, boolean allowCompilerOnlyType) { - this.displayName = displayName; - this.allowCompilerOnlyType = allowCompilerOnlyType; - } - } - private ClassRegistry classRegistry; public DomLirParser(@NotNull ClassRegistry classRegistry) { @@ -93,7 +76,7 @@ public void setClassRegistry(ClassRegistry classRegistry) { for (int pi = 0; pi < params.getLength(); pi++) { var pEl = (Element) params.item(pi); var pname = pEl.getAttribute("name"); - var ptype = parseTypeText(pEl.getAttribute("type"), TypeUseSite.SIGNAL_PARAMETER); + var ptype = parseTypeText(pEl.getAttribute("type"), LirTypeUseSite.SIGNAL_PARAMETER); signal.addParameter(new LirParameterDef(pname, ptype, null, signal)); } signals.add(signal); @@ -109,7 +92,7 @@ public void setClassRegistry(ClassRegistry classRegistry) { for (int pi = 0; pi < pList.getLength(); pi++) { var pEl = (Element) pList.item(pi); var pname = pEl.getAttribute("name"); - var ptype = parseTypeText(pEl.getAttribute("type"), TypeUseSite.PROPERTY); + var ptype = parseTypeText(pEl.getAttribute("type"), LirTypeUseSite.PROPERTY); var isStatic = Boolean.parseBoolean(pEl.getAttribute("is_static")); var init = pEl.hasAttribute("init_func") ? pEl.getAttribute("init_func") : null; var getter = pEl.hasAttribute("getter_func") ? pEl.getAttribute("getter_func") : null; @@ -162,7 +145,7 @@ public void setClassRegistry(ClassRegistry classRegistry) { for (int pi = 0; pi < pList.getLength(); pi++) { var pEl = (Element) pList.item(pi); var pname = pEl.getAttribute("name"); - var ptype = parseTypeText(pEl.getAttribute("type"), TypeUseSite.FUNCTION_PARAMETER); + var ptype = parseTypeText(pEl.getAttribute("type"), LirTypeUseSite.FUNCTION_PARAMETER); var defFunc = pEl.hasAttribute("default_value_func") ? pEl.getAttribute("default_value_func") : null; fn.addParameter(new LirParameterDef(pname, ptype, defFunc, fn)); } @@ -176,7 +159,7 @@ public void setClassRegistry(ClassRegistry classRegistry) { for (int ci = 0; ci < cList.getLength(); ci++) { var cEl = (Element) cList.item(ci); var cname = cEl.getAttribute("name"); - var ctype = parseTypeText(cEl.getAttribute("type"), TypeUseSite.FUNCTION_CAPTURE); + var ctype = parseTypeText(cEl.getAttribute("type"), LirTypeUseSite.FUNCTION_CAPTURE); fn.addCapture(new LirCaptureDef(cname, ctype, fn)); } } @@ -185,7 +168,7 @@ public void setClassRegistry(ClassRegistry classRegistry) { var retNodes = fEl.getElementsByTagName("return_type"); if (retNodes.getLength() > 0) { var rEl = (Element) retNodes.item(0); - var rtype = parseTypeText(rEl.getAttribute("type"), TypeUseSite.FUNCTION_RETURN); + var rtype = parseTypeText(rEl.getAttribute("type"), LirTypeUseSite.FUNCTION_RETURN); fn.setReturnType(rtype); } @@ -197,7 +180,7 @@ public void setClassRegistry(ClassRegistry classRegistry) { for (int vi = 0; vi < vList.getLength(); vi++) { var vEl = (Element) vList.item(vi); var id = vEl.getAttribute("id"); - var t = parseTypeText(vEl.getAttribute("type"), TypeUseSite.FUNCTION_VARIABLE); + var t = parseTypeText(vEl.getAttribute("type"), LirTypeUseSite.FUNCTION_VARIABLE); fn.createAndAddVariable(id, t); } } @@ -256,10 +239,10 @@ public void setClassRegistry(ClassRegistry classRegistry) { /// LIR XML keeps compiler-only types on a dedicated grammar and only accepts them for local /// function variables. All public ABI-like surfaces continue to reuse source-facing parsing. - private @NotNull GdType parseTypeText(@NotNull String rawTypeText, @NotNull TypeUseSite useSite) { + private @NotNull GdType parseTypeText(@NotNull String rawTypeText, @NotNull LirTypeUseSite useSite) { var typeText = rawTypeText.trim(); if (typeText.isEmpty()) { - throw new IllegalArgumentException("Cannot parse type for " + useSite.displayName + ": blank type text"); + throw new IllegalArgumentException("Cannot parse type for " + useSite.displayName() + ": blank type text"); } var compilerOnlyType = tryParseCompilerOnlyType(typeText, useSite); if (compilerOnlyType != null) { @@ -270,16 +253,16 @@ public void setClassRegistry(ClassRegistry classRegistry) { if (parsedType != null) { return parsedType; } - throw new IllegalArgumentException("Cannot parse type for " + useSite.displayName + ": " + rawTypeText); + throw new IllegalArgumentException("Cannot parse type for " + useSite.displayName() + ": " + rawTypeText); } - private @Nullable GdType tryParseCompilerOnlyType(@NotNull String typeText, @NotNull TypeUseSite useSite) { + private @Nullable GdType tryParseCompilerOnlyType(@NotNull String typeText, @NotNull LirTypeUseSite useSite) { if (!typeText.startsWith("compiler::")) { return null; } - if (!useSite.allowCompilerOnlyType) { + if (!useSite.allowCompilerOnlyType()) { throw new IllegalArgumentException( - "compiler-only type leaked into " + useSite.displayName + ": " + typeText + "compiler-only type leaked into " + useSite.displayName() + ": " + typeText ); } if (GdccForRangeIterType.LIR_TYPE_TEXT.equals(typeText)) { diff --git a/src/main/java/gd/script/gdcc/lir/parser/DomLirSerializer.java b/src/main/java/gd/script/gdcc/lir/parser/DomLirSerializer.java index 65327b51..597dffe2 100644 --- a/src/main/java/gd/script/gdcc/lir/parser/DomLirSerializer.java +++ b/src/main/java/gd/script/gdcc/lir/parser/DomLirSerializer.java @@ -21,22 +21,6 @@ /// DOM-based implementation of LirSerializer. public final class DomLirSerializer implements LirSerializer { - private enum TypeUseSite { - SIGNAL_PARAMETER("signal parameter", false), - PROPERTY("property", false), - FUNCTION_PARAMETER("function parameter", false), - FUNCTION_RETURN("function return", false), - FUNCTION_VARIABLE("function variable", true); - - private final @NotNull String displayName; - private final boolean allowCompilerOnlyType; - - TypeUseSite(@NotNull String displayName, boolean allowCompilerOnlyType) { - this.displayName = displayName; - this.allowCompilerOnlyType = allowCompilerOnlyType; - } - } - @Override public void serialize(@NotNull LirModule module, @NotNull Writer out) throws Exception { var docFactory = DocumentBuilderFactory.newInstance(); @@ -73,7 +57,7 @@ public void serialize(@NotNull LirModule module, @NotNull Writer out) throws Exc var p = s.getParameter(i); Element pEl = doc.createElement("parameter"); pEl.setAttribute("name", Objects.requireNonNull(p).name()); - pEl.setAttribute("type", renderTypeText(p.type(), TypeUseSite.SIGNAL_PARAMETER)); + pEl.setAttribute("type", renderTypeText(p.type(), LirTypeUseSite.SIGNAL_PARAMETER)); sEl.appendChild(pEl); } signalsEl.appendChild(sEl); @@ -85,7 +69,7 @@ public void serialize(@NotNull LirModule module, @NotNull Writer out) throws Exc for (var prop : cls.getProperties()) { Element pEl = doc.createElement("property"); pEl.setAttribute("name", prop.getName()); - pEl.setAttribute("type", renderTypeText(prop.getType(), TypeUseSite.PROPERTY)); + pEl.setAttribute("type", renderTypeText(prop.getType(), LirTypeUseSite.PROPERTY)); pEl.setAttribute("is_static", Boolean.toString(prop.isStatic())); if (prop.getInitFunc() != null) pEl.setAttribute("init_func", prop.getInitFunc()); if (prop.getGetterFunc() != null) pEl.setAttribute("getter_func", prop.getGetterFunc()); @@ -127,7 +111,7 @@ public void serialize(@NotNull LirModule module, @NotNull Writer out) throws Exc } Element pEl = doc.createElement("parameter"); pEl.setAttribute("name", p.name()); - pEl.setAttribute("type", renderTypeText(p.type(), TypeUseSite.FUNCTION_PARAMETER)); + pEl.setAttribute("type", renderTypeText(p.type(), LirTypeUseSite.FUNCTION_PARAMETER)); if (p.defaultValueFunc() != null) pEl.setAttribute("default_value_func", p.defaultValueFunc()); paramsEl.appendChild(pEl); } @@ -139,7 +123,7 @@ public void serialize(@NotNull LirModule module, @NotNull Writer out) throws Exc // return type Element retEl = doc.createElement("return_type"); - retEl.setAttribute("type", renderTypeText(fn.getReturnType(), TypeUseSite.FUNCTION_RETURN)); + retEl.setAttribute("type", renderTypeText(fn.getReturnType(), LirTypeUseSite.FUNCTION_RETURN)); fEl.appendChild(retEl); // variables @@ -147,7 +131,7 @@ public void serialize(@NotNull LirModule module, @NotNull Writer out) throws Exc for (var v : fn.getVariables().values()) { Element vEl = doc.createElement("variable"); vEl.setAttribute("id", v.id()); - vEl.setAttribute("type", renderTypeText(v.type(), TypeUseSite.FUNCTION_VARIABLE)); + vEl.setAttribute("type", renderTypeText(v.type(), LirTypeUseSite.FUNCTION_VARIABLE)); varsEl.appendChild(vEl); } fEl.appendChild(varsEl); @@ -193,11 +177,11 @@ public void serialize(@NotNull LirModule module, @NotNull Writer out) throws Exc /// Serializer keeps compiler-only types on the dedicated `compiler::...` grammar and rejects any /// attempt to leak them into outward-facing metadata surfaces. - private @NotNull String renderTypeText(@NotNull GdType type, @NotNull TypeUseSite useSite) { + private @NotNull String renderTypeText(@NotNull GdType type, @NotNull LirTypeUseSite useSite) { if (type instanceof GdCompilerType compilerOnlyType) { - if (!useSite.allowCompilerOnlyType) { + if (!useSite.allowCompilerOnlyType()) { throw new IllegalArgumentException( - "compiler-only type leaked into " + useSite.displayName + ": " + compilerOnlyType.getLirTypeText() + "compiler-only type leaked into " + useSite.displayName() + ": " + compilerOnlyType.getLirTypeText() ); } return compilerOnlyType.getLirTypeText(); diff --git a/src/main/java/gd/script/gdcc/lir/validation/LirPublicAbiValidator.java b/src/main/java/gd/script/gdcc/lir/validation/LirPublicAbiValidator.java index 6e14fd6f..5ef839fa 100644 --- a/src/main/java/gd/script/gdcc/lir/validation/LirPublicAbiValidator.java +++ b/src/main/java/gd/script/gdcc/lir/validation/LirPublicAbiValidator.java @@ -5,8 +5,7 @@ import gd.script.gdcc.lir.LirModule; import gd.script.gdcc.lir.LirPropertyDef; import gd.script.gdcc.lir.LirSignalDef; -import gd.script.gdcc.type.GdCompilerType; -import gd.script.gdcc.type.GdType; +import gd.script.gdcc.util.TypeCheckUtil; import org.jetbrains.annotations.NotNull; /// Validates that compiler-only types never leak into public ABI-like LIR surfaces. @@ -36,53 +35,35 @@ public void validateClass(@NotNull LirClassDef classDef) { public void validateFunction(@NotNull LirClassDef classDef, @NotNull LirFunctionDef functionDef) { for (var parameterDef : functionDef.getParameters()) { - requireNoCompilerOnlyType( + TypeCheckUtil.requireNonCompilerOnly( parameterDef.getType(), - "function parameter", - describeFunction(classDef, functionDef) + "(" + parameterDef.getName() + ")" + "function parameter at " + describeFunction(classDef, functionDef) + "(" + parameterDef.getName() + ")" ); } for (var captureDef : functionDef.getCaptures().values()) { - requireNoCompilerOnlyType( + TypeCheckUtil.requireNonCompilerOnly( captureDef.getType(), - "function capture", - describeFunction(classDef, functionDef) + "(" + captureDef.getName() + ")" + "function capture at " + describeFunction(classDef, functionDef) + "(" + captureDef.getName() + ")" ); } - requireNoCompilerOnlyType( + TypeCheckUtil.requireNonCompilerOnly( functionDef.getReturnType(), - "function return", - describeFunction(classDef, functionDef) + "function return at " + describeFunction(classDef, functionDef) ); } private void validateProperty(@NotNull LirClassDef classDef, @NotNull LirPropertyDef propertyDef) { - requireNoCompilerOnlyType( + TypeCheckUtil.requireNonCompilerOnly( propertyDef.getType(), - "property", - classDef.getName() + "." + propertyDef.getName() + "property at " + classDef.getName() + "." + propertyDef.getName() ); } private void validateSignal(@NotNull LirClassDef classDef, @NotNull LirSignalDef signalDef) { for (var parameterDef : signalDef.getParameters()) { - requireNoCompilerOnlyType( + TypeCheckUtil.requireNonCompilerOnly( parameterDef.getType(), - "signal parameter", - classDef.getName() + "." + signalDef.getName() + "(" + parameterDef.getName() + ")" - ); - } - } - - private void requireNoCompilerOnlyType(@NotNull GdType type, - @NotNull String surfaceName, - @NotNull String owner) { - if (type instanceof GdCompilerType compilerType) { - throw new IllegalArgumentException( - "compiler-only type leaked into " + surfaceName + ": " - + compilerType.getTypeName() - + " at " - + owner + "signal parameter at " + classDef.getName() + "." + signalDef.getName() + "(" + parameterDef.getName() + ")" ); } } diff --git a/src/main/java/gd/script/gdcc/util/TypeCheckUtil.java b/src/main/java/gd/script/gdcc/util/TypeCheckUtil.java new file mode 100644 index 00000000..45f0ff04 --- /dev/null +++ b/src/main/java/gd/script/gdcc/util/TypeCheckUtil.java @@ -0,0 +1,25 @@ +package gd.script.gdcc.util; + +import gd.script.gdcc.type.GdCompilerType; +import gd.script.gdcc.type.GdType; +import org.jetbrains.annotations.NotNull; + +/// Shared type-checking utilities used across the compiler pipeline. +public final class TypeCheckUtil { + private TypeCheckUtil() { + } + + /// Checks that the given type is not a compiler-only type and throws + /// {@link IllegalArgumentException} if it is. + /// + /// @param type the type to check + /// @param description a human-readable description of where the type appeared + /// (included in the exception message) + public static void requireNonCompilerOnly(@NotNull GdType type, @NotNull String description) { + if (type instanceof GdCompilerType compilerType) { + throw new IllegalArgumentException( + "compiler-only type leaked into " + description + ": " + compilerType.getTypeName() + ); + } + } +} diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/CGenHelperTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CGenHelperTest.java index 358751b3..c3ae8586 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/CGenHelperTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/CGenHelperTest.java @@ -229,6 +229,20 @@ void renderBoundMetadataShouldRejectVoidSlot() { assertTrue(ex.getMessage().contains("does not have outward GDExtension metadata"), ex.getMessage()); } + @Test + @DisplayName("renderBoundMetadata should reject compiler-only metadata slots") + void renderBoundMetadataShouldRejectCompilerOnlySlot() { + var ex = assertThrows( + IllegalArgumentException.class, + () -> helper.renderBoundMetadata(GdccForRangeIterType.FOR_RANGE_ITER, "godot_PROPERTY_USAGE_DEFAULT") + ); + + assertEquals( + "compiler-only type leaked into bound slot metadata: GdccForRangeIter", + ex.getMessage() + ); + } + @Test @DisplayName("renderBoundMetadata should add Variant usage flag without rewriting base usage") void renderBoundMetadataShouldAddVariantFlag() { @@ -643,6 +657,37 @@ void renderCallWrapperDestroyStmtShouldSkipObjectAndPrimitiveLocals() { assertEquals("", helper.renderCallWrapperDestroyStmt(GdIntType.INT, "value")); } + @Test + @DisplayName("call wrapper helpers should reject compiler-only types") + void callWrapperHelpersShouldRejectCompilerOnlyTypes() { + var typeGateEx = assertThrows( + IllegalArgumentException.class, + () -> helper.renderCallWrapperVariantTypeGate(GdccForRangeIterType.FOR_RANGE_ITER, "type") + ); + assertEquals( + "compiler-only type leaked into call wrapper type gate: GdccForRangeIter", + typeGateEx.getMessage() + ); + + var unpackEx = assertThrows( + IllegalArgumentException.class, + () -> helper.renderCallWrapperUnpackExpr(GdccForRangeIterType.FOR_RANGE_ITER, "value_ptr", "value_type") + ); + assertEquals( + "compiler-only type leaked into call wrapper unpack expression: GdccForRangeIter", + unpackEx.getMessage() + ); + + var destroyEx = assertThrows( + IllegalArgumentException.class, + () -> helper.renderCallWrapperDestroyStmt(GdccForRangeIterType.FOR_RANGE_ITER, "value") + ); + assertEquals( + "compiler-only type leaked into call wrapper destroy stmt: GdccForRangeIter", + destroyEx.getMessage() + ); + } + @Test @DisplayName("call wrapper type gate should keep narrow primitive widening rules") void renderCallWrapperVariantTypeGateShouldKeepNarrowPrimitiveWideningRules() { diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/CLoadPropertyInsnGenTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CLoadPropertyInsnGenTest.java index 38ee511e..a0f6b8d9 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/CLoadPropertyInsnGenTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/CLoadPropertyInsnGenTest.java @@ -21,6 +21,7 @@ import gd.script.gdcc.type.GdArrayType; import gd.script.gdcc.type.GdBoolType; import gd.script.gdcc.type.GdColorType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdFloatType; import gd.script.gdcc.type.GdFloatVectorType; import gd.script.gdcc.type.GdNilType; @@ -289,6 +290,36 @@ void enginePropertyGetterShouldFailWhenRawMethodMetadataMissing() { assertTrue(ex.getMessage().contains("METHOD_MISSING"), ex.getMessage()); } + @Test + @DisplayName("load_property should reject compiler-only result target") + void loadPropertyShouldRejectCompilerOnlyResultTarget() { + var gdccClass = new LirClassDef("MyClass", "RefCounted", false, false, Map.of(), List.of(), List.of(), List.of()); + gdccClass.addProperty(new LirPropertyDef("value", GdStringType.STRING, false, null, null, null, Map.of())); + + var func = new LirFunctionDef("use_value"); + func.setReturnType(GdVoidType.VOID); + func.addParameter(new LirParameterDef("obj", new GdObjectType("MyClass"), null, func)); + func.createAndAddVariable("tmp", GdccForRangeIterType.FOR_RANGE_ITER); + + var entry = new LirBasicBlock("entry"); + entry.appendInstruction(new LoadPropertyInsn("tmp", "value", "obj")); + entry.appendInstruction(new ReturnInsn(null)); + func.addBasicBlock(entry); + func.setEntryBlockId("entry"); + gdccClass.addFunction(func); + + var module = new LirModule("test_module", List.of(gdccClass)); + var ctx = newContext(new ExtensionAPI(null, List.of(), List.of(), List.of(), List.of(), List.of(), List.of(), List.of(), List.of()), + List.of(gdccClass)); + + var codegen = new CCodegen(); + codegen.prepare(ctx, module); + + var ex = assertThrows(InvalidInsnException.class, () -> codegen.generateFuncBody(gdccClass, func)); + assertInstanceOf(InvalidInsnException.class, ex); + assertTrue(ex.getMessage().contains("compiler-only type leaked into property load result target variable 'tmp'"), ex.getMessage()); + } + @Test @DisplayName("Unreadable engine property should throw") void unreadableEnginePropertyShouldThrow() { diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/CLoadStaticInsnGenTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CLoadStaticInsnGenTest.java index 90215bac..11a71ec8 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/CLoadStaticInsnGenTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/CLoadStaticInsnGenTest.java @@ -18,6 +18,7 @@ import gd.script.gdcc.lir.insn.LoadStaticInsn; import gd.script.gdcc.lir.insn.ReturnInsn; import gd.script.gdcc.scope.ClassRegistry; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdFloatVectorType; import gd.script.gdcc.type.GdIntType; import gd.script.gdcc.type.GdObjectType; @@ -220,6 +221,20 @@ void shouldRejectMissingEngineClassEnumValue() throws IOException { assertTrue(ex.getMessage().contains("Input")); } + @Test + @DisplayName("load_static should reject compiler-only result target") + void shouldRejectCompilerOnlyResultTarget() { + var ex = assertThrows( + InvalidInsnException.class, + () -> generateBody( + singletonFixtureApi(), + setupLoadStaticFunction(GdccForRangeIterType.FOR_RANGE_ITER, new LoadStaticInsn("out", "@GlobalScope", "GameSingleton")) + ) + ); + assertInstanceOf(InvalidInsnException.class, ex); + assertTrue(ex.getMessage().contains("compiler-only type leaked into static load result target variable 'out'"), ex.getMessage()); + } + @Test @DisplayName("load_static should reject missing builtin class enum value") void shouldRejectMissingBuiltinClassEnumValue() throws IOException { diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/COperatorInsnGenTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/COperatorInsnGenTest.java index 262d2395..29dcecd4 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/COperatorInsnGenTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/COperatorInsnGenTest.java @@ -17,6 +17,7 @@ import gd.script.gdcc.lir.insn.UnaryOpInsn; import gd.script.gdcc.scope.ClassRegistry; import gd.script.gdcc.type.GdBoolType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdFloatType; import gd.script.gdcc.type.GdIntType; import gd.script.gdcc.type.GdArrayType; @@ -461,6 +462,26 @@ void variantEvaluatePathUnpacksToBuiltinResultWithRuntimeTypeCheck() { assertTrue(body.contains("variant_evaluate type check failed for operator 'IN': expected bool"), body); } + @Test + @DisplayName("variant_evaluate path should reject compiler-only result target") + void variantEvaluatePathRejectsCompilerOnlyResultTarget() { + var ex = assertThrows( + InvalidInsnException.class, + () -> generateBody( + emptyApi(), + new BinaryOpInsn("result", GodotOperator.ADD, "left", "right"), + List.of( + new VariableSpec("left", GdVariantType.VARIANT, false), + new VariableSpec("right", GdVariantType.VARIANT, false), + new VariableSpec("result", GdccForRangeIterType.FOR_RANGE_ITER, false) + ) + ) + ); + + assertInstanceOf(InvalidInsnException.class, ex); + assertTrue(ex.getMessage().contains("compiler-only type leaked into operator result target variable 'result'"), ex.getMessage()); + } + @Test @DisplayName("variant_evaluate path should allow engine object subclass check before unpack") void variantEvaluatePathSupportsEngineObjectSubtypeCheck() { diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/CPackUnpackVariantInsnGenTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CPackUnpackVariantInsnGenTest.java index f8f090b7..3190397c 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/CPackUnpackVariantInsnGenTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/CPackUnpackVariantInsnGenTest.java @@ -3,6 +3,7 @@ import gd.script.gdcc.backend.CodegenContext; import gd.script.gdcc.backend.ProjectInfo; import gd.script.gdcc.enums.GodotVersion; +import gd.script.gdcc.exception.InvalidInsnException; import gd.script.gdcc.gdextension.ExtensionAPI; import gd.script.gdcc.gdextension.ExtensionGdClass; import gd.script.gdcc.lir.LirBasicBlock; @@ -15,6 +16,7 @@ import gd.script.gdcc.scope.ClassRegistry; import gd.script.gdcc.type.GdArrayType; import gd.script.gdcc.type.GdDictionaryType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdIntType; import gd.script.gdcc.type.GdObjectType; import gd.script.gdcc.type.GdStringNameType; @@ -28,6 +30,8 @@ import java.util.List; import java.util.Map; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -221,6 +225,54 @@ void unpackVariantToTypedDictionaryShouldUseNormalizedDictionarySymbol() { assertFalse(body.contains("godot_new_Dictionary[")); } + @Test + @DisplayName("pack_variant should reject compiler-only source") + void packVariantShouldRejectCompilerOnlySource() { + var workerClass = new LirClassDef("Worker", "RefCounted", false, false, Map.of(), List.of(), List.of(), List.of()); + var func = new LirFunctionDef("pack_compiler_only"); + func.setReturnType(GdVoidType.VOID); + func.createAndAddVariable("result", GdVariantType.VARIANT); + func.createAndAddVariable("value", GdccForRangeIterType.FOR_RANGE_ITER); + + var entry = new LirBasicBlock("entry"); + entry.appendInstruction(new PackVariantInsn("result", "value")); + entry.appendInstruction(new ReturnInsn(null)); + func.addBasicBlock(entry); + func.setEntryBlockId("entry"); + workerClass.addFunction(func); + + var module = new LirModule("test_module", List.of(workerClass)); + var codegen = newCodegen(module, emptyApi(), List.of(workerClass)); + + var ex = assertThrows(InvalidInsnException.class, () -> codegen.generateFuncBody(workerClass, func)); + assertInstanceOf(InvalidInsnException.class, ex); + assertTrue(ex.getMessage().contains("compiler-only type leaked into Variant pack source variable 'value'"), ex.getMessage()); + } + + @Test + @DisplayName("unpack_variant should reject compiler-only target") + void unpackVariantShouldRejectCompilerOnlyTarget() { + var workerClass = new LirClassDef("Worker", "RefCounted", false, false, Map.of(), List.of(), List.of(), List.of()); + var func = new LirFunctionDef("unpack_compiler_only"); + func.setReturnType(GdVoidType.VOID); + func.createAndAddVariable("result", GdccForRangeIterType.FOR_RANGE_ITER); + func.createAndAddVariable("variant", GdVariantType.VARIANT); + + var entry = new LirBasicBlock("entry"); + entry.appendInstruction(new UnpackVariantInsn("result", "variant")); + entry.appendInstruction(new ReturnInsn(null)); + func.addBasicBlock(entry); + func.setEntryBlockId("entry"); + workerClass.addFunction(func); + + var module = new LirModule("test_module", List.of(workerClass)); + var codegen = newCodegen(module, emptyApi(), List.of(workerClass)); + + var ex = assertThrows(InvalidInsnException.class, () -> codegen.generateFuncBody(workerClass, func)); + assertInstanceOf(InvalidInsnException.class, ex); + assertTrue(ex.getMessage().contains("compiler-only type leaked into Variant unpack target: GdccForRangeIter"), ex.getMessage()); + } + private ExtensionAPI emptyApi() { return new ExtensionAPI(null, List.of(), List.of(), List.of(), List.of(), List.of(), List.of(), List.of(), List.of()); } diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/CStorePropertyInsnGenTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CStorePropertyInsnGenTest.java index 383baac3..6ed703ab 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/CStorePropertyInsnGenTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/CStorePropertyInsnGenTest.java @@ -26,6 +26,7 @@ import gd.script.gdcc.type.GdFloatVectorType; import gd.script.gdcc.type.GdNilType; import gd.script.gdcc.type.GdObjectType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdStringNameType; import gd.script.gdcc.type.GdStringType; import gd.script.gdcc.type.GdVariantType; @@ -213,6 +214,30 @@ void gdccSetterCalledOutsideSetter() { assertTrue(body.contains("MyClass__field_setter_value($obj, $value);")); } + @Test + @DisplayName("store_property should reject compiler-only value before setter/property resolution") + void storePropertyShouldRejectCompilerOnlyValue() { + var gdccClass = new LirClassDef("MyClass", "RefCounted", false, false, Map.of(), List.of(), List.of(), List.of()); + gdccClass.addProperty(new LirPropertyDef("value", GdStringType.STRING, false, null, null, "_field_setter_value", Map.of())); + + var func = new LirFunctionDef("set_value"); + func.setReturnType(GdVoidType.VOID); + func.addParameter(new LirParameterDef("obj", new GdObjectType("MyClass"), null, func)); + func.addParameter(new LirParameterDef("value", GdccForRangeIterType.FOR_RANGE_ITER, null, func)); + addEntryStoreAndReturn(func, new StorePropertyInsn("value", "obj", "value")); + gdccClass.addFunction(func); + + var module = new LirModule("test_module", List.of(gdccClass)); + var ctx = newContext(emptyApi(), List.of(gdccClass)); + + var codegen = new CCodegen(); + codegen.prepare(ctx, module); + + var ex = assertThrows(InvalidInsnException.class, () -> codegen.generateFuncBody(gdccClass, func)); + assertInstanceOf(InvalidInsnException.class, ex); + assertTrue(ex.getMessage().contains("compiler-only type leaked into property store value variable 'value'"), ex.getMessage()); + } + @Test @DisplayName("Engine property should use engine setter") void enginePropertyUsesEngineSetter() { diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/CallGlobalInsnGenTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CallGlobalInsnGenTest.java index 8fd34453..6c0133c2 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/CallGlobalInsnGenTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/CallGlobalInsnGenTest.java @@ -284,6 +284,26 @@ void callGlobalShouldRejectCompilerOnlyInitHelper() { assertTrue(ex.getMessage().contains(GdccForRangeIterType.C_INIT_HELPER_NAME), ex.getMessage()); } + @Test + @DisplayName("CALL_GLOBAL should reject compiler-only fixed argument") + void callGlobalShouldRejectCompilerOnlyFixedArgument() { + var clazz = newTestClass(); + var func = newFunction("call_compiler_only_argument"); + func.createAndAddVariable("iter", GdccForRangeIterType.FOR_RANGE_ITER); + func.createAndAddVariable("ret", GdFloatType.FLOAT); + + entry(func).appendInstruction(new CallGlobalInsn( + "ret", + "deg_to_rad", + List.of(new LirInstruction.VariableOperand("iter")) + )); + clazz.addFunction(func); + + var ex = assertThrows(InvalidInsnException.class, () -> generateBody(clazz, func, utilityApi())); + assertInstanceOf(InvalidInsnException.class, ex); + assertTrue(ex.getMessage().contains("compiler-only type leaked into call_global argument #1 variable 'iter'"), ex.getMessage()); + } + @Test @DisplayName("CALL_GLOBAL should reject resultId for void utility") void callGlobalVoidUtilityWithResultId() { diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/CallMethodInsnGenTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CallMethodInsnGenTest.java index f7799e15..0e04e022 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/CallMethodInsnGenTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/CallMethodInsnGenTest.java @@ -22,6 +22,7 @@ import gd.script.gdcc.type.GdFloatType; import gd.script.gdcc.type.GdFloatVectorType; import gd.script.gdcc.type.GdIntType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdObjectType; import gd.script.gdcc.type.GdPackedNumericArrayType; import gd.script.gdcc.type.GdPackedVectorArrayType; @@ -942,6 +943,54 @@ void callMethodDynamicShouldRejectRefResult() { assertTrue(ex.getMessage().contains("cannot be a reference"), ex.getMessage()); } + @Test + @DisplayName("CALL_METHOD should reject compiler-only dynamic argument before Variant pack") + void callMethodShouldRejectCompilerOnlyDynamicArgument() { + var clazz = newClass("Worker"); + var func = newFunction("call_dynamic_with_compiler_only_arg"); + func.createAndAddVariable("obj", new GdObjectType("MysteryObject")); + func.createAndAddVariable("iter", GdccForRangeIterType.FOR_RANGE_ITER); + + entry(func).appendInstruction(new CallMethodInsn( + null, + "compute", + "obj", + List.of(new LirInstruction.VariableOperand("iter")) + )); + clazz.addFunction(func); + + var ex = assertThrows( + InvalidInsnException.class, + () -> generateBody(clazz, func, newApi(List.of(), List.of()), List.of(clazz)) + ); + assertInstanceOf(InvalidInsnException.class, ex); + assertTrue(ex.getMessage().contains("compiler-only type leaked into call_method argument #1"), ex.getMessage()); + } + + @Test + @DisplayName("CALL_METHOD should reject compiler-only dynamic result target before Variant unpack") + void callMethodShouldRejectCompilerOnlyDynamicResultTarget() { + var clazz = newClass("Worker"); + var func = newFunction("call_dynamic_into_compiler_only_result"); + func.createAndAddVariable("obj", new GdObjectType("MysteryObject")); + func.createAndAddVariable("ret", GdccForRangeIterType.FOR_RANGE_ITER); + + entry(func).appendInstruction(new CallMethodInsn( + "ret", + "compute", + "obj", + List.of() + )); + clazz.addFunction(func); + + var ex = assertThrows( + InvalidInsnException.class, + () -> generateBody(clazz, func, newApi(List.of(), List.of()), List.of(clazz)) + ); + assertInstanceOf(InvalidInsnException.class, ex); + assertTrue(ex.getMessage().contains("compiler-only type leaked into dynamic call result target variable 'ret'"), ex.getMessage()); + } + private LirClassDef newClass(String name) { return newClass(name, "RefCounted"); } diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/IndexLoadInsnGenTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/IndexLoadInsnGenTest.java index fbe7f897..5eb7c915 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/IndexLoadInsnGenTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/IndexLoadInsnGenTest.java @@ -17,6 +17,7 @@ import gd.script.gdcc.scope.ClassRegistry; import gd.script.gdcc.type.GdArrayType; import gd.script.gdcc.type.GdDictionaryType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdIntType; import gd.script.gdcc.type.GdStringNameType; import gd.script.gdcc.type.GdType; @@ -101,6 +102,25 @@ void variantGetNonVariantResultUnpack() { assertTrue(body.contains("$result = godot_new_int_with_Variant(&__gdcc_tmp_idx_ret_"), body); } + @Test + @DisplayName("variant_get should reject compiler-only result target") + void variantGetCompilerOnlyResultFails() { + var ex = assertThrows( + InvalidInsnException.class, + () -> generateBody( + new VariantGetInsn("result", "self", "key"), + List.of( + new VariableSpec("self", GdVariantType.VARIANT, false), + new VariableSpec("key", GdVariantType.VARIANT, false), + new VariableSpec("result", GdccForRangeIterType.FOR_RANGE_ITER, false) + ) + ) + ); + + assertInstanceOf(InvalidInsnException.class, ex); + assertTrue(ex.getMessage().contains("compiler-only type leaked into index load result target variable 'result'"), ex.getMessage()); + } + @Test @DisplayName("variant_get should fail-fast when resultId is missing") void variantGetMissingResultFails() { diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/IndexStoreInsnGenTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/IndexStoreInsnGenTest.java index 1816a328..99fda423 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/IndexStoreInsnGenTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/IndexStoreInsnGenTest.java @@ -17,6 +17,7 @@ import gd.script.gdcc.scope.ClassRegistry; import gd.script.gdcc.type.GdArrayType; import gd.script.gdcc.type.GdDictionaryType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdFloatVectorType; import gd.script.gdcc.type.GdIntType; import gd.script.gdcc.type.GdPackedNumericArrayType; @@ -105,6 +106,25 @@ void variantSetNonVariantValuePack() { assertTrue(body.contains("godot_Variant_destroy(&__gdcc_tmp_idx_val_variant_"), body); } + @Test + @DisplayName("variant_set should reject compiler-only value before Variant pack") + void variantSetRejectsCompilerOnlyValue() { + var ex = assertThrows( + InvalidInsnException.class, + () -> generateBody( + new VariantSetInsn("self", "key", "value"), + List.of( + new VariableSpec("self", GdVariantType.VARIANT, false), + new VariableSpec("key", GdVariantType.VARIANT, false), + new VariableSpec("value", GdccForRangeIterType.FOR_RANGE_ITER, false) + ) + ) + ); + + assertInstanceOf(InvalidInsnException.class, ex); + assertTrue(ex.getMessage().contains("compiler-only type leaked into index store value operand variable 'value'"), ex.getMessage()); + } + @Test @DisplayName("variant_set should pack ref String value operand") void variantSetRefStringValuePack() { From 68bc7fd9e2403b20dedec79c4130914d911398e3 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Tue, 30 Jun 2026 19:02:09 +0800 Subject: [PATCH 12/16] feat(backend): seal compiler-only type leaks on remaining codegen paths and sync docs - Add negative tests for compiler-only variable rejection in construct_builtin, own/release and argument address-of paths - Gate compiler-only type from builtin constructor synthesis and object lifecycle paths - Document compiler-only type contracts, LIR-only grammar boundaries and assignability exclusion in type/low-ir/runtime-lib specs - Record phase-9 completion in compiler-type plan with doc diff summary - Add parser guard tests for malformed compiler:: grammar variants --- doc/gdcc_low_ir.md | 15 +++++++- doc/gdcc_runtime_lib.md | 12 ++++++- doc/gdcc_type_system.md | 10 ++++++ .../frontend/frontend_gdcompiler_type_plan.md | 14 ++++++-- .../backend/c/gen/CBodyBuilderPhaseCTest.java | 11 ++++++ .../backend/c/gen/CConstructInsnGenTest.java | 16 +++++++++ .../c/gen/COwnReleaseObjectInsnGenTest.java | 23 ++++++++++++ .../gdcc/lir/parser/DomLirParserTest.java | 35 +++++++++++++++++++ 8 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/gdcc_low_ir.md b/doc/gdcc_low_ir.md index 740ab967..35c9a21c 100644 --- a/doc/gdcc_low_ir.md +++ b/doc/gdcc_low_ir.md @@ -24,7 +24,19 @@ manipulate variables. See [Types](gdcc_type_system.md) for details on the type system used in Low IR. -Low IR can also carry backend-owned compiler-only types. They use the `compiler::` textual grammar and are only valid on backend-owned local variables unless a dedicated validator explicitly allows a wider surface. +Low IR can also carry backend-owned compiler-only types. They use the `compiler::` textual grammar and are only valid on function-local `` storage unless a dedicated validator explicitly allows a wider surface. + +Current MVP contract: + +- Compiler-only type text is LIR-only and does not reuse source-facing declared-type parsing. +- `compiler::` is accepted only for function ``. +- Compiler-only types must not appear on: + - function `` + - function `` + - class `` + - signal parameters + - lambda captures +- The first concrete grammar instance is `compiler::GdccForRangeIter`. ## Instructions @@ -88,6 +100,7 @@ Current assignability rules in backend codegen are: - `Array[SubClass]` to `Array[SuperClass]`. - `Dictionary[K, V]` to `Dictionary` / `Dictionary[Variant, Variant]`. - `Dictionary[K1, V1]` to `Dictionary[K2, V2]` when `K1` is assignable to `K2` and `V1` is assignable to `V2`. +- Compiler-only types are not widened into ordinary ABI-facing assignability. They stay on backend-owned local/intrinsic paths. ``` $ = assign $ ``` diff --git a/doc/gdcc_runtime_lib.md b/doc/gdcc_runtime_lib.md index 275aaa2e..2ff1ea78 100644 --- a/doc/gdcc_runtime_lib.md +++ b/doc/gdcc_runtime_lib.md @@ -57,7 +57,17 @@ extend the runtime-provided `godot_*` surface. - `gdcc_intrinsic.h`: wrapper-only inbound materialization helpers for accepted Variant payloads whose runtime type differs from the published method metadata. It owns vector narrow-payload helpers and string-family `String` / `StringName` cross-case helpers; scalar `int -> float` inbound - materialization remains directly emitted by the generated wrapper code. + materialization remains directly emitted by the generated wrapper code. The same header also owns + compiler-only range-iterator runtime storage helpers: + - `gdcc_for_range_iter` is the backend-owned C storage struct for `compiler::GdccForRangeIter` + - `gdcc_for_range_iter_init()` is the prepare-block default initializer + - `gdcc_for_range_iter_destroy()` is the matching destroy hook; it is intentionally a no-op today + - `gdcc_for_range_iter_from_bounds()` materializes normalized `start/end/step` bounds and reports + `step == 0` through `godot_print_error(...)` before returning a non-looping fallback state + - `gdcc_for_range_iter_should_continue()`, `gdcc_for_range_iter_next()` and + `gdcc_for_range_iter_get()` implement the range intrinsic family + - these helpers are GDCC-owned runtime support and must keep the `gdcc_*` namespace instead of + pretending to be generated `godot_*` wrappers - `gdcc_helper.h`: the aggregate helper header included by generated entry code. It provides runtime error printing, Object property get/set helpers, RefCounted ownership helpers, GDCC wrapper pointer conversion helpers, compatibility constructors, UTF-8 formatting helpers, diff --git a/doc/gdcc_type_system.md b/doc/gdcc_type_system.md index f43f488e..ef4c94a7 100644 --- a/doc/gdcc_type_system.md +++ b/doc/gdcc_type_system.md @@ -10,6 +10,16 @@ - Meta/extension types: `GdMetaType`, `GdExtensionTypeEnum` for annotations and extension points. - Compiler-only types: `GdCompilerType` as the shared abstraction for backend-only storage types, with `GdccForRangeIterType` as the first concrete example. +## Compiler-only Types + +- `GdCompilerType` models GDCC-owned runtime storage that only exists inside compiler / lowering / LIR / backend pipelines. +- Compiler-only types are not part of the GDScript source-facing type set: + - declared type parsers must not resolve them + - type-meta and outward ABI metadata must not publish them + - ordinary user-facing semantic facts such as `expressionTypes()` and ordinary slot typing must not expose them +- `GdccForRangeIterType` is the first concrete `GdCompilerType`. It represents backend-owned range-iterator state storage, not a user-declarable `range(...)` result type. +- Compiler-only types may participate in LIR/backend-local assignment and intrinsic contracts, but they are outside the ordinary frontend assignment/conversion matrix. + ## Major Types (Summary) - `GdPrimitiveType`: atomic values. - `GdIntType`: integers. diff --git a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md index 215a7101..5910740b 100644 --- a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md +++ b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md @@ -6,8 +6,8 @@ ## 文档状态 -- 状态:阶段一至四已完成,后续阶段计划维护中 -- 更新时间:2026-06-29 +- 状态:阶段一至九已完成,进入维护中 +- 更新时间:2026-06-30 - 适用范围: - `src/main/java/gd/script/gdcc/type/**` - `src/main/java/gd/script/gdcc/scope/**` @@ -619,6 +619,16 @@ MVP 采用以下策略: ### 5.9 阶段九:文档同步与回归收口 +状态:已完成(2026-06-30)。 + +产出: + +- `doc/gdcc_type_system.md` 已补充 compiler-only type 的定位、`GdCompilerType` 抽象层归属以及 ordinary compatibility matrix exclusion。 +- `doc/gdcc_low_ir.md` 已补充 `compiler::` grammar 的 LIR-only 边界,并明确 MVP 仅允许 function `` 使用该语法。 +- `doc/gdcc_lir_intrinsic.md` 已收录 `gdcc.for_range_iter.init`、`gdcc.for_range_iter.should_continue`、`gdcc.for_range_iter.next`、`gdcc.for_range_iter.get` 四个 catalog 条目。 +- `doc/gdcc_c_backend.md` 与 `doc/gdcc_runtime_lib.md` 已同步 `gdcc_for_range_iter` C storage 与 `gdcc_for_range_iter_*` helper 的命名/边界,继续明确禁止回退到默认 `godot_*` helper 命名。 +- targeted regression 继续锚定 compiler-only type 只属于内部 storage typing,而不是 source-facing type。 + 目标: - 实现后所有事实源一致。 diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/CBodyBuilderPhaseCTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CBodyBuilderPhaseCTest.java index e160101f..0e7fddf9 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/CBodyBuilderPhaseCTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/CBodyBuilderPhaseCTest.java @@ -182,6 +182,17 @@ void testArrayArgumentWithAddressOf() { assertEquals("func(&$arr);\n", builder.build()); } + @Test + @DisplayName("Compiler-only iterator storage should be passed by reference (&)") + void testCompilerOnlyArgumentWithAddressOf() { + var iterVar = new LirVariable("iter", GdccForRangeIterType.FOR_RANGE_ITER, lirFunctionDef); + var value = builder.valueOfVar(iterVar); + + builder.callVoid("func", List.of(value)); + + assertEquals("func(&$iter);\n", builder.build()); + } + @Test @DisplayName("Ref variable should not add extra &") void testRefVariableNoExtraAddressOf() { diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/CConstructInsnGenTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CConstructInsnGenTest.java index 4606a52f..7dc609cc 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/CConstructInsnGenTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/CConstructInsnGenTest.java @@ -28,6 +28,7 @@ import gd.script.gdcc.type.GdBoolType; import gd.script.gdcc.type.GdDictionaryType; import gd.script.gdcc.type.GdFloatType; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdIntType; import gd.script.gdcc.type.GdObjectType; import gd.script.gdcc.type.GdPackedNumericArrayType; @@ -190,6 +191,21 @@ void constructBuiltinShouldRejectRefResultVariable() { assertTrue(ex.getMessage().contains("Result variable ID 'result' cannot be a reference")); } + @Test + @DisplayName("construct_builtin should reject compiler-only result types instead of inventing Godot helpers") + void constructBuiltinShouldRejectCompilerOnlyResultType() { + var clazz = newTestClass(); + var func = newFunction("construct_builtin_compiler_only_result"); + func.createAndAddVariable("iter", GdccForRangeIterType.FOR_RANGE_ITER); + + entry(func).appendInstruction(new ConstructBuiltinInsn("iter", List.of())); + clazz.addFunction(func); + + var ex = assertThrows(InvalidInsnException.class, () -> generateBody(clazz, func)); + assertTrue(ex.getMessage().contains("Builtin constructor validation failed"), ex.getMessage()); + assertFalse(ex.getMessage().contains("godot_new_GdccForRangeIter"), ex.getMessage()); + } + @Test @DisplayName("construct_builtin should reject unknown argument variables") void constructBuiltinShouldRejectUnknownArgumentVariable() { diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/COwnReleaseObjectInsnGenTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/COwnReleaseObjectInsnGenTest.java index 35f05495..dfc06436 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/COwnReleaseObjectInsnGenTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/COwnReleaseObjectInsnGenTest.java @@ -14,6 +14,7 @@ import gd.script.gdcc.lir.insn.TryOwnObjectInsn; import gd.script.gdcc.lir.insn.TryReleaseObjectInsn; import gd.script.gdcc.scope.ClassRegistry; +import gd.script.gdcc.type.GdccForRangeIterType; import gd.script.gdcc.type.GdIntType; import gd.script.gdcc.type.GdObjectType; import gd.script.gdcc.type.GdVoidType; @@ -130,6 +131,28 @@ void nonObjectVariableShouldThrow() { assertInstanceOf(InvalidInsnException.class, ex); } + @Test + @DisplayName("Compiler-only variable should be rejected before own/release object path") + void compilerOnlyVariableShouldThrow() { + var workerClass = new LirClassDef("Worker", "RefCounted", false, false, Map.of(), List.of(), List.of(), List.of()); + var func = new LirFunctionDef("invalid_compiler_only_own"); + func.setReturnType(GdVoidType.VOID); + func.createAndAddVariable("iter", GdccForRangeIterType.FOR_RANGE_ITER); + + var entry = new LirBasicBlock("entry"); + entry.appendInstruction(new TryOwnObjectInsn("iter", LifecycleProvenance.USER_EXPLICIT)); + func.addBasicBlock(entry); + func.setEntryBlockId("entry"); + workerClass.addFunction(func); + + var module = new LirModule("test_module", List.of(workerClass)); + var codegen = new CCodegen(); + codegen.prepare(newContext(new ExtensionAPI(null, List.of(), List.of(), List.of(), List.of(), List.of(), List.of(), List.of(), List.of()), List.of(workerClass)), module); + + var ex = assertThrows(InvalidInsnException.class, () -> codegen.generateFuncBody(workerClass, func)); + assertTrue(ex.getMessage().contains("not of object type"), ex.getMessage()); + } + @Test @DisplayName("AUTO_GENERATED provenance should be rejected for try_own_object") void autoGeneratedTryOwnShouldThrow() { diff --git a/src/test/java/gd/script/gdcc/lir/parser/DomLirParserTest.java b/src/test/java/gd/script/gdcc/lir/parser/DomLirParserTest.java index f36ac9f2..8ae625d2 100644 --- a/src/test/java/gd/script/gdcc/lir/parser/DomLirParserTest.java +++ b/src/test/java/gd/script/gdcc/lir/parser/DomLirParserTest.java @@ -11,6 +11,7 @@ import org.junit.jupiter.api.Test; import java.io.StringReader; +import java.util.List; import static org.junit.jupiter.api.Assertions.*; @@ -360,4 +361,38 @@ public void parse_rejectsUnknownCompilerOnlyGrammarWithoutGuessingObjectType() t assertTrue(exception.getMessage().contains("Unknown compiler-only type text: compiler::UnknownIter"), exception.getMessage()); assertFalse(exception.getMessage().contains("GdObjectType"), exception.getMessage()); } + + @Test + public void parse_rejectsMalformedCompilerOnlyGrammarWithoutGuessingObjectType() throws Exception { + var malformedTypeTexts = List.of("compiler::", "compiler:OnlyOneColon", "compiler ::GdccForRangeIter"); + + for (var typeText : malformedTypeTexts) { + var xml = """ + + + + + + + + + + + return; + + + + + """.formatted(typeText); + + var parser = new DomLirParser(new ClassRegistry(ExtensionApiLoader.loadDefault())); + var exception = assertThrows(IllegalArgumentException.class, () -> parser.parse(new StringReader(xml)), typeText); + + assertFalse(exception.getMessage().contains("GdObjectType"), exception.getMessage()); + assertTrue( + exception.getMessage().contains("compiler") || exception.getMessage().contains("Cannot parse type"), + exception.getMessage() + ); + } + } } From e493ded4de2003ce2ff61acfb0882da437aaec1d Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Wed, 1 Jul 2026 00:12:22 +0800 Subject: [PATCH 13/16] feat(backend): elevate compiler-only C storage contracts from implicit conventions to explicit protocol - Add isPassedByPointerInC/getCCopyHelperName/isDirectStructAssignmentSafe/validateCStorageContract triad to GdCompilerType interface - Migrate CGenHelper, CBodyBuilder, CBodyBuilderAliasSafetySupport and CCodegen to read explicit protocol over empty-string heuristics - Implement explicit protocol in GdccForRangeIterType to lock in direct-assignment and pointer-parameter behavior - Add fail-fast validation preventing silent fallback to godot_* copy paths for future compiler-only types - Extend test coverage for compiler-only assignment, return, destruct, alias-safety edges and protocol contract assertions --- .../frontend/frontend_gdcompiler_type_plan.md | 55 ++++++++++++ .../gdcc/backend/c/gen/CBodyBuilder.java | 22 +++++ .../c/gen/CBodyBuilderAliasSafetySupport.java | 21 ++++- .../script/gdcc/backend/c/gen/CCodegen.java | 2 +- .../script/gdcc/backend/c/gen/CGenHelper.java | 16 +++- .../gd/script/gdcc/type/GdCompilerType.java | 44 ++++++++++ .../gdcc/type/GdccForRangeIterType.java | 5 ++ .../CBodyBuilderAliasSafetySupportTest.java | 87 +++++++++++++++++++ .../backend/c/gen/CBodyBuilderPhaseCTest.java | 47 ++++++++++ .../gdcc/backend/c/gen/CCodegenTest.java | 38 ++++++++ .../backend/c/gen/CDestructInsnGenTest.java | 27 ++++++ .../gdcc/backend/c/gen/CGenHelperTest.java | 9 ++ .../script/gdcc/type/GdCompilerTypeTest.java | 14 +++ 13 files changed, 384 insertions(+), 3 deletions(-) create mode 100644 src/test/java/gd/script/gdcc/backend/c/gen/CBodyBuilderAliasSafetySupportTest.java diff --git a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md index 5910740b..cdc113d4 100644 --- a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md +++ b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md @@ -488,6 +488,61 @@ MVP 采用以下策略: - `CBodyBuilderPhaseBTest` - `CBodyBuilderPhaseCTest` +### 5.6a 阶段六-a:`GdCompilerType` 协议显式化(传参、赋值、copy) + +状态:已完成(2026-06-30)。 + +产出: + +- 在 `GdCompilerType` 中补充显式协议方法,描述 compiler-only 类型在 C 层是按引用传参还是按值传参、是否需要 deep copy helper,以及赋值是否可直接走 struct assignment。 +- 将 compiler-only 类型的 `init / copy / destroy` 三元组提升为接口级合同,避免 consumer 继续通过 `instanceof GdCompilerType` 加空字符串约定推导赋值语义。 +- `CGenHelper`、`CBodyBuilder`、`CBodyBuilderAliasSafetySupport` 等 consumer 迁移到新的接口语义,不再把“空 copy helper”当作唯一的 direct assignment 信号。 +- 新增 `GdCompilerType` 契约测试,覆盖默认传参与赋值行为。 +- 已完成:并行调研 `doc/gdcc_type_system.md`、`doc/gdcc_c_backend.md`、`doc/gdcc_ownership_lifecycle_spec.md`、`doc/analysis/gdcompiler_type_design_risk_analysis.md` 以及 backend 实现/测试,确认当前行为正确但依赖隐式协议。 +- 已完成:`GdCompilerType` 协议层已补入 `isPassedByPointerInC()`、`getCCopyHelperName()`、`isDirectStructAssignmentSafe()` 及一致性校验入口;`GdccForRangeIterType` 已显式声明当前合同,固定既有行为。 +- 已完成:`CGenHelper.renderCopyAssignFunctionName(...)`、`CBodyBuilder.needsAddressOf(...)`、`prepareRhsValue(...)`、`prepareReturnValue(...)` 与 `CBodyBuilderAliasSafetySupport.requiresStableCarrier(...)` 已迁移为优先读取 `GdCompilerType` 显式协议,不再把空 copy helper 当作唯一的 direct-assignment 语义来源。 +- 已完成:对 future compiler-only deep-copy 场景增加 fail-fast 防线;若 `isDirectStructAssignmentSafe()==false` 且缺少 copy helper,会在 helper / alias-safety 层直接报错,而不是静默落回 direct assignment。 +- 已完成:补充 builder / destroy 路径单测,锚定 compiler-only 传参仍走 `&slot`、赋值/返回仍走 direct assignment、`__prepare__` 首写不销毁旧值、`__finally__` auto-generated cleanup 仍调用 `gdcc_*_destroy`。 +- 已完成:新增 alias-safety 专项测试,确认 compiler-only direct assignment 不会误入 stable-carrier 路径,而 `String` / `Variant` 自赋值仍保持现有 stable-carrier 合同。 +- 已验证:`script/run-gradle-targeted-tests.sh --tests "gd.script.gdcc.type.GdCompilerTypeTest,gd.script.gdcc.backend.c.gen.CGenHelperTest,gd.script.gdcc.backend.c.gen.CBodyBuilderPhaseBTest,gd.script.gdcc.backend.c.gen.CBodyBuilderPhaseCTest,gd.script.gdcc.backend.c.gen.CDestructInsnGenTest,gd.script.gdcc.backend.c.gen.CBodyBuilderAliasSafetySupportTest,gd.script.gdcc.backend.c.gen.CCodegenTest"` 通过。 + +目标: + +- 把 compiler-only 类型在 C 后端的传参与赋值语义从隐式约定提升为显式协议。 +- 允许未来新的 compiler-only 类型自行声明:是否使用 `&` 传参、是否需要 copy helper、是否允许 direct struct assignment。 +- 保持当前 `GdccForRangeIterType` 的行为不变,但把它所依赖的默认合同固定下来。 + +建议实施内容: + +- 在 `GdCompilerType` 中增加方法,例如: + - `isPassedByPointerInC()`:是否在 C 参数位置使用引用传参。默认 `true`,匹配当前 compiler-only storage 作为 C struct 传入 helper 时使用 `&slot` 的行为。 + - `getCCopyHelperName()`:返回赋值 / 拷贝 helper 名;默认空字符串,表示可直接 struct assignment。 + - `isDirectStructAssignmentSafe()`:显式表达是否允许 direct assignment。默认可由 `getCCopyHelperName().isEmpty()` 派生,但 consumer 应优先读取该语义而不是直接解释空字符串。 +- 如需要保持 init 路径对称,可在 `CGenHelper` 增加 `renderCompilerOnlyInitFunctionName(GdCompilerType)`,使 init / copy / destroy helper 渲染统一经过 helper 层,同时避免误传普通 Godot 类型。 +- `CBodyBuilder.needsAddressOf(...)`、`prepareRhsValue(...)`、`prepareAssignedNonObjectRhs(...)`、`prepareReturnValue(...)` 改为优先读取 `GdCompilerType` 的显式协议。 +- `CBodyBuilderAliasSafetySupport.requiresStableCarrier(...)` 直接依据 compiler-only 的赋值合同判断,避免仅靠当前 `hasCopyHelper` 的间接推导。 +- 如果未来某个 compiler-only 类型需要 deep copy 或非引用传参,必须先在此阶段扩展接口合同,再改 consumer。 + +验收细则: + +- happy path: + - `GdccForRangeIterType` 仍然按值语义存储、按引用传参、direct assignment、显式 destroy。 + - 新增 compiler-only 类型时,可以只通过接口合同描述其传参/赋值行为。 +- negative path: + - consumer 不再仅依赖 `GdType` family 和空字符串约定推导 compiler-only 赋值/传参。 + - 未声明协议的新增 compiler-only 类型必须 fail-fast。 + - deep-copy compiler-only 类型如果没有提供 copy helper,不允许静默落回 direct assignment。 + +测试锚点: + +- `GdCompilerTypeTest` +- `CGenHelperTest` +- `CBodyBuilderPhaseBTest` +- `CBodyBuilderPhaseCTest` +- `CAssignInsnGenTest` +- `CDestructInsnGenTest` +- `CBodyBuilderAliasSafetySupportTest`(如存在) + ### 5.7 阶段七:`GdccForRangeIterType` intrinsic 最小闭环 状态:已完成(2026-06-30)。 diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/CBodyBuilder.java b/src/main/java/gd/script/gdcc/backend/c/gen/CBodyBuilder.java index 1bc9413c..0ef5ae57 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/CBodyBuilder.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/CBodyBuilder.java @@ -967,6 +967,10 @@ public static String renderDefaultValueExpr(@NotNull GdType type) { /// - Object pointers: NO /// - Value-semantic types (String, Variant, Array, etc.): YES private boolean needsAddressOf(@NotNull GdType type) { + if (type instanceof GdCompilerType compilerType) { + compilerType.validateCStorageContract(); + return compilerType.isPassedByPointerInC(); + } // Primitives are passed by value // Object pointers are already pointers // All other types (String, StringName, Variant, Array, Dictionary, etc.) @@ -986,6 +990,10 @@ private boolean needsAddressOf(@NotNull GdType type) { return new RenderResult(code, List.of()); } + if (checkCompilerOnlyDirectAssignment(type)) { + return new RenderResult(code, List.of()); + } + // Value-semantic types: need to copy // For String, StringName, Variant, Array, Dictionary, etc. var copyFunc = helper.renderCopyAssignFunctionName(type); @@ -1011,6 +1019,7 @@ private boolean needsAddressOf(@NotNull GdType type) { canDestroyOldValue(target), target, value, + value.type(), !helper.renderCopyAssignFunctionName(value.type()).isEmpty() )) { return PreparedAssignmentRhs.ordinary(rhsResult); @@ -1054,6 +1063,10 @@ private boolean needsAddressOf(@NotNull GdType type) { return new RenderResult(code, List.of()); } + if (checkCompilerOnlyDirectAssignment(type)) { + return new RenderResult(code, List.of()); + } + // Value-semantic types: need to copy for return var copyFunc = helper.renderCopyAssignFunctionName(type); if (!copyFunc.isEmpty()) { @@ -1065,6 +1078,15 @@ private boolean needsAddressOf(@NotNull GdType type) { return new RenderResult(code, List.of()); } + /// Compiler-only direct assignment is an explicit protocol, not an empty-helper side effect. + private boolean checkCompilerOnlyDirectAssignment(@NotNull GdType type) { + if (!(type instanceof GdCompilerType compilerType)) { + return false; + } + compilerType.validateCStorageContract(); + return compilerType.isDirectStructAssignmentSafe(); + } + private void emitTempDecls(@NotNull List temps) { for (var temp : temps) { declareTempVar(temp); diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/CBodyBuilderAliasSafetySupport.java b/src/main/java/gd/script/gdcc/backend/c/gen/CBodyBuilderAliasSafetySupport.java index 5eaa6a2a..2b402db0 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/CBodyBuilderAliasSafetySupport.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/CBodyBuilderAliasSafetySupport.java @@ -1,6 +1,8 @@ package gd.script.gdcc.backend.c.gen; +import gd.script.gdcc.type.GdCompilerType; import gd.script.gdcc.type.GdObjectType; +import gd.script.gdcc.type.GdType; import org.jetbrains.annotations.NotNull; import java.util.Objects; @@ -26,6 +28,7 @@ static boolean requiresStableCarrier( boolean canDestroyOldValue, @NotNull CBodyBuilder.TargetRef target, @NotNull CBodyBuilder.ValueRef value, + @NotNull GdType valueType, boolean hasCopyHelper ) { if (inPrepareBlock || !canDestroyOldValue) { @@ -35,7 +38,23 @@ static boolean requiresStableCarrier( if (!targetType.isDestroyable() || targetType instanceof GdObjectType) { return false; } - if (value.ownership() != CBodyBuilder.OwnershipKind.BORROWED || !hasCopyHelper) { + if (value.ownership() != CBodyBuilder.OwnershipKind.BORROWED) { + return false; + } + if (valueType instanceof GdCompilerType compilerType) { + compilerType.validateCStorageContract(); + if (compilerType.isDirectStructAssignmentSafe()) { + return false; + } + if (compilerType.getCCopyHelperName().isBlank()) { + throw new IllegalStateException( + "Compiler-only type '" + compilerType.getTypeName() + + "' requires a copy helper before alias-safe overwrite" + ); + } + return classifyNonObjectSlotWriteAliasSafety(target, value) == NonObjectSlotWriteAliasSafety.MAY_ALIAS; + } + if (!hasCopyHelper) { return false; } return classifyNonObjectSlotWriteAliasSafety(target, value) == NonObjectSlotWriteAliasSafety.MAY_ALIAS; diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/CCodegen.java b/src/main/java/gd/script/gdcc/backend/c/gen/CCodegen.java index dcc85b88..1e4f2e2b 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/CCodegen.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/CCodegen.java @@ -217,7 +217,7 @@ private void generateFunctionPrepareBlock() { var initInsn = switch (variable.type()) { case GdCompilerType compilerType -> new CallIntrinsicInsn( variable.id(), - compilerType.getCInitHelperName(), + helper.renderCompilerOnlyInitFunctionName(compilerType), List.of() ); case GdObjectType _ -> new LiteralNullInsn(variable.id()); diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/CGenHelper.java b/src/main/java/gd/script/gdcc/backend/c/gen/CGenHelper.java index 6b84ad2f..40452edc 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/CGenHelper.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/CGenHelper.java @@ -629,9 +629,22 @@ public boolean checkEngineMethodHelperRequiresLocalValueSlot(@NotNull EngineMeth } } + /// Render the prepare-block init helper for compiler-only storage. + /// + /// This is intentionally narrower than ordinary constructor/default-value rendering: + /// only `GdCompilerType` may use this helper-name path, while regular Godot builtins keep + /// their existing `Construct*` / literal initialization flow in `CCodegen`. + public @NotNull String renderCompilerOnlyInitFunctionName(@NotNull GdCompilerType compilerType) { + compilerType.validateCStorageContract(); + return compilerType.getCInitHelperName(); + } + public @NotNull String renderCopyAssignFunctionName(@NotNull GdType type) { return switch (type) { - case GdCompilerType _ -> ""; + case GdCompilerType compilerType -> { + compilerType.validateCStorageContract(); + yield compilerType.getCCopyHelperName(); + } case GdObjectType _, GdPrimitiveType _ -> ""; case GdVoidType _, GdNilType _ -> throw new IllegalArgumentException("Type " + type.getTypeName() + " does not support copy assignment"); @@ -647,6 +660,7 @@ public boolean checkEngineMethodHelperRequiresLocalValueSlot(@NotNull EngineMeth throw new IllegalArgumentException("Type " + type.getTypeName() + " is not destroyable"); } if (type instanceof GdCompilerType compilerType) { + compilerType.validateCStorageContract(); return compilerType.getCDestroyHelperName(); } if (type instanceof GdObjectType) { diff --git a/src/main/java/gd/script/gdcc/type/GdCompilerType.java b/src/main/java/gd/script/gdcc/type/GdCompilerType.java index bbc80b2c..41326ae8 100644 --- a/src/main/java/gd/script/gdcc/type/GdCompilerType.java +++ b/src/main/java/gd/script/gdcc/type/GdCompilerType.java @@ -27,6 +27,50 @@ public sealed interface GdCompilerType extends GdType /// C destroy helper function name for lifecycle cleanup (e.g. `gdcc_for_range_iter_destroy`). @NotNull String getCDestroyHelperName(); + /// Compiler-only storage is passed to C helpers by address unless a concrete type explicitly + /// opts into value passing. This keeps the current `&slot` ABI for internal helpers stable. + default boolean isPassedByPointerInC() { + return true; + } + + /// Compiler-only assignment defaults to direct struct assignment. + /// Concrete types that need deep-copy semantics must override this with a non-empty `gdcc_*` + /// helper and return `false` from `isDirectStructAssignmentSafe()`. + default @NotNull String getCCopyHelperName() { + return ""; + } + + /// Direct struct assignment is the default compiler-only copy contract. + /// Consumers read this semantic first instead of inferring behavior from an empty helper name. + default boolean isDirectStructAssignmentSafe() { + return getCCopyHelperName().isEmpty(); + } + + /// Defensive validation so future compiler-only types fail fast instead of silently falling back + /// to the wrong `godot_*` copy path. + default void validateCStorageContract() { + var copyHelperName = getCCopyHelperName(); + if (isDirectStructAssignmentSafe() && !copyHelperName.isBlank()) { + throw new IllegalStateException( + "Compiler-only type '" + getTypeName() + + "' must not publish a copy helper when direct struct assignment is enabled: " + + copyHelperName + ); + } + if (!isDirectStructAssignmentSafe() && copyHelperName.isBlank()) { + throw new IllegalStateException( + "Compiler-only type '" + getTypeName() + + "' requires a non-empty copy helper when direct struct assignment is unsafe" + ); + } + if (copyHelperName.startsWith("godot_")) { + throw new IllegalStateException( + "Compiler-only type '" + getTypeName() + + "' must not use godot_* copy helpers: " + copyHelperName + ); + } + } + /// Compiler-only types carry no GDExtension metadata. @Override default @Nullable GdExtensionTypeEnum getGdExtensionType() { diff --git a/src/main/java/gd/script/gdcc/type/GdccForRangeIterType.java b/src/main/java/gd/script/gdcc/type/GdccForRangeIterType.java index 5c3ebcb0..e9cfbbb8 100644 --- a/src/main/java/gd/script/gdcc/type/GdccForRangeIterType.java +++ b/src/main/java/gd/script/gdcc/type/GdccForRangeIterType.java @@ -36,4 +36,9 @@ public final class GdccForRangeIterType implements GdCompilerType { public @NotNull String getCDestroyHelperName() { return C_DESTROY_HELPER_NAME; } + + @Override + public boolean isDirectStructAssignmentSafe() { + return true; + } } diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/CBodyBuilderAliasSafetySupportTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CBodyBuilderAliasSafetySupportTest.java new file mode 100644 index 00000000..e5c3bb7d --- /dev/null +++ b/src/test/java/gd/script/gdcc/backend/c/gen/CBodyBuilderAliasSafetySupportTest.java @@ -0,0 +1,87 @@ +package gd.script.gdcc.backend.c.gen; + +import gd.script.gdcc.lir.LirFunctionDef; +import gd.script.gdcc.lir.LirVariable; +import gd.script.gdcc.type.GdStringType; +import gd.script.gdcc.type.GdVariantType; +import gd.script.gdcc.type.GdccForRangeIterType; +import gd.script.gdcc.type.GdVoidType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.LinkedHashMap; + +import static org.junit.jupiter.api.Assertions.*; + +class CBodyBuilderAliasSafetySupportTest { + @Test + @DisplayName("compiler-only direct-assignment overwrite should not require a stable carrier") + void compilerOnlyDirectAssignmentShouldNotRequireStableCarrier() { + var func = createFunctionDef(); + var iter = new LirVariable("iter", GdccForRangeIterType.FOR_RANGE_ITER, func); + var target = new CBodyBuilder.VarTargetRef(iter); + var value = new CBodyBuilder.VarValue(iter, CBodyBuilder.PtrKind.NON_OBJECT); + + var needsStableCarrier = CBodyBuilderAliasSafetySupport.requiresStableCarrier( + false, + true, + target, + value, + iter.type(), + false + ); + + assertFalse(needsStableCarrier, "compiler-only direct assignment must stay off the stable-carrier path"); + } + + @Test + @DisplayName("String self overwrite should still require a stable carrier") + void stringSelfOverwriteShouldRequireStableCarrier() { + var func = createFunctionDef(); + var valueVar = new LirVariable("s", GdStringType.STRING, func); + var target = new CBodyBuilder.VarTargetRef(valueVar); + var value = new CBodyBuilder.VarValue(valueVar, CBodyBuilder.PtrKind.NON_OBJECT); + + var needsStableCarrier = CBodyBuilderAliasSafetySupport.requiresStableCarrier( + false, + true, + target, + value, + valueVar.type(), + true + ); + + assertTrue(needsStableCarrier, "destroyable wrapper self-overwrite still needs a stable carrier"); + } + + @Test + @DisplayName("Variant self overwrite should still classify as may-alias") + void variantSelfOverwriteShouldStillClassifyAsMayAlias() { + var func = createFunctionDef(); + var valueVar = new LirVariable("payload", GdVariantType.VARIANT, func); + var target = new CBodyBuilder.VarTargetRef(valueVar); + var value = new CBodyBuilder.VarValue(valueVar, CBodyBuilder.PtrKind.NON_OBJECT); + + var aliasSafety = CBodyBuilderAliasSafetySupport.classifyNonObjectSlotWriteAliasSafety(target, value); + + assertSame(aliasSafety, CBodyBuilderAliasSafetySupport.NonObjectSlotWriteAliasSafety.MAY_ALIAS, "self overwrite must remain may-alias so wrapper types still stage a stable carrier"); + } + + private static LirFunctionDef createFunctionDef() { + return new LirFunctionDef( + "alias_safety", + false, + false, + false, + false, + false, + Collections.emptyMap(), + Collections.emptyList(), + Collections.emptyMap(), + GdVoidType.VOID, + Collections.emptyMap(), + new LinkedHashMap<>() + ); + } +} diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/CBodyBuilderPhaseCTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CBodyBuilderPhaseCTest.java index 0e7fddf9..9406b28c 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/CBodyBuilderPhaseCTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/CBodyBuilderPhaseCTest.java @@ -448,6 +448,38 @@ void testPrepareBlockNonObjectAssignSkipsDestroy() { "Prepare-block assignment should not materialize a copy temp"); } + @Test + @DisplayName("__prepare__ compiler-only assignment should keep first-write direct assignment without destroy") + void testPrepareBlockCompilerOnlyAssignSkipsDestroyAndCopyHelper() { + var prepareBlock = new LirBasicBlock("__prepare__"); + builder.beginBasicBlock("__prepare__"); + builder.setCurrentPosition(prepareBlock, 0, new NopInsn()); + var target = new LirVariable("iter", GdccForRangeIterType.FOR_RANGE_ITER, lirFunctionDef); + var source = new LirVariable("src", GdccForRangeIterType.FOR_RANGE_ITER, lirFunctionDef); + + builder.assignVar(builder.targetOfVar(target), builder.valueOfVar(source)); + + var result = builder.build(); + assertTrue(result.contains("$iter = $src;"), result); + assertFalse(result.contains("gdcc_for_range_iter_destroy(&$iter);"), result); + assertFalse(result.contains("godot_new_GdccForRangeIter_with_GdccForRangeIter"), result); + assertFalse(result.contains("__gdcc_tmp_"), result); + } + + @Test + @DisplayName("self compiler-only assignment should stay on direct assignment path without stable carrier") + void testSelfCompilerOnlyAssignmentStaysDirect() { + var target = new LirVariable("iter", GdccForRangeIterType.FOR_RANGE_ITER, lirFunctionDef); + + builder.assignVar(builder.targetOfVar(target), builder.valueOfVar(target)); + + var result = builder.build(); + assertTrue(result.contains("gdcc_for_range_iter_destroy(&$iter);"), result); + assertTrue(result.contains("$iter = $iter;"), result); + assertFalse(result.contains("__gdcc_tmp_gdccforrangeiter"), result); + assertFalse(result.contains("godot_new_GdccForRangeIter_with_GdccForRangeIter"), result); + } + @Test @DisplayName("RefCounted object assignment should capture old, own new, then release captured old") void testRefCountedObjectAssignment() { @@ -634,6 +666,21 @@ void testReturnObject() { assertEquals("return $obj;\n", objectBuilder.build()); } + @Test + @DisplayName("Returning compiler-only value should stay on explicit direct assignment contract") + void testReturnCompilerOnlyDirect() { + var iterBuilder = createBuilderWithReturnType(GdccForRangeIterType.FOR_RANGE_ITER); + setFinallyBlockContext(iterBuilder); + var iterVar = new LirVariable("iter", GdccForRangeIterType.FOR_RANGE_ITER, iterBuilder.func()); + + iterBuilder.returnValue(iterBuilder.valueOfVar(iterVar)); + + var result = iterBuilder.build(); + assertEquals("return $iter;\n", result); + assertFalse(result.contains("godot_new_GdccForRangeIter_with_GdccForRangeIter"), result); + assertFalse(result.contains("__gdcc_tmp_"), result); + } + @Test @DisplayName("Returning String expression should destroy temp after copy") void testReturnStringExprTempOrder() { diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/CCodegenTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CCodegenTest.java index d7ede7d4..ef4c19bf 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/CCodegenTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/CCodegenTest.java @@ -317,6 +317,44 @@ public void generateShouldEmitCompilerOnlyPrepareInitCallForLocalVariables() { assertFalse(cCode.contains("godot_new_GdccForRangeIter"), cCode); } + @Test + public void generateShouldKeepCompilerOnlyPrepareInitOnExplicitHelperPath() { + var workerClass = new LirClassDef("Worker", "RefCounted"); + var func = new LirFunctionDef("prepare_compiler_only_local_again"); + func.setReturnType(GdVoidType.VOID); + func.createAndAddVariable("iter", GdccForRangeIterType.FOR_RANGE_ITER); + var entryBlock = new LirBasicBlock("entry"); + entryBlock.appendInstruction(new ReturnInsn(null)); + func.addBasicBlock(entryBlock); + func.setEntryBlockId("entry"); + workerClass.addFunction(func); + + var module = new LirModule("compiler_only_prepare_again", List.of(workerClass)); + var classRegistry = new ClassRegistry(new ExtensionAPI( + null, + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of() + )); + ProjectInfo projectInfo = new ProjectInfo("test", GodotVersion.V451, Path.of(".")) { + }; + var ctx = new CodegenContext(projectInfo, classRegistry); + var codegen = new CCodegen(); + codegen.prepare(ctx, module); + + var files = codegen.generate(); + var cCode = generatedFileText(files, "entry.c"); + + assertTrue(cCode.contains("$iter = gdcc_for_range_iter_init();"), cCode); + assertFalse(cCode.contains("ConstructBuiltinInsn"), cCode); + assertFalse(cCode.contains("godot_new_GdccForRangeIter"), cCode); + } + @Test public void entryTemplateShouldSetMinimumInitializationLevelAndGuardLifecycleLevels() throws Exception { var workerClass = new LirClassDef("GDEntryWorker", "RefCounted"); diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/CDestructInsnGenTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CDestructInsnGenTest.java index a6465798..e123656e 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/CDestructInsnGenTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/CDestructInsnGenTest.java @@ -199,6 +199,33 @@ void autoGeneratedDestructRefCountedGdccObjectShouldUseReleaseObject() { assertFalse(body.contains("try_destroy_object(")); } + @Test + @DisplayName("AUTO_GENERATED destruct of compiler-only value should still use gdcc destroy helper in __finally__") + void autoGeneratedDestructCompilerOnlyShouldUseDestroyHelper() { + var workerClass = new LirClassDef("Worker", "RefCounted", false, false, Map.of(), List.of(), List.of(), List.of()); + var func = new LirFunctionDef("auto_destruct_compiler_only"); + func.setReturnType(GdVoidType.VOID); + func.createAndAddVariable("iter", GdccForRangeIterType.FOR_RANGE_ITER); + + var entry = new LirBasicBlock("entry"); + entry.appendInstruction(new ReturnInsn(null)); + func.addBasicBlock(entry); + + var finallyBlock = new LirBasicBlock("__finally__"); + finallyBlock.appendInstruction(new DestructInsn("iter", LifecycleProvenance.AUTO_GENERATED)); + finallyBlock.appendInstruction(new ReturnInsn(null)); + func.addBasicBlock(finallyBlock); + func.setEntryBlockId("entry"); + workerClass.addFunction(func); + + var module = new LirModule("test_module", List.of(workerClass)); + var codegen = newCodegen(module, List.of(workerClass), emptyApi()); + + var body = codegen.generateFuncBody(workerClass, func); + assertTrue(body.contains("gdcc_for_range_iter_destroy(&$iter);"), body); + assertFalse(body.contains("godot_GdccForRangeIter_destroy"), body); + } + @Test @DisplayName("AUTO_GENERATED destruct of unknown object should use try_release_object") void autoGeneratedDestructUnknownObjectShouldUseTryReleaseObject() { diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/CGenHelperTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CGenHelperTest.java index c3ae8586..3b99fe54 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/CGenHelperTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/CGenHelperTest.java @@ -69,6 +69,7 @@ void compilerOnlyRangeIteratorShouldUseGdccHelpersAndRejectVariantHelpers() { assertEquals("gdcc_for_range_iter", helper.renderGdTypeInC(type)); assertEquals("gdcc_for_range_iter*", helper.renderGdTypeRefInC(type)); assertEquals("GdccForRangeIter", helper.renderGdTypeName(type)); + assertEquals("gdcc_for_range_iter_init", helper.renderCompilerOnlyInitFunctionName(type)); assertEquals("", helper.renderCopyAssignFunctionName(type)); assertEquals("gdcc_for_range_iter_destroy", helper.renderDestroyFunctionName(type)); assertFalse(helper.renderGdTypeInC(type).contains("godot_")); @@ -77,6 +78,14 @@ void compilerOnlyRangeIteratorShouldUseGdccHelpersAndRejectVariantHelpers() { assertThrows(IllegalArgumentException.class, () -> helper.renderUnpackFunctionName(type)); } + @Test + @DisplayName("compiler-only helper rendering should keep explicit direct-assignment contract") + void compilerOnlyHelperRenderingShouldKeepExplicitDirectAssignmentContract() { + var type = GdccForRangeIterType.FOR_RANGE_ITER; + assertTrue(type.isDirectStructAssignmentSafe(), "existing compiler-only type should stay on direct assignment path"); + assertEquals("", helper.renderCopyAssignFunctionName(type), "consumer should still observe the explicit direct-assignment contract"); + } + @Test @DisplayName("checkVirtualMethod should accept exact engine virtual signatures") void checkVirtualMethodShouldAcceptExactEngineVirtualSignatures() { diff --git a/src/test/java/gd/script/gdcc/type/GdCompilerTypeTest.java b/src/test/java/gd/script/gdcc/type/GdCompilerTypeTest.java index 9344d9c3..d1b07890 100644 --- a/src/test/java/gd/script/gdcc/type/GdCompilerTypeTest.java +++ b/src/test/java/gd/script/gdcc/type/GdCompilerTypeTest.java @@ -58,6 +58,20 @@ void protocolMethodsReturnGdccHelpers() { assertEquals("gdcc_for_range_iter", type.getCStorageTypeName()); assertEquals("gdcc_for_range_iter_init", type.getCInitHelperName()); assertEquals("gdcc_for_range_iter_destroy", type.getCDestroyHelperName()); + assertTrue(type.isPassedByPointerInC()); + assertEquals("", type.getCCopyHelperName()); + assertTrue(type.isDirectStructAssignmentSafe()); + } + + @Test + @org.junit.jupiter.api.DisplayName("compiler-only default copy contract is direct struct assignment") + void defaultCopyContractIsDirectStructAssignment() { + var type = (GdCompilerType) GdccForRangeIterType.FOR_RANGE_ITER; + + assertTrue(type.isPassedByPointerInC(), "compiler-only helper ABI stays pointer-based by default"); + assertEquals("", type.getCCopyHelperName(), "direct-assignment compiler-only types expose empty copy helper"); + assertTrue(type.isDirectStructAssignmentSafe(), "compiler-only types default to direct struct assignment"); + type.validateCStorageContract(); } @Test From 00f2c0cf1c163f1f5d40cc6f83b3dac526392f78 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Thu, 2 Jul 2026 23:47:15 +0800 Subject: [PATCH 14/16] refactor(test,doc): decouple intrinsic tests from C header internals - Remove source-level assertions that tightly couple tests to C helper implementation text - Clarify zero-step fallback as always-terminating with step=1, document as programming error - Drop unused imports --- doc/gdcc_runtime_lib.md | 4 +++- .../c/gen/GdccForRangeIterIntrinsicTest.java | 16 ---------------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/doc/gdcc_runtime_lib.md b/doc/gdcc_runtime_lib.md index 2ff1ea78..56c3c37d 100644 --- a/doc/gdcc_runtime_lib.md +++ b/doc/gdcc_runtime_lib.md @@ -63,7 +63,9 @@ extend the runtime-provided `godot_*` surface. - `gdcc_for_range_iter_init()` is the prepare-block default initializer - `gdcc_for_range_iter_destroy()` is the matching destroy hook; it is intentionally a no-op today - `gdcc_for_range_iter_from_bounds()` materializes normalized `start/end/step` bounds and reports - `step == 0` through `godot_print_error(...)` before returning a non-looping fallback state + `step == 0` through `godot_print_error(...)` before returning an always-terminating fallback state with + `step=1`. A zero-step argument is a programming error; frontend lowering and generated code must not + emit zero-step range iterators under any normal path. - `gdcc_for_range_iter_should_continue()`, `gdcc_for_range_iter_next()` and `gdcc_for_range_iter_get()` implement the range intrinsic family - these helpers are GDCC-owned runtime support and must keep the `gdcc_*` namespace instead of diff --git a/src/test/java/gd/script/gdcc/backend/c/gen/GdccForRangeIterIntrinsicTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/GdccForRangeIterIntrinsicTest.java index 8f0d537c..661dc5b7 100644 --- a/src/test/java/gd/script/gdcc/backend/c/gen/GdccForRangeIterIntrinsicTest.java +++ b/src/test/java/gd/script/gdcc/backend/c/gen/GdccForRangeIterIntrinsicTest.java @@ -25,8 +25,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.Map; @@ -97,20 +95,6 @@ void rangeIterationIntrinsicsShouldEmitContinueNextAndGetHelpers() { """, fixture.builder().build()); } - @Test - @DisplayName("range iterator runtime helpers should anchor exclusive end negative step and zero step policy") - void runtimeHelpersShouldAnchorRangeIteratorSemantics() throws IOException { - var source = Files.readString(Path.of("src/main/c/codegen/include_451/gdcc/gdcc_intrinsic.h")); - - assertTrue(source.contains("typedef struct gdcc_for_range_iter"), source); - assertTrue(source.contains("godot_print_error(\"range step argument is zero\""), source); - assertTrue(source.contains("if (iter->step > 0)"), source); - assertTrue(source.contains("return iter->current < iter->end;"), source); - assertTrue(source.contains("return iter->current > iter->end;"), source); - assertTrue(source.contains(".current = iter->current + iter->step"), source); - assertTrue(source.contains("return iter->current;"), source); - } - @Test @DisplayName("range intrinsics should reject missing ref or wrong result slots") void rangeIntrinsicsShouldRejectBadResultSlots() { From 7f3ca28cf599aa1ab300c0f871c0a11397184076 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Fri, 3 Jul 2026 08:33:30 +0800 Subject: [PATCH 15/16] docs(frontend/type): promote implementation doc as long-term fact source - Replace the planning document with the implementation document as the canonical reference for compiler-only type - Update the four range-iterator intrinsic sections in the LIR spec to point at the new doc - Archive the planning doc now that all phases are completed and recorded - Tighten the doc comment of the compiler-only range-iterator type to reflect its post-MVP status - Keep the new implementation document untracked as the durable source of truth --- doc/gdcc_lir_intrinsic.md | 8 +- ...frontend_gdcompiler_type_implementation.md | 281 ++++++ .../frontend/frontend_gdcompiler_type_plan.md | 802 ------------------ .../gdcc/type/GdccForRangeIterType.java | 4 +- 4 files changed, 287 insertions(+), 808 deletions(-) create mode 100644 doc/module_impl/frontend/frontend_gdcompiler_type_implementation.md delete mode 100644 doc/module_impl/frontend/frontend_gdcompiler_type_plan.md diff --git a/doc/gdcc_lir_intrinsic.md b/doc/gdcc_lir_intrinsic.md index 53aa379c..b54aa707 100644 --- a/doc/gdcc_lir_intrinsic.md +++ b/doc/gdcc_lir_intrinsic.md @@ -206,7 +206,7 @@ Lifecycle / ownership: 长期事实源: -- `doc/module_impl/frontend/frontend_gdcompiler_type_plan.md` +- `doc/module_impl/frontend/frontend_gdcompiler_type_implementation.md` ### `gdcc.for_range_iter.should_continue` @@ -238,7 +238,7 @@ Lifecycle / ownership: 长期事实源: -- `doc/module_impl/frontend/frontend_gdcompiler_type_plan.md` +- `doc/module_impl/frontend/frontend_gdcompiler_type_implementation.md` ### `gdcc.for_range_iter.next` @@ -271,7 +271,7 @@ Lifecycle / ownership: 长期事实源: -- `doc/module_impl/frontend/frontend_gdcompiler_type_plan.md` +- `doc/module_impl/frontend/frontend_gdcompiler_type_implementation.md` ### `gdcc.for_range_iter.get` @@ -303,7 +303,7 @@ Lifecycle / ownership: 长期事实源: -- `doc/module_impl/frontend/frontend_gdcompiler_type_plan.md` +- `doc/module_impl/frontend/frontend_gdcompiler_type_implementation.md` ## 新增 Intrinsic Checklist diff --git a/doc/module_impl/frontend/frontend_gdcompiler_type_implementation.md b/doc/module_impl/frontend/frontend_gdcompiler_type_implementation.md new file mode 100644 index 00000000..61f3596f --- /dev/null +++ b/doc/module_impl/frontend/frontend_gdcompiler_type_implementation.md @@ -0,0 +1,281 @@ +# Frontend GdCompilerType 实现说明 + +> 本文档作为 `GdCompilerType` 及其 frontend / LIR / backend compiler-only storage 合同的长期事实源,记录当前定位、边界、协议、intrinsic 约束与维护规则。本文档替代旧的计划文档与阶段流水账,不再保留实施顺序、已完成阶段列表或进度记录。 + +## 文档状态 + +- 状态:事实源维护中(`GdCompilerType` sealed 抽象层、`GdccForRangeIterType`、LIR-only grammar、frontend/backend leak guards、public ABI validator、range iterator intrinsic 已落地) +- 更新时间:2026-07-03 +- 适用范围: + - `src/main/java/gd/script/gdcc/type/**` + - `src/main/java/gd/script/gdcc/frontend/**` + - `src/main/java/gd/script/gdcc/lir/**` + - `src/main/java/gd/script/gdcc/backend/c/**` + - `src/main/c/codegen/**` + - `src/test/java/gd/script/gdcc/type/**` + - `src/test/java/gd/script/gdcc/frontend/**` + - `src/test/java/gd/script/gdcc/lir/**` + - `src/test/java/gd/script/gdcc/backend/c/**` +- 关联文档: + - `doc/analysis/gdcompiler_type_design_risk_analysis.md` + - `doc/gdcc_type_system.md` + - `doc/gdcc_low_ir.md` + - `doc/gdcc_lir_intrinsic.md` + - `doc/gdcc_c_backend.md` + - `doc/gdcc_runtime_lib.md` + - `doc/gdcc_ownership_lifecycle_spec.md` + - `doc/module_impl/common_rules.md` + - `frontend_rules.md` + - `frontend_chain_binding_expr_type_implementation.md` + - `frontend_local_type_stabilization_implementation.md` + - `frontend_type_check_analyzer_implementation.md` + - `frontend_implicit_conversion_matrix.md` +- 明确非目标: + - 不把 compiler-only type 暴露为 GDScript source-facing declared type + - 不把 compiler-only type 纳入 ordinary implicit conversion matrix + - 不让 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 + +--- + +## 1. 当前定位与边界 + +### 1.1 当前定位 + +`GdCompilerType` 当前表示 GDCC compiler / lowering / LIR / backend 自己拥有的 runtime storage type。它的作用是: + +- 承载内部 local / temp variable 的静态类型 +- 作为 backend-owned intrinsic 的 operand / result type +- 为 C backend 提供稳定的 storage / init / copy / destroy 合同 + +它不是以下任何一种东西: + +- 用户可声明的 GDScript 类型 +- `Variant` 的特例或伪装载体 +- `TYPE_META` / `ScopeTypeMeta` 路由的一部分 +- Godot object / builtin metadata 的补充分支 + +当前首个 concrete type 固定为 `GdccForRangeIterType`,表示 `for i in range(...)` lowering 所需的 compiler-owned iterator state storage。它当前已经作为 compiler-only 类型体系的事实锚点存在,但这并不等于 frontend 已开放 `for` lowering 支持面。 + +### 1.2 禁止边界 + +`GdCompilerType` 当前明确禁止进入: + +- source-facing declared type parser / resolver +- `ScopeTypeMeta` namespace 与普通类型元值路由 +- ordinary `expressionTypes()` published fact +- ordinary local / parameter / property / return `slotTypes()` +- property / signal / callable outward ABI +- generated binding metadata 与 `call_func` wrapper surface +- ordinary typed boundary、`Variant` pack / unpack、dynamic call result unpack +- engine method / utility / global / operator / index / property 普通调用路径 + +当前工程语义固定为:compiler-only 类型只能在内部 storage typing 路径显式流动,任何越界都必须 fail-fast,而不是退化成默认 `Variant`、默认 `godot_*` helper 或 object guess。 + +--- + +## 2. 当前类型协议 + +### 2.1 抽象层合同 + +当前 `GdType` sealed hierarchy 通过 `GdCompilerType` 挂入 compiler-only 分支,而不是直接把某个 concrete type 挂在根层。`GdCompilerType` 当前稳定提供以下协议: + +- `getLirTypeText()`:LIR-only type text +- `getCStorageTypeName()`:C storage type 名称 +- `getCInitHelperName()`:prepare/init helper 名称 +- `getCDestroyHelperName()`:destroy helper 名称 +- `isPassedByPointerInC()`:C helper 传参形状 +- `getCCopyHelperName()`:deep-copy helper 名称;当前空字符串仅表示“没有专用 copy helper” +- `isDirectStructAssignmentSafe()`:是否允许 direct struct assignment +- `validateCStorageContract()`:用于 consumer 侧 fail-fast 的一致性校验入口 + +共享默认合同已经冻结为: + +- 无 GDExtension metadata +- 非 nullable +- destroyable non-object value +- 默认按地址传给 C helper + +consumer 必须优先读取显式语义方法,而不是继续通过 `getTypeName()`、空 helper 名或某个 concrete type 的 `instanceof` 约定去猜测赋值/传参行为。 + +### 2.2 `GdccForRangeIterType` 当前事实 + +`GdccForRangeIterType` 当前是唯一 concrete `GdCompilerType`,其合同固定为: + +- internal name:`GdccForRangeIter` +- LIR text:`compiler::GdccForRangeIter` +- C storage type:`gdcc_for_range_iter` +- init helper:`gdcc_for_range_iter_init` +- destroy helper:`gdcc_for_range_iter_destroy` +- 传参:按地址传入 C helper +- copy 语义:direct struct assignment + +当前代码库没有第二个 compiler-only type,因此所有扩展规则都必须以 `GdCompilerType` 抽象层为准,而不是继续复制 `GdccForRangeIterType` 的散点特判。 + +--- + +## 3. Frontend、LIR 与 ABI 合同 + +### 3.1 Frontend 侧边界 + +frontend 当前已经冻结以下 compiler-only 边界: + +- source-facing declared type resolver 不能解析 `compiler::GdccForRangeIter` 或 bare `GdccForRangeIter` +- ordinary typed-boundary compatibility 不接受 compiler-only source 或 target +- local `:=` stabilization、condition typing、expression type publication、writeback 与 lowering boundary materialization 都显式拒绝 compiler-only 泄漏 + +这意味着: + +- compiler-only type 不会作为 ordinary published expression fact 进入下游用户语义消费 +- compiler-only type 不会被 ordinary local/property/return 类型门禁当作普通 `GdType` 兼容结果静默放行 +- lowering 不会把 compiler-only value 物化成 `PackVariantInsn`、`UnpackVariantInsn`、`ConstructBuiltinInsn` 或普通 frontend boundary cast 路径 + +### 3.2 LIR XML 合同 + +当前 LIR-only grammar 已冻结为: + +- 文本形态:`compiler::` +- 当前唯一合法实例:`compiler::GdccForRangeIter` +- 当前只允许出现在 function `` + +以下 surface 明确禁止 compiler-only type: + +- function `` +- function `` +- `` +- `` parameter +- lambda `` +- hidden function parameter / return + +`DomLirParser` / `DomLirSerializer` 当前都按这条合同工作:local variable surface 可 round-trip;其余 ABI-like surface 必须 fail-fast,且不得退化成普通 object type 解析。 + +### 3.3 Public ABI validator + +`LirPublicAbiValidator` 当前是 compiler-only public ABI 边界的统一最终防线。它在 backend codegen 前工作,负责拒绝: + +- property type +- signal parameter type +- function parameter type +- function return type +- lambda capture type + +当前 hidden function 不享有 compiler-only ABI 豁免。若未来要开放 backend-owned hidden helper ABI,必须先同步更新本文档、`doc/gdcc_low_ir.md` 与对应 backend contract,再改实现。 + +--- + +## 4. Backend 与 Intrinsic 合同 + +### 4.1 C backend 当前合同 + +backend 当前对 compiler-only 类型的稳定合同是: + +- C type 渲染使用 `getCStorageTypeName()`,不走默认 `godot_` 路径 +- prepare block 的 compiler-only local 初始化走专用 intrinsic / helper 路径,不走 `ConstructBuiltinInsn` +- 赋值与返回遵守 `GdCompilerType` 显式 copy / direct-assignment 合同 +- destroy 走 `gdcc_*_destroy` helper,不落入默认 no-op +- public metadata、typed container leaf、wrapper unpack/destroy、普通 Godot runtime 路径都显式拒绝 compiler-only type + +当前 `GdccForRangeIterType` 的成功路径固定使用: + +- `gdcc_for_range_iter` storage +- `gdcc_for_range_iter_init` prepare helper +- `gdcc_for_range_iter_destroy` cleanup helper +- direct struct assignment + +因此当前生成结果中不应出现: + +- `godot_GdccForRangeIter` +- `godot_new_GdccForRangeIter...` +- `godot_new_Variant_with_GdccForRangeIter` +- `godot_new_GdccForRangeIter_with_Variant` +- `godot_GdccForRangeIter_destroy` + +### 4.2 Range iterator intrinsic 当前合同 + +当前 backend-owned range iterator intrinsic 已冻结为四个: + +- `gdcc.for_range_iter.init` +- `gdcc.for_range_iter.should_continue` +- `gdcc.for_range_iter.next` +- `gdcc.for_range_iter.get` + +它们的当前类型合同是: + +- `init(start: int, end: int, step: int) -> compiler::GdccForRangeIter` +- `should_continue(iter: compiler::GdccForRangeIter) -> bool` +- `next(iter: compiler::GdccForRangeIter) -> compiler::GdccForRangeIter` +- `get(iter: compiler::GdccForRangeIter) -> int` + +当前 runtime / lifecycle 合同同步冻结为: + +- helper 只操作 compiler-owned iterator state,不引入 Godot object ownership +- `init` 使用 `gdcc_for_range_iter_from_bounds(...)` 建立按值 iterator state +- `step == 0` 通过明确 runtime error helper 路径处理,不允许静默形成无限循环语义 +- `should_continue` / `next` 对正负步长的边界处理以 runtime helper 语义为准 + +prepare block 额外保留 `gdcc_for_range_iter_init()` 作为 local 默认初始化 helper,它服务的是 slot 生命周期初始化,而不是 `range(...)` 语义层面的 bounds 归一化。 + +--- + +## 5. 工程约束与维护规则 + +### 5.1 单一真源约束 + +以下事实源分工继续保持冻结: + +- `frontend_implicit_conversion_matrix.md`:ordinary typed-boundary conversion 的唯一真源 +- `doc/gdcc_low_ir.md`:LIR XML surface 与 `compiler::` grammar 的唯一真源 +- `doc/gdcc_lir_intrinsic.md`:intrinsic catalog 与 textual shape 的唯一真源 +- `doc/gdcc_c_backend.md` / `doc/gdcc_runtime_lib.md`:C helper / runtime helper 命名与行为边界的唯一真源 + +本文档不维护第二份 ordinary conversion matrix,也不重复 intrinsic catalog 的逐条实现细节;这里只记录 compiler-only 类型这条 feature 的边界与接线合同。 + +### 5.2 扩展规则 + +未来若新增第二个及以上 `GdCompilerType`,必须同时满足: + +- 先在本文档补充其定位与边界,再改代码 +- 明确声明 LIR text、C storage、init/copy/destroy 合同 +- 明确它是否允许 direct struct assignment、是否按地址传参 +- 明确它是否需要新的 intrinsic catalog 与 runtime helper +- 同步补齐 frontend leak guard、LIR parser/serializer、public ABI validator、backend helper 与 targeted tests + +任何扩展都不得通过下列方式偷渡: + +- 复用 `Variant` 路径伪装 compiler-only carrier +- 复用默认 `godot_*` helper 命名 +- 让 parser / resolver / metadata generator 通过普通 `GdType` 分支静默接受新类型 +- 把 hidden function 当作绕过 ABI validator 的后门 + +### 5.3 错误信息与恢复约定 + +compiler-only 泄漏的错误信息应继续保持明确语义,例如 `compiler-only type leaked into ...`。目标是让故障停在最早的合同边界,而不是把问题下沉成: + +- `getGdExtensionType() == null` +- 缺失 `godot_*` symbol +- object / builtin / `Variant` 默认路径的晚期异常 + +--- + +## 6. 稳定测试锚点 + +当前与 compiler-only type 合同直接相关的稳定测试锚点包括: + +- 类型协议:`GdCompilerTypeTest`、`GdccForRangeIterTypeTest` +- frontend 边界:`FrontendVariantBoundaryCompatibilityTest`、`FrontendLocalTypeStabilizationAnalyzerTest`、`FrontendTypeCheckAnalyzerTest`、`FrontendBodyLoweringSessionTest`、`FrontendLoweringBodyInsnPassTest`、`FrontendWritableTypeWritebackSupportTest` +- LIR parser / serializer / validator:`DomLirParserTest`、`DomLirSerializerTest`、`LirPublicAbiValidatorTest`、`SimpleLirBlockInsnParserTest`、`SimpleLirBlockInsnSerializerTest` +- backend / intrinsic:`CGenHelperTest`、`CBodyBuilderPhaseBTest`、`CBodyBuilderPhaseCTest`、`CDestructInsnGenTest`、`CBodyBuilderAliasSafetySupportTest`、`CCodegenTest`、`CIntrinsicManagerTest`、`CallIntrinsicInsnGenTest`、`GdccForRangeIterIntrinsicTest` + +后续工程若改动 compiler-only 行为,应优先用这些 targeted tests 锚定回归,而不是只依赖全量 build 或手工检查生成 C 代码。 + +--- + +## 7. 当前结论 + +当前代码库已经把 `GdCompilerType` 固定为“GDCC 内部 runtime storage typing”这一角色,而不是 source-facing type system 的一部分。后续工程若继续扩展 compiler-only 类型,必须保持这三个总原则不变: + +- 先封边界,再开成功路径 +- 先补事实源,再扩实现 +- 所有跨层 consumer 都必须面向 `GdCompilerType` 显式合同,而不是回退到默认 `GdType` / `Variant` / `godot_*` 路径 diff --git a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md b/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md deleted file mode 100644 index cdc113d4..00000000 --- a/doc/module_impl/frontend/frontend_gdcompiler_type_plan.md +++ /dev/null @@ -1,802 +0,0 @@ -# Frontend GdCompilerType Plan - -> 本文档记录 `GdCompilerType` 的实施计划、跨模块边界与验收细则。 -> `GdCompilerType` 只用于 GDCC compiler / lowering / LIR / backend 内部 runtime storage typing, -> 不属于 GDScript source-facing 类型系统。 - -## 文档状态 - -- 状态:阶段一至九已完成,进入维护中 -- 更新时间:2026-06-30 -- 适用范围: - - `src/main/java/gd/script/gdcc/type/**` - - `src/main/java/gd/script/gdcc/scope/**` - - `src/main/java/gd/script/gdcc/frontend/**` - - `src/main/java/gd/script/gdcc/lir/**` - - `src/main/java/gd/script/gdcc/backend/c/**` - - `src/main/c/codegen/**` -- 关联文档: - - `doc/analysis/gdcompiler_type_design_risk_analysis.md` - - `doc/gdcc_type_system.md` - - `doc/gdcc_low_ir.md` - - `doc/gdcc_lir_intrinsic.md` - - `doc/gdcc_c_backend.md` - - `doc/gdcc_runtime_lib.md` - - `doc/gdcc_ownership_lifecycle_spec.md` - - `doc/module_impl/common_rules.md` - - `doc/module_impl/frontend/frontend_rules.md` - - `doc/module_impl/frontend/frontend_implicit_conversion_matrix.md` - - `doc/module_impl/frontend/frontend_lowering_(un)pack_implementation.md` - - `doc/module_impl/frontend/frontend_dynamic_call_lowering_implementation.md` - - Godot docs:`tutorials/scripting/gdscript/gdscript_advanced.rst` 的 `range(...)` / custom iterator 说明 - ---- - -## 0. 维护合同 - -- 本文档是 `GdccCompilerType` 实施顺序、禁止边界和验收细则的计划事实源。 -- `frontend_implicit_conversion_matrix.md` 仍是 ordinary typed-boundary compatibility 的唯一真源;本文档不得维护第二份 - source/target conversion 矩阵。 -- `gdcc_lir_intrinsic.md` 仍是 `call_intrinsic` surface、backend registry 和 intrinsic catalog 的事实源;本文档只规定 - compiler-only 类型必须通过该通道操作。 -- `gdcc_low_ir.md` 仍是 LIR XML surface 的事实源;本文档的 LIR XML 计划落地后必须同步更新该文档。 -- 实现时不得用 `Variant`、`DYNAMIC`、`TYPE_META` 或 Godot object metadata 伪装 compiler-only storage。 -- 若实现过程中发现某个计划项需要改变 source-facing typing、ordinary boundary、public ABI 或 runtime helper - 命名,必须先更新对应事实源文档,再修改代码与测试。 -- 参考`gdcompiler_type_design_risk_analysis.md`中的内容进行实施,`GdccCompilerType`是值语义类型,使用复制语义传参,必须使用intrinsic进行操作。 - ---- - -## 1. 目标边界 - -`GdCompilerType` 的设计目标: - -- 表示 compiler/backend 为运行时实现需要保存的 C storage 类型。 -- 允许作为 LIR 内部 local/temp variable 的类型。 -- 允许作为 backend-owned intrinsic 的 operand / result type。 -- 每个具体 compiler-only 类型必须提供: - - C storage type name。 - - init helper function name。 - - destroy helper function name。 -- compiler-only 类型按值传递且不可变,这是设计前提,不在实现中额外证明。 - -`GdCompilerType` 明确不允许进入: - -- source-facing declared type parser。 -- `ScopeTypeMeta` / type-meta namespace。 -- 用户可见 `expressionTypes()` 语义事实。 -- 用户 ordinary `slotTypes()`,包括 local / parameter / property / return slot。 -- 自定义函数公开签名。 -- property / signal / callable outward ABI。 -- `Variant` pack / unpack。 -- engine method / utility / global / property / index / operator ABI。 -- generated `call_func` wrapper metadata、argument unpack 或 cleanup surface。 - -Godot upstream 的对齐依据是:`GDScriptFunctionState` 注册为 internal class,脚本 analyzer 不把它作为可声明标识符;for-loop -iterator state 也停留在 bytecode / VM stack slot / opcode operand 层。GDCC 的 iterator、function state 等 C runtime -storage 同样应保持在 IR / backend / runtime support 层,而不是扩展成 GDScript source-facing 类型。 - -首个 concrete compiler-only type 固定为 `GdccForRangeIterType`,服务未来 GDScript `for i in range(...)` lowering。Godot -文档说明 `range` 支持 `range(n)`、`range(b, n)`、`range(b, n, s)` 三种形态,起点 inclusive、终点 exclusive,默认起点为 `0` -、默认步长为 `1`,负步长用于反向迭代;custom iterator 合同也把初始化、推进、取值拆成 `_iter_init()`、`_iter_next()`、 -`_iter_get()` 三类动作。当前阶段不实现 `for` lowering,只用 `GdccForRangeIterType` 与对应 intrinsics 验证 compiler-only -type 的 LIR/backend 内部通路。 - ---- - -## 2. 当前冲突面 - -当前 `GdType` 被多个层面共同消费。新增 compiler-only 分支时,以下默认路径会静默误处理: - -- `GdType` 当前 sealed permits 不包含 compiler-only 分支。 -- `ClassRegistry.tryParseStrictTextType(...)` 和 `ScopeTypeResolver.tryResolveDeclaredType(...)` 是 source-facing - declared type 入口,不能认识 compiler-only 名称。 -- `ClassRegistry.findType(...)` 当前被 `DomLirParser` 用于 LIR XML 类型文本解析;若 LIR XML 需要 compiler-only - 类型,不能把它塞进普通 declared type parser。 -- `ClassRegistry.checkAssignable(...)` 第一条规则是 `getTypeName()` 同名即 assignable,会让 compiler-only 类型穿过 - ordinary typed boundary。 -- `FrontendVariantBoundaryCompatibility` 当前会允许 stable type -> `Variant` pack、`Variant` -> concrete unpack,并在 - default 分支回退 `checkAssignable(...)`。 -- `FrontendBodyLoweringSession.materializeFrontendBoundaryValue(...)` 会把 accepted boundary 物化为 `PackVariantInsn`、 - `UnpackVariantInsn`、`CallIntrinsicInsn` 或 `ConstructBuiltinInsn`。 -- condition lowering 对非 `bool` / `Variant` stable type 当前可能走 `pack_variant -> unpack_variant(bool)`。 -- dynamic call backend path 会把非 `Variant` 参数 pack 成 `Variant`,也可能把 dynamic result unpack 到 target type。 -- `DomLirSerializer` 直接写 `GdType.getTypeName()`;`DomLirParser` 直接用 `ClassRegistry.findType(...)` 读回类型文本。 -- `CGenHelper.renderGdTypeInC(...)` / `renderGdTypeRefInC(...)` default 会生成 `godot_` / `godot_*`。 -- `CGenHelper.renderPackFunctionName(...)` / `renderUnpackFunctionName(...)` default 会生成 - `godot_new_Variant_with_` / `godot_new__with_Variant`。 -- `CGenHelper.renderCopyAssignFunctionName(...)` / `renderDestroyFunctionName(...)` default 会生成 - `godot_new__with_` / `godot__destroy`。 -- `CCodegen.generateFunctionPrepareBlock(...)` 对未知非 void local default 到 `ConstructBuiltinInsn`。 -- `CBodyBuilder.renderDefaultValueExpr(...)` 对未知 type default 到 `godot_new_()`。 -- `DestructInsnGen` 当前只显式处理 Godot value/object/meta/container family;compiler-only destroyable type 不能落到 - no-op。 -- `CGenHelper.renderBoundMetadata(...)` 会通过 `getGdExtensionType() == null` late fail,但这不是足够早的 public ABI 边界。 -- `func.ftl` 对函数返回和参数直接调用 `renderGdTypeInC(...)` / `renderGdTypeRefInC(...)`,public 函数签名若混入 - compiler-only type 会生成错误 C ABI。 - ---- - -## 3. 实施原则 - -- 先定义边界,再放开路径。任何默认兼容、默认 C helper 命名、默认 metadata 生成都必须对 compiler-only type 显式处理。 -- 实现应从 `GdccForRangeIterType` 和一组 range iterator intrinsic 闭环开始;随后补入 `GdCompilerType` 作为 compiler-only - 类型的共同抽象层,并把已实现的具体类型迁移到该层下。 -- compiler-only 类型不参与 ordinary frontend typed-boundary matrix。内部 LIR slot 的 direct assignment 由 backend/LIR - 合同处理,不由 source-facing semantic compatibility 扩面。 -- helper 命名使用 `gdcc_*`,不得伪造 `godot_*` generated binding helper。 -- lifecycle 走非对象 destroyable value 路径,不走 object ownership。 -- parser、serializer、backend 和 frontend boundary 的错误信息应明确说明 `compiler-only type leaked into ...`,避免让使用者看到晚期 - `getGdExtensionType()==null` 或 C symbol 缺失。 - ---- - -## 4. LIR XML 策略 - -MVP 采用以下策略: - -- 允许 compiler-only 类型出现在 LIR XML 的 function `` 中。 -- 类型文本使用 LIR-only grammar:`compiler::`;本阶段唯一合法实例是 `compiler::GdccForRangeIter`。 -- 该 grammar 只由 LIR parser / serializer 识别,不进入 `ScopeTypeResolver`、`ClassRegistry.tryParseStrictTextType(...)` 或 - source-facing type-meta namespace。 -- compiler-only 类型禁止出现在以下 LIR XML surface: - - function ``。 - - function ``。 - - ``。 - - `` parameter。 - - lambda ``。 -- `is_hidden=true` 函数在 MVP 中也不允许 compiler-only parameter / return type。若未来 backend-owned hidden helper 需要 - compiler-only ABI,必须先单独更新本文档和 `gdcc_low_ir.md`,并证明不会生成 outward binding metadata 或 call wrapper。 - -验收细则: - -- happy path: - - `` 可解析为 `GdccForRangeIterType`。 - - `DomLirSerializer` 对该 compiler-only local 输出稳定的 `compiler::GdccForRangeIter`。 - - parser / serializer round-trip 不通过 ordinary source-facing resolver。 -- negative path: - - 用户 declared type resolver 不能解析 `compiler::GdccForRangeIter` 或 bare `GdccForRangeIter`。 - - property / signal / parameter / capture / return type 使用 `compiler::GdccForRangeIter` 时 fail-fast。 - - malformed `compiler::` grammar 报错清晰,不退化成 `GdObjectType("compiler::...")`。 - ---- - -## 5. 分步骤实施计划 - -### 5.1 阶段一:类型协议与 `GdccForRangeIterType` - -状态:已完成(2026-06-29)。 - -产出: - -- `GdType` sealed hierarchy 最初直接允许 `GdccForRangeIterType`,阶段四已补入 `GdCompilerType` 抽象层并完成回迁。 -- `GdccForRangeIterType` 固定内部名、LIR-only text、C storage/init/destroy helper、不可空、无 GDExtension - metadata、destroyable 协议。 -- C helper 对该类型使用 `gdcc_for_range_iter` / `gdcc_for_range_iter_destroy`,Variant pack/unpack 和 default-value 路径 - fail-fast,copy assignment 不走 `godot_new_*_with_*`。 -- Frontend ordinary boundary/writeback helper 对该类型显式拒绝,避免阶段一后由默认兼容路径泄漏到用户语义。 -- 本阶段未新增任何 `for` parser / analyzer / lowering 行为;prepare 初始化仍按计划留给阶段五专用 init 路径。 - -目标: - -- 让 `GdType` 能承载 compiler-only storage type,但不把它暴露给用户类型系统。 -- 锁定 `GdccForRangeIterType` 的 C storage、init、destroy 协议。 -- 为未来 `for i in range(...)` lowering 准备内部状态类型;本阶段不实现 `for` 语义 lowering。 - -建议实施内容: - -- 更新 `GdType` sealed permits。 -- 新增首个具体 compiler-only type:`GdccForRangeIterType`。先完成首个 concrete type 再在阶段四补入 `GdCompilerType` - 抽象层并回迁,避免为一个实现过早制造抽象层。 -- 类型协议至少覆盖: - - `getTypeName()`:建议稳定为 `GdccForRangeIter`,用于内部 identity,不作为 source-facing declared type text。 - - LIR-only text:`compiler::GdccForRangeIter`。 - - C storage type name:建议为 `gdcc_for_range_iter`。 - - init helper function name:建议为 `gdcc_for_range_iter_init`。 - - destroy helper function name:建议为 `gdcc_for_range_iter_destroy`。 - - `isNullable() == false`。 - - `getGdExtensionType() == null`。 - - `isDestroyable()` 根据 destroy helper 策略返回 true。 -- `GdccForRangeIterType` 代表按值保存的 range iterator state,不表示 GDScript `range(...)` 调用返回的 `Array`。 -- 如需 direct C struct assignment,明确该类型不使用 `renderCopyAssignFunctionName(...)` 的 `godot_new_*_with_*` 路径。 - -验收细则: - -- happy path: - - type unit test 覆盖 `GdccForRangeIterType` 的 stable internal name、LIR-only text、C type name、init helper、destroy - helper、nullability、extension metadata。 - - Java sealed switch 编译强制覆盖新增分支。 -- negative path: - - `GdccForRangeIterType` 不被当作 `GdPrimitiveType`、`GdObjectType`、`GdVariantType` 或 `GdMetaType`。 - - 不出现 `godot_GdccForRangeIter`、`godot_new_GdccForRangeIter...`、`godot_GdccForRangeIter_destroy` 形式的默认 - helper。 - - 不新增任何 `for` parser / analyzer / lowering 行为。 - -测试锚点: - -- 新增 `src/test/java/gd/script/gdcc/type/GdccForRangeIterTypeTest.java` 或同等具体 type test。 -- 更新需要覆盖 sealed switch 的现有 type/backend tests。 - -### 5.2 阶段二:source-facing resolver 禁止与 LIR-only parser - -状态:已完成(2026-06-29)。 - -产出: - -- `ClassRegistry.tryParseStrictTextType(...)`、`ScopeTypeResolver.tryResolveDeclaredType(...)` 与 - `ClassRegistry.resolveTypeMetaHere(...)` 继续保持 source-facing strict namespace,不识别 `compiler::GdccForRangeIter` 或 - bare `GdccForRangeIter`。 -- `DomLirParser` 新增按 use-site 区分的 LIR type parser:只有 function `` 可解析 `compiler::GdccForRangeIter` - ,其余 public ABI-like surface 统一以 `compiler-only type leaked into ...` fail-fast。 -- `DomLirSerializer` 新增对应的 compiler-only type text renderer:function local variable 稳定输出 - `compiler::GdccForRangeIter`,同时拒绝把 compiler-only 类型序列化到 parameter / return / property / signal surface。 -- ordinary builtin / object / container 的 XML 解析与序列化继续复用既有 `ClassRegistry.findType(...)` / `getTypeName()` - 路径,阶段二不改变非 compiler-only 类型行为。 -- 针对 source-facing declared type、registry type-meta、frontend declared type fallback、LIR parser/serializer round-trip 与 - leak negative path 的测试已补齐,明确锚定“变量面可用、ABI 面禁止、未知 `compiler::` grammar 不退化为 object guess”。 - -目标: - -- source-facing type namespace 完全看不到 compiler-only type。 -- LIR XML 只在 `` 中通过 `compiler::` grammar 承载 compiler-only type。 - -建议实施内容: - -- 保持 `ClassRegistry.tryParseStrictTextType(...)` 不识别 compiler-only type。 -- 保持 `ScopeTypeResolver.tryResolveDeclaredType(...)` 不识别 compiler-only type。 -- 不向 `ClassRegistry.resolveTypeMetaHere(...)` 注册 compiler-only `ScopeTypeMeta`。 -- 给 `DomLirParser` 增加 LIR-only type parser,并带 use-site 参数区分 public ABI surface 和 variable surface。 -- 给 `DomLirSerializer` 增加 compiler-only type text renderer。 -- 在 parser 层提前禁止 public ABI surface 出现 compiler-only type。 - -验收细则: - -- happy path: - - local variable 的 `compiler::` XML round-trip 稳定。 - - ordinary builtin / object / container type XML 行为保持不变。 -- negative path: - - `ScopeTypeResolverTest` 证明 declared type 文本无法解析 compiler-only type。 - - `ClassRegistryTypeMetaTest` 证明 registry 不发布 compiler-only type-meta。 - - `DomLirParserTest` 证明 parameter / return / property / signal / capture surface 拒绝 compiler-only type。 - - `ClassRegistry.findType(...)` 不把 `compiler::` 猜成 object。 - -测试锚点: - -- `ScopeTypeResolverTest` -- `ScopeTypeParsersTest` -- `ClassRegistryTypeMetaTest` -- `FrontendDeclaredTypeSupportTest` -- `DomLirParserTest` -- `DomLirSerializerTest` - -### 5.3 阶段三:frontend semantic 与 lowering 边界封堵 - -状态:已完成(2026-06-29)。 - -产出: - -- `FrontendExprTypeAnalyzer` 在 `expressionTypes()` 发布入口与 inferred-local backfill 路径上对 compiler-only type - fail-fast,避免 compiler-only published fact 进入用户 expression/local semantic facts。 -- `FrontendLocalTypeStabilizationAnalyzer` 在 local slot 写回前显式拒绝 compiler-only resolved initializer,保持 ordinary - local `:=` 只接受用户语义类型。 -- `FrontendTypeCheckAnalyzer` 对 condition published fact 新增 compiler-only guard,防止 leaked compiler-only condition - 静默通过 shared type-check 消费。 -- `FrontendBodyLoweringSession.materializeFrontendBoundaryValue(...)` 新增 compiler-only source/target 二次防线;即使 - shared boundary helper 未来回归或 synthetic CFG/value fact 被篡改,也不会生成 `PackVariantInsn`、`UnpackVariantInsn`、 - `ConstructBuiltinInsn` 或 intrinsic-cast boundary。 -- `FrontendCfgNodeInsnLoweringProcessors` 对 condition normalization 新增 compiler-only fail-fast,阻止 compiler-only - condition 走 `pack_variant -> unpack_variant(bool)` lowering。 -- 针对 shared boundary、semantic facts、local stabilization、condition type-check、boundary materialization 与 condition - lowering 的正反测试已补齐,并继续保留既有 ordinary boundary happy path。 - -目标: - -- compiler-only type 不进入用户 semantic facts。 -- ordinary typed boundary 不接受 compiler-only source/target。 -- lowering 不生成 pack/unpack/construct_builtin 来处理 compiler-only type。 - -建议实施内容: - -- 在 `FrontendVariantBoundaryCompatibility.determineFrontendBoundaryDecision(...)` 最前面拒绝 compiler-only source 或 - target。 -- 明确 `ClassRegistry.checkAssignable(...)` 的 source-facing consumer 不得把 compiler-only 同名视作 ordinary boundary - success;可以通过 frontend helper 前置拒绝实现,不必污染 strict backend assignability 基线。 -- 在 `FrontendLocalTypeStabilizationAnalyzer` 和 `FrontendExprTypeAnalyzer` 的 local backfill 风险点增加 fail-closed / - fail-fast 策略,防止 compiler-only published expression type 写回 ordinary local slot。 -- 在 condition lowering 的非 bool / non Variant pack path 前显式拒绝 compiler-only type。 -- 在 `FrontendBodyLoweringSession.materializeFrontendBoundaryValue(...)` 增加 invariant guard,作为 shared helper 的二次防线。 -- 在 call materialization 中,fixed args、vararg tail、dynamic route 均不得接受 compiler-only value。 - -验收细则: - -- happy path: - - 现有 ordinary boundary:`Variant` pack/unpack、`int -> float`、`Vector*i -> Vector*`、`String <-> StringName` 测试保持通过。 -- negative path: - - compiler-only -> `Variant` reject,不生成 `PackVariantInsn`。 - - `Variant` -> compiler-only reject,不生成 `UnpackVariantInsn`。 - - compiler-only -> compiler-only 不通过 ordinary user boundary。 - - compiler-only condition 不生成 `pack_variant -> unpack_variant(bool)`。 - - fixed call argument、vararg tail、return slot、property store、subscript key/index 都不能 materialize compiler-only - boundary。 - - artificial published compiler-only expression type 不能写回 user local slot。 - -测试锚点: - -- `FrontendVariantBoundaryCompatibilityTest` -- `FrontendTypeCheckAnalyzerTest` -- `FrontendLocalTypeStabilizationAnalyzerTest` -- `FrontendBodyLoweringSupportTest` -- `FrontendLoweringBodyInsnPassTest` -- `FrontendWritableTypeWritebackSupportTest` - -### 5.4 阶段四:`GdCompilerType` 抽象层与既有实现回迁 - -状态:已完成(2026-06-29)。 - -产出: - -- 新增 `GdCompilerType` sealed interface,承载所有 compiler-only 类型的共同协议:`getLirTypeText()`、 - `getCStorageTypeName()`、`getCInitHelperName()`、`getCDestroyHelperName()`,以及共享默认实现 `isNullable() == false`、 - `getGdExtensionType() == null`、`isDestroyable() == true`。 -- `GdccForRangeIterType` 从直接 `implements GdType` 迁移为 `implements GdCompilerType`,移除了与抽象层默认实现重复的 - `isNullable()`、`getGdExtensionType()`、`isDestroyable()` 覆写,保留其 internal name、LIR-only text、C helper 协议不变。 -- `GdType` permits 从直接包含 `GdccForRangeIterType` 改为包含 `GdCompilerType`,使 compiler-only 类型通过统一抽象层挂入 - sealed hierarchy。 -- 所有 consumer 侧的 `instanceof GdccForRangeIterType` / `case GdccForRangeIterType` 判断已迁移为面向 `GdCompilerType` - 的判断,覆盖 frontend leak guards(`FrontendVariantBoundaryCompatibility`、`FrontendExprTypeAnalyzer`、 - `FrontendLocalTypeStabilizationAnalyzer`、`FrontendTypeCheckAnalyzer`、`FrontendBodyLoweringSession`、 - `FrontendCfgNodeInsnLoweringProcessors`、`FrontendWritableTypeWritebackSupport`)、C backend guards(`CGenHelper`、 - `CCodegen`、`CBodyBuilder`、`DestructInsnGen`、`EngineMethodAbiCodec`)和 LIR serializer(`DomLirSerializer`)。 -- `DomLirParser` 的 text-to-instance dispatch 保留对 `GdccForRangeIterType.LIR_TYPE_TEXT` 的引用,因为该路径是文本到具体实例的映射,不是类型判断。 -- 新增 `GdCompilerTypeTest` 契约测试,覆盖抽象层协议方法、共享默认值、sealed hierarchy 归属、user-facing family 排除和 - `gdcc_*` helper 命名约束的正反两面。 -- `GdccForRangeIterTypeTest` 已更新,增加 `assertInstanceOf(GdCompilerType.class, type)` 断言以锚定抽象层归属。 - -目标: - -- 让 compiler-only 类型共享统一协议,而不再依赖单个 concrete type 直接承载所有约定。 -- 为后续新增第二个及以上 compiler-only 类型预留扩展点。 -- 保持现有 `GdccForRangeIterType` 的行为和边界不变,只改变它的类型归属与共同协议来源。 - -建议实施内容: - -- 在 `src/main/java/gd/script/gdcc/type/` 中新增 `GdCompilerType` sealed interface,定义 compiler-only 类型共同协议。 -- 让 `GdccForRangeIterType` 实现 `GdCompilerType`,并把 shared contract 从 concrete type 的重复实现中收拢到接口默认实现或公共辅助方法中。 -- 更新 `GdType` permits,使其包含 `GdCompilerType`,而不是把 compiler-only concrete type 继续直接散落在根层 permits。 -- 调整阶段一里关于“暂不抽象化”的表述,改为“先完成首个 concrete type,再在当前计划中补入抽象层与迁移”。 -- 检查后续源码和测试中对 compiler-only 类型的判断,优先改为面向 `GdCompilerType` 的判断。 - -验收细则: - -- happy path: - - `GdccForRangeIterType` 继续通过所有现有 type/backend/serialization 边界测试。 - - 新增的抽象层测试覆盖 `GdCompilerType` 的共同契约。 -- negative path: - - 不再出现“所有 compiler-only 类型都必须直接挂到 `GdType` 根层”的实现假设。 - - `GdccForRangeIterType` 仍不进入 source-facing type namespace、public ABI 或 ordinary boundary。 - -测试锚点: - -- `GdType` / `GdccForRangeIterType` 相关单测。 -- 新增 `GdCompilerType` 契约测试。 - -### 5.5 阶段五:LIR public ABI validator - -状态:已完成(2026-06-30)。 - -产出: - -- 新增 `gd.script.gdcc.lir.validation.LirPublicAbiValidator`,在 backend codegen 前统一扫描 property、signal - parameter、function parameter、function return 与 lambda capture,拒绝任何 `GdCompilerType` 泄漏。 -- `CCodegen.generate()` 现在在 helper 合成、prepare/finally 注入与模板渲染之前执行该 validator,确保失败早于 property init - synthesize、bound metadata 组装和 `getGdExtensionType()==null` 等晚期错误。 -- validator 面向 `GdCompilerType` 抽象层工作,不把 `GdccForRangeIterType` 写死在规则里,为后续 compiler-only concrete type - 复用同一 ABI 边界。 -- hidden function 在 MVP 中继续与 public function 共用 parameter/return 限制;`is_hidden=true` 不再被视作可绕过 ABI - 校验的特殊通道。 -- 新增 `LirPublicAbiValidatorTest`,覆盖 happy path(compiler-only local variable 合法)以及 property、signal - parameter、function parameter、hidden function return、lambda capture 的 negative path,错误信息统一锚定 - `compiler-only type leaked into ...`。 -- 本阶段只负责 ABI-like surface fail-fast,不改变 function `` 的 compiler-only 合法性,也不替代后续阶段在具体 - codegen/helper 路径上的二次封堵。 - -目标: - -- compiler-only type 即使由手写 LIR 或 parser 注入,也不能流入 public ABI。 - -建议实施内容: - -- 增加 LIR validator 或在现有 validation/codegen 前置流程中增加 pass。 -- 校验范围至少覆盖: - - class property type。 - - signal parameter type。 - - public and hidden function parameter type。 - - public and hidden function return type。 - - lambda capture type。 - - generated call wrapper / binding data collection surface。 -- MVP 允许 compiler-only type 只出现在 function variables。 -- 错误信息使用 `compiler-only type leaked into public ABI` 或具体 surface 名称。 - -验收细则: - -- happy path: - - 含 compiler-only local variable 和 intrinsic 的 LIR module 可以进入 backend codegen。 -- negative path: - - function parameter / return / property / signal / capture 使用 compiler-only type fail-fast。 - - hidden function parameter / return 在 MVP 中同样 fail-fast。 - - failure 早于 `CGenHelper.renderBoundMetadata(...)` 的 `getGdExtensionType()==null`。 - -测试锚点: - -- 新增或扩展 LIR validation tests。 -- `DomLirParserTest` -- `CGenHelperTest` -- backend integration shape tests 中加入 public ABI negative cases。 - -### 5.6 阶段六:C 后端类型渲染、初始化、赋值与销毁 - -状态:已完成(2026-06-30,脚手架版;不包含具体 compiler-only runtime helper 实现)。 - -产出: - -- `CCodegen.generateFunctionPrepareBlock(...)` 对 compiler-only local 生成 `CALL_INTRINSIC` 初始化指令,禁止走 `CALL_GLOBAL`。 -- 新增 compiler-only range iterator init intrinsic 脚手架,只负责发出 `gdcc_*_init` 调用形状与窄类型校验,不实现具体运行时语义。 -- 赋值、显式销毁与 generated C shape 测试覆盖 direct struct assignment、`gdcc_*_destroy`、`gdcc_*_init` 以及 `godot_*` default helper negative path。 - -目标: - -- compiler-only storage 在 C 中使用显式 `gdcc_*` helper 和 C type。 -- 封堵所有 `godot_*` 默认 helper 路径。 - -建议实施内容: - -- `CGenHelper.renderGdTypeInC(...)` 对 compiler-only type 使用其 C storage type name。 -- `CGenHelper.renderGdTypeRefInC(...)` 仅在允许的 internal function / intrinsic helper surface 使用明确策略;public ABI - 已由 validator 禁止。 -- `renderPackFunctionName(...)` / `renderUnpackFunctionName(...)` 对 compiler-only type fail-fast。 -- `renderCopyAssignFunctionName(...)` 对 compiler-only type 不返回 `godot_new_*_with_*`。若所有 compiler-only type 允许 C - struct assignment,返回空字符串并让 assignment 走 direct path;若未来需要 deep copy,先扩展 copy helper 协议。 -- `renderDestroyFunctionName(...)` 对 compiler-only type 返回其 `gdcc_*_destroy` helper。 -- `CCodegen.generateFunctionPrepareBlock(...)` 对 compiler-only local 生成专用初始化路径,不生成 `ConstructBuiltinInsn`。 -- `CBodyBuilder.renderDefaultValueExpr(...)` 对 compiler-only type fail-fast,除非该 use-site 明确是 internal helper 且能调用 - init helper。 -- `CBodyBuilder.needsAddressOf(...)`、`prepareRhsValue(...)`、`prepareReturnValue(...)`、`emitDestroy(...)` 明确处理 - compiler-only direct assignment 与 destroy。 -- `DestructInsnGen` 对 compiler-only destroyable type 调用 `gdcc_*_destroy`,不能 default no-op。 - -验收细则: - -- happy path: - - compiler-only local declaration 使用 C storage type name。 - - prepare block 调用 `gdcc_*_init` 或等价专用 init instruction/code path。 - - overwrite、scope cleanup、discarded destroyable value 调用 `gdcc_*_destroy`。 - - direct assignment 不调用 `godot_new_*_with_*`。 -- negative path: - - 生成结果中不出现 `godot_`。 - - 不出现 `godot_new_()`。 - - 不出现 `godot_new_Variant_with_`。 - - 不出现 `godot_new__with_Variant`。 - - 不出现 `godot_new__with_`。 - - 不出现 `godot__destroy`。 - -测试锚点: - -- `CGenHelperTest` -- `CConstructInsnGenTest` -- `CDestructInsnGenTest` -- `CAssignInsnGenTest` -- `CBodyBuilderPhaseBTest` -- `CBodyBuilderPhaseCTest` - -### 5.6a 阶段六-a:`GdCompilerType` 协议显式化(传参、赋值、copy) - -状态:已完成(2026-06-30)。 - -产出: - -- 在 `GdCompilerType` 中补充显式协议方法,描述 compiler-only 类型在 C 层是按引用传参还是按值传参、是否需要 deep copy helper,以及赋值是否可直接走 struct assignment。 -- 将 compiler-only 类型的 `init / copy / destroy` 三元组提升为接口级合同,避免 consumer 继续通过 `instanceof GdCompilerType` 加空字符串约定推导赋值语义。 -- `CGenHelper`、`CBodyBuilder`、`CBodyBuilderAliasSafetySupport` 等 consumer 迁移到新的接口语义,不再把“空 copy helper”当作唯一的 direct assignment 信号。 -- 新增 `GdCompilerType` 契约测试,覆盖默认传参与赋值行为。 -- 已完成:并行调研 `doc/gdcc_type_system.md`、`doc/gdcc_c_backend.md`、`doc/gdcc_ownership_lifecycle_spec.md`、`doc/analysis/gdcompiler_type_design_risk_analysis.md` 以及 backend 实现/测试,确认当前行为正确但依赖隐式协议。 -- 已完成:`GdCompilerType` 协议层已补入 `isPassedByPointerInC()`、`getCCopyHelperName()`、`isDirectStructAssignmentSafe()` 及一致性校验入口;`GdccForRangeIterType` 已显式声明当前合同,固定既有行为。 -- 已完成:`CGenHelper.renderCopyAssignFunctionName(...)`、`CBodyBuilder.needsAddressOf(...)`、`prepareRhsValue(...)`、`prepareReturnValue(...)` 与 `CBodyBuilderAliasSafetySupport.requiresStableCarrier(...)` 已迁移为优先读取 `GdCompilerType` 显式协议,不再把空 copy helper 当作唯一的 direct-assignment 语义来源。 -- 已完成:对 future compiler-only deep-copy 场景增加 fail-fast 防线;若 `isDirectStructAssignmentSafe()==false` 且缺少 copy helper,会在 helper / alias-safety 层直接报错,而不是静默落回 direct assignment。 -- 已完成:补充 builder / destroy 路径单测,锚定 compiler-only 传参仍走 `&slot`、赋值/返回仍走 direct assignment、`__prepare__` 首写不销毁旧值、`__finally__` auto-generated cleanup 仍调用 `gdcc_*_destroy`。 -- 已完成:新增 alias-safety 专项测试,确认 compiler-only direct assignment 不会误入 stable-carrier 路径,而 `String` / `Variant` 自赋值仍保持现有 stable-carrier 合同。 -- 已验证:`script/run-gradle-targeted-tests.sh --tests "gd.script.gdcc.type.GdCompilerTypeTest,gd.script.gdcc.backend.c.gen.CGenHelperTest,gd.script.gdcc.backend.c.gen.CBodyBuilderPhaseBTest,gd.script.gdcc.backend.c.gen.CBodyBuilderPhaseCTest,gd.script.gdcc.backend.c.gen.CDestructInsnGenTest,gd.script.gdcc.backend.c.gen.CBodyBuilderAliasSafetySupportTest,gd.script.gdcc.backend.c.gen.CCodegenTest"` 通过。 - -目标: - -- 把 compiler-only 类型在 C 后端的传参与赋值语义从隐式约定提升为显式协议。 -- 允许未来新的 compiler-only 类型自行声明:是否使用 `&` 传参、是否需要 copy helper、是否允许 direct struct assignment。 -- 保持当前 `GdccForRangeIterType` 的行为不变,但把它所依赖的默认合同固定下来。 - -建议实施内容: - -- 在 `GdCompilerType` 中增加方法,例如: - - `isPassedByPointerInC()`:是否在 C 参数位置使用引用传参。默认 `true`,匹配当前 compiler-only storage 作为 C struct 传入 helper 时使用 `&slot` 的行为。 - - `getCCopyHelperName()`:返回赋值 / 拷贝 helper 名;默认空字符串,表示可直接 struct assignment。 - - `isDirectStructAssignmentSafe()`:显式表达是否允许 direct assignment。默认可由 `getCCopyHelperName().isEmpty()` 派生,但 consumer 应优先读取该语义而不是直接解释空字符串。 -- 如需要保持 init 路径对称,可在 `CGenHelper` 增加 `renderCompilerOnlyInitFunctionName(GdCompilerType)`,使 init / copy / destroy helper 渲染统一经过 helper 层,同时避免误传普通 Godot 类型。 -- `CBodyBuilder.needsAddressOf(...)`、`prepareRhsValue(...)`、`prepareAssignedNonObjectRhs(...)`、`prepareReturnValue(...)` 改为优先读取 `GdCompilerType` 的显式协议。 -- `CBodyBuilderAliasSafetySupport.requiresStableCarrier(...)` 直接依据 compiler-only 的赋值合同判断,避免仅靠当前 `hasCopyHelper` 的间接推导。 -- 如果未来某个 compiler-only 类型需要 deep copy 或非引用传参,必须先在此阶段扩展接口合同,再改 consumer。 - -验收细则: - -- happy path: - - `GdccForRangeIterType` 仍然按值语义存储、按引用传参、direct assignment、显式 destroy。 - - 新增 compiler-only 类型时,可以只通过接口合同描述其传参/赋值行为。 -- negative path: - - consumer 不再仅依赖 `GdType` family 和空字符串约定推导 compiler-only 赋值/传参。 - - 未声明协议的新增 compiler-only 类型必须 fail-fast。 - - deep-copy compiler-only 类型如果没有提供 copy helper,不允许静默落回 direct assignment。 - -测试锚点: - -- `GdCompilerTypeTest` -- `CGenHelperTest` -- `CBodyBuilderPhaseBTest` -- `CBodyBuilderPhaseCTest` -- `CAssignInsnGenTest` -- `CDestructInsnGenTest` -- `CBodyBuilderAliasSafetySupportTest`(如存在) - -### 5.7 阶段七:`GdccForRangeIterType` intrinsic 最小闭环 - -状态:已完成(2026-06-30)。 - -产出: - -- 新增 `gdcc.for_range_iter.init`、`gdcc.for_range_iter.should_continue`、`gdcc.for_range_iter.next`、`gdcc.for_range_iter.get` - 四个 backend-owned intrinsic,并在 `CIntrinsicManager` 注册白名单。 -- `init` 接受已归一化的 `start/end/step` 三个 `int` slot,返回非 ref `compiler::GdccForRangeIter`;`should_continue` 返回 `bool`; - `next` 返回推进后的新 iterator state;`get` 返回当前 `int` 值。 -- Runtime helper 使用 `gdcc_for_range_iter_from_bounds` / `gdcc_for_range_iter_should_continue` / - `gdcc_for_range_iter_next` / `gdcc_for_range_iter_get`,保留 `gdcc_for_range_iter_init()` 作为 prepare-block 默认初始化 helper。 -- `step == 0` 采用明确 runtime error helper 策略:`gdcc_for_range_iter_from_bounds(...)` 调用 `godot_print_error(...)` 后返回不可无限循环的 fallback state。 -- 测试覆盖 parser / serializer 文本保留、registry dispatch、完整 `CallIntrinsicInsnGen` codegen、正负合同错误、literal operand 拒绝、零步长策略和 - `GdccForRangeIterType` 不能传给非 range intrinsic。 - -目标: - -- compiler-only type 只通过 backend-owned intrinsic 操作。 -- 每个 intrinsic 的类型合同窄而明确。 -- 用 range iterator 的初始化、继续判断、推进、取值闭环验证 compiler-only type 可以支撑未来 `for i in range(...)` lowering。 - -建议实施内容: - -- 为 `GdccForRangeIterType` 新增一组最小 intrinsic。推荐命名如下,最终名称可按现有 intrinsic 命名风格调整,但语义必须保持: - - `gdcc.for_range_iter.init`:根据 `start`、`end`、`step` 初始化 iterator state。 - - `gdcc.for_range_iter.should_continue`:读取当前 state,返回是否还有当前值可用。 - - `gdcc.for_range_iter.next`:推进到下一个值,并返回推进后的新 iterator state。 - - `gdcc.for_range_iter.get`:读取当前迭代值。 -- intrinsic 类型合同: - - `init` 的 result 必须是非 ref 的 `compiler::GdccForRangeIter`;arguments 为三个 `int` 值,对应 normalized `start`、 - `end`、`step`。 - - `should_continue` 的 result 必须是非 ref 的 `bool`;argument 为一个 `compiler::GdccForRangeIter`。 - - `next` 的 result 必须是非 ref 的 `compiler::GdccForRangeIter`;argument 为一个 `compiler::GdccForRangeIter`,返回推进后的新 - iterator state。 - - `get` 的 result 必须是非 ref 的 `int`;argument 为一个 `compiler::GdccForRangeIter`。 -- 未来 lowering 的基本形状应为:`iter = init(start,end,step)`,每轮先 `should_continue(iter)`,再 `value = get(iter)` - ,循环体结束后 `iter = next(iter)`。这保证 `GdccForRangeIterType` 仍按不可变值语义传递,不需要 ref mutation。 -- `range(n)` / `range(b, n)` / `range(b, n, s)` 的参数归一化属于未来 frontend lowering:本阶段只要求 intrinsic 能接受已归一化的三个 - `int` 参数。 -- `step == 0` 的处理策略必须在 intrinsic catalog 中写清。建议 fail-fast 或 runtime error helper,不允许生成无限循环语义。 -- 负步长必须作为合法输入进入 helper 语义:`should_continue` / `next` 对正 step 使用 `< end`,对负 step 使用 `> end`。 -- 更新 `doc/gdcc_lir_intrinsic.md` catalog,记录: - - intrinsic name。 - - LIR textual shape。 - - result 是否必须存在。 - - result 是否允许 ref。 - - argument 数量与类型。 - - C backend 语义。 - - lifecycle / ownership 说明。 -- 在 `CIntrinsicManager` 注册白名单。 -- 每个 `CIntrinsicFunction` 自己校验 result / arg / arity / ref。 -- 成功路径优先复用 `CBodyBuilder.assignVar(...)` 或 `callAssign(...)`,除非该 compiler-only type 需要更窄的写入策略。 - -验收细则: - -- happy path: - - parser / serializer 保留 `call_intrinsic` textual shape。 - - backend registry 能找到 `gdcc.for_range_iter.init`、`gdcc.for_range_iter.should_continue`、 - `gdcc.for_range_iter.next`、`gdcc.for_range_iter.get`。 - - 成功 codegen 使用 `gdcc_*` helper,不绕过 slot lifecycle。 - - `range(10)` 归一化后的 `start=0,end=10,step=1`、`range(5,10)` 归一化后的 `start=5,end=10,step=1`、`range(10,0,-1)` - 归一化后的 `start=10,end=0,step=-1` 都能用手写 LIR intrinsic 序列生成预期 C helper 调用形状。 -- negative path: - - unknown intrinsic fail-fast。 - - bad arity fail-fast。 - - missing result / unexpected result fail-fast。 - - result ref 不符合合同 fail-fast。 - - result type 错误 fail-fast。 - - argument type 错误 fail-fast。 - - literal operand fail-fast。 - - `step == 0` 的手写 LIR 用例按 catalog 约定 fail-fast 或进入明确 runtime error helper,不允许静默生成无限循环 helper - 调用。 - - `GdccForRangeIterType` 不能被传给非上述四个 range iterator intrinsic。 - -测试锚点: - -- `SimpleLirBlockInsnParserTest` -- `SimpleLirBlockInsnSerializerTest` -- `CIntrinsicManagerTest` -- `CallIntrinsicInsnGenTest` -- 新增 `GdccForRangeIter` intrinsic tests,覆盖正步长、负步长、exclusive end、zero step 策略和类型错误。 - -### 5.8 阶段八:普通 Godot / Variant / engine 路径封堵 - -状态:已完成(2026-06-30)。 - -产出: - -- `InsnGenSupport` 新增统一 compiler-only 泄漏检测与 Variant unpack 辅助,普通 `Variant` pack/unpack、dynamic call result unpack、index/property writeback 统一走显式 fail-fast 边界。 -- `PackUnpackVariantInsnGen`、`CallMethodInsnGen`、`BackendMethodCallResolver`、`CallGlobalInsnGen`、`OperatorInsnGen`、`IndexLoadInsnGen`、`IndexStoreInsnGen`、property/static path 补齐 compiler-only receiver / operand / argument / result target 拒绝。 -- `CGenHelper` 对 outward metadata、typed container leaf、`call_func` wrapper type gate / unpack expression / destroy stmt 增加 compiler-only 显式拒绝,避免错误下沉成模糊 helper 失败。 -- targeted 单元测试覆盖 pack/unpack、dynamic call、utility call、operator、index、property、static load 与 metadata/wrapper negative path,并保留关键 happy path 回归锚点。 - -目标: - -- 即使手写 LIR 绕过 frontend,compiler-only type 也不能进入普通 Godot runtime ABI。 - -建议实施内容: - -- `PackUnpackVariantInsnGen` 显式拒绝 compiler-only pack / unpack。 -- `CallMethodInsnGen`、`BackendMethodCallResolver`、`CallGlobalInsnGen`、static call path 显式拒绝 compiler-only receiver / - argument / return target。 -- operator / index / property instruction generators 显式拒绝 compiler-only operand / receiver / value。 -- typed array / dictionary metadata 和 runtime guard leaf 渲染拒绝 compiler-only leaf。 -- generated `call_func` wrapper argument gate、unpack expression、destroy stmt 不接受 compiler-only type。 - -验收细则: - -- negative path: - - `PACK_VARIANT` source 是 compiler-only type fail-fast。 - - `UNPACK_VARIANT` result 是 compiler-only type fail-fast。 - - dynamic call argument 是 compiler-only type 不被 pack 成 `Variant`。 - - dynamic result 不可 unpack 到 compiler-only type。 - - method/global/static/operator/index/property path 遇到 compiler-only type fail-fast。 - - `Array[compiler::]` / `Dictionary[String, compiler::]` 不能生成 outward metadata。 - -测试锚点: - -- `CPackUnpackVariantInsnGenTest` -- `CallMethodInsnGenTest` -- `CallGlobalInsnGenTest` -- `COperatorInsnGenTest` -- `IndexLoadInsnGenTest` -- `IndexStoreInsnGenTest` -- `CLoadPropertyInsnGenTest` -- `CStorePropertyInsnGenTest` -- `CGenHelperTest` - -### 5.9 阶段九:文档同步与回归收口 - -状态:已完成(2026-06-30)。 - -产出: - -- `doc/gdcc_type_system.md` 已补充 compiler-only type 的定位、`GdCompilerType` 抽象层归属以及 ordinary compatibility matrix exclusion。 -- `doc/gdcc_low_ir.md` 已补充 `compiler::` grammar 的 LIR-only 边界,并明确 MVP 仅允许 function `` 使用该语法。 -- `doc/gdcc_lir_intrinsic.md` 已收录 `gdcc.for_range_iter.init`、`gdcc.for_range_iter.should_continue`、`gdcc.for_range_iter.next`、`gdcc.for_range_iter.get` 四个 catalog 条目。 -- `doc/gdcc_c_backend.md` 与 `doc/gdcc_runtime_lib.md` 已同步 `gdcc_for_range_iter` C storage 与 `gdcc_for_range_iter_*` helper 的命名/边界,继续明确禁止回退到默认 `godot_*` helper 命名。 -- targeted regression 继续锚定 compiler-only type 只属于内部 storage typing,而不是 source-facing type。 - -目标: - -- 实现后所有事实源一致。 -- 回归覆盖证明 compiler-only type 是内部 storage typing,而不是 source-facing type。 - -建议实施内容: - -- 更新 `doc/gdcc_type_system.md`: - - 增加 compiler-only type 的定位。 - - 明确它不属于 GDScript source-facing type set。 - - 明确 `GdCompilerType` 是 compiler-only 共同抽象层,`GdccForRangeIterType` 是其首个具体实现。 - - 明确 ordinary compatibility matrix 不接受它。 -- 更新 `doc/gdcc_low_ir.md`: - - 记录 `compiler::` LIR-only type grammar。 - - 记录只允许 function variables 的 MVP 约束。 -- 更新 `doc/gdcc_lir_intrinsic.md`: - - 增加 `GdccForRangeIterType` 的四个 intrinsic catalog:init、should_continue、next、get。 -- 更新 `doc/gdcc_c_backend.md`: - - 记录 `gdcc_for_range_iter` C storage type、init/destroy helper、禁止 `godot_*` default helper。 -- 更新 `doc/gdcc_runtime_lib.md`: - - 记录新增 `gdcc_for_range_iter_*` helper 声明/实现边界。 -- 如 lifecycle 行为扩面,更新 `doc/gdcc_ownership_lifecycle_spec.md` 或 backend lifecycle contract。 - -验收细则: - -- 所有新增文档不维护第二份 frontend conversion matrix。 -- 所有新增 intrinsic 均能从 `gdcc_lir_intrinsic.md` 找到 catalog 条目。 -- 计划文档、类型系统、Low IR、C backend、runtime helper 文档中的边界措辞一致。 - ---- - -## 6. 非目标 - -当前计划不覆盖: - -- 把 compiler-only type 暴露给 GDScript 用户声明。 -- 让 `GdCompilerType` 参与 ordinary implicit conversion。 -- 让 compiler-only storage 和 `Variant` 相互转换。 -- 把 compiler-only type 传给 engine method、utility function、global function、property、index 或 operator。 -- 支持 `Array[CompilerOnly]` / `Dictionary[K, CompilerOnly]` 作为 typed container ABI。 -- 支持 public 或 hidden function 使用 compiler-only parameter / return type。 -- 把 Godot internal class,例如 `GDScriptFunctionState`,建模为 source-facing GDCC type。 -- 一次性实现完整 iterator / async lowering。 -- 在本阶段实现 `for` parser / analyzer / lowering,或把 GDScript `range(...)` ordinary call 改写为内部 iterator;当前只实现 - `GdccForRangeIterType` 与四个 range iterator intrinsics,作为未来任务的实现与验收锚点。 - ---- - -## 7. 建议 targeted test 命令 - -优先从纯单元测试开始: - -```bash -script/run-gradle-targeted-tests.sh --tests "gd.script.gdcc.frontend.sema.analyzer.support.FrontendVariantBoundaryCompatibilityTest" -``` - -```bash -script/run-gradle-targeted-tests.sh --tests "gd.script.gdcc.scope.resolver.ScopeTypeResolverTest,gd.script.gdcc.scope.resolver.ScopeTypeParsersTest,gd.script.gdcc.scope.ClassRegistryTypeMetaTest,gd.script.gdcc.frontend.sema.FrontendDeclaredTypeSupportTest" -``` - -```bash -script/run-gradle-targeted-tests.sh --tests "gd.script.gdcc.lir.parser.DomLirParserTest,gd.script.gdcc.lir.parser.DomLirSerializerTest,gd.script.gdcc.lir.parser.SimpleLirBlockInsnParserTest,gd.script.gdcc.lir.parser.SimpleLirBlockInsnSerializerTest" -``` - -```bash -script/run-gradle-targeted-tests.sh --tests "gd.script.gdcc.backend.c.gen.CGenHelperTest,gd.script.gdcc.backend.c.gen.CPackUnpackVariantInsnGenTest,gd.script.gdcc.backend.c.gen.CIntrinsicManagerTest,gd.script.gdcc.backend.c.gen.CallIntrinsicInsnGenTest" -``` - -`GdccForRangeIterType` 与四个 intrinsic 实现后补充运行: - -```bash -script/run-gradle-targeted-tests.sh --tests "gd.script.gdcc.type.GdccForRangeIterTypeTest,gd.script.gdcc.backend.c.gen.GdccForRangeIterIntrinsicTest" -``` - -生命周期和 C body shape 实现后再跑: - -```bash -script/run-gradle-targeted-tests.sh --tests "gd.script.gdcc.backend.c.gen.CConstructInsnGenTest,gd.script.gdcc.backend.c.gen.CDestructInsnGenTest,gd.script.gdcc.backend.c.gen.CAssignInsnGenTest,gd.script.gdcc.backend.c.gen.CBodyBuilderPhaseBTest,gd.script.gdcc.backend.c.gen.CBodyBuilderPhaseCTest" -``` - -普通 Godot/backend path 封堵实现后再跑: - -```bash -script/run-gradle-targeted-tests.sh --tests "gd.script.gdcc.backend.c.gen.CallMethodInsnGenTest,gd.script.gdcc.backend.c.gen.CallGlobalInsnGenTest,gd.script.gdcc.backend.c.gen.COperatorInsnGenTest,gd.script.gdcc.backend.c.gen.IndexLoadInsnGenTest,gd.script.gdcc.backend.c.gen.IndexStoreInsnGenTest,gd.script.gdcc.backend.c.gen.CLoadPropertyInsnGenTest,gd.script.gdcc.backend.c.gen.CStorePropertyInsnGenTest" -``` - -frontend lowering shape 实现后再跑: - -```bash -script/run-gradle-targeted-tests.sh --tests "gd.script.gdcc.frontend.lowering.FrontendBodyLoweringSupportTest,gd.script.gdcc.frontend.lowering.FrontendLoweringBodyInsnPassTest,gd.script.gdcc.frontend.lowering.FrontendLoweringFunctionPreparationPassTest" -``` - -最终可以用一个较保守的非 runtime fixture 组合验证边界闭环: - -```bash -script/run-gradle-targeted-tests.sh --tests "gd.script.gdcc.frontend.sema.analyzer.support.FrontendVariantBoundaryCompatibilityTest,gd.script.gdcc.scope.resolver.ScopeTypeResolverTest,gd.script.gdcc.lir.parser.DomLirParserTest,gd.script.gdcc.lir.parser.SimpleLirBlockInsnParserTest,gd.script.gdcc.backend.c.gen.CGenHelperTest,gd.script.gdcc.backend.c.gen.CPackUnpackVariantInsnGenTest,gd.script.gdcc.backend.c.gen.CallIntrinsicInsnGenTest" -``` - ---- - -## 8. 推进顺序 - -建议按以下顺序实施并提交,每一步都保持可编译、可回归: - -1. 类型协议与 `GdccForRangeIterType`。 -2. LIR-only parser / serializer grammar,并先冻结 source-facing resolver 禁止。 -3. frontend ordinary typed-boundary、local stabilization、condition lowering、call materialization 的封堵。 -4. `GdCompilerType` 抽象层与既有实现回迁(已完成)。 -5. LIR public ABI validator。 -6. C 后端 C type / init / destroy / assignment / pack-unpack / metadata 封堵。 -7. `GdccForRangeIterType` 的 init / should_continue / next / get intrinsic 和 runtime `gdcc_for_range_iter_*` helper。 -8. 普通 method / global / operator / index / property / wrapper path 负例补齐。 -9. 文档同步和 targeted regression。 - -这条顺序的核心约束是:先让 compiler-only type 不能从用户世界进入,再允许它在 LIR/backend 内部出现;先封掉默认 `Variant` / -`godot_*` 路径,再接入具体 intrinsic 成功路径。 diff --git a/src/main/java/gd/script/gdcc/type/GdccForRangeIterType.java b/src/main/java/gd/script/gdcc/type/GdccForRangeIterType.java index e9cfbbb8..51acaef6 100644 --- a/src/main/java/gd/script/gdcc/type/GdccForRangeIterType.java +++ b/src/main/java/gd/script/gdcc/type/GdccForRangeIterType.java @@ -2,8 +2,8 @@ import org.jetbrains.annotations.NotNull; -/// Compiler-only storage type for future lowered `for i in range(...)` iterator state. -/// It is intentionally not source-facing and must only be serialized through LIR-only text. +/// Compiler-only storage type for lowered `for i in range(...)` iterator state. +/// It remains internal to GDCC and is serialized only through the LIR-only compiler type text. public final class GdccForRangeIterType implements GdCompilerType { public static final @NotNull GdccForRangeIterType FOR_RANGE_ITER = new GdccForRangeIterType(); From 733c4a1d3c3b233d2a9dbc736321daed15b816c7 Mon Sep 17 00:00:00 2001 From: Iridium-Zero Date: Fri, 3 Jul 2026 09:06:04 +0800 Subject: [PATCH 16/16] fix(backend): remove duplicate wildcard imports in CGenHelper - remove duplicated `gd.script.gdcc.scope.*` import - remove duplicated `gd.script.gdcc.type.*` import - keep the change limited to import cleanup for review feedback --- src/main/java/gd/script/gdcc/backend/c/gen/CGenHelper.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/gd/script/gdcc/backend/c/gen/CGenHelper.java b/src/main/java/gd/script/gdcc/backend/c/gen/CGenHelper.java index 40452edc..5e26326b 100644 --- a/src/main/java/gd/script/gdcc/backend/c/gen/CGenHelper.java +++ b/src/main/java/gd/script/gdcc/backend/c/gen/CGenHelper.java @@ -15,10 +15,8 @@ import gd.script.gdcc.lir.insn.BinaryOpInsn; import gd.script.gdcc.lir.insn.UnaryOpInsn; import gd.script.gdcc.scope.*; -import gd.script.gdcc.scope.*; import gd.script.gdcc.scope.resolver.ScopeTypeParsers; import gd.script.gdcc.type.*; -import gd.script.gdcc.type.*; import gd.script.gdcc.util.TypeCheckUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable;