Are you sure the original version you wrote (without @eval) doesn’t already do what you want? I would expect it is, making a closure over X so that any changes to the variable X are seen in its run-time evaluation.

If not, you could do something like the following:

_ddx(v,X) = ((v[4] + v[6]) / 2 - v[5]) / X.^ 2 # define once
ddx = let X=X;
v -> _ddx(v,X)
end # run this every loop to capture the updated X

In case you really do want to invoke the compiler (which might be the case if a high-level iteration of your loop takes a long time to execute, otherwise it wouldn’t be worth it performance-wise), here are two options.

Note that isbits(X) has to hold if you want to move X into the type domain, so in practice it probably has to be a static array, as in StaticArrays.jl.

Untested:

# Example function
some_function_scalar(x, a, b, c) = ((a + b)/2 - c) / x .^ 2
function some_function_vector(x, v)
(a, b, c) = v
some_function_scalar(x, a, b, c)
end
# Option 1, move `X` into the type domain
compiled1_impl(f::F, ::Val{x}) where {F, x} = let f = f
v -> f(x, v)
end
compiled1(f::F, x) where {F} = compiled1_impl(f, Val(x))
# Option 2, move the entire function into the type domain
closure(f::F, x) where {F} = let f = f, x = x
v -> f(x, v)
end
compiled2_impl(::Val{f}) where {f} = v -> f(v)
compiled2(f::F) where {F} = compiled2_impl(Val(f))
compiled2(f::F, x) where {F} = compiled2(closure(f, x))

Then you can create your compileable function like compiled1(some_function_vector, x) or compiled2(some_function_vector, x), and it will get compiled (with x as a compile-time constant) the first time you run it.

Keep in mind that you also need to use function barriers properly (see the Performance tips page in the Manual), so your inner loops will have to be in their own functions.