function call_fc(fc::FunctionCall{VectorT, 0}, cache::Dict{Symbol, Any}) where {VectorT <: SVector{1}}
    cache[fc.return_symbol] = fc.func(cache[fc.arguments[1]])
    return nothing
end

function call_fc(fc::FunctionCall{VectorT, 1}, cache::Dict{Symbol, Any}) where {VectorT <: SVector{1}}
    cache[fc.return_symbol] = fc.func(fc.additional_arguments[1], cache[fc.arguments[1]])
    return nothing
end

function call_fc(fc::FunctionCall{VectorT, 0}, cache::Dict{Symbol, Any}) where {VectorT <: SVector{2}}
    cache[fc.return_symbol] = fc.func(cache[fc.arguments[1]], cache[fc.arguments[2]])
    return nothing
end

function call_fc(fc::FunctionCall{VectorT, 1}, cache::Dict{Symbol, Any}) where {VectorT <: SVector{2}}
    cache[fc.return_symbol] = fc.func(fc.additional_arguments[1], cache[fc.arguments[1]], cache[fc.arguments[2]])
    return nothing
end

function call_fc(fc::FunctionCall{VectorT, 1}, cache::Dict{Symbol, Any}) where {VectorT}
    cache[fc.return_symbol] = fc.func(fc.additional_arguments[1], getindex.(Ref(cache), fc.arguments))
    return nothing
end

"""
    call_fc(fc::FunctionCall, cache::Dict{Symbol, Any})

Execute the given [`FunctionCall`](@ref) on the dictionary.

Several more specialized versions of this function exist to reduce vector unrolling work for common cases.
"""
function call_fc(fc::FunctionCall{VectorT, M}, cache::Dict{Symbol, Any}) where {VectorT, M}
    cache[fc.return_symbol] = fc.func(fc.additional_arguments..., getindex.(Ref(cache), fc.arguments)...)
    return nothing
end

function expr_from_fc(fc::FunctionCall{VectorT, 0}) where {VectorT}
    return Meta.parse(
        "$(eval(gen_access_expr(fc.device, fc.return_symbol))) = $(fc.func)($(unroll_symbol_vector(eval.(gen_access_expr.(Ref(fc.device), fc.arguments)))))",
    )
end

"""
    expr_from_fc(fc::FunctionCall)

For a given function call, return an expression evaluating it.
"""
function expr_from_fc(fc::FunctionCall{VectorT, M}) where {VectorT, M}
    func_call = Expr(
        :call,
        Symbol(fc.func),
        fc.additional_arguments...,
        eval.(gen_access_expr.(Ref(fc.device), fc.arguments))...,
    )

    expr = :($(eval(gen_access_expr(fc.device, fc.return_symbol))) = $func_call)
    return expr
end

"""
    gen_cache_init_code(machine::Machine)

For each [`AbstractDevice`](@ref) in the given [`Machine`](@ref), returning a `Vector{Expr}` doing the initialization.
"""
function gen_cache_init_code(machine::Machine)
    initializeCaches = Vector{Expr}()

    for device in machine.devices
        push!(initializeCaches, gen_cache_init_code(device))
    end

    return initializeCaches
end

"""
    part_from_x(type::Type, index::Int, x::AbstractProcessInput)

Return the [`ParticleValue`](@ref) of the given type of particle with the given `index` from the given process input.

Function is wrapped into a [`FunctionCall`](@ref) in [`gen_input_assignment_code`](@ref).
"""
part_from_x(type::Type, index::Int, x::AbstractProcessInput) =
    ParticleValue{type, ComplexF64}(get_particle(x, type, index), one(ComplexF64))

"""
    gen_input_assignment_code(
        inputSymbols::Dict{String, Vector{Symbol}},
        processDescription::AbstractProcessDescription,
        machine::Machine,
        processInputSymbol::Symbol = :input,
    )

Return a `Vector{Expr}` doing the input assignments from the given `processInputSymbol` onto the `inputSymbols`.
"""
function gen_input_assignment_code(
    inputSymbols::Dict{String, Vector{Symbol}},
    processDescription::AbstractProcessDescription,
    machine::Machine,
    processInputSymbol::Symbol = :input,
)
    @assert length(inputSymbols) >=
            sum(values(in_particles(processDescription))) + sum(values(out_particles(processDescription))) "Number of input Symbols is smaller than the number of particles in the process description"

    assignInputs = Vector{FunctionCall}()
    for (name, symbols) in inputSymbols
        (type, index) = type_index_from_name(model(processDescription), name)
        # make a function for this, since we can't use anonymous functions in the FunctionCall

        for symbol in symbols
            device = entry_device(machine)
            push!(
                assignInputs,
                FunctionCall(
                    # x is the process input
                    part_from_x,
                    SVector{1, Symbol}(processInputSymbol),
                    SVector{2, Any}(type, index),
                    symbol,
                    device,
                ),
            )
        end
    end

    return assignInputs
end

"""
    gen_tape(graph::DAG, process::AbstractProcessDescription, machine::Machine)

Generate the code for a given graph. The return value is a [`Tape`](@ref).

See also: [`execute`](@ref), [`execute_tape`](@ref)
"""
function gen_tape(graph::DAG, process::AbstractProcessDescription, machine::Machine)
    schedule = schedule_dag(GreedyScheduler(), graph, machine)

    # get inSymbols
    inputSyms = Dict{String, Vector{Symbol}}()
    for node in get_entry_nodes(graph)
        if !haskey(inputSyms, node.name)
            inputSyms[node.name] = Vector{Symbol}()
        end

        push!(inputSyms[node.name], Symbol("$(to_var_name(node.id))_in"))
    end

    # get outSymbol
    outSym = Symbol(to_var_name(get_exit_node(graph).id))

    initCaches = gen_cache_init_code(machine)
    assignInputs = gen_input_assignment_code(inputSyms, process, machine, :input)

    return Tape(initCaches, assignInputs, schedule, inputSyms, outSym, Dict(), process, machine)
end

"""
    execute_tape(tape::Tape, input::AbstractProcessInput)

Execute the given tape with the given input.

For implementation reasons, this disregards the set [`CacheStrategy`](@ref) of the devices and always uses a dictionary.
"""
function execute_tape(tape::Tape, input::AbstractProcessInput)
    cache = Dict{Symbol, Any}()
    cache[:input] = input
    # simply execute all the code snippets here
    # TODO: `@assert` that process input fits the tape.process
    for expr in tape.initCachesCode
        @eval $expr
    end

    for function_call in tape.inputAssignCode
        call_fc(function_call, cache)
    end
    for function_call in tape.computeCode
        call_fc(function_call, cache)
    end

    return cache[tape.outputSymbol]
end
