Skip to content

Extend commands

These commands add behaviour to objects and modules that already exist in just-makeit.toml. Run them from the project root after scaffolding.


just-makeit method

just-makeit method <object> <method_name>
    [--module name]
    [--param name:type ...]
    --return-type TYPE
    [--variable-output] [--arg-type TYPE]
    [--multi-output TYPE ...]

Add a named execute method to an existing object.

jm method is additive and splice-free: it injects the method's declaration into <obj>_core.h and appends a fresh C stub to <obj>_core.c, then regenerates the glue (_ext.c, .pyi) with the new Python binding. Existing bodies in _core.c are never re-rendered — only the new stub is appended, ready for you to implement.

Arguments

Argument Description
object Object name (must already exist in just-makeit.toml).
method_name Snake-case name for the new method.
--module name Module the object belongs to. Optional — jm reads it from the manifest (gh-963); pass it to be explicit, and it is validated.
--param name:type Named typed scalar parameter. Repeatable.
--param name:type=default Optional scalar parameter (e.g. gain:double=1.0) — omit it for the default. Makes the method keyword-capable; optional params must follow required ones; plain scalars only (gh-240).
--param name:type[] Named numpy array parameter. Repeatable. Generates const elem_t *name, size_t name_len in C.
--return-type TYPE C type of the return value (void for no return).
--arg-type TYPE C type of a single array-style input. Use void for count-only inputs.
--variable-output Self-sizing output: allocate a NumPy-owned array per call and trim to the returned count. See below.
--multi-output TYPE Add a second (or further) output array. Repeatable; produces a tuple return.
--out-type TYPE Allocate a complex64 (or other) output array per call and pass *out to C. The C stub receives (... , elem_t *out) and the Python wrapper allocates and returns the ndarray automatically. The output length equals in_len / out_divisor.
--out-divisor N Divide the input length by N to determine the output array length when --out-type is active (default: 1). Use 2 for methods that interpret the input as interleaved I/Q pairs (e.g. a CI8 buffer where each complex sample is 2 bytes).
--batch Generate a 1:1-rate array transform. The C stub receives (state, const in_t *in, size_t n, out_t *out) (or (state, size_t n, out_t *out) for --arg-type void). The Python wrapper allocates an output array of length n per call and returns it. Use when output length equals input length and is unknown at init time.
--varargs Generate a *args/**kwargs Python binding. See below. Mutually exclusive with --arg-type, --param, and --variable-output.
--error-on-empty Treat a zero count from a --variable-output kernel as a REFUSAL and raise, instead of returning an empty array. Its one return value is the length, so it has no status to carry; without this a kernel that validates its input and returns 0 produces a well-formed empty result and the caller carries on. Names the exception with --error and the text with --error-message. Mutually exclusive with none_on_empty, which reads the same zero as "nothing to report".
--pass-capacity Emit the 5-arg (…, out, size_t max_out) C form for a --variable-output method (a bounds-checking C API receives the buffer capacity).
--nogil Release the GIL across the pure-C kernel of a --variable-output method (numpy accessors hoisted out first), so a thread-per-shard worker scales across cores. Opt-in: sound only when the object is not shared across threads concurrently (one object per stream). See below.
--count-default EXPR C expression seeding the synthesized leading count argument of a void-input --variable-output method, e.g. "state->num_taps". Its value is the zero-arg call's behaviour, and jm cannot derive it — it lives in your C (gh-1051).
--count-name NAME What that synthesized argument is called (default count). See Naming the synthesized count.
--result-field name:T[:doc] Declare one field of a returned record struct (repeatable). The method returns a list of these record tuples; pair with --return-type <record_struct> (the record's C type). The optional third component documents that field — it reaches the PyStructSequence field and the record class in the .pyi, so it requires --single (gh-646). The result-count cap (max_results, default 64) is TOML-only for now — no --max-results CLI flag yet.
--single With --result-field, return one named record (a PyStructSequence: attribute access + unpacking) instead of a list[tuple]. The C kernel returns the --return-type record struct by value (gh-244).
--record-name NAME With --single, the public name of the record type (e.g. ToneMetrics), overriding the name derived from the C --return-type (gh-257). Also settable per method in the manifest as record_name = "…".
--record-module MOD With --single, the __module__ of the record type (e.g. my_pkg.dsp), so type(r).__module__ / repr(r) matches the project's import path instead of the C component name. Also settable per method in the manifest as record_module = "…" (gh-261).
--record-doc "text" With --single, the record type's own documentation — what help(ToneMetrics) shows, and the record class's .pyi docstring. Defaults to the CPython-style synopsis (ToneMetrics(enob, sfdr_dbc)), never to nothing. A field with no :doc falls back to its trailing ///< member doc in the sacred header (gh-671). Also settable per method in the manifest as record_doc = "…" (gh-646).
--impl file::funcname Lift the method body from funcname in file instead of emitting a blank <<IMPLEMENT>> stub.
--impl file::N:M Lift lines N..M (inclusive, 1-based) instead of a named function body. Out-of-bounds or inverted ranges error cleanly.
--replace old::new String substitution applied to the body lifted by --impl. Repeatable.
--doc "text" Python docstring for the method.
--py-return-type STR Override the .pyi return-type annotation (the C --return-type still drives the C signature).
--view ClassName Attach the method to a view of the object instead of the object itself — adds a view-only method, or overrides a parent method by reusing its name. What --fn does or does not say decides which override it is; see Overriding a parent method. Views are a module-object feature, so the object must live in a module.
--fn SYMBOL C function this method binds, when it is not the derived <comp>_<name>. For adopting existing C under its own prefix, or binding a validating variant (--fn dp_tlm_emit_checked) under the plain Python name. The Python face is unchanged. On a view it also selects a signature override (below). Persists as fn = "…" on the method.
--error-negative The int return is a value unless it is negative, in which case it is an error code and the method raises. Distinct from --status-return, where the int carries nothing but status. Signed integer return types only. Persists as error_negative = "true".
--status-return The int return carries only status (0 = ok): the binding raises on non-zero and the method returns None, so no status code reaches Python. Mutually exclusive with --error-negative, which is for an int that is both a value and an error channel. Persists as status_return = "true".
--manual-stub This method's C binding is already hand-written in a sacred _ext_<obj>_extra.c fragment. jm declares nothing for it and only preserves its .pyi placeholder verbatim across regeneration (gh-428). Persists as manual_stub = "true".
--error EXC Exception --error-negative raises (default ValueError); one of jm's error categories. Persists as error = "…".
--error-message TEXT Text for that exception; jm appends (rc=%d). Persists as error_message = "…".
--record-dtype STRUCT With --variable-output: return one numpy structured array whose dtype is that C struct's own layout, one row per record, and --result-field names its columns. The dtype is built at runtime by the generated C from offsetof/sizeof — jm never sees the struct, so it cannot guess C's padding (gh-788). Contrast --single, which returns one record; see Record shapes below.
--no-bench Exclude this method from the generated C benchmark.

Named parameters (--param)

Use --param name:type when the method takes multiple distinct typed scalar inputs. This generates named parameters in both the C stub and the Python wrapper.

just-makeit method nco configure --module dsp \
    --param freq:float \
    --param phase:float \
    --param mode:int32_t \
    --return-type void

Generated C stub:

void
nco_configure(nco_state_t *state, float freq, float phase, int32_t mode)
{
    (void)state; (void)freq; (void)phase; (void)mode;
}

Python call:

nco.configure(0.1, 0.0, 2)

All scalar types in _CTYPE_META are supported as --param types (float, double, int, int32_t, uint32_t, size_t, float _Complex, etc.).

Array parameters (--param name:type[]) generate a numpy array input. The C stub receives (const elem_t *name, size_t name_len) and the Python wrapper performs PyArray_FROM_OTF automatically:

just-makeit method resamp execute_ctrl --module resample \
    --param ctrl:"float _Complex[]" \
    --return-type size_t

Generated C stub:

size_t
resamp_execute_ctrl(resamp_state_t *state,
                    const float _Complex *ctrl, size_t ctrl_len)
{
    (void)state; (void)ctrl; (void)ctrl_len;
    return (size_t)0;
}

Python call: resamp.execute_ctrl(np.zeros(64, dtype=np.complex64))

--param and --arg-type are mutually exclusive per method.


Varargs methods (--varargs)

Use --varargs when a method needs fully flexible Python argument parsing — for example, configure(rate=48000, mode="fast") where the parameter set is open-ended or includes types that fall outside the fixed _CTYPE_META table.

just-makeit method filter configure --varargs

What gets generated:

  • native/src/<comp>/<comp>_<name>_core.c (sacred — never regenerated). A Python-aware C file compiled directly into the Python extension DSO (not the pure-C OBJECT library), so it may include <Python.h> freely. Contains a PyObject * function with an <<IMPLEMENT>> stub:

    PyObject *
    filter_configure(PyObject *self, PyObject *args, PyObject *kwargs)
    {
        (void)self; (void)args; (void)kwargs;
        Py_RETURN_NONE;
    }
    
  • <comp>_ext.c (regenerated). An extern PyObject * declaration pulls the symbol in from the binding file, and the PyMethodDef entry uses METH_VARARGS | METH_KEYWORDS:

    extern PyObject *
    filter_configure(PyObject *, PyObject *, PyObject *);
    
    /* in PyMethodDef array: */
    {"configure", (PyCFunction)(void *)filter_configure,
     METH_VARARGS | METH_KEYWORDS, "configure(*args, **kwargs)."},
    
  • native/src/<comp>/CMakeLists.txt (surgically updated). The binding .c file is spliced into the Python3_add_library(...) source list so cmake compiles it into the same DSO.

  • .pyi stub (regenerated):

    def configure(self, *args: Any, **kwargs: Any) -> Any: ...
    
  • just-makeit.toml: varargs = true is recorded under [[<comp>.methods]].

Accessing the C state inside the binding:

The self pointer is a <Comp>Object * (the Python object), not the raw state struct. Cast it to reach the handle:

typedef struct { PyObject_HEAD; filter_state_t *handle; } Obj;
filter_state_t *state = ((Obj *)self)->handle;

The comment at the top of the generated sacred file shows this cast verbatim.

Constraint: --varargs is mutually exclusive with --arg-type, --param, and --variable-output. Those flags all imply a specific typed C signature; --varargs bypasses the type system entirely and gives you raw Python argument access.


Output modes

Choosing the right output mode depends on whether the maximum output count is knowable at init time.


--batch — 1:1-rate array transform

Use --batch when output length equals input length and is unknown at init time. The generated C stub receives (state, const in_t *in, size_t n, out_t *out) and the Python wrapper allocates and returns an output array of length n per call.

just-makeit method nco steps_u32 --module source \
    --arg-type void --return-type uint32_t --batch

just-makeit method nco steps_ctrl --module source \
    --arg-type float --return-type float --batch

Generated C stubs:

void nco_steps_u32(nco_state_t *state, size_t n, uint32_t *out);
void nco_steps_ctrl(nco_state_t *state, const float *in, size_t n, float *out);

Python calls:

ph  = nco.steps_u32(1024)    # returns uint32 ndarray of length 1024
out = nco.steps_ctrl(ctrl)   # ctrl is float32 ndarray; returns float32 ndarray

--variable-output — self-sizing output

Use --variable-output when the method's output count is not simply the input count — decimators, FIFOs, detectors, anything that returns "however many it produced". The generated binding allocates a NumPy-owned array of max(<method>_max_out(state), n) per call, lets the kernel write straight into it, and returns it trimmed to the count the kernel reported.

Each result is independent: it owns its memory, survives destroy(), and never aliases another call's result. See Array memory ownership for the policy and the measurements.

just-makeit method hbdecim execute --module resample \
    --arg-type "float _Complex" --return-type "float _Complex" \
    --variable-output

Generated C stubs:

/* Return maximum output samples possible given current state. */
size_t hbdecim_execute_max_out(hbdecim_state_t *state);

/* Process n_in samples; write up to _max_out results; return actual count. */
size_t hbdecim_execute(hbdecim_state_t *state,
                       const float _Complex *in, size_t n_in,
                       float _Complex *out);

Python call:

out = decim.execute(block)   # a fresh NumPy-owned array, trimmed to n_out
Use case _max_out at init Use --variable-output?
Decimator, fixed ratio R, block size B ceil(B / R) Yes
Buffer / FIFO with fixed capacity C C Yes
FIR filter, output ≤ input length 0 (unknown) Yes — lazy-alloc kicks in
NCO extended outputs, 1:1 rate 0 (unknown) Yes — lazy-alloc kicks in

max_out() returning 0 is legal and means "unknown" — the binding then sizes the allocation from the call itself. It is a sizing contract, not a guarantee: the real bound is n_out <= max(max_out(state), n_requested), because a generator's steps(count) writes exactly count. If your kernel must know the capacity it was actually given, add pass_capacity = true — which also lets the binding size the allocation from max_out() alone, but only once max_out() takes the count and can answer for this call. See Array memory ownership for the exact rule.

Naming the synthesized count

A --variable-output method with --arg-type void and no --param is the generator shape: there is no input to size the output from, so jm synthesizes a leading count argument. Its default value is --count-default; its name is --count-name, and defaults to count.

just-makeit method delay ptr \
    --arg-type void --return-type "double _Complex" \
    --variable-output --count-default "state->n" --count-name n
delay.ptr(n=64)             # was `count=64`
delay.ptr_max_out(n=64)     # jm already derived this name from your C

Say it when your C API calls the quantity something else. The paired <comp>_<name>_max_out() already takes its parameter name from the C signature — deliberately, "rather than inventing a fourth name for the same concept" (gh-607) — so leaving the method's own kwarg hard-coded put the two halves of one generated pair at odds: ptr(count=…) beside ptr_max_out(n=…), for the same number (gh-1074).

Declaring the count as a real --param is not the same thing, even though it produces a byte-identical C prototype. It leaves the generator shape, and the default goes with it:

synthesized count declared --param
name settable --count-name yes, it is the param name
default yes, --count-default no

For a generator that is not a small loss: the zero-arg call's behaviour is its default. (Until gh-1079 the declared form also gave up the out= buffer; that shape has it back, so this table has one row fewer than it used to.)

The name reaches the binding's _kwlist, both .pyi generators, the runtime docstring and its worked call example — one accessor, so a project cannot end up with a stub advertising one name and a binding parsing another.

Two names are refused: anything that is not a Python identifier, and out — which is the other slot in the same _kwlist.


Accumulating results is safe. Every call returns an independent NumPy-owned array, so holding them in a list, concatenating them, or passing them on cannot be disturbed by a later call. This used to require a weakref liveness probe over a shared buffer (gh-437); that machinery is gone, and the guarantee is now structural rather than defended at runtime (gh-604).


Record shapes

When a method's output is records rather than plain samples, three shapes are available and --result-field names the fields in all three. What you get back depends on which of --single / --record-dtype you pass:

you pass Python gets the C kernel writes
neither a list[tuple] <T> *result, size_t max_results
--single one record, a named PyStructSequence the struct by value
--record-dtype STRUCT an array of records, a structured ndarray <STRUCT> *out

The two are mutually exclusive — the CLI rejects them together, because "one record" and "an array of records" are different results, not two spellings of one.

# an array of records: one row per telemetry entry, as a structured ndarray
just-makeit method tlm read --module link --arg-type void \
    --variable-output --record-dtype dp_tlm_rec_t \
    --result-field n:uint64_t --result-field flags:uint8_t
rows = tlm.read(64)
rows["n"]              # a column, no per-record Python objects
rows.dtype.itemsize    # C's itemsize, padding included

The dtype is built at runtime by the generated C, from offsetof and sizeof on the author's struct. That is not an optimisation — it is the only correct source. jm never sees the struct definition (it lives in the sacred header, which is yours), and numpy packs a field list where C pads it: a {uint8_t; uint64_t} struct is 16 bytes in C and 9 as a numpy dtype built from [(name, format), ...]. Deriving the layout from the field list would silently mis-read every row after the first.


--multi-output

Each --multi-output TYPE adds a parallel output array, producing a tuple return. Combine with --variable-output:

just-makeit method nco steps_u32_ovf --module resample \
    --arg-type void --return-type uint32_t \
    --variable-output --multi-output uint8_t

--nogil — release the GIL for thread-per-shard scaling

For a --variable-output execute method, --nogil wraps the pure-C kernel call in Py_BEGIN_ALLOW_THREADS / Py_END_ALLOW_THREADS:

just-makeit method ddc execute --module ddc \
    --param x:"float _Complex[]" --variable-output --pass-capacity --nogil
/* generated binding (abridged) */
const float _Complex *_ng0 = (const float _Complex *)PyArray_DATA(x_arr);
size_t _ng1 = (size_t)PyArray_SIZE(x_arr);
size_t n_out;
Py_BEGIN_ALLOW_THREADS
n_out = ddc_execute(self->handle, _ng0, _ng1, _d0, _cap);
Py_END_ALLOW_THREADS

Every numpy accessor is hoisted into a local before the block — including the output data pointer _d0 — so no Python C-API runs while the GIL is dropped; the allocation and any error-raising stay above it, under the GIL. A worker that gives each thread its own object then scales across cores instead of serialising on the GIL.

It is opt-in because releasing the GIL is sound only under that one-object-per-stream contract — jm cannot verify it, so you assert it by setting the flag. Generated, not hand-patched: the release is declarative and regenerates with the binding.


just-makeit property

just-makeit property <object> <prop_name>
    [--module name]
    --type TYPE
    [--writable] [--field] [--enum NAME]
    [--value-type TYPE] [--count-fn FN] [--key-fn FN] [--value-fn FN]

Add a read-only (or read-write) Python property to an existing object.

just-makeit property nco phase --module source --type uint32_t
just-makeit property nco phase_inc --module source --type uint32_t
just-makeit property buffer capacity --type size_t --writable
just-makeit property reader samples_read --module conv --type uint32_t --field
just-makeit property reader file_type --type int --field --enum ftype
just-makeit property reader stages --type list --value-type "const char *"
just-makeit property reader keywords --type dict --value-type object

Like jm method, a computed property is additive and splice-free: it injects a get_<prop>() declaration into <obj>_core.h and appends a fresh stub to <obj>_core.c (plus set_<prop>() if --writable) for you to implement, then regenerates the Python getter/setter glue in _ext.c. With --field no stub is generated — it injects one TYPE prop_name; member directly into the state struct and auto-implements the getter as return state->prop_name. Existing _core.c bodies are never re-rendered.

Arguments

Argument Description
object Object name (must already exist in just-makeit.toml).
prop_name Snake-case property name.
--module name Module the object belongs to. Optional — jm reads it from the manifest (gh-963); pass it to be explicit, and it is validated.
--type TYPE C type of the property value.
--writable Also generate a setter. Without this flag the property is read-only.
--field Add a TYPE prop_name; field to the state struct and auto-implement the getter as return state->prop_name. No <<IMPLEMENT>> stub is generated — the field is the implementation. Combine with --writable for a read-write struct field property. Requires the state struct to be a complete type — see Choosing a property kind.
--doc "text" Python docstring for the getter (and setter, if --writable).
--enum NAME Present the property as a string from the named [[enum]] SSOT instead of the raw int (gh-519). See below.
--value-type TYPE Element type of a dict/list/tuple property (gh-543). A C type means jm emits the conversion and your accessor stays pure C; object means --value-fn returns a PyObject * itself. See Container properties.
--count-fn FN Entry-count accessor for a container property. Default <obj>_num_<prop>.
--key-fn FN Key accessor for a dict property. Default <obj>_<prop>_key.
--value-fn FN Value accessor for a container property. Default <obj>_<prop>_value.
--capsule NAME With --type capsule: publish a borrowed pointer as a PyCapsule under that name, so a peer extension can consume it by name (gh-788). The capsule does not own the pointer and carries no liveness — the getter checks the object is alive before handing it out, and the capsule's destructor is NULL by contract. Persists as capsule = "NAME".
--view ClassName Attach the property to a view of the object instead of the object itself — adds a property the parent lacks, or overrides a parent property (e.g. its doc) by reusing its name. Views are a module-object feature, so the object must live in a module.

Choosing a property kind — and what each costs your header

A property is backed one of five ways. The flags are not variations on a theme: they decide who writes the C and, crucially, whether your state struct has to be a complete type in the public header.

Kind Declared with Who implements the read State struct
Computed no backing flag you — jm declares <obj>_get_<prop>(const <obj>_state_t *) for the sacred _core.c may stay opaque
Field --field jm — injects TYPE prop; and returns state->prop must be complete
Expression --expr "…" jm — emits your C expression inline complete, if the expr reads a member
Buffer --buf-field name jm — returns a numpy view over that member must be complete
Container --type dict\|list\|tuple jm — generates the loop, refcounting and error paths; you supply count/key/value may stay opaque

Omitting --field is not "unsupported" — it means "you implement the accessor". That is the computed kind, and it is the one to reach for when the struct is deliberately private: a file handle, a scratch buffer, a decoded header. jm declares

int rdr_get_fd(const rdr_state_t *state);

which takes a pointer to the state, so an incomplete type is fine — you can forward-declare typedef struct rdr_state rdr_state_t; in the header and keep the definition in your _core.c. The generated binding never touches a member.

The other three kinds read a member directly, so they need the definition. With an opaque struct a --field property fails at compile time:

rdr_ext.c:115:46: error: invalid use of incomplete typedef 'rdr_state_t'

If you see that, the property wants to be computed.

A computed property composes with everything else here, including --enum and its out-of-range check — the accessor's return value is decoded exactly as a field's would be:

just-makeit property rdr file_type --type int --enum ftype   # computed + enum

Enum-valued properties (--enum)

A property whose C value is an index into a [[enum]] table can present that value to Python as its string, exactly as a kind = "handle" module's getters have always done. C still stores the int; only the Python face changes.

[[enum]]
name = "ftype"
values = ["raw", "wav", "blue"]     # order IS the C int: raw=0, wav=1, blue=2

[[reader.properties]]
name = "file_type"
type = "int"
field = true
enum = "ftype"
>>> r.file_type
'blue'
>>> r.file_type = "wav"      # only when the property is --writable
>>> r.file_type
'wav'
>>> r.file_type = "nope"
ValueError: invalid file_type 'nope' (choices: raw, wav, blue)

The stub types it as Literal["raw", "wav", "blue"], so a typo is caught by mypy rather than at runtime.

Two things worth knowing:

  • The value is range-checked. If the C side holds an index the table cannot contain — say a format code decoded from a file header that names a variant you do not support — reading the property raises ValueError: file_type holds out-of-range ftype value 99 (valid: 0..2) rather than reading past the table (gh-521).
  • enum is not the same as string_enum:. enum = "name" references the shared [[enum]] SSOT and is what properties, handle getters, and --param name:enum:<ename> use. The inline string_enum:a,b,c form spells its choices in the type itself and applies to constructor init-params. Prefer the SSOT whenever the same choice set is used in more than one place — that is the whole reason it exists.

--enum cannot be combined with a buf_field property: an array of enum strings has no decoded form, and jm rejects it with a diagnostic rather than generating something misleading.

Container properties — dict, list and tuple

A property whose --type is dict, list or tuple is backed by a small iteration protocol your core implements. jm generates the loop, the container, the refcounting and every error path; you supply the accessors.

[[reader.properties]]
name       = "keywords"
type       = "dict"
count_fn   = "reader_num_keywords"    # size_t       (const state *)
key_fn     = "reader_keyword_tag"     # const char * (const state *, size_t)
value_fn   = "reader_keyword_value"   # <value_type> (const state *, size_t)
value_type = "object"

All three accessor names default from the object and property name (<obj>_num_<prop>, <obj>_<prop>_key, <obj>_<prop>_value), so the common declaration names none of them. key_fn applies to dict only — a list or tuple is keyed by position, and passing it there is an error rather than a silently ignored flag.

Container properties are read-only. jm builds the container fresh on every read, so a setter would mutate a copy the caller never sees; expose a method that mutates the core instead.

Choosing a value_type

This is the one real decision, and it is about who owns the conversion.

value_type Conversion Where value_fn lives
a C type (double, const char *, …) jm emits it the sacred _core.cpure C
object you emit it a hand-written *_ext_extra.c

Prefer a C type. Your accessor returns an ordinary C value, jm converts it, and your core never includes Python.h:

const char *rc_stage_name(const rc_state_t *state, size_t i);   /* -> list[str] */

Reach for object when the value's Python type is data-dependent and so cannot be annotated statically. The motivating case is a BLUE extended header, where each keyword's type comes from a code stored in the file itself — one keyword yields a str, the next an int, the next a list[float]. There, value_fn returns a PyObject * directly:

PyObject *reader_keyword_value(const reader_state_t *state, size_t i);

It must return a new reference, or NULL with an exception set. Because it needs Python.h it cannot live in the pure-C core — put it in a hand-written <obj>_ext_extra.c (standalone) or <module>_ext_<obj>_extra.c (module), which jm wires in and never modifies. jm forward-declares it above the getter, so the #include order works out.

Stubs, and why this beats hand-writing it

The generated .pyi annotates as dict[str, T], list[T] or tuple[T, ...]Any for the object escape hatch. That is half the point: a property hand-written into a sacred fragment never reaches the stub at all, so it stays invisible to a type checker even though the runtime is correct.

Errors you get for free

A key_fn or a pointer-valued value_fn that returns NULL raises a RuntimeError naming the property, the accessor and the index. Both are real guards, not decoration: PyUnicode_FromString(NULL) reaches strlen(NULL) and crashes, so the unchecked form segfaults rather than raising. A partially built container is released on every failure path.

Removing a method or property

just-makeit remove <kind> <name> --object <obj> [--module <mod>] [--force], where kind is method or property (also object, module, function, state, warning, or error for those respectively — --object/--module only apply where relevant; warning and error are addressed differently, see their own sections below):

just-makeit remove method configure --object nco --module dsp
just-makeit remove property phase --object nco --module dsp

This regenerates the glue (_ext.c, .pyi) without the entry, so the binding stops exposing it. It is splice-free, so it leaves the orphaned _core.c body (and the _core.h declaration, or the field-backed struct member) in place with a "delete by hand" note — your code is never silently rewritten. Remove the stub yourself once you're sure. (Removing state via just-makeit remove state <name> --object <obj> is structural and rebuilds the object via the regenerate path instead.)


just-makeit view

just-makeit view <object> <ViewClassName>
    --module name
    --create-fn fn
    [--init-param name:type[:default] ...]
    [--exclude-property name ...] [--exclude-method name ...]
    [--doc "text"]

Add a second Python class over the same generated C core (gh-504). The view shares the object's <obj>_state_t, its _core.c, and its step(); it differs only in the C constructor it calls, the constructor arguments it takes, and the Python surface it exposes. Use it when one algorithm has two front doors — a continuous mode and a burst mode, an empty accumulator and a pre-seeded one — and a second object would mean a second copy of the C.

just-makeit view acc SeededAcc --module bank \
    --create-fn acc_create_seeded \
    --init-param seed:double:0.0 \
    --exclude-method total

This records a [[<obj>.views]] entry, injects acc_state_t *acc_create_seeded(double seed); into <obj>_core.h, appends an <<IMPLEMENT>> stub for it to the sacred <obj>_core.c (so the module still compiles before you have written a line), and regenerates the module glue with the extra class registered. The view lands in its own binding fragment, <mod>_ext_<viewclass>.c, next to the parent's — no second core library and no second state struct.

Views are a module-object feature: the multi-type module machinery is what registers the extra class, so --module is required. --create-fn is also required and must differ from the parent's <obj>_create — a view exists precisely to build from a different constructor.

Arguments

Argument Description
object Object the view sits over (must already exist in just-makeit.toml).
ViewClassName Python class name for the view. Must be unique across every class the module exposes — each object's class name and every existing view.
--module name Module the object belongs to. Required.
--create-fn fn C constructor the view's __init__ calls (e.g. acc_create_seeded). Required; must differ from <obj>_create. Scaffolded as a stub in the shared core.
--init-param name:type[:default] The view's own constructor parameter. Repeatable; same syntax as jm object --init-param. Omit entirely to inherit the parent's constructor shape.
--exclude-property name Parent property to omit from the view's Python surface. Repeatable; must name an existing property of the parent.
--exclude-method name Parent method to omit. Repeatable; must name an existing [[<obj>.methods]] entry. The builtins step/steps/reset are not excludable.
--doc "text" Docstring for the view class.

Diverging, not only trimming

--exclude-property / --exclude-method take members away. To add a member the parent lacks, or override one it has, pass --view <ClassName> to jm property, jm method, or jm warning — reusing a parent member's name overrides it, a new name adds it:

# `runs` exists on SeededAcc only
just-makeit property acc runs --module bank --type size_t --field \
    --doc "reseed count" --view SeededAcc

# same name as the parent's property -> overrides its docstring on the view
just-makeit property acc depth --module bank --type size_t --field \
    --doc "seed depth" --view SeededAcc

These persist as [[<obj>.views.properties]], [[<obj>.views.methods]], and [[<obj>.views.warnings]] under the matching view, and are merged over the parent's when the glue is generated. See Configuration → Schema reference.

Excluding a method drops only the view's Python wrapper and its PyMethodDef entry — the shared C function stays in the core, so nothing dangles and the parent keeps working.


Overriding a parent method

A view method that reuses a parent method's name is an override, and there are two kinds. --fn decides which (gh-1012, gh-1011):

you pass you get the view calls
--doc only (or a matching signature) a doc-only override the parent's <obj>_<name>
--fn <symbol> a signature override its own <symbol>, scaffolded into the shared core

This is not a flag standing in for a second question. The parent's C symbol carries the parent's prototype, so a different signature is only callable through a different symbol — which is exactly what --fn supplies:

# doc-only: same signature, the view just words it differently
just-makeit method acc plain --module bank --view SeededAcc \
    --doc "seeded plain"
#   -> View 'SeededAcc' overrides the doc of 'plain' (shares acc_plain)

# signature override: its own arg_type, so its own C function
just-makeit method acc scaled --module bank --view SeededAcc \
    --fn acc_scaled_seeded --arg-type float --return-type double \
    --doc "seeded scale"
#   -> Implement acc_scaled_seeded() in acc_core.c

Both prototypes then stand side by side in the sacred header, and only the view's fragment calls the override:

double acc_scaled(acc_state_t *state, double x);         /* parent */
double acc_scaled_seeded(acc_state_t *state, float x);   /* the view's */

A differing signature without --fn is refused, rather than silently ignored as it was before 0.62.0 — the view would have called the parent's symbol, so the declaration could only ever have been dropped:

error: view 'SeededAcc' redeclares parent method 'scaled' with a different arg_type.
  Without --fn the view calls the parent's acc_scaled, which has the parent's
  signature, so this could only be ignored.
  Pass --fn <symbol> to bind its own C function (gh-1012), or drop the
  differing key(s) for a doc-only override.

Only the keys you actually state are compared, so a hand-written manifest entry carrying nothing but doc is a doc-only override and stays legal. And --fn naming the parent's own symbol is refused up front, rather than left for the C compiler to report as a conflicting redefinition:

error: view 'SeededAcc' overrides method 'scaled' with fn 'acc_scaled', which is
the symbol the parent already binds.
  A signature override needs its OWN C function — give --fn a different name, or
  drop it for a doc-only override.

Properties and warnings have no second kind: reusing a parent's name overrides how it reads, as in the depth example above.


Limitation: a view cannot yet sit over a parent whose init-params use per-array dtype-dispatch or optional-array forms (real_type, real_create_fn, create_fn, optional); those paths embed <obj>_create directly and would silently ignore the view's create_fn, so jm view rejects them with a diagnostic.

The Views example builds the whole thing end to end.


just-makeit warning

just-makeit warning <object>
    --condition FIELD
    --message TEXT
    [--module name] [--category NAME] [--stacklevel N]

Declare a warning that fires after construction when a boolean state field is set — a way to flag a degenerate-but-legal configuration without failing the constructor. The generated __init__ glue checks the field once the object is built and, if set, calls PyErr_WarnEx with your message and category.

just-makeit warning agc underpowered \
    --message "AGC gain floor reached; output may clip" \
    --category RuntimeWarning

This is declarative: the condition, message, and category live in just-makeit.toml as a [[<object>.warnings]] entry, and the check is regenerated into _ext.c — you write no C. --condition names a bool-ish field on the object's state struct (emitted as self->handle-><field>), so it must already exist as state (add it with just-makeit add if needed).

Arguments

Argument Description
object Object name (must already exist in just-makeit.toml).
--condition FIELD Bool-valued state field that triggers the warning. Required.
--message TEXT Warning text shown to the Python caller. Required.
--category NAME Warning class — a Python built-in warning (UserWarning, DeprecationWarning, RuntimeWarning, …). Default UserWarning.
--module name Module the object belongs to. Optional — jm reads it from the manifest (gh-963); pass it to be explicit, and it is validated.
--stacklevel N PyErr_WarnEx stacklevel, so the warning points at the caller's frame. Default 1.
--after POINT Where the check runs. Only __init__ (the default) is supported — anything else is refused rather than accepted and ignored, so the flag is a declared extension point, not a knob with hidden values.
--view ClassName Attach the warning to a view rather than the object, so a second front door over the shared core gets its own PyErr_WarnEx. Requires --module (gh-509).

An object may carry more than one warning — each just-makeit warning call on a distinct --condition adds another. Re-running with the same condition replaces that entry.

Remove one with just-makeit remove warning <condition> --object <obj> — a warning has no name of its own, so its condition is what identifies it.


just-makeit error

just-makeit error <object>
    --category NAME
    --message TEXT
    [--module name]
    [--view ClassName]

Translate a create() failure into a specific Python exception. By default a failed constructor raises a blanket MemoryError; this replaces that with an exception class and message of your choosing.

just-makeit error biquad \
    --category ValueError \
    --message "biquad coefficients unstable (|poles| >= 1)"

Also declarative: the choice is stored as create_error / create_error_message in just-makeit.toml and regenerated into the _ext.c failure path. There is one failure channel — a NULL from create() cannot say why it failed — so this applies to every create() failure, a genuine allocation failure included. Choose a category that reads sensibly for the most likely cause.

Arguments

Argument Description
object Object name (must already exist in just-makeit.toml).
--category NAME Exception class — a Python built-in exception (ValueError, RuntimeError, OverflowError, …). Required.
--message TEXT Text for the raised exception. Required.
--module name Module the object belongs to (required for module objects).
--view ClassName Give a view its own translation instead of the object's. Views are a module-object feature, so the object must live in a module.

Each object has a single failure translation, so re-running replaces it rather than accumulating. Remove it with just-makeit remove error <obj> --object <obj>error takes no name because there is only ever one per object.

Views inherit their parent's translation

A view is a second Python class over the same C core, so declaring the error on the parent covers both front doors — you do not need to repeat it:

just-makeit error rateconv --module resample \
    --category ValueError \
    --message "RateConverter: invalid parameter (need rate > 0)"
# RateConverter(...)        -> ValueError
# MatchedRateConverter(...) -> ValueError, same message

This matters because a view exists precisely when the constructor takes different — usually more — parameters, so it is the constructor with the most ways to be handed something invalid.

Add --view when the view's extra parameters deserve their own wording:

just-makeit error rateconv --module resample --view MatchedRateConverter \
    --category ValueError \
    --message "MatchedRateConverter: invalid pulse/beta/span combination"

Only an explicit --view declaration is written to the manifest; an inherited translation keeps tracking the parent rather than being frozen into a copy. Note the contrast with just-makeit warning, where a view does not inherit: a warning describes a condition the view may not have, while an error describes the same object refusing to construct.


just-makeit function

just-makeit function <name> --module <mod>
    [--param name:type ...]
    [--return-type TYPE]
    [--doc "text"]

Add a stateless C function to an existing module — no struct, no lifecycle, no persistent state.

Writes a C stub to the function's own sacred source file native/src/<module>/<name>.c (never regenerated — your implementation is safe) and injects the declaration into native/inc/<module>/<module>_core.h. Each function thus owns one translation unit, which the module's CMakeLists compiles into the module's OBJECT library. Then regenerates <module>_ext.c to add a _bind_<name> Python wrapper and wire it into the PyMethodDef array.

With --inline the function instead lives entirely as a static inline body in <module>_core.h and gets no .c file.

The generated _bind_<name> wrapper is positional-or-keyword (METH_VARARGS | METH_KEYWORDS): callers may pass arguments positionally or by name (fn(input=x, n=8)). A no-parameter function stays METH_NOARGS. Keyword capability is ~free unless keywords are actually used — see Arguments: positional vs keyword.

Arguments

Argument Description
name Snake-case function name.
--module mod Module the function belongs to (required).
--param name:type Named typed scalar parameter. Repeatable.
--param name:type=default Optional scalar parameter — omitting it yields default (e.g. gain:double=1.0). Optional params must come after required ones; plain scalars only (gh-240).
--param name:type[] Named numpy array parameter. Repeatable. Generates const elem_t *name, size_t name_len in C.
--param name:path Filesystem path parameter. Python accepts str \| os.PathLike; C receives const char * via PyUnicode_FSConverter (gh-353).
--param name:enum:<ename>[=d] String-choice parameter validated against the named [[enum]] SSOT; C receives the int index; optional default d is the string value (gh-353).
--return-type TYPE C return type (default: void).
--check-return Treat a non-zero int return as failure: raises RuntimeError(rc), returns None on success. Requires an integer --return-type (gh-363).
--out-type TYPE Allocate a 1-D output array of this element type per call and append out last to the C call.
--variable-output With --out-type: the function allocates its own 1-D output rather than returning a scalar — no caller buffer and no cached instance buffer. out is appended last to the C call, and the binding returns the ndarray (gh-335). A size_t-returning function is trimmed to the count it reports; a void one returns the full allocation.
--out-size EXPR Length of that output, as a verbatim C expression over the function's own arguments — including each array param's generated <name>_len (e.g. x_len * factor, or a call like wfm_rrc_ntaps(sps, span)). Omit it and the length falls back to the first array parameter's length.
--inline Emit a static inline body in <module>_core.h instead of a separate <name>.c.
--doc "text" Python docstring for the function.
--impl file::funcname Lift the function body from funcname in file instead of emitting a blank <<IMPLEMENT>> stub.
--impl file::N:M Lift lines N..M (inclusive, 1-based) instead of a named function body. Ranges error cleanly.
--replace old::new String substitution applied to the body lifted by --impl. Repeatable.

Example — no parameters:

just-makeit function fft_global_setup --module fft --doc "Initialize FFT tables."

native/src/fft/fft_global_setup.c (yours to implement):

/*
 * fft_global_setup.c — fft module-level function.
 */
#include "fft/fft_core.h"

/* <<IMPLEMENT: fft_global_setup>> */
void
fft_global_setup(void)
{
}

fft_core.h (declaration injected automatically):

void fft_global_setup(void);

Example — with parameters:

just-makeit function compute_window \
    --module fft \
    --param n:size_t \
    --param beta:float \
    --return-type float

native/src/fft/compute_window.c:

/*
 * compute_window.c — fft module-level function.
 */
#include "fft/fft_core.h"

/* <<IMPLEMENT: compute_window>> */
float
compute_window(size_t n, float beta)
{
    (void)n; (void)beta;
    return (float)0.0f; /* placeholder */
}

Python call:

from my_pkg import fft
w = fft.compute_window(512, 5.0)

Array parameters work identically to jm method: append [] to the type.

just-makeit function apply_window \
    --module fft \
    --param data:"float _Complex[]" \
    --return-type void

Path parameters (name:path) accept a str | os.PathLike from Python, coerce it to bytes via PyUnicode_FSConverter, and forward const char * to C — the same coercion used by the handle generator:

just-makeit function load_calibration \
    --module dsp \
    --param path:path \
    --return-type void

Python call:

from pathlib import Path
import my_pkg.dsp as dsp
dsp.load_calibration(Path("/data/cal.bin"))
dsp.load_calibration("/data/cal.bin")        # str also works

Enum parameters (name:enum:<ename>[=default]) accept a choice string, validate it against the [[enum]] SSOT in just-makeit.toml, and forward the int index to C. Requires a [[enum]] with the matching name to be declared in just-makeit.toml first.

# just-makeit.toml
[[enum]]
name = "color_space"
values = ["rgb", "hsv", "lab"]
just-makeit function convert_image \
    --module img \
    --param path:path \
    --param src_cs:enum:color_space=rgb \
    --param dst_cs:enum:color_space \
    --return-type int \
    --check-return

--check-return makes the generated binding treat a non-zero int return value as a failure: it captures the result, raises RuntimeError on a non-zero code, and returns None on success. Requires --return-type to be an integer type (int, size_t, …). It is the module-function analog of the handle generator's close_returns and composes naturally with path and enum args.

from my_pkg import img
img.convert_image("input.png", dst_cs="lab")   # → None, or raises RuntimeError

C stub (native/src/img/convert_image.c — yours to implement):

#include "img/img_core.h"

int
convert_image(const char *path, int src_cs, int dst_cs)
{
    /* <<IMPLEMENT: convert_image>> */
    return 0;
}