Hey everyone!
I’ve been having a lot of fun working on a side project called Numeta. It’s a super simple Just-In-Time (JIT) compiler for Python that focuses on metaprogramming. Think of it as a dumb and lightweight version of Numba, but instead of trying to do all the fancy bytecode stuff, I’m keeping it simple by trying not to read the AST and generating good old Fortran code (because real programmers write Fortran in any language, no, actually because its very easy to translate Numpy code to Fortran ).
The idea is to use Python’s type hints to tell apart what’s compiled and what’s done at runtime. So far, it’s been really fun seeing this thing take shape! It’s in a very alpha stage (currently depends on gcc
and gfortran
, but there’s no inherent blocker for using other compilers, provided they support iso_c_binding
). The code generation and JITting works, and I’m currently trying to figure out the next steps for the project.
Right now, I’m considering some possible directions:
- General improvements (e.g., adding support for return types is a must).
- Expand support for more NumPy functionalities. This might require linking against a LAPACK library, which introduces challenges in managing LAPACK as a dependency.
- Make it possible to run the code without the decorator (as in Numba), mainly to facilitate debugging.
- Expose more of Fortran’s functionalities directly, though this could complicate the previous point.
- Change the backend to translate directly to a compiler intermediate representation (preferably at a higher level). While this would involve significant work, it could remove the need to generate source code, making the JIT process more efficient and seamless.
However, here’s a quick taste of what it does:
import numeta as nm
@nm.jit
def mixed_loops(n, array: nm.f8[:, :]) -> None:
for i in range(n):
for j in nm.frange(n):
array[j, i] = i + j
This generates this Fortran code (n=3) that after is compiled and executed:
subroutine mixed_loops(fc_n1, array) bind(C)
use iso_c_binding, only: c_int64_t
use iso_c_binding, only: c_size_t
use iso_c_binding, only: c_double
implicit none
integer(c_size_t), dimension(0:1), intent(in) :: fc_n1
real(c_double), dimension(0:(fc_n1(1)+(-1_c_int64_t)), 0:(fc_n1(0)+(-1_c_int64_t))), intent(inout) :: array
integer(c_int64_t) :: fc_i1
integer(c_int64_t) :: fc_i2
integer(c_int64_t) :: fc_i3
do fc_i1 = 0_c_int64_t, 2_c_int64_t
array(0, fc_i1) = (0_c_int64_t + fc_i1)
end do
do fc_i2 = 0_c_int64_t, 2_c_int64_t
array(1, fc_i2) = (1_c_int64_t + fc_i2)
end do
do fc_i3 = 0_c_int64_t, 2_c_int64_t
array(2, fc_i3) = (2_c_int64_t + fc_i3)
end do
end subroutine mixed_loops
If anyone’s curious or has suggestions (especially if you know a reasonable way to ship or link LAPACK dependencies, or have insights on the directions I’ve mentioned), I’d love to hear your thoughts. I’ve just started adding more features and would appreciate any feedback.
Feel free to check it out here: GitLab Repo