The goal of this library is to allow for a simplified style of writing
@generated
functions, inspired by zig comptime features.
(minimal example)
The core feature of CompTime is the ability to write functions that optionally have some of their code pre-run at compile time.
The central tenet of CompTime is that this does not allow you to write anything that you would not otherwise be able to write, from a semantics perspective. However, having a function partially evaluated at compile time may enable functions that would normally not be type checkable to be type checked, so from a type-checking standpoint this is a win, and of course having a function partially evaluated at compile time enables all sorts of other speedups.
Every function declared with @ct_enable
can be used in three modes.
- Compile-time mode. This compiles the function specially for the compile-time arguments to the function, and then runs the function. Under the hood, this uses
@generated
functions, and passes in all of the compile-time parameters as types, so this compilation is cached just like a normal@generated
function, as long as all of the compile-time parameters can be resolved using constant-propagation. - Run-time mode. This does no compile-time computation, and just runs the function as if all of the macros from CompTime.jl were not there.
- Syntax mode. This outputs the syntax that would be compiled for arguments of a certain type. This is very useful for debugging.
The arguments available at compile time are precisely the type arguments in the where
clause.
Here's an example. Suppose we have a type of static vectors, here written for simplicity as a wrapper around the type of normal vectors.
struct SVector{T,n}
v::Vector{T}
function SVector(v::Vector{T}) where {T}
new{T,length(v)}(v)
end
function SVector{T,n}(v::Vector{T}) where {T,n}
assert(n == length(v))
new{T,n}(v)
end
function SVector{T,n}() where {T,n}
new{T,n}(Vector{T}(undef,n))
end
end
Then we can write the following function to unroll a for-loop to add two static vectors.
@ct_enable function add(v1::SVector{T,n}, v2::SVector{T,n}) where {T,n}
vout = SVector{(@ct T), (@ct n)}()
@ct_ctrl for i in 1:n
vout[@ct i] = v1[@ct i] + v2[@ct i]
end
vout
end
This should be roughly equivalent to the following code
function add(v1::SVector{T,n}, v2::SVector{T,n}) where {T,n}
comptime(add, v1, v2)
end
function generate_code(::typeof(add), ::Type{SVector{T,n}}, ::Type{SVector{T,n}}) where {T,n}
Expr(:block,
:(vout = SVector{$T}(Vector{$T}(undef, $n))),
begin
code = Expr(:block)
for i in 1:n
push!(code.args, :(vout[$i] = v1[$i] + v2[$i]))
end
code
end,
:(vout)
)
end
@generated function comptime(::typeof(add), v1::SVector{T,n}, v2::SVector{T,n}) where {T,n}
generate_code(add, SVector{T,n}, SVector{T,n})
end
function runtime(::typeof(add), v1::SVector{T,n}, v2::SVector{T,n}) where {T,n}
vout = SVector{T,n}()
for i in 1:n
vout[i] = v1[i] + v2[i]
end
vout
end
If you want the compiler to have the freedom to decide whether to use the runtime or comptime version (e.g. this can be advantageous in the presence of type instabilities or when running code in a debugger), you can add optional=true
to make an optionally generated function. In our example, this would look like
@ct_enable optional=true function add(v1::SVector{T,n}, v2::SVector{T,n}) where {T,n}
vout = SVector{(@ct T), (@ct n)}()
@ct_ctrl for i in 1:n
vout[@ct i] = v1[@ct i] + v2[@ct i]
end
vout
end