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..a4b522df --- /dev/null +++ b/doc/analysis/gdcompiler_type_design_risk_analysis.md @@ -0,0 +1,467 @@ +# 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; + } +} +``` + +当前实施计划已经把 `GdCompilerType` 抽象层纳入后续阶段,因此这里应视为目标协议而不是是否抽象的开放问题。需要注意: + +- 如果所有具体类型都必须 init/destroy,那么协议方法放在 `GdCompilerType` 上是合理的。 +- 如果某些 compiler-only 类型没有 destroy 语义,需要决定是返回 no-op helper,还是允许 `destroyFunctionName()` 为空。当前需求说“返回初始化函数名和销毁函数名”,报告按必有 helper 处理。 + +计划文档已要求在首个 concrete compiler-only type 落地后补入该抽象层,因此后续实现应以 `GdCompilerType` 为准,不再沿用“只有一个实现时暂不抽象”的旧策略。 + +### 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/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_lir_intrinsic.md b/doc/gdcc_lir_intrinsic.md index e36edfd1..b54aa707 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_implementation.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_implementation.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_implementation.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_implementation.md` + ## 新增 Intrinsic Checklist 新增 intrinsic 时按以下顺序维护: diff --git a/doc/gdcc_low_ir.md b/doc/gdcc_low_ir.md index 43118958..35c9a21c 100644 --- a/doc/gdcc_low_ir.md +++ b/doc/gdcc_low_ir.md @@ -24,6 +24,20 @@ 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 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 Each instruction has an optional return value, a string id, and a list of operands: @@ -86,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..56c3c37d 100644 --- a/doc/gdcc_runtime_lib.md +++ b/doc/gdcc_runtime_lib.md @@ -57,7 +57,19 @@ 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 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 + 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 703437f7..ef4c94a7 100644 --- a/doc/gdcc_type_system.md +++ b/doc/gdcc_type_system.md @@ -6,8 +6,19 @@ - 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. + +## 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. @@ -60,7 +71,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_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/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/CBodyBuilder.java b/src/main/java/gd/script/gdcc/backend/c/gen/CBodyBuilder.java index f38d8f69..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 @@ -889,6 +889,9 @@ public static String renderStaticStringNameLiteral(@NotNull String value) { @NotNull public static String renderDefaultValueExpr(@NotNull GdType type) { return switch (type) { + case GdCompilerType _ -> throw new IllegalArgumentException( + "compiler-only type leaked into default value expression: " + type.getTypeName() + ); case GdVoidType _ -> ""; case GdBoolType _ -> "false"; case GdIntType _ -> "0"; @@ -964,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.) @@ -983,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); @@ -1008,6 +1019,7 @@ private boolean needsAddressOf(@NotNull GdType type) { canDestroyOldValue(target), target, value, + value.type(), !helper.renderCopyAssignFunctionName(value.type()).isEmpty() )) { return PreparedAssignmentRhs.ordinary(rhsResult); @@ -1051,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()) { @@ -1062,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 2f289c7f..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 @@ -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(); @@ -157,6 +160,9 @@ private void generateDefaultGetterSetterInitialization() { var bb = new LirBasicBlock("entry"); func.addBasicBlock(bb); switch (propertyDef.getType()) { + case GdCompilerType _ -> 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 +215,11 @@ private void generateFunctionPrepareBlock() { continue; } var initInsn = switch (variable.type()) { + case GdCompilerType compilerType -> new CallIntrinsicInsn( + variable.id(), + helper.renderCompilerOnlyInitFunctionName(compilerType), + List.of() + ); case GdObjectType _ -> new LiteralNullInsn(variable.id()); case GdVariantType _, GdNilType _ -> new LiteralNilInsn(variable.id()); case GdBoolType _ -> new LiteralBoolInsn(variable.id(), false); @@ -527,6 +538,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/backend/c/gen/CGenHelper.java b/src/main/java/gd/script/gdcc/backend/c/gen/CGenHelper.java index 440734f4..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,9 @@ 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; @@ -265,6 +264,7 @@ private void collectBindingData(@NotNull List classDefs) { public @NotNull String renderGdTypeInC(@NotNull GdType gdType) { return switch (gdType) { + case GdCompilerType compilerType -> compilerType.getCStorageTypeName(); case GdContainerType gdContainerType -> switch (gdContainerType) { case GdArrayType gdArrayType -> { if (gdArrayType.getValueType() instanceof GdVariantType) { @@ -298,6 +298,7 @@ private void collectBindingData(@NotNull List classDefs) { public @NotNull String renderGdTypeRefInC(@NotNull GdType gdType) { return switch (gdType) { + case GdCompilerType compilerType -> compilerType.getCStorageTypeName() + "*"; case GdContainerType gdContainerType -> switch (gdContainerType) { case GdArrayType gdArrayType -> { if (gdArrayType.getValueType() instanceof GdVariantType) { @@ -467,6 +468,7 @@ public boolean checkEngineMethodHelperRequiresLocalValueSlot(@NotNull EngineMeth public @NotNull String renderGdTypeName(@NotNull GdType gdType) { return switch (gdType) { + case GdCompilerType _ -> gdType.getTypeName(); case GdContainerType gdContainerType -> switch (gdContainerType) { case GdArrayType _ -> "Array"; case GdDictionaryType _ -> "Dictionary"; @@ -516,6 +518,9 @@ public boolean checkEngineMethodHelperRequiresLocalValueSlot(@NotNull EngineMeth } public @NotNull String renderUnpackFunctionName(@NotNull GdType type) { + if (type instanceof GdCompilerType) { + 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"; @@ -534,6 +539,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 || " @@ -541,7 +547,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() + ")"; @@ -555,7 +561,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() + ")"; } } @@ -573,6 +579,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 + ")"; @@ -603,21 +610,39 @@ 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 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); } } + /// 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 compilerType -> { + compilerType.validateCStorageContract(); + yield compilerType.getCCopyHelperName(); + } case GdObjectType _, GdPrimitiveType _ -> ""; case GdVoidType _, GdNilType _ -> throw new IllegalArgumentException("Type " + type.getTypeName() + " does not support copy assignment"); @@ -632,6 +657,10 @@ public boolean checkEngineMethodHelperRequiresLocalValueSlot(@NotNull EngineMeth if (!type.isDestroyable()) { throw new IllegalArgumentException("Type " + type.getTypeName() + " is not destroyable"); } + if (type instanceof GdCompilerType compilerType) { + compilerType.validateCStorageContract(); + return compilerType.getCDestroyHelperName(); + } if (type instanceof GdObjectType) { return "godot_object_destroy"; } else { @@ -645,6 +674,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 ""; } @@ -718,7 +748,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; @@ -755,7 +785,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"); @@ -780,6 +812,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(); @@ -886,6 +921,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/CIntrinsicManager.java b/src/main/java/gd/script/gdcc/backend/c/gen/CIntrinsicManager.java index b9b59a93..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,6 +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.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; @@ -16,11 +18,21 @@ public final class CIntrinsicManager { public CIntrinsicManager() { var intToFloat = new CIntToFloatIntrinsic(); + 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/binding/EngineMethodAbiCodec.java b/src/main/java/gd/script/gdcc/backend/c/gen/binding/EngineMethodAbiCodec.java index 7df3c71c..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,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.GdCompilerType; 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 GdCompilerType _ -> 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/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/DestructInsnGen.java b/src/main/java/gd/script/gdcc/backend/c/gen/insn/DestructInsnGen.java index 27ad8bb4..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,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.GdCompilerType; 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 GdCompilerType _ -> { + 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/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/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/frontend/lowering/FrontendWritableTypeWritebackSupport.java b/src/main/java/gd/script/gdcc/frontend/lowering/FrontendWritableTypeWritebackSupport.java index ae62adec..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,6 +2,7 @@ import gd.script.gdcc.type.GdArrayType; import gd.script.gdcc.type.GdDictionaryType; +import gd.script.gdcc.type.GdCompilerType; 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 GdCompilerType _ -> 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/lowering/pass/body/FrontendBodyLoweringSession.java b/src/main/java/gd/script/gdcc/frontend/lowering/pass/body/FrontendBodyLoweringSession.java index c1d44a5a..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,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.GdCompilerType; 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 GdCompilerType compilerOnlyType) { + throw new IllegalStateException( + "compiler-only type leaked into frontend boundary source '" + + use + + "': " + + compilerOnlyType.getTypeName() + ); + } + if (target instanceof GdCompilerType 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..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,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.GdCompilerType; 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 GdCompilerType 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..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,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.GdCompilerType; 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 GdCompilerType 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 GdCompilerType 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 GdCompilerType compilerOnlyType) { + throw new IllegalStateException( + "compiler-only type leaked into frontend local backfill: " + + compilerOnlyType.getTypeName() + ); + } if (!(existingLocal.type() instanceof GdVariantType)) { if (!sameType(existingLocal.type(), backfilledType)) { throw new IllegalStateException( 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..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,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.GdCompilerType; 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 GdCompilerType 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..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,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.GdCompilerType; 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 GdCompilerType compilerOnlyType) { + throw new IllegalStateException( + "compiler-only type leaked into frontend condition fact: " + + compilerOnlyType.getTypeName() + ); + } } protected record TypeCheckAccess( 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..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,6 +1,7 @@ package gd.script.gdcc.frontend.sema.analyzer.support; import gd.script.gdcc.scope.ClassRegistry; +import gd.script.gdcc.type.GdCompilerType; 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 GdCompilerType || target instanceof GdCompilerType) { + 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/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 03ee966a..f727c86b 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.*; @@ -75,10 +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 = 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"), LirTypeUseSite.SIGNAL_PARAMETER); signal.addParameter(new LirParameterDef(pname, ptype, null, signal)); } signals.add(signal); @@ -94,10 +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 = 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"), 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; @@ -150,10 +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 = 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"), LirTypeUseSite.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 +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 = 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"), LirTypeUseSite.FUNCTION_CAPTURE); fn.addCapture(new LirCaptureDef(cname, ctype, fn)); } } @@ -179,10 +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 = 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"), LirTypeUseSite.FUNCTION_RETURN); fn.setReturnType(rtype); } @@ -194,10 +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 = 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"), LirTypeUseSite.FUNCTION_VARIABLE); fn.createAndAddVariable(id, t); } } @@ -253,4 +236,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 LirTypeUseSite 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 LirTypeUseSite 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..597dffe2 100644 --- a/src/main/java/gd/script/gdcc/lir/parser/DomLirSerializer.java +++ b/src/main/java/gd/script/gdcc/lir/parser/DomLirSerializer.java @@ -57,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", p.type().getTypeName()); + pEl.setAttribute("type", renderTypeText(p.type(), LirTypeUseSite.SIGNAL_PARAMETER)); sEl.appendChild(pEl); } signalsEl.appendChild(sEl); @@ -69,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", prop.getType().getTypeName()); + 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()); @@ -111,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", p.type().getTypeName()); + pEl.setAttribute("type", renderTypeText(p.type(), LirTypeUseSite.FUNCTION_PARAMETER)); if (p.defaultValueFunc() != null) pEl.setAttribute("default_value_func", p.defaultValueFunc()); paramsEl.appendChild(pEl); } @@ -123,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", fn.getReturnType().getTypeName()); + retEl.setAttribute("type", renderTypeText(fn.getReturnType(), LirTypeUseSite.FUNCTION_RETURN)); fEl.appendChild(retEl); // variables @@ -131,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", v.type().getTypeName()); + vEl.setAttribute("type", renderTypeText(v.type(), LirTypeUseSite.FUNCTION_VARIABLE)); varsEl.appendChild(vEl); } fEl.appendChild(varsEl); @@ -174,4 +174,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 LirTypeUseSite useSite) { + if (type instanceof GdCompilerType 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/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..5ef839fa --- /dev/null +++ b/src/main/java/gd/script/gdcc/lir/validation/LirPublicAbiValidator.java @@ -0,0 +1,74 @@ +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.util.TypeCheckUtil; +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()) { + TypeCheckUtil.requireNonCompilerOnly( + parameterDef.getType(), + "function parameter at " + describeFunction(classDef, functionDef) + "(" + parameterDef.getName() + ")" + ); + } + for (var captureDef : functionDef.getCaptures().values()) { + TypeCheckUtil.requireNonCompilerOnly( + captureDef.getType(), + "function capture at " + describeFunction(classDef, functionDef) + "(" + captureDef.getName() + ")" + ); + } + TypeCheckUtil.requireNonCompilerOnly( + functionDef.getReturnType(), + "function return at " + describeFunction(classDef, functionDef) + ); + } + + private void validateProperty(@NotNull LirClassDef classDef, @NotNull LirPropertyDef propertyDef) { + TypeCheckUtil.requireNonCompilerOnly( + propertyDef.getType(), + "property at " + classDef.getName() + "." + propertyDef.getName() + ); + } + + private void validateSignal(@NotNull LirClassDef classDef, @NotNull LirSignalDef signalDef) { + for (var parameterDef : signalDef.getParameters()) { + TypeCheckUtil.requireNonCompilerOnly( + parameterDef.getType(), + "signal parameter at " + classDef.getName() + "." + signalDef.getName() + "(" + parameterDef.getName() + ")" + ); + } + } + + private @NotNull String describeFunction(@NotNull LirClassDef classDef, @NotNull LirFunctionDef functionDef) { + return classDef.getName() + "." + functionDef.getName(); + } +} 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..41326ae8 --- /dev/null +++ b/src/main/java/gd/script/gdcc/type/GdCompilerType.java @@ -0,0 +1,91 @@ +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 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() { + 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 918df941..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 { + 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 new file mode 100644 index 00000000..51acaef6 --- /dev/null +++ b/src/main/java/gd/script/gdcc/type/GdccForRangeIterType.java @@ -0,0 +1,44 @@ +package gd.script.gdcc.type; + +import org.jetbrains.annotations.NotNull; + +/// 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(); + + 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"; + } + + @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 isDirectStructAssignmentSafe() { + return true; + } +} 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/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/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 e160101f..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 @@ -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() { @@ -437,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() { @@ -623,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 911b0f6f..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 @@ -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,150 @@ 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 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("$iter = gdcc_for_range_iter_init();"), cCode); + 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/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/CDestructInsnGenTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/CDestructInsnGenTest.java index 0d38b661..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 @@ -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() { @@ -176,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 d84a539d..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 @@ -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,31 @@ 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("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_")); + assertFalse(helper.renderDestroyFunctionName(type).contains("godot_")); + assertThrows(IllegalArgumentException.class, () -> helper.renderPackFunctionName(type)); + 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() { @@ -212,6 +238,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() { @@ -626,6 +666,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() { @@ -780,6 +851,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/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/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/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 530e78dd..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 @@ -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,46 @@ 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 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/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/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/GdccForRangeIterIntrinsicTest.java b/src/test/java/gd/script/gdcc/backend/c/gen/GdccForRangeIterIntrinsicTest.java new file mode 100644 index 00000000..661dc5b7 --- /dev/null +++ b/src/test/java/gd/script/gdcc/backend/c/gen/GdccForRangeIterIntrinsicTest.java @@ -0,0 +1,281 @@ +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.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 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/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() { 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/FrontendLoweringBodyInsnPassTest.java b/src/test/java/gd/script/gdcc/frontend/lowering/FrontendLoweringBodyInsnPassTest.java index e2c58b0d..bc769db7 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; @@ -74,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; @@ -955,6 +960,292 @@ 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 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( @@ -2846,7 +3137,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/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/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/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/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, 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/lir/parser/DomLirParserTest.java b/src/test/java/gd/script/gdcc/lir/parser/DomLirParserTest.java index 0c8fb635..8ae625d2 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; @@ -10,6 +11,7 @@ import org.junit.jupiter.api.Test; import java.io.StringReader; +import java.util.List; import static org.junit.jupiter.api.Assertions.*; @@ -217,4 +219,180 @@ 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()); + } + + @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() + ); + } + } } 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/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")); 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; + } +} 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()); 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..d1b07890 --- /dev/null +++ b/src/test/java/gd/script/gdcc/type/GdCompilerTypeTest.java @@ -0,0 +1,126 @@ +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()); + 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 + @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 new file mode 100644 index 00000000..d65224c5 --- /dev/null +++ b/src/test/java/gd/script/gdcc/type/GdccForRangeIterTypeTest.java @@ -0,0 +1,49 @@ +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; + + // 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()); + } + + @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()); + } + } +}