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(state_t *s) → T
Mutating generator --arg-type void --mutable step(state_t *s) → T
Sink --return-type void step(const 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; } engine_state_t;

static inline float complex
engine_step(const 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
nco_step(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

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 (--return-type void is rejected as a no-op object).

/* no input parameter */
static inline float complex
tone_gen_step(tone_gen_state_t *state) {  }

void tone_gen_steps(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().

static inline void
iq_writer_step(const 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 suppresses the state struct entirely; --init-param declares constructor parameters that are passed in at creation time but not stored as struct fields (you implement storage yourself in the <<IMPLEMENT>> stubs).

/* No state struct generated.  You define storage in the create stub. */
clipper_state_t *clipper_create(float threshold);
void             clipper_destroy(clipper_state_t *state);
void             clipper_reset(clipper_state_t *state);

static inline float complex
clipper_step(const 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 --return-type "float _Complex[]" --variable-output

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.

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.