跳转到内容

LIR 参考

LIR 是 GDCC 在较低层使用的函数级中间表示。它面向 GDExtension 这类显式调用边界设计:类型、变量、临时值、控制流、对象生命周期和 Variant 动态边界都需要在 IR 中被明确表达。

这份参考描述 LIR 的公开结构和每条指令的语义意图,不描述 GDCC 内部如何把这些指令落到具体实现。阅读时可以把 LIR 看成一个“接近目标运行时,但仍保留编译器语义”的线性表示。

一个 LIR 单元由类定义组成。类定义之下包含信号、属性和函数;函数体是 LIR 的核心执行单元。

  • 模块:包含一个或多个类定义,表示一次编译输出中的脚本类集合。
  • 类定义:包含类名、父类、注解、信号、属性和函数,描述 Godot 可见的脚本类表面。
  • 信号:包含名称和参数列表,描述类发出的 typed signal。
  • 属性:包含名称、类型、静态性、初始化 helper 和访问 helper,描述类字段与 Godot 属性表面。
  • 函数:包含签名、参数、返回类型、变量表和基本块,表示可执行逻辑。
  • 变量表:记录参数、局部变量、常量或栈存储,给指令中的 $id 提供类型和存储语义。
  • 基本块:包含一个入口、一串普通指令和一个终结指令,表示线性代码片段和控制流节点。
  • 指令:包含 opcode、可选结果和操作数,表示具体计算、调用、读写或跳转。

LIR 不采用 SSA。一个值可以通过变量槽、显式赋值、加载和存储继续流动。这样做的设计目标是让 IR 更接近 GDExtension C 代码的生成模型:变量槽、属性槽、返回槽和生命周期边界都可以被直接表达。

基本指令文本形态如下:

($<result_id> = )?<opcode> <operand>...

结果值写在左侧,变量操作数使用 $id,字符串和操作符名称使用引号,基本块标签直接写标签名。没有结果的指令只写 opcode 和操作数。

LIR 文件是 XML 结构:顶层 class_def 之下包含 signals、properties 和 functions。类定义和函数除了承载指令,还携带 Godot 表面和编译器内部所需的元数据。

<class_def name="RotatingCamera"
super="Camera3D"
is_abstract="false"
is_tool="false">
<annotation key="tool" value=""/>
<signals>...</signals>
<properties>...</properties>
<functions>...</functions>
</class_def>
  • name 对匿名类是可选。
  • is_abstract、is_tool 可选,默认 false。
  • is_tool 是脚本级状态:一个 @tool 脚本编译成多个类(顶层类与内部类)时,每个 class_def 都写 is_tool="true",但只有顶层类带 tool annotation。

属性包含名称、类型、静态性、init_func、getter_func、setter_func 和注解。

  • init_func 可选,其返回值用于初始化属性;非静态属性的初始化函数第一个参数必须是表示 self 的 Object 类型。
  • getter_func / setter_func 可选:getter 接收 self 并返回属性同类型值,setter 接收 self 与同类型值;省略时生成默认访问器。
  • @export* 家族的 annotation value 原样编码 Godot 的 hint_string:不带引号、不带注解名前缀。例如 @export_range(0, 20, 0.5) 写成 <annotation key="export_range" value="0,20,0.5"/>;空值表示空 hint_string。
<function name="get_pitch"
is_static="false"
is_abstract="false"
is_lambda="false"
is_vararg="false"
is_hidden="false"
is_coroutine="false">
<meta call_site_context="<call_site_context>"
source_identity_key="<Class>::<enclosingFunc>#<ordinal>"/>
<parameters>
<parameter name="to_radius" type="bool"
default_value_func="_default_get_pitch$to_radius"/>
</parameters>
<captures>
<capture name="value" type="int"/>
</captures>
<return_type type="float"/>
<variables>...</variables>
<basic_blocks entry="entry">...</basic_blocks>
</function>
  • is_coroutine 标记栈式协程函数。只有它为 true 时,函数体内才允许出现 await。
  • is_lambda 标记 lambda 函数。lambda 必须携带 meta,普通函数不携带。
  • meta 是 lambda 的热重载身份元数据:source_identity_key 采用 <Class>::<enclosingFunc>#<ordinal> 形式,call_site_context 是可选的附加重绑定条件。
  • is_hidden 标记编译器生成的辅助函数,例如默认参数 helper _default_get_pitch$to_radius 和属性初始化 helper _field_init_pitch_degree;它们不对应源码中可直接调用的方法表面。
  • captures 只属于 lambda,记录捕获变量的名称和类型。
  • 参数的 default_value_func 可选。调用方省略该参数时,编译器调用该函数并用其返回值作为实参;没有 default_value_func 又省略参数则报错。

指令按返回值形态分为三类:

  • 必须有结果:指令会产生新值,写作 $result = opcode ...。
  • 可选结果:调用类指令可保留返回值,也可在只关心副作用时省略结果。
  • 无结果:指令只产生副作用、控制流或调试标记。

操作数常见形态:

  • 变量:写作 $value,引用变量表或临时值中的变量 id。
  • 字符串:写作 "name",表示类名、函数名、属性名、字符串字面量等。
  • 整数、浮点和布尔:写作 1、1.5、true,表示立即值。
  • 标签:写作 entry、exit,表示基本块 id。
  • 操作符:写作 "ADD"、"LESS",表示 Godot 操作符枚举名称。

这些指令把常量或已有变量变成 LIR 值。它们通常是表达式 lowering 的叶子节点。

创建 bool 常量。适合条件、比较结果替代值、布尔字面量。

$flag = literal_bool true

创建整数常量。LIR 表面按整数值表达,目标变量类型决定后续边界。

$count = literal_int 42

创建浮点常量。用于 float 字面量或需要浮点临时值的位置。

$ratio = literal_float 0.5

创建 String 常量,字符串内容按 UTF-8 文本表达。

$text = literal_string "hello"

创建 StringName 常量。适合方法名、属性名、信号名等身份化名称边界。

$name = literal_string_name "position"

创建 NodePath 常量。源码 ^"..." 字面量在 LIR 中写成普通字符串操作数,操作数是去掉 ^ 前缀和外侧引号后的路径文本。

$path = literal_node_path "Camera3D/Pivot"

创建 Variant 的 Nil 值。用于动态空值或 Variant 空边界。

$v = literal_nil

创建对象空引用。用于对象类型的 null 值。

$obj = literal_null

把一个变量的值写入另一个结果变量,显式表达类型兼容边界。

$target = assign $source

assign 不只是文本拷贝。它表达“把 source 放入 result 所属类型槽”的语义,因此需要满足同类型、对象上行转换、Variant 边界、有限容器协变,或已经按受支持的隐式转换规则物化后的值。

这些指令负责创建复杂值、处理生命周期,以及执行 Godot 操作符。

按结果变量类型构造内建值,例如向量、颜色、变换等。参数列表对应构造输入。

$vec = construct_builtin $x $y $z

构造 Array 或 Packed*Array。普通 Array 可带元素类型提示;packed array 由结果变量类型决定。

$items = construct_array "Node"
$packed = construct_array

构造 Dictionary,可选择提供 key/value 类型名。省略类型名表示动态字典。

$map = construct_dictionary "StringName" "Node"

从已经物化的操作数槽构造并填充一个新的 Array 或 Dictionary。容器族只由结果变量类型决定:

  • 结果类型必须是普通(非 ref)Array 或 Dictionary,不能是 Packed*Array 或其它内建类型。
  • Array 的操作数按源码顺序作为 element0..elementN;空操作数合法。
  • Dictionary 的操作数按 key0/value0/key1/value1… 成对排列,数量必须为偶数。
  • 操作数到元素/键/值类型的边界转换(如 int -> float、String -> StringName、Variant 打包/解包)由前端在发射该指令前完成。

该指令不替代构造空容器的 construct_array / construct_dictionary;__prepare__、属性默认初始化和显式空容器构造仍使用后者。

$arr = construct_container_literal $a $b $c
$dict = construct_container_literal $k0 $v0 $k1 $v1

构造某个类的对象实例。结果变量类型应能承载该类实例。

$node = construct_object "Node"

从存活的对象 receiver 和编译期信号名构造 Signal 值。结果是一个可销毁的内建值,不会延长 receiver 的生命周期。

$sig = construct_signal $self "timeout"

从实例 receiver 和编译期方法名构造 Callable。操作数固定为 (receiver, method_name) 两个;旧的一操作数形式非法。

$cb = construct_callable $self "_on_timeout"

receiver 的静态类型决定分派方式:

  • Object 类型:通过 godot_new_Callable_with_Object_StringName 绑定到该对象实例。
  • 非对象内建类型:先打包成临时 Variant,再创建绑定到该值的 Callable(静态包装不接收 self,内部 ptrcall base 为 NULL)。
  • Variant、静态方法或 utility 函数:非法,不能作为 receiver。

结果同样是可销毁的内建值,不会延长 receiver 的生命周期。静态或全局函数应使用 construct_standalone_callable。

为没有实例 receiver 的编译期已知函数构造 Callable。kind 取 utility、static_gdcc 或 static_engine:

  • utility:utility 函数,owner 必须为空字符串。
  • static_gdcc / static_engine:静态函数,owner 必须是类名。
$cb = construct_standalone_callable "utility" "" "print"
$cb2 = construct_standalone_callable "static_gdcc" "MathUtil" "clamp_i"

从 lambda 函数和捕获列表创建 Callable。第一个操作数是 lambda 函数名,之后是捕获变量。

$cb = construct_lambda "lambda_1" $self $value

销毁或释放变量持有的可销毁资源。只应出现在有明确来源的生命周期路径中。

destruct $value "AUTO_GENERATED"

尝试为对象取得引用所有权;非引用计数对象上语义上可为空操作。

try_own_object $obj "INTERNAL"

尝试释放对象引用所有权;非引用计数对象上语义上可为空操作。

try_release_object $obj "USER_EXPLICIT"

生命周期指令可以带来源字符串:AUTO_GENERATED、INTERNAL、USER_EXPLICIT 或 UNKNOWN。省略时等价于兼容旧格式的未知来源。新的 LIR 应尽量显式写出来源,避免无法判断这条生命周期操作由谁负责。

执行一元 Godot 操作符,例如取负或逻辑非。

$out = unary_op "NEGATE" $value

可用的一元操作符:

操作符常见源码写法
NEGATEunary -
POSITIVEunary +
BIT_NOT~
NOTnot、!

执行二元 Godot 操作符,例如加法、比较、位运算。

$sum = binary_op "ADD" $left $right

可用的二元操作符:

操作符常见源码写法
EQUAL==
NOT_EQUAL!=
LESS<
LESS_EQUAL<=
GREATER>
GREATER_EQUAL>=
ADDbinary +
SUBTRACTbinary -
MULTIPLY*
DIVIDE/
MODULE%
POWER**
SHIFT_LEFT<<
SHIFT_RIGHT>>
BIT_AND&
BIT_OR|
BIT_XOR^
ANDand、&&
ORor、||
XORxor
INin

LIR 文本中的操作符操作数写枚举名,例如 "ADD"、"NEGATE",不是写源码符号 + 或 -。

补充约定:

  • MODULE 同时承载数值取模和字符串格式化:左操作数为 String 时,% 按 Godot 的格式化语义执行。
  • not in 按复合规则支持,等价于对 IN 的结果取 NOT。
  • 对象值在 LIR 中携带类型身份,因此对象之间的 EQUAL / NOT_EQUAL 按 Godot 的实例 identity 语义比较;它不要求两侧静态类相关,也不等同于 is 的继承判断。

索引指令面向 Variant、字典、数组、对象和动态 keyed/named/indexed 访问。它们把“通过 key/name/index 访问一个动态承载值”的语义显式拆出来。

通过另一个 Variant key 从 Variant 中读取值。适合最通用的动态 get。

$out = variant_get $variant $key

通过 key 从 keyed Variant 读取值,通常用于字典类访问。

$out = variant_get_keyed $dict_like $key

通过 StringName 从 named Variant 读取值,通常用于对象属性或命名成员。

$out = variant_get_named $object_like $name

通过整数 index 从 Variant 读取值,通常用于数组或 packed array。

$out = variant_get_indexed $array_like $index

通过 Variant key 向 Variant 写入值。

variant_set $variant $key $value

通过 key 向 keyed Variant 写入值。

variant_set_keyed $dict_like $key $value

通过 StringName 向 named Variant 写入值。

variant_set_named $object_like $name $value

通过整数 index 向 Variant 写入值。

variant_set_indexed $array_like $index $value

这些指令的结果通常仍是 Variant 或后续要进入 typed boundary 的值。若后续需要稳定类型,应配合 unpack_variant、assign 或显式构造边界。

类型指令用于运行时类型查询、对象判断、对象转换,以及稳定类型和 Variant 之间的打包/解包。

读取 Variant 当前承载值的 Godot 类型 id。

$type_id = get_variant_type $variant

取得变量的类型名或对象类名,结果是 String。

$name = get_class_name $value

当 as 的目标是对象类时执行运行时类转换。源可以是 Object、Variant 或 Nil。

$node = object_cast "Node" $value

成功时返回目标类型的同实例对象(携带目标类型身份),不改变所有权类别;失败(空引用、已释放、非对象 Variant 或类不匹配)时产生 null。结果可以省略,此时转换在表面上只做校验。

当 as 的目标是非对象、非 Variant、非 Nil 的内建类型时执行运行时转换,包括参数化的 Array[T] / Dictionary[K, V]。语义对齐 Godot 的 Variant::construct / can_convert。

$v = builtin_cast "Vector2" $value
$ints = builtin_cast "PackedInt32Array" $value
  • 结果必须存在;参数化容器保留完整声明类型文本。
  • 完全同类型和 as Variant 使用 assign / pack_variant,不走这里。
  • 例外:同族 Packed*Array 的 as 不是保持身份的 assign,而会产生一个 COW 新身份副本,与 Godot 4.5 解释器一致。Packed 的 is 测试则是精确的 Variant.get_type() 族匹配。

判断值是否为编译期指定类型的实例(源码 is)。$value 可以是任意普通有类型的值,包括 Variant、内建类型和参数化容器,不再只限对象。

$ok = is_instance_of "Node" $value
$is_int_array = is_instance_of "Array[int]" $value
  • 结果始终是 bool;对于非 Variant 目标,null / nil 对象得到 false。
  • 类名使用 canonical / Godot 面向的名称;参数化容器使用完整类型文本。
  • Variant 是顶层类型:x is Variant 会被折叠为 true,x is not Variant 折叠为 false,稳定的 LIR 不应携带 is_instance_of "Variant"。
  • 裸 Array / Dictionary 针对参数化目标(如 "Array[T]")仍然执行运行时类型元数据检查,不能只依据静态裸槽类型折叠。

把稳定类型值装入 Variant。用于进入动态边界。

$variant = pack_variant $value

从 Variant 解出结果变量所需类型。用于离开动态边界。

$value = unpack_variant $variant

pack_variant 和 unpack_variant 是 LIR 中最重要的动态边界标记。它们让 Variant 的进入和离开变成显式动作,而不是由调用或赋值隐式猜测。

判断 Variant 是否为 Nil。

$is_nil = variant_is_nil $variant

判断对象引用是否为空。

$is_null = object_is_null $obj

控制流指令只能作为基本块终结指令使用。每个基本块应以一个终结指令结束,从而让控制流图清晰可验证。

无条件跳转到目标基本块。

goto exit

根据 bool 条件跳转到 true/false 两个基本块。

go_if $cond then_block else_block

go_if 的条件应是布尔值。来自 Variant 或其它稳定类型的条件通常需要先经过显式归一化,再进入 go_if。

从当前函数返回,可带返回值,也可无返回值。

return
return $value

调用指令都允许结果可选:如果调用返回值被使用,就写 $result = ...;如果只需要副作用,可以省略结果。

调用全局函数,例如全局工具函数或 Godot 全局函数。

$out = call_global "print" $message

调用对象实例方法。第一个变量操作数是 receiver,之后是参数。

$out = call_method "get_child" $node $index

调用父类方法。用于源码中的 super 调用语义。

call_super_method "_ready" $self

调用类上的静态方法。

$out = call_static_method "MathUtil" "clamp_i" $value $min $max

调用后端按名称注册的 intrinsic。需要显式物化的隐式转换会使用这种形态,例如 c_int_to_float 和 c_vector3i_to_vector3。

$out = call_intrinsic "intrinsic_name" $arg

调用参数按顺序列出。返回值是否保留应由源码上下文决定:表达式位置通常需要结果,statement 位置的 void 或纯副作用调用通常不需要结果。

挂起当前协程函数,直到被等待的操作数产生结果,然后把该结果作为指令值发布。挂起在运行时是栈式协程帧上的栈切换;在 LIR 表面上 await 仍然像一条同步指令,不拆分基本块。

$result = await $operand
  • 只能在 is_coroutine="true" 的函数中。
  • 操作数的静态类型决定分派路径:
    • Signal:一次性等待信号。
    • compiler::GdccCoroState:等待一个静态协程调用,即 call_method、call_super_method 或 call_static_method 命中协程函数时产生的结果;这类调用的结果变量必须声明为该类型。
    • Variant:运行时动态分派。
  • 其它静态类型非法。分派完全由静态类型驱动,不做名字推导或跨指令配对。
  • 结果类型由前端为结果变量发布:0 参信号为 Variant;1 参信号为声明的参数类型;多参信号为 Array[Variant];协程调用为被调函数的声明返回类型(void 被调函数得到承载 nil 的 Variant);动态等待为 Variant。
  • await 是产出值的普通指令,不是基本块终结指令,因此不改变入口块和终结指令规则。
  • compiler::GdccCoroState 的单消费者所有权规则见编译器专用类型。

这组指令表达对象属性和静态成员的直接读写。它们与 variant_get_named / variant_set_named 的区别是:这里的属性名和类名是 LIR 表面上的稳定字符串,而不是动态 Variant 路径。

从对象读取命名属性。

$value = load_property "position" $node

向对象写入命名属性。

store_property "position" $node $value

读取类上的静态变量或静态属性。

$value = load_static "GameState" "score"

写入类上的静态变量或静态属性。

store_static "GameState" "score" $value

属性读写应由类型和可见性分析保证目标存在、可读或可写。LIR 本身通过指令形态表达“这是一个稳定属性路径”,而不是重新描述源码解析规则。

空操作。用于占位、测试或保持指令序列结构。

nop

标记后续指令对应的源码行号,服务诊断或调试信息。line_number 不改变程序语义。

line_number 27

用户级条件断言。条件为 false 时,当前函数进入稳定的运行时错误/默认返回清理路径(goto __finally__)。

assert $cond
assert $cond $message

$cond 必须已经是 bool;真值归一化属于前端 lowering,不由该指令处理。可选 $message 必须可赋给 String。该指令无结果、不改变对象状态、不需要生命周期来源,也不是终结指令。

对象存活解引用保护。在解引用对象引用前断言其仍存活;对象为空或已释放时进入与 assert 相同的清理路径。

assert_object_live $node

该指令只用于方法 receiver、属性 receiver、_super 链访问和直接访问 GDCC 字段/方法所有者,不用于用户条件检查、相等比较或所有权操作。self 在方法执行期间保证存活,因此豁免;RefCounted 对象只要持有引用就保证存活,同样豁免。

  • 数据:literal_bool、literal_int、literal_float、literal_string、literal_string_name、literal_node_path、literal_nil、literal_null、assign。
  • 构造、生命周期和运算:construct_builtin、construct_array、construct_dictionary、construct_container_literal、construct_object、construct_signal、construct_callable、construct_standalone_callable、construct_lambda、destruct、try_own_object、try_release_object、unary_op、binary_op。
  • 索引和动态访问:variant_get、variant_get_keyed、variant_get_named、variant_get_indexed、variant_set、variant_set_keyed、variant_set_named、variant_set_indexed。
  • 类型:get_variant_type、get_class_name、object_cast、builtin_cast、is_instance_of、pack_variant、unpack_variant、variant_is_nil、object_is_null。
  • 控制流:goto、go_if、return。
  • 调用:call_global、call_method、call_super_method、call_static_method、call_intrinsic。
  • 协程:await。
  • 存取:load_property、store_property、load_static、store_static。
  • 其它:nop、line_number、assert、assert_object_live。