Skip to content

Stateful vs Pure Objects

Shape Flags step() signature
Stateful (default) (none) step(const state_t *s, T x) → T
Mutating --mutable step(state_t *s, T x) → T
Generator --arg-type void step(const state_t *s) → T
Mutating generator --arg-type void --mutable step(state_t *s) → T
Sink --return-type void step(state_t *s, T x) → void
Pure function --no-state step(const state_t *s, T x) → T
Method-only --no-step (no step generated)

The sections below explain each shape and when to reach for it. Each flag also works at scaffold time, e.g. jm new my_proj --object framer --no-step or --no-state in a single command.


Default: stateful, const step

just-makeit object engine --state gain:double:1.0

The default shape: a C struct holds state between calls; step() takes a const pointer to that struct and a single input sample.

typedef struct { double gain; } my_proj_engine_state_t;

static inline float _Complex
my_proj_engine_step(const my_proj_engine_state_t *state, float _Complex x)
{
    /* <<IMPLEMENT>> */
    return x;
}

const on the state pointer is intentional — it signals that step() reads state but does not change it. The compiler can exploit this for optimisation, and it prevents accidental mutation in the hot path.

Use this shape for anything whose algorithm is a pure function of the current input and stored parameters: filters, mixers, scalers, correlators.


Mutating generator: --mutable

just-makeit object nco --arg-type void --return-type "float _Complex" --mutable \
    --state phase:double:0.0 --state freq_hz:double:1000.0

Remove const from the state pointer so step() can write back to the struct. Required for objects that advance internal state on every call — an NCO that increments its phase, a counter, a PRNG.

static inline float _Complex
my_proj_nco_step(my_proj_nco_state_t *state)   /* no const */
{
    /* advance phase, return sample */
}

--arg-type void (see below) and --mutable are independent flags; combine them whenever the generator needs to mutate state.


Generator / source: --arg-type void

just-makeit object tone_gen --arg-type void --return-type "float _Complex" --mutable \
    --state freq_hz:double:1000.0

A generator produces samples without consuming input. Pass --arg-type void to suppress the input parameter from step() and steps(). With no explicit --return-type, a void-arg object defaults to a float _Complex return. Combining --arg-type void --return-type void is also valid — it produces a "tick" object whose step() takes no input and returns nothing, only mutating state (a counter, a clock).

/* no input parameter */
static inline float _Complex
my_proj_tone_gen_step(my_proj_tone_gen_state_t *state) { … }

void my_proj_tone_gen_steps(my_proj_tone_gen_state_t *state, float _Complex *output, size_t n);

Python side:

gen = ToneGen(freq_hz=1000.0)
sample = gen.step()                             # no argument
block  = gen.steps(1024)                        # only length, no input array

Sink: --return-type void

just-makeit object iq_writer --return-type void

A sink consumes input but produces no output. Pass --return-type void to drop the return value from step() and steps(). A sink mutates its state, so its step() is never const.

static inline void
my_proj_iq_writer_step(my_proj_iq_writer_state_t *state, float _Complex x) { … }

Python side:

sink = IqWriter()
sink.step(sample)           # returns None
sink.steps(block)           # returns None

Pure function: --no-state

just-makeit object clipper --no-state \
    --init-param threshold:float:1.0

Some operations require no persistent state — they are pure functions of their inputs. --no-state generates an empty state struct (a <<IMPLEMENT: add fields>> marker) instead of one field per --state. --init-param declares constructor parameters that are not stored automatically: add the fields you need and assign them in the create() stub.

/* Empty state struct: add your fields; assign them in create(). */
my_proj_clipper_state_t *my_proj_clipper_create(float threshold);
void             my_proj_clipper_destroy(my_proj_clipper_state_t *state);
void             my_proj_clipper_reset(my_proj_clipper_state_t *state);

static inline float _Complex
my_proj_clipper_step(const my_proj_clipper_state_t *state, float _Complex x) { … }

Use --no-state when the constructor signature cannot be expressed as a flat list of typed fields — for example, a filter initialised from a const float *taps, size_t num_taps pair.


Method-only object: --no-step

just-makeit object framer --no-step \
    --state buf_len:size_t:0
just-makeit method framer push --param data:"float _Complex[]" --return-type size_t
just-makeit method framer flush --variable-output --return-type "float _Complex"

When the object's interface consists entirely of named methods, suppress step() and steps() with --no-step. The lifecycle functions (create, destroy, reset) are still generated; only the generic execute pair is omitted.

A --variable-output method names the element type; the array is the method's output. flush becomes size_t my_proj_framer_flush(my_proj_framer_state_t *state, size_t n, float _Complex *out) in C and flush(count=1, out=None) -> NDArray[complex64] in Python.

Use this shape for stateful objects with non-uniform interfaces: re-framers, packetisers, protocol encoders.

See Scaffold commands for the full flag reference, and Examples for complete walkthroughs of each shape.