Popularity
70 Stars
Updated Last
7 Months Ago
Started In
January 2022

CompTime

Build Status

The goal of this library is to allow for a simplified style of writing @generated functions, inspired by zig comptime features.

Get Started

(minimal example)

Theory

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.

  1. 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.
  2. 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.
  3. 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

Required Packages