Skip to main content

wyrd/authoring/
composer.rs

1//! Typed, closure-scoped authoring for dynamically generated [`Weave`](crate::Weave)s.
2//!
3//! [`Weave::compose`](crate::Weave::compose) is deliberately a thin layer over
4//! [`WeaveBuilder`](crate::WeaveBuilder): it gives common graph operations
5//! domain-typed wires while retaining [`Composer::knot`] and
6//! [`Composer::thread`] for every catalog operation.
7
8use core::marker::PhantomData;
9
10use crate::authoring::{
11    BuildError, InputPort, KnotHandle, OutputPort, Pattern, PatternInstance, ValidationError,
12    Weave, WeaveBuilder,
13};
14use crate::foundation::{
15    CalcOp, CompareOp, FlagPriority, KnotKind, Signal, SignalDomain, TimerMode,
16};
17
18/// Error produced while composing a generated weave.
19#[derive(Clone, Debug, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum ComposeError {
22    /// A scoped composer operation was rejected before final validation.
23    Build(BuildError),
24    /// The complete generated graph failed normal weave validation.
25    Validation(ValidationError),
26}
27
28impl From<BuildError> for ComposeError {
29    fn from(value: BuildError) -> Self {
30        Self::Build(value)
31    }
32}
33
34impl From<ValidationError> for ComposeError {
35    fn from(value: ValidationError) -> Self {
36        Self::Validation(value)
37    }
38}
39
40impl core::fmt::Display for ComposeError {
41    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
42        match self {
43            Self::Build(error) => error.fmt(f),
44            Self::Validation(error) => error.fmt(f),
45        }
46    }
47}
48
49#[cfg(feature = "std")]
50impl std::error::Error for ComposeError {}
51
52/// A signal domain that may be represented by a typed [`Wire`].
53pub trait WireDomain {
54    /// Runtime domain enforced by helpers that accept this wire.
55    const DOMAIN: SignalDomain;
56}
57
58/// Marker for boolean graph wires.
59#[derive(Clone, Copy, Debug)]
60pub struct Bool;
61
62/// Marker for continuous level graph wires.
63#[derive(Clone, Copy, Debug)]
64pub struct Level;
65
66/// Marker for whole-count graph wires.
67#[derive(Clone, Copy, Debug)]
68pub struct Count;
69
70impl WireDomain for Bool {
71    const DOMAIN: SignalDomain = SignalDomain::Bool;
72}
73
74impl WireDomain for Level {
75    const DOMAIN: SignalDomain = SignalDomain::Level;
76}
77
78impl WireDomain for Count {
79    const DOMAIN: SignalDomain = SignalDomain::Count;
80}
81
82/// A typed wire domain accepted by numeric catalog operations.
83pub trait NumericWireDomain: WireDomain {}
84
85impl NumericWireDomain for Level {}
86impl NumericWireDomain for Count {}
87
88/// A typed output wire owned by the active [`Composer`].
89///
90/// It can be cloned to intentionally fan out to several downstream knots.
91#[derive(Clone, Debug)]
92pub struct Wire<D: WireDomain> {
93    port: OutputPort,
94    marker: PhantomData<D>,
95}
96
97impl<D: WireDomain> Wire<D> {
98    /// Underlying catalog-checked output port for raw escape-hatch operations.
99    pub fn port(&self) -> &OutputPort {
100        &self.port
101    }
102}
103
104/// Boolean wire alias used by composition examples and helpers.
105pub type BoolWire = Wire<Bool>;
106/// Level wire alias used by composition examples and helpers.
107pub type LevelWire = Wire<Level>;
108/// Count wire alias used by composition examples and helpers.
109pub type CountWire = Wire<Count>;
110
111/// Dynamic graph composer scoped to one generated [`Weave`].
112///
113/// Use typed helpers for common semantic operations. For an operation that has
114/// no helper yet, use [`Self::knot`], [`Self::input`], [`Self::output`], and
115/// [`Self::thread`]; these delegate directly to `WeaveBuilder` and therefore
116/// retain the complete catalog and its diagnostics.
117pub struct Composer {
118    builder: WeaveBuilder,
119}
120
121impl Composer {
122    pub(crate) fn new(id: impl Into<std::string::String>) -> Result<Self, BuildError> {
123        Ok(Self {
124            builder: WeaveBuilder::new(id)?,
125        })
126    }
127
128    pub(crate) fn build(self) -> Result<Weave, ValidationError> {
129        self.builder.build()
130    }
131
132    /// Add any catalog knot under an explicit author id.
133    pub fn knot(
134        &mut self,
135        id: impl Into<std::string::String>,
136        kind: KnotKind,
137    ) -> Result<KnotHandle, BuildError> {
138        self.builder.knot(id, kind)
139    }
140
141    /// Resolve any catalog input port for [`Self::thread`].
142    pub fn input(&self, knot: &KnotHandle, name: &str) -> Result<InputPort, BuildError> {
143        self.builder.input(knot, name)
144    }
145
146    /// Resolve any catalog output port for [`Self::thread`].
147    pub fn output(&self, knot: &KnotHandle, name: &str) -> Result<OutputPort, BuildError> {
148        self.builder.output(knot, name)
149    }
150
151    /// Connect raw catalog ports, retaining `WeaveBuilder` ownership and
152    /// fixed-domain diagnostics.
153    pub fn thread(&mut self, from: &OutputPort, to: &InputPort) -> Result<(), BuildError> {
154        self.builder.connect(from.clone(), to.clone())?;
155        Ok(())
156    }
157
158    /// Include a reusable pattern through the authoritative builder expansion.
159    pub fn include(
160        &mut self,
161        instance_id: impl Into<std::string::String>,
162        pattern: &Pattern,
163    ) -> Result<PatternInstance, BuildError> {
164        self.builder.include(instance_id, pattern)
165    }
166
167    /// Host boolean sense source.
168    pub fn bool_input(
169        &mut self,
170        id: impl Into<std::string::String>,
171    ) -> Result<BoolWire, BuildError> {
172        self.source(id, KnotKind::signal_in(SignalDomain::Bool))
173    }
174
175    /// Host continuous-level sense source.
176    pub fn level_input(
177        &mut self,
178        id: impl Into<std::string::String>,
179    ) -> Result<LevelWire, BuildError> {
180        self.source(id, KnotKind::signal_in(SignalDomain::Level))
181    }
182
183    /// Host whole-count sense source.
184    pub fn count_input(
185        &mut self,
186        id: impl Into<std::string::String>,
187    ) -> Result<CountWire, BuildError> {
188        self.source(id, KnotKind::signal_in(SignalDomain::Count))
189    }
190
191    /// Boolean literal source.
192    pub fn bool_constant(
193        &mut self,
194        id: impl Into<std::string::String>,
195        value: bool,
196    ) -> Result<BoolWire, BuildError> {
197        self.source(id, KnotKind::constant_bool(value))
198    }
199
200    /// One-shot boolean source emitted on the first settle after bind.
201    pub fn on_start(&mut self, id: impl Into<std::string::String>) -> Result<BoolWire, BuildError> {
202        self.source(id, KnotKind::OnStart)
203    }
204
205    /// Level literal source.
206    pub fn level_constant(
207        &mut self,
208        id: impl Into<std::string::String>,
209        value: f32,
210    ) -> Result<LevelWire, BuildError> {
211        self.source(id, KnotKind::constant_level(value))
212    }
213
214    /// Count literal source.
215    pub fn count_constant(
216        &mut self,
217        id: impl Into<std::string::String>,
218        value: i32,
219    ) -> Result<CountWire, BuildError> {
220        self.source(id, KnotKind::constant_count(value))
221    }
222
223    /// Bind a typed wire to a host output path under an explicit knot id.
224    pub fn signal_out<D: WireDomain>(
225        &mut self,
226        id: impl Into<std::string::String>,
227        path: impl Into<std::string::String>,
228        wire: &Wire<D>,
229    ) -> Result<(), BuildError> {
230        let knot = self.knot(id, KnotKind::signal_out(path, D::DOMAIN))?;
231        let input = self.input(&knot, "in")?;
232        self.thread(wire.port(), &input)
233    }
234
235    /// Boolean inversion.
236    pub fn not(
237        &mut self,
238        id: impl Into<std::string::String>,
239        input: &BoolWire,
240    ) -> Result<BoolWire, BuildError> {
241        self.unary(id, KnotKind::not(), input)
242    }
243
244    /// Boolean conjunction with two inputs.
245    pub fn and(
246        &mut self,
247        id: impl Into<std::string::String>,
248        a: &BoolWire,
249        b: &BoolWire,
250    ) -> Result<BoolWire, BuildError> {
251        self.binary(id, KnotKind::and2(), a, b, "in_0", "in_1")
252    }
253
254    /// Boolean disjunction with two inputs.
255    pub fn or(
256        &mut self,
257        id: impl Into<std::string::String>,
258        a: &BoolWire,
259        b: &BoolWire,
260    ) -> Result<BoolWire, BuildError> {
261        self.binary(id, KnotKind::or2(), a, b, "in_0", "in_1")
262    }
263
264    /// Boolean exclusive-or.
265    pub fn xor(
266        &mut self,
267        id: impl Into<std::string::String>,
268        a: &BoolWire,
269        b: &BoolWire,
270    ) -> Result<BoolWire, BuildError> {
271        self.binary(id, KnotKind::xor(), a, b, "a", "b")
272    }
273
274    /// Rising truthiness edge, preserving the source domain only at the input.
275    pub fn rising<D: WireDomain>(
276        &mut self,
277        id: impl Into<std::string::String>,
278        input: &Wire<D>,
279    ) -> Result<BoolWire, BuildError> {
280        self.unary(id, KnotKind::rising_from_zero(), input)
281    }
282
283    /// Falling truthiness edge.
284    pub fn falling<D: WireDomain>(
285        &mut self,
286        id: impl Into<std::string::String>,
287        input: &Wire<D>,
288    ) -> Result<BoolWire, BuildError> {
289        self.unary(id, KnotKind::falling_to_zero(), input)
290    }
291
292    /// Any truthiness-change edge.
293    pub fn change<D: WireDomain>(
294        &mut self,
295        id: impl Into<std::string::String>,
296        input: &Wire<D>,
297    ) -> Result<BoolWire, BuildError> {
298        self.unary(id, KnotKind::change(), input)
299    }
300
301    /// State latch. Optional inputs map directly to the catalog's optional ports.
302    pub fn flag(
303        &mut self,
304        id: impl Into<std::string::String>,
305        priority: FlagPriority,
306        set: Option<&BoolWire>,
307        reset: Option<&BoolWire>,
308        toggle: Option<&BoolWire>,
309    ) -> Result<BoolWire, BuildError> {
310        let knot = self.knot(id, KnotKind::flag(priority, toggle.is_some()))?;
311        self.optional_thread(set, &knot, "set")?;
312        self.optional_thread(reset, &knot, "reset")?;
313        self.optional_thread(toggle, &knot, "toggle")?;
314        self.typed_output(&knot, "out")
315    }
316
317    /// Counter with optional increment, decrement, and reset controls.
318    pub fn counter(
319        &mut self,
320        id: impl Into<std::string::String>,
321        increment: Option<&BoolWire>,
322        decrement: Option<&BoolWire>,
323        reset: Option<&BoolWire>,
324    ) -> Result<CountWire, BuildError> {
325        let knot = self.knot(id, KnotKind::counter())?;
326        self.optional_thread(increment, &knot, "inc")?;
327        self.optional_thread(decrement, &knot, "dec")?;
328        self.optional_thread(reset, &knot, "reset")?;
329        self.typed_output(&knot, "count")
330    }
331
332    /// Pulse-hold timer started by a boolean edge.
333    pub fn pulse_hold(
334        &mut self,
335        id: impl Into<std::string::String>,
336        ticks: u16,
337        start: &BoolWire,
338    ) -> Result<BoolWire, BuildError> {
339        let knot = self.knot(id, KnotKind::timer(TimerMode::PulseHold, ticks))?;
340        let input = self.input(&knot, "start")?;
341        self.thread(start.port(), &input)?;
342        self.typed_output(&knot, "active")
343    }
344
345    /// Countdown timer fed by a boolean level.
346    pub fn fed_countdown(
347        &mut self,
348        id: impl Into<std::string::String>,
349        ticks: u16,
350        feed: &BoolWire,
351    ) -> Result<BoolWire, BuildError> {
352        let knot = self.knot(id, KnotKind::timer(TimerMode::FedCountdown, ticks))?;
353        let input = self.input(&knot, "feed")?;
354        self.thread(feed.port(), &input)?;
355        self.typed_output(&knot, "active")
356    }
357
358    /// Compare two wires in the same typed domain.
359    pub fn compare<D: WireDomain>(
360        &mut self,
361        id: impl Into<std::string::String>,
362        op: CompareOp,
363        lhs: &Wire<D>,
364        rhs: &Wire<D>,
365    ) -> Result<BoolWire, BuildError> {
366        self.binary(
367            id,
368            KnotKind::compare(op, None, D::DOMAIN),
369            lhs,
370            rhs,
371            "lhs",
372            "rhs",
373        )
374    }
375
376    /// Compare a wire to a catalog literal in the same typed domain.
377    pub fn compare_constant<D: WireDomain>(
378        &mut self,
379        id: impl Into<std::string::String>,
380        op: CompareOp,
381        lhs: &Wire<D>,
382        rhs: Signal,
383    ) -> Result<BoolWire, BuildError> {
384        let knot = self.knot(id, KnotKind::compare(op, Some(rhs), D::DOMAIN))?;
385        let input = self.input(&knot, "lhs")?;
386        self.thread(lhs.port(), &input)?;
387        self.typed_output(&knot, "out")
388    }
389
390    /// Binary arithmetic in one typed numeric domain.
391    pub fn calc<D: NumericWireDomain>(
392        &mut self,
393        id: impl Into<std::string::String>,
394        op: CalcOp,
395        a: &Wire<D>,
396        b: &Wire<D>,
397    ) -> Result<Wire<D>, BuildError> {
398        self.binary(id, KnotKind::calc(op, D::DOMAIN), a, b, "a", "b")
399    }
400
401    /// Linear mapping in one typed numeric domain.
402    pub fn map<D: NumericWireDomain>(
403        &mut self,
404        id: impl Into<std::string::String>,
405        input: &Wire<D>,
406        in_min: Signal,
407        in_max: Signal,
408        out_min: Signal,
409        out_max: Signal,
410    ) -> Result<Wire<D>, BuildError> {
411        self.unary(
412            id,
413            KnotKind::map(in_min, in_max, out_min, out_max, D::DOMAIN),
414            input,
415        )
416    }
417
418    /// Threshold a typed numeric wire into boolean state and edge outputs.
419    pub fn threshold<D: NumericWireDomain>(
420        &mut self,
421        id: impl Into<std::string::String>,
422        input: &Wire<D>,
423        high: Signal,
424        low: Signal,
425        use_hysteresis: bool,
426    ) -> Result<ThresholdWires, BuildError> {
427        let knot = self.knot(
428            id,
429            KnotKind::Threshold {
430                domain: D::DOMAIN,
431                high,
432                low,
433                use_hysteresis,
434            },
435        )?;
436        let target = self.input(&knot, "in")?;
437        self.thread(input.port(), &target)?;
438        Ok(ThresholdWires {
439            out: self.typed_output(&knot, "out")?,
440            crossed_up: self.typed_output(&knot, "crossed_up")?,
441            crossed_down: self.typed_output(&knot, "crossed_down")?,
442        })
443    }
444
445    /// Delay a typed wire without changing its domain.
446    pub fn delay<D: WireDomain>(
447        &mut self,
448        id: impl Into<std::string::String>,
449        ticks: u16,
450        input: &Wire<D>,
451    ) -> Result<Wire<D>, BuildError> {
452        self.unary(id, KnotKind::Delay { ticks }, input)
453    }
454
455    /// Absolute value in a typed numeric domain.
456    pub fn abs<D: NumericWireDomain>(
457        &mut self,
458        id: impl Into<std::string::String>,
459        input: &Wire<D>,
460    ) -> Result<Wire<D>, BuildError> {
461        self.unary(id, KnotKind::abs(D::DOMAIN), input)
462    }
463
464    /// Negation in a typed numeric domain.
465    pub fn neg<D: NumericWireDomain>(
466        &mut self,
467        id: impl Into<std::string::String>,
468        input: &Wire<D>,
469    ) -> Result<Wire<D>, BuildError> {
470        self.unary(id, KnotKind::neg(D::DOMAIN), input)
471    }
472
473    /// Square root in a typed numeric domain.
474    pub fn sqrt<D: NumericWireDomain>(
475        &mut self,
476        id: impl Into<std::string::String>,
477        input: &Wire<D>,
478    ) -> Result<Wire<D>, BuildError> {
479        self.unary(id, KnotKind::sqrt(D::DOMAIN), input)
480    }
481
482    /// Clamp in a typed numeric domain.
483    pub fn clamp<D: NumericWireDomain>(
484        &mut self,
485        id: impl Into<std::string::String>,
486        input: &Wire<D>,
487        min: Signal,
488        max: Signal,
489    ) -> Result<Wire<D>, BuildError> {
490        self.unary(id, KnotKind::clamp(min, max, D::DOMAIN), input)
491    }
492
493    /// Digitize a typed numeric wire into a fixed number of bins.
494    pub fn digitize<D: NumericWireDomain>(
495        &mut self,
496        id: impl Into<std::string::String>,
497        input: &Wire<D>,
498        steps: u16,
499    ) -> Result<Wire<D>, BuildError> {
500        self.unary(id, KnotKind::digitize(steps, D::DOMAIN), input)
501    }
502
503    /// Random source, optionally bounded by typed wires and gated by a boolean.
504    pub fn random<D: NumericWireDomain>(
505        &mut self,
506        id: impl Into<std::string::String>,
507        require_gate: bool,
508        min: Option<&Wire<D>>,
509        max: Option<&Wire<D>>,
510        gate: Option<&BoolWire>,
511    ) -> Result<Wire<D>, BuildError> {
512        let knot = self.knot(id, KnotKind::random(require_gate, D::DOMAIN))?;
513        self.optional_thread(min, &knot, "min")?;
514        self.optional_thread(max, &knot, "max")?;
515        self.optional_thread(gate, &knot, "gate")?;
516        self.typed_output(&knot, "out")
517    }
518
519    /// Select between two wires of the same typed domain.
520    pub fn select<D: WireDomain>(
521        &mut self,
522        id: impl Into<std::string::String>,
523        select: &BoolWire,
524        a: &Wire<D>,
525        b: &Wire<D>,
526    ) -> Result<Wire<D>, BuildError> {
527        let knot = self.knot(id, KnotKind::select())?;
528        for (source, name) in [(select.port(), "sel"), (a.port(), "a"), (b.port(), "b")] {
529            let input = self.input(&knot, name)?;
530            self.thread(source, &input)?;
531        }
532        self.typed_output(&knot, "out")
533    }
534
535    /// Explicitly convert one typed domain to another.
536    pub fn convert<F: WireDomain, T: WireDomain>(
537        &mut self,
538        id: impl Into<std::string::String>,
539        input: &Wire<F>,
540    ) -> Result<Wire<T>, BuildError> {
541        self.unary(id, KnotKind::convert(F::DOMAIN, T::DOMAIN), input)
542    }
543
544    /// Emit a named host command from a boolean trigger.
545    pub fn emit(
546        &mut self,
547        id: impl Into<std::string::String>,
548        name: impl Into<std::string::String>,
549        trigger: &BoolWire,
550    ) -> Result<(), BuildError> {
551        let knot = self.knot(id, KnotKind::emit_command(name))?;
552        let input = self.input(&knot, "trigger")?;
553        self.thread(trigger.port(), &input)
554    }
555
556    fn source<D: WireDomain>(
557        &mut self,
558        id: impl Into<std::string::String>,
559        kind: KnotKind,
560    ) -> Result<Wire<D>, BuildError> {
561        let knot = self.knot(id, kind)?;
562        self.typed_output(&knot, "out")
563    }
564
565    fn unary<D: WireDomain, R: WireDomain>(
566        &mut self,
567        id: impl Into<std::string::String>,
568        kind: KnotKind,
569        input: &Wire<D>,
570    ) -> Result<Wire<R>, BuildError> {
571        let knot = self.knot(id, kind)?;
572        let target = self.input(&knot, "in")?;
573        self.thread(input.port(), &target)?;
574        self.typed_output(&knot, "out")
575    }
576
577    fn binary<D: WireDomain, E: WireDomain, R: WireDomain>(
578        &mut self,
579        id: impl Into<std::string::String>,
580        kind: KnotKind,
581        a: &Wire<D>,
582        b: &Wire<E>,
583        a_port: &str,
584        b_port: &str,
585    ) -> Result<Wire<R>, BuildError> {
586        let knot = self.knot(id, kind)?;
587        let a_target = self.input(&knot, a_port)?;
588        let b_target = self.input(&knot, b_port)?;
589        self.thread(a.port(), &a_target)?;
590        self.thread(b.port(), &b_target)?;
591        self.typed_output(&knot, "out")
592    }
593
594    fn optional_thread<D: WireDomain>(
595        &mut self,
596        wire: Option<&Wire<D>>,
597        knot: &KnotHandle,
598        port: &str,
599    ) -> Result<(), BuildError> {
600        if let Some(wire) = wire {
601            let input = self.input(knot, port)?;
602            self.thread(wire.port(), &input)?;
603        }
604        Ok(())
605    }
606
607    fn typed_output<D: WireDomain>(
608        &self,
609        knot: &KnotHandle,
610        port: &str,
611    ) -> Result<Wire<D>, BuildError> {
612        Ok(Wire {
613            port: self.output(knot, port)?,
614            marker: PhantomData,
615        })
616    }
617}
618
619/// Boolean outputs of a threshold knot.
620#[derive(Clone, Debug)]
621pub struct ThresholdWires {
622    /// Current threshold state.
623    pub out: BoolWire,
624    /// One-frame rising threshold crossing.
625    pub crossed_up: BoolWire,
626    /// One-frame falling threshold crossing.
627    pub crossed_down: BoolWire,
628}