朱莉娅*阿斯特斯
Julia有两种代码表示。 首先,解析器返回一个表面语法AST(例如 元。解析,解析函数),并由宏操作。 它是代码的结构化表示,因为它是由 朱莉娅-解析器。供应链管理 从字符流。 接下来是一个降低的形式,或IR(中间表示),它被类型推断和代码生成所使用。 在降低的形式中,节点类型较少,所有宏都被扩展,所有控制流都被转换为显式分支和语句序列。 降低的形式是由 julia-语法。供应链管理.
首先,我们将重点放在AST上,因为它是编写宏所需要的。
表面语法AST
前端Ast几乎完全由 Exprs和原子(例如符号,数字)。 对于每种视觉上不同的句法形式,一般都有不同的表达头。 示例将在s-expression语法中给出。 每个带括号的列表对应一个Expr,其中第一个元素是head。 例如 (呼叫f x) 对应于 Expr(:呼叫,:f,:x) 在朱莉娅。
电话
| 输入 | AST |
|---|---|
|
|
|
|
|
|
|
|
做 语法:
f(x) do a,b
body
end
解析为 (do(call f x)(->(元组a b)(块体))).
营办商
运算符的大多数用法只是函数调用,所以它们是用head来解析的 打电话. 然而,一些运算符是特殊形式(不一定是函数调用),在这些情况下,运算符本身就是表达式头。 在julia-parser中。scm这些被称为"句法运算符"。 一些运营商(+ 和 *)使用N-ary解析;链式调用被解析为单个N参数调用。 最后,比较链有自己特殊的表达结构。
| 输入 | AST |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
括号内的表格
| 输入 | AST |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
宏
| 输入 | AST |
|---|---|
|
|
|
|
|
|
弦乐器
| 输入 | AST |
|---|---|
|
|
|
|
|
|
|
|
|
|
Doc字符串语法:
"some docs"
f(x) = x
解析为 (macrocall(/。/Core'@doc)(line)"some docs"(=(call f x)(block x))).
进口等
| 输入 | AST |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
使用 具有与 进口,但与表情头 :使用 而不是 :进口.
数字
Julia支持比许多方案实现更多的数字类型,因此并非所有数字都在AST中直接表示为方案编号。
| 输入 | AST |
|---|---|
|
|
|
|
|
|
集体表格
语句块被解析为 (块stmt1stmt2...).
If声明:
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))))
A 而 循环解析为 (当身体状况时).
A 为 循环解析为 (对于(=var iter)身体). 如果有多个迭代规范,它们将被解析为一个块: (for(block(=v1iter1)(=v2iter2))body).
休息 和 继续 被解析为0参数表达式 (休息) 和 (继续).
让 被解析为 (让(=var val)身体) 或 (让(块(=var1val1)(=var2val2)...)身体),像 为 循环。
一个基本的函数定义被解析为 (函数(调用f x)体). 一个更复杂的例子:
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_block var catch_block finally_block). 如果之后没有变量存在 渔获, 瓦尔 是 #f. 如果没有 最后 条款,那么最后一个参数不存在。
降低表格
降低形式(ir)对编译器来说更重要,因为它用于类型推断,内联等优化和代码生成。 它对人类来说也不那么明显,因为它是由于输入语法的显着重新排列而产生的。
除了 符号s和一些数字类型,以下数据类型以较低的形式存在:
* Expr
具有由 头 场,和一个 阿格斯 字段是一个 向量{Any} 子表达式。 虽然表面AST的几乎每个部分都由 Expr,IR只使用有限数量的 Exprs,主要用于呼叫和一些仅限顶级的表单。
* 睡眠者,睡眠者
通过连续编号标识参数和局部变量。 它有一个整数值 身份证 字段给出时隙索引。 这些插槽的类型可在 斯洛特型 他们的领域 代码信息 对象。
* 论点
同为 睡眠者,睡眠者,但只出现后优化。 指示引用的槽是封闭函数的参数。
* 代码信息
包装一组语句的IR。 其 密码 field是要执行的表达式数组。
* 戈多诺德
无条件分支。 参数是分支目标,表示为要跳转到的代码数组中的索引。
* GotoIfNot
条件分支。 如果 康德 字段评估为false,转到由 德斯特 场。
* 返回者,返回者
返回其参数( 瓦尔 字段)作为包围函数的值。 如果 瓦尔 字段是未定义的,那么这表示一个不可达的语句.
* 报价/报价
将任意值包装为数据引用。 例如,函数 f()=:a 包含一个 报价/报价 谁的 价值 字段是符号 a,以便返回符号本身而不是评估它。
* [医]GlobalRef
指全局变量 姓名 在模块中 国防部.
* [医]沙瓦鲁
指编译器插入的连续编号(从1开始)静态单赋值(SSA)变量。 数(身份证)的 [医]沙瓦鲁 是表示其值的表达式的代码数组索引。
* 新万诺德
标记创建变量(插槽)的点。 这具有将变量重置为未定义的效果。
Expr 类别
这些符号出现在 头 的领域 Exprs在降低的形式。
* 打电话
函数调用(动态调度)。 args[1] 是要调用的函数, args[2:结束] 是论据。
* 调用
函数调用(静态调度)。 args[1] 是要调用的MethodInstance, args[2:结束] 是参数(包括正在调用的函数,在 args[2]).
* 静态参数
通过索引引用静态参数。
* =
任务。 在IR中,第一个参数始终是 睡眠者,睡眠者 或一个 [医]GlobalRef.
* 方法
将方法添加到泛型函数,并在必要时分配结果。
具有1参数形式和3参数形式。 1参数形式产生于语法 函数foo end. 在1参数形式中,参数是一个符号。 如果这个符号已经在当前作用域中命名了一个函数,则什么都不会发生。 如果符号未定义,则会创建一个新函数并将其分配给符号指定的标识符。 如果定义了符号但命名为非函数,则会引发错误。 "命名函数"的定义是绑定是常量,并且指的是单例类型的对象。 这样做的理由是单例类型的实例唯一标识要添加方法的类型。 当类型具有字段时,不清楚该方法是被添加到实例还是其类型。
3参数形式具有以下参数:
** args[1]
函数名,或 什么都没有 如果未知或不需要。 如果是一个符号,那么表达式的行为就像上面的1参数形式一样。 这个论点从此被忽略。 它可以是 什么都没有 当方法严格按类型添加时, (::T)(x)=x,或将方法添加到现有函数时, MyModule的。f(x)=x.
** args[2]
A [医]单纯型 参数类型数据。 args[2][1] 是一个 [医]单纯型 参数类型,以及 args[2][2] 是一个 [医]单纯型 与方法的静态参数对应的类型变量。
** args[3]
A 代码信息 方法本身。 对于"超出作用域"的方法定义(将方法添加到函数中,该函数也具有在不同作用域中定义的方法),这是一个计算为 :lambda 表达。
* 结构_类型
定义一个新的7参数表达式 结构体:
** args[1]
的名称 结构体
** args[2]
A 打电话 创建一个表达式 [医]单纯型 指定其参数
** args[3]
A 打电话 创建一个表达式 [医]单纯型 指定其字段名
** args[4]
A 符号, [医]全球化,或 Expr 指定超类型(例如, :整数, GlobalRef(核心,:任何),或 :(核心。apply_type(AbstractArray,T,N)))
** args[5]
A 打电话 创建一个表达式 [医]单纯型 指定其字段类型
** args[6]
一个布尔,如果是真的 可变的
** args[7]
要初始化的参数数量。 这将是字段数,或内部构造函数调用的最小字段数 新的 声明。
* abstract_type
定义新抽象类型的3参数表达式。 参数与参数1、2和4相同 结构_类型 表情。
* 原始类型
定义新基元类型的4参数表达式。 参数1、2和4与 结构_类型. 参数3是位数。
+
|
兼容性
朱莉娅1.5 |
结构_类型, abstract_type,而 原始类型 在茱莉亚1.5中被删除,取而代之的是对新内建的调用。
* 全球
声明全局绑定。
* 康斯特
将(全局)变量声明为常量。
* 新的
分配一个新的类似结构的对象。 第一个参数是类型。 该 新的伪函数降到这个,类型总是由编译器插入。 这在很大程度上是一个仅限内部的功能,并且不进行检查。 评估任意 新的 表达式可以很容易地分段。
* 新的,新的
类似于 新的,除了字段值作为单个元组传递。 工作原理类似于 splat(新) 如果 新的 是一流的函数,因此得名。
* 被定义的
Expr(:被定义,:x) 返回一个Bool,指示是否 x 已经在当前范围中定义了。
* the_exception异常
在a中生成捕获的异常 渔获 块,如返回 jl_current_exception(ct).
* 进入
进入异常处理程序(setjmp). args[1] 是要在error时跳转到的catch块的标签。 产生一个由 pop_exception.
* 离开
Pop异常处理程序。 args[1] 是要弹出的处理程序的数量。
* pop_exception
弹出当前异常的堆栈回到关联的状态 进入 时离开一个捕获块。 args[1] 包含来自关联的令牌 进入.
+
|
兼容性
朱莉娅1.1 |
pop_exception 是新的朱莉娅1.1。
* [医]内袋
控制打开或关闭边界检查。 一个堆栈被维护;如果这个表达式的第一个参数是真或假(真的 意味着边界检查被禁用),它被推到堆栈上。 如果第一个参数是 :流行,堆栈弹出。
* boundscheck
具有价值 错误 如果内联到标记为 @inbounds,否则具有值 真的.
* 环线信息
标志着a循环的结束。 包含传递到的元数据 N.低频,低频 要标记的内循环 @simd 表达式,或将信息传播到LLVM循环传递.
* 复制/复制
准引用的部分实现。 参数是一个表面语法AST,它简单地递归复制并在运行时返回。
* 元,元
元数据。 args[1] 通常是指定元数据类型的符号,其余参数为自由格式。 常用的元数据有以下几种:
** :内联 和 :noinline:内联提示。
* 外汇储备
静态计算容器 ccall 信息。 字段是:
** args[1] :姓名
将为外部函数解析的表达式。
** args[2]::类型 :RT
(字面量)返回类型,在定义包含方法时静态计算。
** args[3]::SimpleVector (种类):在
参数类型的(字面量)向量,在定义包含方法时静态计算。
** args[4]::Int :nreq
Varargs函数定义所需参数的数量。
** args[5]::QuoteNode{<:Union{Symbol,Tuple{Symbol,UInt16},元组{Symbol,UInt16,Bool}}:调用约定
调用的调用约定,可选地具有效果,以及 gc_安全 (安全地并发执行到GC。).
** args[6:5+长度(args[3])] :论点
所有参数的值(每个参数的类型在args[3]中给出)。
** args[6+长度(args[3])+1:结束] :gc-根
在调用期间可能需要以gc为根的其他对象。 见 与LLVM一起工作这些是从哪里派生的以及它们是如何处理的。
* new_opaque_closure
构造一个新的不透明闭包。 字段是:
** args[1] :签名
不透明闭包的函数签名。 不透明闭包不参与调度,但输入类型可以受到限制。
** args[2] :磅
输出类型的下限。 (默认为 联合{})
** args[3] :ub
输出类型的上限。 (默认为 任何)
** args[4] :康斯特普
指示不透明闭包的标识是否可用于常量传播。 该 @不透明 宏默认情况下启用此功能,但这会导致额外的推断,这可能是不受欢迎的,并阻止代码在预编译期间运行。 如果 args[4] 是一个方法,参数被认为是跳过的。
** args[5] :方法
实际的方法作为一个 opaque_closure_method 表达。
** args[6:结束] :捕捉
不透明闭包捕获的值。
+
|
兼容性
朱莉娅1.7 |
在Julia1.7中添加了不透明闭包
方法
描述单个方法的共享元数据的唯一ded容器。
* 姓名, 模块, 档案, 行, sig
元数据来唯一地标识用于计算机和人类的方法。
* 安比格
可能与此方法有歧义的其他方法的缓存。
* 专业知识
为该方法创建的所有MethodInstance的缓存,用于确保唯一性。 效率需要唯一性,特别是对于方法无效的增量预编译和跟踪。
* 资料来源
原始源代码(如果可用,通常是压缩的)。
* 发电机
一个可调用的对象,可以执行以获得特定方法签名的专用源。
* 根
指向已插值到AST中的非AST事物的指针,这是ast压缩、类型推断或本机代码生成所要求的。
* 纳格斯, 伊斯瓦, 被称为, is_for_opaque_closure,
此方法源代码的描述性位字段。
* 原始世界
"拥有"这种方法的世界时代。
方法/方法
描述方法的单个可调用签名的唯一’d容器。 特别见 多线程锁的正确维护和护理有关如何安全地修改这些字段的重要详细信息。
* 眼镜型
此MethodInstance的主键。 唯一性是通过一个保证 def.专业知识 查找。
* 德夫
该 方法 这个函数描述了一个特殊的. 或一个 模块,如果这是在模块中扩展的顶级Lambda,并且不是方法的一部分。
* [医]节律
静态参数的值 眼镜型. 为 方法/方法 在 方法。unspecialized,这是空的 [医]单纯型. 但是对于运行时 方法/方法 从 方法表 缓存,这将始终是定义和可索引的。
* 背景,背景
我们存储缓存依赖关系的反向列表,以便有效跟踪新方法定义后可能需要的增量重新分析/重新编译工作。 这是通过保留另一个列表来工作的 方法/方法 已经推断或优化以包含对此的可能调用 方法/方法. 这些优化结果可能存储在 缓存,或者它可能是我们不想缓存的东西的结果,例如常量传播。 因此,我们将所有这些backedge合并到这里的各种缓存条目(几乎总是只有一个适用的缓存条目具有max_world的sentinel值)。
* 缓存
的缓存 [医]代码 共享此模板实例化的对象。
[医]代码
* 德夫
该 方法/方法 这个缓存条目是从.
* 业主
表示此所有者的令牌 [医]代码. 将使用 jl_法律 相匹配。
* [医]重型/rettype_const
的推断返回类型 特殊功能对象 字段,它(在大多数情况下)通常也是函数的计算返回类型。
* 推断的
可能包含此函数的推断源的缓存,也可以将其设置为 什么都没有 只是表明 [医]重型 被推断。
* [医]ftpr
通用jlcall入口点。
* jlcall_api
调用时要使用的ABI fptr. 一些重要的 include:
**0-尚未编译
** 1 - JL_可回收 jl_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_世界 / 最大世界
此方法实例有效调用的世界年龄范围。 如果max_world是特殊令牌值 -1,该值尚不清楚。 它可能会继续使用,直到我们遇到需要我们重新考虑的backedge。
*定时字段
** 时间,时间:计算总成本 推断的 最初是从开始到结束的墙壁时间。
** 时间_infer_cache_saved:节省的成本 时间,时间 通过缓存。 将此添加到 时间,时间 应该给出一个稳定的估计,用于比较两个实现或一个实现随时间的成本。 这通常是对推断某些内容的时间的过度估计,因为缓存在处理重复工作时通常是有效的。
** 时间-自我:Julia推理的自我成本 推断的 (一部分的 时间,时间). 这只是编译这一个方法的增量成本,如果给定所有调用目标的完全填充缓存,甚至包括常量推理结果和LimitedAccuracy结果,这些结果通常不在缓存中。
** 时间_compile:Llvm JIT编译的自我成本(例如计算) 调用 从 推断的). 总成本估计可以通过走所有的计算 边缘 内容和总结,同时考虑周期和重复。 (此字段目前不包括任何测量的aot编译时间。)
代码信息
一个(通常是临时的)容器,用于保存降低的(可能是推断的)源代码。
* 密码
一个 任何 语句数组
* slot名称
给出每个槽(参数或局部变量)名称的符号数组。
* [医]污点
A UInt8 插槽属性数组,表示为位标志:
**0x02-assigned(只有在左侧有带有此var的_no_赋值语句时才为false)
**0x08-使用(如果有插槽的任何读或写)
**0x10-静态分配一次
**0x20-可能在分配前使用。 此标志仅在类型推断后有效。
* [医]ssavaluetypes
数组或数组 Int型.
如果 Int型,它给出了函数中编译器插入的临时位置的数量( 密码 阵列)。 如果是数组,则为每个位置指定一个类型。
* ssaflags,ssaflags
函数中每个表达式的语句级32位标志。 见的定义 jl_code_info_t 在朱莉娅。h了解更多详情。
这些仅在推理之后填充(或在某些情况下由生成的函数填充):
* debuginfo,debuginfo
用于检索每个语句的源信息的对象,请参阅 如何解释行号在a 代码信息 物。
* [医]重型
降低形式(IR)的推断返回类型。 默认值为 任何. 这主要是为了方便起见,因为(由于OpaqueClosures的工作方式)它不一定是codegen使用的rettype。
* 家长/家长
该 方法/方法 "拥有"此对象(如果适用)。
* 边缘
必须无效的方法实例的前缘。
* min_世界/最大世界
此代码在推断时有效的世界年龄范围。
可选字段:
* 斯洛特型
插槽的类型数组。
* 方法_for_inference_limit_heuristics
该 方法_for_inference_heuristics 在推理过程中,如果需要,将扩展给定方法的生成器。
布尔属性:
* 传播_inbounds
这是否应该传播 @inbounds 为了eliding而内联时 @boundscheck 街区。
UInt8 设置:
* 康斯特普, 可内联
**0=使用启发式
**1=攻击性
**2=无
* 纯度;纯度 由5位标志构造:
** 0x01<<0 =此方法保证一致返回或终止(:一致)
** 0x01<<1 =此方法没有外部语义上可见的副作用(:effect_free)
** 0x01<<2 =此方法保证不抛出异常(:nothrow)
** 0x01<<3 =此方法保证终止(:terminates_globally)
** 0x01<<4 =该方法内的句法控制流保证终止(:终端_局部)
+
请参阅 基地。@assume_effects 有关更多详情。
如何解释行号 代码信息 对象
此数据有两种常见形式:一种用于内部压缩数据,另一种用于编译器。 它们包含相同的基本信息,但编译器版本都是可变的,而内部使用的版本则不是。
许多消费者可能可以打电话 基地。IRShow。buildLineInfoNode, 基地。IRShow。append_scopes!,或 Stacktraces。查找(::InterpreterIP) 以避免需要(重新)具体实施这些细节。
其中每一个的定义是:
struct Core.DebugInfo
@noinline
def::Union{Method,MethodInstance,Symbol}
linetable::Union{Nothing,DebugInfo}
edges::SimpleVector{DebugInfo}
codelocs::String # compressed data
end
mutable struct Core.Compiler.DebugInfoStream
def::Union{Method,MethodInstance,Symbol}
linetable::Union{Nothing,DebugInfo}
edges::Vector{DebugInfo}
firstline::Int32 # the starting line for this block (specified by an index of 0)
codelocs::Vector{Int32} # for each statement:
# index into linetable (if defined), else a line number (in the file represented by def)
# then index into edges
# then index into edges[linetable]
end
* 德夫 :这在哪里 DebugInfo,DebugInfo 被定义( 方法, 方法/方法,或 符号 例如,文件范围)
* 线形/线形
另一个 DebugInfo,DebugInfo 这是从它派生的,它包含实际的行号,这样这个DebugInfo只包含它的索引。 这样可以避免复制,并且可以跟踪每个单独的语句如何从源转换为优化,而不仅仅是单独的行号。 如果 德夫 不是一个符号,那么该对象将替换当前函数对象的元数据,该元数据在概念上正在执行什么函数(例如,这里认为盒式磁带转换)。 该 codelocs 下面描述的值也被解释为索引到 codelocs 在这个对象中,而不是作为行号本身。
* 边缘 :内联到this中的每个函数的唯一DebugInfo的向量(它递归地具有内联到它们中的所有内容的边)。
* 第一行 (未压缩到DebugInfoStream时)
与 開始啦。 语句(或其他关键字,如 功能 或 报价)这划定了这个代码定义"开始"的位置。
* codelocs (当未压缩到 DebugInfoStream)
一个索引向量,IR中的每个语句有3个值,块的起始点有一个值,用于描述从该点开始的stacktrace:
.. 整数索引到 linetable的。codelocs 字段,给出与每个语句相关联的原始位置(包括其句法边),或零,表示从以前执行的语句(不一定是句法或词法上的)没有改变行号,或行号本身,如果 线形/线形 场是 什么都没有.
.. 整数索引到 边缘,给予 DebugInfo,DebugInfo 在那里内联,如果没有边,则为零。
.. (如果条目2非零)整数索引到 边缘[]。codelocs,为内联堆栈中的每个函数递归地解释,或零指示使用 边缘[]。第一行 作为行号。
+
特别守则 include:
** (零,零,*):没有更改前一条语句的行号或边(您可以选择从语法或词法上解释)。 内联深度也可能已经改变,尽管大多数调用者应该忽略这一点。
** (零,非零,*) :没有行号,只是边缘(通常是因为宏扩展到顶级代码)。