IPython and Jupyter Notebooks¶
PRIK provides optional %%fortran, %%c, and %%pyi cell magics for compiling
Fortran and C code inside IPython or a Jupyter notebook. Install the notebook
dependency and load the extension once per session:
python3 -m pip install "prik[jupyter]"
%load_ext prik.jupyter
Run native code¶
Fortran¶
The cell body is ordinary Fortran source:
%%fortran
module maths
contains
real(8) function square(x)
real(8), intent(in) :: x
square = x*x
end function
end module
PRIK publishes the declared Fortran module namespace:
import numpy as np
maths.square(np.float64(4.0))
Each declared module becomes a notebook name. Standalone procedures are published directly.
C¶
C functions are published directly in the notebook namespace:
%%c
double square(double x) {
return x * x;
}
square(np.float64(4.0))
Edit the generated .pyi¶
Add --pyi when the generated Python API is not the one you want. PRIK
compiles nothing yet; it persists the source and hands back an editable
contract cell.
%%c --pyi
#include <stddef.h>
void scale(size_t count, double *values) {
for (size_t i = 0; i < count; ++i) values[i] *= 2.0;
}
PRIK inserts the contract it derived from that source:
%%pyi
# prik: source-sha256=<generated digest>
from prik.contracts import Float64, UInt64
def scale(
count: UInt64,
values: Float64[...]
) -> None: ...
This is a faithful reading of the C, but it is not a good Python API: the
caller has to pass a length that NumPy already knows, and Float64[...]
accepts any rank because double * does not say which one it is. Edit the
cell, keep the # prik: line, and run it:
%%pyi
# prik: source-sha256=<generated digest>
from prik.contracts import Arg, Float64, native_call
@native_call([Arg(0).size, Arg(0)])
def scale(values: Float64[:]) -> None: ...
Float64[:] requires a rank-one array, and Arg(0).size supplies count
from it, so count disappears from the Python signature:
values = np.array([1.0, 2.0, 3.0])
scale(values)
values # array([2., 4., 6.]) -- updated in place
The native code and the compiler options are unchanged; only the Python API you call is different.
Contract cells and their names¶
Fortran source generates one editable cell per declared module, each carrying
a file= field:
%%pyi
# prik: file=maths.pyi source-sha256=<generated digest>
The file= leaf name is the Fortran module name, not a name you choose —
maths.pyi is the contract for module maths, and it publishes maths in
the notebook. C source and standalone Fortran declarations have no module to
name, so their cells carry no file= field and publish their functions
directly.
Jupyter inserts every generated cell at once; terminal IPython presents them in sequence as you execute each one. Compiler and build options are copied from the source cell, so rerun the source cell to change them.
The same workflow works with %%fortran --pyi.
Wrap an existing source file¶
Use %%pyi directly when the native source already exists as a file. There is
no source cell to generate from, so you write the contract yourself and name
the sources on the magic line:
%%pyi --native-fortran-sources maths.f90
# prik: file=maths.pyi
from prik.contracts import Addr, Arg, Float64, native_call
@native_call([Addr(Arg(0))])
def square(x: Float64) -> Float64: ...
Here file=maths.pyi is the contract for module maths inside maths.f90,
and the cell publishes maths. The leaf name must match the Fortran module,
whatever the file is called — a mismatch reaches the Fortran compiler as a
missing .mod, not a PRIK diagnostic. To expose a second module from the same
source, write another %%pyi cell naming that module, such as
# prik: file=helpers.pyi.
List multiple source files in compilation order when needed:
%%pyi --native-fortran-sources maths.f90 helpers.f90
%%pyi --native-c-sources square.c
For C or standalone Fortran, omit the # prik: line entirely. Standalone
Fortran declarations still use @standalone. Relative source paths start from
the kernel's current working directory.
Compilers and flags¶
%%fortran --compiler ifx --native-compile-flags="-O3 -march=native"
--native-compile-flagsapplies to the cell's own Fortran or C source. A cell has one native language, so there is a single option here; the command line splits the same setting into--native-compile-flagsfor Fortran and--native-c-compile-flagsfor C.--wrapper-fortran-flagsapplies to generated Fortran bridge source.--wrapper-c-flagsapplies to generated C binding source and extension linking.--compiler-argadds one preprocessing argument and may be repeated.
A value that starts with a dash needs the equals form. --native-compile-flags -O3
reads -O3 as another option and fails; write --native-compile-flags=-O3.
The three flag options also accept several flags as one quoted group:
%%c --native-compile-flags="-O3 -march=native"
--compiler-arg is not split, so it carries exactly one argument. Repeat it
for more:
%%fortran --compiler-arg=-fdefault-real-8 --compiler-arg=-I/opt/include
Cell cache¶
PRIK reuses a compiled result when the cell, compiler, flags, and native files
are unchanged. Use --force to rebuild or --verbose to show build activity:
%%fortran --force
%%fortran --verbose
The cache persists between sessions in ~/.cache/prik/jupyter, or under
$XDG_CACHE_HOME/prik/jupyter or $PRIK_CACHE_DIR/jupyter when either is
set. Each distinct cell text gets its own entry holding that cell's source,
generated wrapper, and compiled extension, so editing a cell repeatedly leaves
one entry per version. Nothing is evicted automatically. Delete the directory
to reclaim the space:
rm -rf ~/.cache/prik/jupyter
The next run of each cell rebuilds it.
Limitations¶
Source magics compile one self-contained cell. Use PRIK's CLI or Python build API for larger projects, external libraries, mixed-language builds, and advanced link configuration.