Julia ASTs
Julia 有两种代码表示形式。首先是由解析器返回的表面语法 AST(例如 Meta.parse 函数),并由宏进行操作。它是代码书写时的结构化表示,由 julia-parser.scm 从字符流构建。接下来是降低形式,或 IR(中间表示),用于类型推断和代码生成。在降低形式中,节点的类型更少,所有宏都被展开,所有控制流都转换为显式分支和语句序列。降低形式由 julia-syntax.scm 构建。
首先我们将重点关注 AST,因为它是编写宏所必需的。
Surface syntax AST
前端 AST 几乎完全由 Expr 和原子(例如符号、数字)组成。通常,每种视觉上不同的语法形式都有一个不同的表达式头。将以 s 表达式语法给出示例。每个带括号的列表对应一个 Expr,其中第一个元素是头。例如,(call f x) 对应于 Julia 中的 Expr(:call, :f, :x)。
Calls
| Input | AST | 
|---|---|
f(x) | (call f x) | 
f(x, y=1, z=2) | (call f x (kw y 1) (kw z 2)) | 
f(x; y=1) | (call f (parameters (kw y 1)) x) | 
f(x...) | (call f (... x)) | 
do 语法:
f(x) do a,b
    body
end解析为 (do (call f x) (-> (tuple a b) (block body)))。
Operators
大多数运算符的使用只是函数调用,因此它们被解析为头部 call。然而,一些运算符是特殊形式(不一定是函数调用),在这些情况下,运算符本身就是表达式的头部。在 julia-parser.scm 中,这些被称为“语法运算符”。一些运算符(+ 和 *)使用 N-元解析;链式调用被解析为单个 N-参数调用。最后,比较链有其自己的特殊表达式结构。
| Input | AST | 
|---|---|
x+y | (call + x y) | 
a+b+c+d | (call + a b c d) | 
2x | (call * 2 x) | 
a&&b | (&& a b) | 
x += 1 | (+= x 1) | 
a ? 1 : 2 | (if a 1 2) | 
a,b | (tuple a b) | 
a==b | (call == a b) | 
1<i<=n | (comparison 1 < i <= n) | 
a.b | (. a (quote b)) | 
a.(b) | (. a (tuple b)) | 
Bracketed forms
| Input | AST | 
|---|---|
a[i] | (ref a i) | 
t[i;j] | (typed_vcat t i j) | 
t[i j] | (typed_hcat t i j) | 
t[a b; c d] | (typed_vcat t (row a b) (row c d)) | 
t[a b;;; c d] | (typed_ncat t 3 (row a b) (row c d)) | 
a{b} | (curly a b) | 
a{b;c} | (curly a (parameters c) b) | 
[x] | (vect x) | 
[x,y] | (vect x y) | 
[x;y] | (vcat x y) | 
[x y] | (hcat x y) | 
[x y; z t] | (vcat (row x y) (row z t)) | 
[x;y;; z;t;;;] | (ncat 3 (nrow 2 (nrow 1 x y) (nrow 1 z t))) | 
[x for y in z, a in b] | (comprehension (generator x (= y z) (= a b))) | 
T[x for y in z] | (typed_comprehension T (generator x (= y z))) | 
(a, b, c) | (tuple a b c) | 
(a; b; c) | (block a b c) | 
Macros
| Input | AST | 
|---|---|
@m x y | (macrocall @m (line) x y) | 
Base.@m x y | (macrocall (. Base (quote @m)) (line) x y) | 
@Base.m x y | (macrocall (. Base (quote @m)) (line) x y) | 
Strings
| Input | AST | 
|---|---|
"a" | "a" | 
x"y" | (macrocall @x_str (line) "y") | 
x"y"z | (macrocall @x_str (line) "y" "z") | 
"x = $x" | (string "x = " x) | 
`a b c` | (macrocall @cmd (line) "a b c") | 
文档字符串语法:
"some docs"
f(x) = x解析为 (macrocall (|.| Core '@doc) (line) "一些文档" (= (call f x) (block x)))。
Imports and such
| Input | AST | 
|---|---|
import a | (import (. a)) | 
import a.b.c | (import (. a b c)) | 
import ...a | (import (. . . . a)) | 
import a.b, c.d | (import (. a b) (. c d)) | 
import Base: x | (import (: (. Base) (. x))) | 
import Base: x, y | (import (: (. Base) (. x) (. y))) | 
export a, b | (export a b) | 
using 的表示与 import 相同,但表达式头为 :using 而不是 :import。
Numbers
Julia 支持比许多 Scheme 实现更多的数字类型,因此并非所有数字都直接作为 AST 中的 Scheme 数字表示。
| Input | AST | 
|---|---|
11111111111111111111 | (macrocall @int128_str nothing "11111111111111111111") | 
0xfffffffffffffffff | (macrocall @uint128_str nothing "0xfffffffffffffffff") | 
1111...many digits... | (macrocall @big_str nothing "1111....") | 
Block forms
一个语句块被解析为 (block stmt1 stmt2 ...)。
如果语句:
if a
    b
elseif c
    d
else
    e
end解析为:
(if a (block (line 2) b)
    (elseif (block (line 3) c) (block (line 4) d)
            (block (line 6 e))))一个 while 循环解析为 (while condition body)。
一个 for 循环解析为 (for (= var iter) body)。如果有多个迭代规范,它们被解析为一个块:(for (block (= v1 iter1) (= v2 iter2)) body)。
break 和 continue 被解析为 0 个参数的表达式 (break) 和 (continue)。
let 被解析为 (let (= var val) body) 或 (let (block (= var1 val1) (= var2 val2) ...) body),类似于 for 循环。
一个基本的函数定义被解析为 (function (call f x) body)。一个更复杂的例子:
function f(x::T; k = 1) where T
    return x+1
end解析为:
(function (where (call f (parameters (kw k 1))
                       (:: x T))
                 T)
          (block (line 2) (return (call + x 1))))类型定义:
mutable struct Foo{T<:S}
    x::T
end解析为:
(struct true (curly Foo (<: T S))
        (block (line 2) (:: x T)))第一个参数是一个布尔值,指示类型是否可变。
try 块解析为 (try try_block var catch_block finally_block)。如果在 catch 后没有变量,则 var 为 #f。如果没有 finally 子句,则最后一个参数不存在。
Quote expressions
Julia 源代码语法形式用于代码引用(quote 和 :( ))支持使用 $ 进行插值。在 Lisp 术语中,这意味着它们实际上是“反引号”或“准引号”形式。在内部,也需要没有插值的代码引用。在 Julia 的方案代码中,不插值的引用用表达式头 inert 表示。
inert 表达式被转换为 Julia 的 QuoteNode 对象。这些对象包装了任何类型的单个值,并在评估时简单地返回该值。
一个 quote 表达式,其参数是一个原子,也会被转换为 QuoteNode。
Line numbers
源位置信息表示为 (line line_num file_name),其中第三个组件是可选的(当当前行号变化但文件名不变时省略)。
这些表达式在 Julia 中表示为 LineNumberNode。
Macros
宏卫生通过表达式头对 escape 和 hygienic-scope 来表示。宏扩展的结果会自动包裹在 (hygienic-scope block module) 中,以表示新作用域的结果。用户可以在内部插入 (escape block) 来插入调用者的代码。
Lowered form
降低形式(IR)对编译器更为重要,因为它用于类型推断、内联等优化以及代码生成。对于人类来说,它也不那么明显,因为它是输入语法经过重大重组的结果。
除了 Symbol 和一些数字类型,以下数据类型以降低形式存在:
表达式具有由
head字段指示的节点类型,以及一个args字段,该字段是一个Vector{Any}的子表达式。虽然几乎表面 AST 的每个部分都由Expr表示,但 IR 仅使用有限数量的Expr,主要用于调用和一些仅限于顶层的形式。插槽编号通过连续编号识别参数和局部变量。它具有一个整数值的
id字段,给出插槽索引。这些插槽的类型可以在其CodeInfo对象的slottypes字段中找到。论点与
SlotNumber相同,但仅在优化后出现。表示引用的插槽是封闭函数的一个参数。代码信息将一组语句的 IR 包装起来。它的
code字段是一个要执行的表达式数组。GotoNode无条件跳转。参数是跳转目标,表示为代码数组中的索引。
GotoIfNot条件分支。如果
cond字段的值为假,则跳转到dest字段所标识的索引。返回节点将其参数(
val字段)作为封闭函数的值返回。如果val字段未定义,则这表示一个不可达语句。引用节点将任意值包装为引用数据。例如,函数
f() = :a包含一个QuoteNode,其value字段是符号a,以便返回符号本身而不是对其进行求值。全球引用引用模块
mod中的全局变量name。SSA值指由编译器插入的连续编号(从1开始)的静态单赋值(SSA)变量。
SSAValue的编号(id)是表示其值的表达式的代码数组索引。新变量节点标记一个变量(插槽)被创建的点。这会导致变量重置为未定义。
Expr types
这些符号出现在 head 字段的 Expr 中的小写形式。
呼叫函数调用(动态调度)。
args[1]是要调用的函数,args[2:end]是参数。调用函数调用(静态调度)。
args[1]是要调用的 MethodInstance,args[2:end]是参数(包括正在被调用的函数,在args[2])。静态参数通过索引引用静态参数。
=作业。在 IR 中,第一个参数始终是
SlotNumber或GlobalRef。方法添加一个方法到泛型函数,并在必要时分配结果。
具有1个参数形式和3个参数形式。1个参数形式源自语法
function foo end。在1个参数形式中,参数是一个符号。如果这个符号已经在当前作用域中命名了一个函数,则不会发生任何事情。如果符号未定义,则会创建一个新函数并将其分配给符号指定的标识符。如果符号已定义但命名的是一个非函数,则会引发错误。“命名一个函数”的定义是绑定是常量,并且指向一个单例类型的对象。这样做的理由是,单例类型的实例唯一标识要添加方法的类型。当类型具有字段时,添加的方法是添加到实例还是其类型就不清楚了。3个参数形式具有以下参数:
args[1]一个函数名称,或者如果未知或不需要则为
nothing。如果是一个符号,则表达式首先像上面的1个参数形式那样行为。从那时起,这个参数将被忽略。当方法严格按类型添加时,它可以是nothing,(::T)(x) = x,或者当一个方法被添加到一个现有函数时,MyModule.f(x) = x。args[2]一个
SimpleVector的参数类型数据。args[2][1]是一个参数类型的SimpleVector,而args[2][2]是一个与方法的静态参数对应的类型变量的SimpleVector。args[3]一个方法本身的
CodeInfo。对于“超出范围”的方法定义(向一个函数添加一个在不同范围内定义的方法),这是一个求值为:lambda表达式的表达式。
结构类型一个定义新
struct的 7 个参数表达式:args[1]struct的名称args[2]一个
call表达式,用于创建一个SimpleVector并指定其参数args[3]一个
call表达式,用于创建一个SimpleVector并指定其字段名称args[4]一个
Symbol、GlobalRef或Expr,指定超类型(例如,:Integer、GlobalRef(Core, :Any)或:(Core.apply_type(AbstractArray, T, N)))args[5]一个
call表达式,用于创建一个SimpleVector并指定其字段类型args[6]一个布尔值,如果
mutable则为真args[7]初始化的参数数量。这将是字段的数量,或由内部构造函数的
new语句调用的最小字段数量。
抽象类型一个定义新抽象类型的三参数表达式。参数与
struct_type表达式的参数 1、2 和 4 相同。原始类型一个定义新原始类型的4个参数表达式。参数1、2和4与
struct_type相同。参数3是位数。Julia 1.5 struct_type、abstract_type和primitive_type在 Julia 1.5 中被移除,并被新的内置函数替代。全球声明一个全局绑定。
常量声明一个(全局)变量为常量。
新分配一个新的类似结构的对象。第一个参数是类型。
new伪函数被简化为此,类型始终由编译器插入。这确实是一个内部专用特性,并且不进行任何检查。评估任意的new表达式可能会导致段错误。splatnew类似于
new,但字段值作为一个单一的元组传递。其工作方式类似于splat(new),如果new是一个一等函数的话,因此得名。已定义Expr(:isdefined, :x)返回一个布尔值,指示x是否已经在当前作用域中定义。the_exception在
catch块中返回捕获的异常,由jl_current_exception(ct)返回。输入进入异常处理程序(
setjmp)。args[1]是在出错时跳转到的捕获块的标签。产生一个由pop_exception消耗的令牌。离开弹出异常处理程序。
args[1]是要弹出的处理程序数量。pop_exception在离开捕获块时,将当前异常的堆栈弹出到与关联的
enter相同的状态。args[1]包含来自关联enter的令牌。Julia 1.1 pop_exception是 Julia 1.1 中的新功能。入站控制开启或关闭边界检查。维护一个栈;如果该表达式的第一个参数为真或假(
true表示禁用边界检查),则将其推入栈中。如果第一个参数为:pop,则弹出栈。边界检查如果内联到标记为
@inbounds的代码段中,则值为false,否则值为true。循环信息标记循环的结束。包含传递给
LowerSimdLoop的元数据,用于标记@simd表达式的内部循环,或将信息传播给 LLVM 循环传递。copyast部分的准引用实现。参数是一个表面语法 AST,它被递归复制并在运行时返回。
元元数据。
args[1]通常是一个符号,用于指定元数据的类型,其余参数为自由格式。以下是常用的几种元数据类型::inline和:noinline:内联提示。
foreigncall静态计算的
ccall信息容器。字段包括:args[1]: 名称将被解析的外部函数的表达式。
args[2]::Type: RT返回类型(字面意思),在定义包含方法时静态计算。
args[3]::SimpleVector(类型):AT包含方法定义时静态计算的参数类型的(字面)向量。
args[4]::Int: nreq可变参数函数定义所需的参数数量。
args[5]::QuoteNode{Symbol}: 调用约定调用约定。
args[6:5+length(args[3])]: 参数所有参数的值(每个参数的类型在 args[3] 中给出)。
args[6+length(args[3])+1:end]: gc-roots在调用期间可能需要被 gc-rooted 的附加对象。请参见 Working with LLVM 以了解这些对象的来源及其处理方式。
new_opaque_closure构造一个新的不透明闭包。字段包括:
args[1]: 签名不透明闭包的函数签名。不透明闭包不参与调度,但输入类型可以被限制。
args[2]: isva指示闭包是否接受可变参数。
args[3]: lb输出类型的下界。(默认为
Union{})args[4]: ub输出类型的上限。(默认为
Any)args[5]: 方法实际方法作为
opaque_closure_method表达式。args[6:end]: 捕获不透明闭包捕获的值。
Julia 1.7 不透明闭包是在 Julia 1.7 中添加的
Method
一个独特的容器,描述单个方法的共享元数据。
名称,模块,文件,行,签名元数据用于唯一识别计算机和人类的方法。
模糊与此方法可能存在歧义的其他方法的缓存。
专业化此方法创建的所有 MethodInstance 的缓存,用于确保唯一性。唯一性对于效率至关重要,特别是在增量预编译和方法失效跟踪时。
源原始源代码(如果可用,通常是压缩的)。
生成器一个可调用对象,可以执行以获取特定方法签名的专用源代码。
根指向非AST事物的指针,这些事物已被插入到AST中,这是AST压缩、类型推断或本机代码生成所必需的。
nargs,isva,called,is_for_opaque_closure,描述此方法源代码的位域。
主要世界拥有这种方法的世界时代。
MethodInstance
一个独特的容器,描述了一个方法的单一可调用签名。特别请参阅 Proper maintenance and care of multi-threading locks 以获取有关如何安全修改这些字段的重要细节。
specTypes此 MethodInstance 的主键。通过
def.specializations查找保证唯一性。def该函数描述的
Method的特化。或者是一个Module,如果这是在模块中展开的顶级 Lambda,并且不属于任何方法。sparam_valsspecTypes中静态参数的值。对于Method.unspecialized的MethodInstance,这是空的SimpleVector。但是对于来自MethodTable缓存的运行时MethodInstance,这将始终被定义并且可索引。未推断未压缩的顶层 thunk 源代码。此外,对于生成的函数,这是可能找到源代码的众多地方之一。
反向边我们存储缓存依赖项的反向列表,以便有效跟踪在新方法定义后可能需要的增量重新分析/重新编译工作。这是通过保持其他
MethodInstance的列表来实现的,这些MethodInstance已被推断或优化为可能调用此MethodInstance。这些优化结果可能存储在cache的某个地方,或者可能是我们不想缓存的某些结果,例如常量传播。因此,我们在这里将所有这些反向边合并到各种缓存条目中(无论如何,几乎总是只有一个适用的缓存条目,具有最大世界的哨兵值)。缓存共享此模板实例化的
CodeInstance对象的缓存。
CodeInstance
def该缓存条目派生自的
MethodInstance。所有者一个表示此
CodeInstance拥有者的令牌。将使用jl_egal进行匹配。
rettype/rettype_constspecFunctionObject字段的推断返回类型,通常也是该函数的一般计算返回类型。推断可能包含此函数推断源的缓存,或者可以设置为
nothing以仅指示rettype是推断的。ftpr通用的 jlcall 入口点。
jlcall_api调用
fptr时使用的 ABI。一些重要的包括:- 0 - 尚未编译
 - 1 - 
JL_CALLABLEjl_value_t *(*)(jl_function_t *f, jl_value_t *args[nargs], uint32_t nargs) - 2 - 常量(存储在 
rettype_const中的值) - 3 - 使用静态参数转发 
jl_value_t *(*)(jl_svec_t *sparams, jl_function_t *f, jl_value_t *args[nargs], uint32_t nargs) - 4 - 在解释器中运行 
jl_value_t *(*)(jl_method_instance_t *meth, jl_function_t *f, jl_value_t *args[nargs], uint32_t nargs) 
min_world/max_world此方法实例有效调用的世界年龄范围。如果 max_world 是特殊标记值
-1,则该值尚不清楚。在我们遇到需要重新考虑的回边之前,它可能会继续被使用。
CodeInfo
一个(通常是临时的)用于保存降级源代码的容器。
代码一个
Any语句数组插槽名称一个符号数组,为每个槽(参数或局部变量)提供名称。
slotflags一个
UInt8数组的槽属性,表示为位标志:- 0x02 - 已分配(仅在此变量左侧没有任何赋值语句时为假)
 - 0x08 - 已使用(如果有对插槽的读取或写入)
 - 0x10 - 静态分配一次
 - 0x20 - 可能在赋值之前使用。此标志仅在类型推断后有效。
 
ssavaluetypes要么是一个数组,要么是一个
Int。如果是
Int,则给出函数中编译器插入的临时位置的数量(code数组的长度)。如果是数组,则为每个位置指定一个类型。ssaflags每个表达式的语句级 32 位标志。在 julia.h 中查看
jl_code_info_t的定义以获取更多详细信息。linetable一个源位置对象的数组
codelocs一个整数索引数组,指向
linetable,给出与每个语句相关联的位置。
可选字段:
槽类型一个用于插槽的类型数组。
rettype推断的返回类型为降低形式(IR)。默认值为
Any。推理限制启发式方法method_for_inference_heuristics将在推理过程中根据需要扩展给定方法的生成器。父母拥有此对象的
MethodInstance(如果适用)。边缘将边缘转发到必须失效的方法实例。
min_world/max_world该代码在推断时有效的世界年龄范围。
布尔属性:
推断这是否是通过类型推断生成的。
内联的是否应该符合内联的条件。
传播内部是否在内联时应传播
@inbounds以省略@boundscheck块。
UInt8 设置:
constprop- 0 = 使用启发式方法
 - 1 = 侵略性
 - 2 = 无
 
purity由 5 个位标志构成:- 0x01 << 0 = 此方法保证一致地返回或终止 (
:consistent) - 0x01 << 1 = 该方法没有外部语义可见的副作用 (
:effect_free) - 0x01 << 2 = 此方法保证不会抛出异常 (
:nothrow) - 0x01 << 3 = 此方法保证终止 (
:terminates_globally) - 0x01 << 4 = 此方法中的语法控制流保证会终止 (
:terminates_locally) 
请参阅
Base.@assume_effects的文档以获取更多详细信息。- 0x01 << 0 = 此方法保证一致地返回或终止 (