axon-lang 1.38.1

AXON v1.5.1 — first crates.io publication of the AXON language full-stack runtime. Lexer/parser/type-checker/IR generator (re-exported from axon-frontend) plus the native Rust runtime: typed channels (TypedEventBus with QoS×5, π-calculus mobility, capability extrusion via shield D8 — Fase 13.f.2), Free Monad CPS handlers (Fase 2), lease kernel + reconcile loop (Fase 3+5), Epistemic Security Kernel (ESK Fase 6), Trust Types + ReplayLog (Fase 11.a+11.c), Stateful PEM over WebSocket (Fase 11.d), Ontological Tool Synthesis (Fase 11.e), Mobile Typed Channels (Fase 13). Crate publishes as `axon-lang` to mirror the Python PyPI package; library import remains `use axon::*` so existing call sites keep working unchanged.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
//! [`Transformer`] trait + [`TransformerRegistry`] + [`Pipeline`]
//! builder.
//!
//! Path finding is Dijkstra over a directed graph whose nodes are
//! [`BufferKind`]s and whose edges are registered transformers.
//! Edge weight = `cost_hint` so adopters can bias toward native
//! paths even when a cheap ffmpeg chain would also work.

use std::collections::{BinaryHeap, HashMap};
use std::sync::Arc;

use crate::buffer::{BufferKind, ZeroCopyBuffer};

// ── Errors ───────────────────────────────────────────────────────────

#[derive(Debug)]
pub enum OtsError {
    /// No path exists from source to sink in the current registry.
    NoPath {
        from: BufferKind,
        to: BufferKind,
    },
    /// Transformer execution failed; message carries adopter-supplied
    /// detail.
    TransformFailed(String),
    /// The pipeline hit a kind mismatch mid-flight — typically means
    /// the registry was mutated between path-find and execute.
    KindMismatch {
        expected: BufferKind,
        actual: BufferKind,
    },
}

impl std::fmt::Display for OtsError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::NoPath { from, to } => write!(
                f,
                "no OTS path from {from} to {to} in the current registry"
            ),
            Self::TransformFailed(m) => write!(f, "transform failed: {m}"),
            Self::KindMismatch { expected, actual } => write!(
                f,
                "pipeline kind mismatch — expected {expected}, got {actual}"
            ),
        }
    }
}

impl std::error::Error for OtsError {}

// ── Transformer trait ───────────────────────────────────────────────

/// Backend classification. Consumed by the checker to enforce the
/// `sensitive + legal:HIPAA.* + ots:backend:ffmpeg` rejection.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TransformerBackend {
    /// Pure-Rust transcoder. No data crosses a process boundary.
    Native,
    /// Subprocess delegation (ffmpeg, sox, …). Data leaves the
    /// address space of the hosting process.
    Subprocess,
}

impl TransformerBackend {
    pub fn slug(self) -> &'static str {
        match self {
            TransformerBackend::Native => "native",
            TransformerBackend::Subprocess => "ffmpeg",
        }
    }
}

/// Stable identifier used for caching + metrics. `"mulaw8->pcm16"`
/// is the canonical format.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TransformerId(pub String);

impl TransformerId {
    pub fn new(from: &BufferKind, to: &BufferKind) -> Self {
        TransformerId(format!("{from}->{to}"))
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

/// The bread-and-butter of OTS. Adopters implement this for each
/// conversion they want to register.
pub trait Transformer: Send + Sync {
    fn source_kind(&self) -> BufferKind;
    fn sink_kind(&self) -> BufferKind;
    fn backend(&self) -> TransformerBackend;

    /// Relative cost. Dijkstra's shortest-path picks the path with
    /// the smallest sum. Native transcoders should be ~1; ffmpeg
    /// subprocess ~10 so native wins when both exist.
    fn cost_hint(&self) -> u32 {
        1
    }

    /// Perform the conversion. `input.kind()` MUST equal
    /// [`Self::source_kind`]; the returned buffer's `kind()` MUST
    /// equal [`Self::sink_kind`]. The pipeline verifies this
    /// invariant to catch registry drift.
    fn transform(
        &self,
        input: &ZeroCopyBuffer,
    ) -> Result<ZeroCopyBuffer, OtsError>;
}

// ── Registry ────────────────────────────────────────────────────────

/// Directed multi-graph of transformers. Lookup returns a cheapest
/// path (Dijkstra). Safe for concurrent reads after construction;
/// writes happen once at startup.
pub struct TransformerRegistry {
    // Each source kind maps to the transformers that ACCEPT it.
    edges: HashMap<BufferKind, Vec<Arc<dyn Transformer>>>,
}

impl TransformerRegistry {
    pub fn new() -> Self {
        TransformerRegistry {
            edges: HashMap::new(),
        }
    }

    pub fn install(&mut self, transformer: Arc<dyn Transformer>) {
        let src = transformer.source_kind();
        self.edges
            .entry(src)
            .or_default()
            .push(transformer);
    }

    pub fn transformers_from(
        &self,
        kind: &BufferKind,
    ) -> &[Arc<dyn Transformer>] {
        self.edges.get(kind).map(|v| v.as_slice()).unwrap_or(&[])
    }

    /// Return the cheapest sequence of transformers that converts
    /// `from → to`, or [`OtsError::NoPath`] if none exists. An
    /// identity path (`from == to`) returns an empty `Vec`.
    pub fn shortest_path(
        &self,
        from: &BufferKind,
        to: &BufferKind,
    ) -> Result<Vec<Arc<dyn Transformer>>, OtsError> {
        if from == to {
            return Ok(Vec::new());
        }

        // Dijkstra: state = (cost, kind); parent map lets us
        // reconstruct the path once the sink is popped.
        let mut best_cost: HashMap<BufferKind, u32> = HashMap::new();
        let mut parent: HashMap<BufferKind, (BufferKind, Arc<dyn Transformer>)> =
            HashMap::new();
        let mut heap: BinaryHeap<std::cmp::Reverse<(u32, BufferKind)>> =
            BinaryHeap::new();

        best_cost.insert(from.clone(), 0);
        heap.push(std::cmp::Reverse((0, from.clone())));

        while let Some(std::cmp::Reverse((cost, kind))) = heap.pop() {
            if &kind == to {
                // Reconstruct path.
                let mut path = Vec::new();
                let mut cur = kind;
                while let Some((prev, t)) = parent.remove(&cur) {
                    path.push(t);
                    cur = prev;
                }
                path.reverse();
                return Ok(path);
            }
            if cost > *best_cost.get(&kind).unwrap_or(&u32::MAX) {
                continue;
            }
            for t in self.transformers_from(&kind) {
                let next = t.sink_kind();
                let new_cost = cost.saturating_add(t.cost_hint());
                let existing = best_cost.get(&next).copied().unwrap_or(u32::MAX);
                if new_cost < existing {
                    best_cost.insert(next.clone(), new_cost);
                    parent
                        .insert(next.clone(), (kind.clone(), Arc::clone(t)));
                    heap.push(std::cmp::Reverse((new_cost, next)));
                }
            }
        }

        Err(OtsError::NoPath {
            from: from.clone(),
            to: to.clone(),
        })
    }

    pub fn has_path(&self, from: &BufferKind, to: &BufferKind) -> bool {
        self.shortest_path(from, to).is_ok()
    }

    /// List every source kind known to the registry. Sorted for
    /// stable diagnostic output.
    pub fn known_sources(&self) -> Vec<BufferKind> {
        let mut v: Vec<BufferKind> = self.edges.keys().cloned().collect();
        v.sort_by(|a, b| a.slug().cmp(b.slug()));
        v
    }
}

impl Default for TransformerRegistry {
    fn default() -> Self {
        Self::new()
    }
}

// ── Pipeline (executable chain) ─────────────────────────────────────

#[derive(Debug, Clone)]
pub struct PipelineStep {
    pub id: TransformerId,
    pub backend: TransformerBackend,
}

/// Executable chain. Clone is cheap (Arcs all the way down).
#[derive(Clone)]
pub struct Pipeline {
    steps: Vec<Arc<dyn Transformer>>,
}

impl Pipeline {
    pub fn from_registry(
        registry: &TransformerRegistry,
        from: &BufferKind,
        to: &BufferKind,
    ) -> Result<Self, OtsError> {
        let steps = registry.shortest_path(from, to)?;
        Ok(Pipeline { steps })
    }

    pub fn len(&self) -> usize {
        self.steps.len()
    }

    pub fn is_empty(&self) -> bool {
        self.steps.is_empty()
    }

    pub fn steps(&self) -> Vec<PipelineStep> {
        self.steps
            .iter()
            .map(|t| PipelineStep {
                id: TransformerId::new(&t.source_kind(), &t.sink_kind()),
                backend: t.backend(),
            })
            .collect()
    }

    /// True when any step uses a subprocess backend. Used by the
    /// type checker to reject `sensitive + HIPAA + ffmpeg`
    /// combinations.
    pub fn crosses_process_boundary(&self) -> bool {
        self.steps
            .iter()
            .any(|t| t.backend() == TransformerBackend::Subprocess)
    }

    /// Run every step in order. Each step's output is the next
    /// step's input; kind mismatches raise [`OtsError::KindMismatch`].
    pub fn execute(
        &self,
        input: &ZeroCopyBuffer,
    ) -> Result<ZeroCopyBuffer, OtsError> {
        let mut current = input.clone();
        for step in &self.steps {
            let expected = step.source_kind();
            if current.kind() != expected {
                return Err(OtsError::KindMismatch {
                    expected,
                    actual: current.kind(),
                });
            }
            current = step.transform(&current)?;
        }
        Ok(current)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // Identity-ish transformer for path-search tests: converts
    // between named kinds with a fixed cost, copying bytes.
    struct Identity {
        from: BufferKind,
        to: BufferKind,
        cost: u32,
        backend: TransformerBackend,
    }

    impl Transformer for Identity {
        fn source_kind(&self) -> BufferKind {
            self.from.clone()
        }
        fn sink_kind(&self) -> BufferKind {
            self.to.clone()
        }
        fn backend(&self) -> TransformerBackend {
            self.backend
        }
        fn cost_hint(&self) -> u32 {
            self.cost
        }
        fn transform(
            &self,
            input: &ZeroCopyBuffer,
        ) -> Result<ZeroCopyBuffer, OtsError> {
            Ok(input.retag(self.to.clone()))
        }
    }

    fn ident(
        from: &str,
        to: &str,
        cost: u32,
        backend: TransformerBackend,
    ) -> Arc<dyn Transformer> {
        Arc::new(Identity {
            from: BufferKind::new(from),
            to: BufferKind::new(to),
            cost,
            backend,
        })
    }

    #[test]
    fn identity_path_is_empty() {
        let reg = TransformerRegistry::new();
        let a = BufferKind::new("a");
        let path = reg.shortest_path(&a, &a).unwrap();
        assert!(path.is_empty());
    }

    #[test]
    fn single_edge_path() {
        let mut reg = TransformerRegistry::new();
        reg.install(ident("a", "b", 1, TransformerBackend::Native));
        let from = BufferKind::new("a");
        let to = BufferKind::new("b");
        let path = reg.shortest_path(&from, &to).unwrap();
        assert_eq!(path.len(), 1);
    }

    #[test]
    fn multi_edge_picks_lowest_cost_path() {
        let mut reg = TransformerRegistry::new();
        // Two disjoint paths a → c:
        //   1. a → b → c   (cost 1 + 1 = 2, native)
        //   2. a → c       (cost 10, ffmpeg)
        reg.install(ident("a", "b", 1, TransformerBackend::Native));
        reg.install(ident("b", "c", 1, TransformerBackend::Native));
        reg.install(ident("a", "c", 10, TransformerBackend::Subprocess));

        let from = BufferKind::new("a");
        let to = BufferKind::new("c");
        let path = reg.shortest_path(&from, &to).unwrap();
        assert_eq!(path.len(), 2, "cheaper 2-hop native path must win");
        assert_eq!(
            path[0].backend(),
            TransformerBackend::Native
        );
        assert_eq!(
            path[1].backend(),
            TransformerBackend::Native
        );
    }

    #[test]
    fn no_path_returns_typed_error() {
        let mut reg = TransformerRegistry::new();
        reg.install(ident("a", "b", 1, TransformerBackend::Native));
        let from = BufferKind::new("a");
        let to = BufferKind::new("z");
        // §Fase 12.c — `.unwrap_err()` requires the Ok variant to be
        // `Debug`, but the Ok type here is `Vec<Arc<dyn Transformer>>`
        // and `dyn Transformer` is not `Debug`. `.err().expect(...)`
        // drops the Ok variant before unwrapping and needs no bound.
        let err = reg
            .shortest_path(&from, &to)
            .err()
            .expect("expected NoPath error");
        matches!(err, OtsError::NoPath { .. });
    }

    #[test]
    fn pipeline_crosses_process_boundary_flag() {
        let mut reg = TransformerRegistry::new();
        reg.install(ident("a", "b", 10, TransformerBackend::Subprocess));

        let from = BufferKind::new("a");
        let to = BufferKind::new("b");
        let p = Pipeline::from_registry(&reg, &from, &to).unwrap();
        assert!(p.crosses_process_boundary());
    }

    #[test]
    fn pipeline_native_only_does_not_cross_boundary() {
        let mut reg = TransformerRegistry::new();
        reg.install(ident("a", "b", 1, TransformerBackend::Native));
        reg.install(ident("b", "c", 1, TransformerBackend::Native));

        let from = BufferKind::new("a");
        let to = BufferKind::new("c");
        let p = Pipeline::from_registry(&reg, &from, &to).unwrap();
        assert!(!p.crosses_process_boundary());
    }

    #[test]
    fn execute_runs_every_step() {
        let mut reg = TransformerRegistry::new();
        reg.install(ident("a", "b", 1, TransformerBackend::Native));
        reg.install(ident("b", "c", 1, TransformerBackend::Native));

        let from = BufferKind::new("a");
        let to = BufferKind::new("c");
        let p = Pipeline::from_registry(&reg, &from, &to).unwrap();

        let input = ZeroCopyBuffer::from_bytes(
            vec![1, 2, 3],
            BufferKind::new("a"),
        );
        let out = p.execute(&input).unwrap();
        assert_eq!(out.kind().slug(), "c");
        // Identity transformer keeps bytes intact.
        assert_eq!(out.as_slice(), &[1, 2, 3]);
    }

    #[test]
    fn execute_detects_kind_mismatch_on_wrong_input() {
        let mut reg = TransformerRegistry::new();
        reg.install(ident("a", "b", 1, TransformerBackend::Native));

        let from = BufferKind::new("a");
        let to = BufferKind::new("b");
        let p = Pipeline::from_registry(&reg, &from, &to).unwrap();

        let wrong_input = ZeroCopyBuffer::from_bytes(
            vec![1],
            BufferKind::new("wrong"),
        );
        let err = p.execute(&wrong_input).unwrap_err();
        matches!(err, OtsError::KindMismatch { .. });
    }
}