Skip to main content

oxilean_std/io/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use oxilean_kernel::{BinderInfo, Declaration, Environment, Expr, Level, Name};
6
7use super::functions::*;
8use std::collections::{HashMap, VecDeque};
9
10/// A line-oriented record writer.
11///
12/// Wraps a `BufferedWriter` and writes records as newline-separated strings.
13#[allow(dead_code)]
14pub struct RecordWriter {
15    inner: BufferedWriter,
16    record_count: usize,
17}
18impl RecordWriter {
19    /// Create a record writer with the given flush threshold.
20    #[allow(dead_code)]
21    pub fn new(flush_threshold: usize) -> Self {
22        Self {
23            inner: BufferedWriter::new(flush_threshold),
24            record_count: 0,
25        }
26    }
27    /// Write a record (followed by newline).
28    #[allow(dead_code)]
29    pub fn write_record(&mut self, record: &str) {
30        self.inner.writeln(record);
31        self.record_count += 1;
32    }
33    /// Number of records written.
34    #[allow(dead_code)]
35    pub fn record_count(&self) -> usize {
36        self.record_count
37    }
38    /// Total bytes written (including newlines).
39    #[allow(dead_code)]
40    pub fn total_written(&self) -> usize {
41        self.inner.total_written()
42    }
43    /// Flush the internal buffer.
44    #[allow(dead_code)]
45    pub fn flush(&mut self) {
46        self.inner.flush();
47    }
48}
49/// A capability token representing a permission level for IO operations.
50#[allow(dead_code)]
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct Capability {
53    /// The permission level (higher = more privileged).
54    pub level: u32,
55    /// Human-readable description.
56    pub description: String,
57}
58#[allow(dead_code)]
59impl Capability {
60    /// Create a new capability token.
61    pub fn new(level: u32, description: impl Into<String>) -> Self {
62        Self {
63            level,
64            description: description.into(),
65        }
66    }
67    /// Null capability (no permissions).
68    pub fn null() -> Self {
69        Self::new(0, "null")
70    }
71    /// Read-only capability.
72    pub fn read_only() -> Self {
73        Self::new(1, "read-only")
74    }
75    /// Read-write capability.
76    pub fn read_write() -> Self {
77        Self::new(2, "read-write")
78    }
79    /// Full (root) capability.
80    pub fn full() -> Self {
81        Self::new(u32::MAX, "full")
82    }
83    /// Check if this capability is sufficient for the required level.
84    pub fn is_sufficient_for(&self, required: u32) -> bool {
85        self.level >= required
86    }
87    /// Attenuate (reduce) the capability to a lower level.
88    pub fn attenuate(&self, max_level: u32) -> Capability {
89        Capability::new(
90            self.level.min(max_level),
91            format!("attenuated({})", max_level),
92        )
93    }
94}
95/// A simple stream delimiter splitter.
96///
97/// Splits a byte stream on a given delimiter character and yields records.
98#[allow(dead_code)]
99pub struct DelimiterSplitter {
100    buffer: Vec<u8>,
101    delimiter: u8,
102}
103impl DelimiterSplitter {
104    /// Create a new splitter with the given delimiter.
105    #[allow(dead_code)]
106    pub fn new(delimiter: u8) -> Self {
107        Self {
108            buffer: Vec::new(),
109            delimiter,
110        }
111    }
112    /// Feed bytes into the splitter.
113    #[allow(dead_code)]
114    pub fn feed(&mut self, data: &[u8]) {
115        self.buffer.extend_from_slice(data);
116    }
117    /// Extract all complete records from the buffer.
118    ///
119    /// A record ends at the delimiter. Incomplete records remain buffered.
120    #[allow(dead_code)]
121    pub fn drain(&mut self) -> Vec<Vec<u8>> {
122        let mut records = Vec::new();
123        while let Some(pos) = self.buffer.iter().position(|&b| b == self.delimiter) {
124            let record = self.buffer.drain(..pos).collect();
125            self.buffer.drain(..1);
126            records.push(record);
127        }
128        records
129    }
130    /// How many bytes are currently buffered (waiting for delimiter).
131    #[allow(dead_code)]
132    pub fn buffered_len(&self) -> usize {
133        self.buffer.len()
134    }
135}
136/// Simulated file metadata.
137#[derive(Debug, Clone)]
138pub struct FileMetadata {
139    /// File path.
140    pub path: String,
141    /// Size in bytes.
142    pub size: u64,
143    /// Whether this is a directory.
144    pub is_dir: bool,
145    /// Whether this is a regular file.
146    pub is_file: bool,
147    /// Whether the file is read-only.
148    pub read_only: bool,
149}
150impl FileMetadata {
151    /// Create metadata for a regular file.
152    pub fn regular_file(path: impl Into<String>, size: u64) -> Self {
153        Self {
154            path: path.into(),
155            size,
156            is_dir: false,
157            is_file: true,
158            read_only: false,
159        }
160    }
161    /// Create metadata for a directory.
162    pub fn directory(path: impl Into<String>) -> Self {
163        Self {
164            path: path.into(),
165            size: 0,
166            is_dir: true,
167            is_file: false,
168            read_only: false,
169        }
170    }
171    /// Mark the file as read-only.
172    pub fn with_read_only(mut self) -> Self {
173        self.read_only = true;
174        self
175    }
176}
177/// A simple in-memory key-value store that simulates persistent IO.
178///
179/// This can be used in tests or elaboration to mock file system operations.
180#[allow(dead_code)]
181#[derive(Debug, Default, Clone)]
182pub struct MockFs {
183    files: std::collections::HashMap<String, Vec<u8>>,
184}
185impl MockFs {
186    /// Create an empty mock file system.
187    #[allow(dead_code)]
188    pub fn new() -> Self {
189        Self::default()
190    }
191    /// Write bytes to a file.
192    #[allow(dead_code)]
193    pub fn write(&mut self, path: &str, data: Vec<u8>) {
194        self.files.insert(path.to_string(), data);
195    }
196    /// Write a string to a file.
197    #[allow(dead_code)]
198    pub fn write_str(&mut self, path: &str, content: &str) {
199        self.write(path, content.as_bytes().to_vec());
200    }
201    /// Read the contents of a file.
202    #[allow(dead_code)]
203    pub fn read(&self, path: &str) -> Result<Vec<u8>, IoError> {
204        self.files
205            .get(path)
206            .cloned()
207            .ok_or_else(|| IoError::not_found(path))
208    }
209    /// Read the contents of a file as a string.
210    #[allow(dead_code)]
211    pub fn read_str(&self, path: &str) -> Result<String, IoError> {
212        let bytes = self.read(path)?;
213        String::from_utf8(bytes).map_err(|_| IoError::invalid_data("invalid UTF-8"))
214    }
215    /// Check if a file exists.
216    #[allow(dead_code)]
217    pub fn exists(&self, path: &str) -> bool {
218        self.files.contains_key(path)
219    }
220    /// Delete a file. Returns true if it existed.
221    #[allow(dead_code)]
222    pub fn remove(&mut self, path: &str) -> bool {
223        self.files.remove(path).is_some()
224    }
225    /// List all file paths in the mock file system.
226    #[allow(dead_code)]
227    pub fn list_paths(&self) -> Vec<&str> {
228        self.files.keys().map(String::as_str).collect()
229    }
230    /// File size in bytes, or an error if not found.
231    #[allow(dead_code)]
232    pub fn file_size(&self, path: &str) -> Result<u64, IoError> {
233        self.files
234            .get(path)
235            .map(|v| v.len() as u64)
236            .ok_or_else(|| IoError::not_found(path))
237    }
238}
239/// A simple Hoare-logic verifier for IO programs.
240///
241/// Records pre/postcondition pairs for IO operations and checks consistency.
242#[allow(dead_code)]
243pub struct HoareVerifier {
244    /// Each entry: (operation name, precondition description, postcondition description).
245    triples: Vec<(String, String, String)>,
246}
247#[allow(dead_code)]
248impl HoareVerifier {
249    /// Create an empty verifier.
250    pub fn new() -> Self {
251        Self {
252            triples: Vec::new(),
253        }
254    }
255    /// Add a Hoare triple.
256    pub fn add_triple(
257        &mut self,
258        op: impl Into<String>,
259        pre: impl Into<String>,
260        post: impl Into<String>,
261    ) {
262        self.triples.push((op.into(), pre.into(), post.into()));
263    }
264    /// Count registered triples.
265    pub fn triple_count(&self) -> usize {
266        self.triples.len()
267    }
268    /// Look up the postcondition for a given operation.
269    pub fn postcondition_of(&self, op: &str) -> Option<&str> {
270        self.triples
271            .iter()
272            .find(|(o, _, _)| o == op)
273            .map(|(_, _, post)| post.as_str())
274    }
275    /// Check if an operation has been registered.
276    pub fn has_triple(&self, op: &str) -> bool {
277        self.triples.iter().any(|(o, _, _)| o == op)
278    }
279    /// Get all registered operation names.
280    pub fn operations(&self) -> Vec<&str> {
281        self.triples.iter().map(|(o, _, _)| o.as_str()).collect()
282    }
283}
284/// Kinds of I/O errors that can occur in the IO monad.
285#[derive(Debug, Clone, PartialEq, Eq)]
286pub enum IoErrorKind {
287    /// File not found.
288    NotFound,
289    /// Permission denied.
290    PermissionDenied,
291    /// Connection refused.
292    ConnectionRefused,
293    /// Connection timed out.
294    TimedOut,
295    /// Unexpected end of file.
296    UnexpectedEof,
297    /// Write failed (disk full, etc.).
298    WriteZero,
299    /// Invalid data or format error.
300    InvalidData,
301    /// An unrecognised I/O error.
302    Other,
303}
304/// A typed session channel enforcing a protocol.
305#[allow(dead_code)]
306pub struct SessionChannel {
307    protocol: Vec<SessionAction>,
308    cursor: usize,
309    messages: Vec<String>,
310}
311#[allow(dead_code)]
312impl SessionChannel {
313    /// Create a channel with the given protocol.
314    pub fn new(protocol: Vec<SessionAction>) -> Self {
315        Self {
316            protocol,
317            cursor: 0,
318            messages: Vec::new(),
319        }
320    }
321    /// Attempt to perform a send action.
322    ///
323    /// Returns `Ok(())` if the protocol expects a send at this point.
324    pub fn send(&mut self, msg: impl Into<String>) -> Result<(), String> {
325        if self.cursor >= self.protocol.len() {
326            return Err("protocol exhausted".to_string());
327        }
328        if self.protocol[self.cursor] != SessionAction::Send {
329            return Err(format!(
330                "protocol violation: expected {:?}, got Send",
331                self.protocol[self.cursor]
332            ));
333        }
334        self.messages.push(msg.into());
335        self.cursor += 1;
336        Ok(())
337    }
338    /// Attempt to perform a receive action.
339    ///
340    /// Returns the next message if the protocol expects a receive.
341    pub fn recv(&mut self) -> Result<Option<String>, String> {
342        if self.cursor >= self.protocol.len() {
343            return Err("protocol exhausted".to_string());
344        }
345        if self.protocol[self.cursor] != SessionAction::Recv {
346            return Err(format!(
347                "protocol violation: expected {:?}, got Recv",
348                self.protocol[self.cursor]
349            ));
350        }
351        self.cursor += 1;
352        Ok(self.messages.pop())
353    }
354    /// Check if the protocol has been fully followed.
355    pub fn is_complete(&self) -> bool {
356        self.cursor >= self.protocol.len()
357    }
358    /// Number of steps remaining in the protocol.
359    pub fn remaining_steps(&self) -> usize {
360        self.protocol.len().saturating_sub(self.cursor)
361    }
362}
363/// An asynchronous task queue.
364///
365/// Simulates a queue of pending async IO tasks identified by integer handles.
366#[allow(dead_code)]
367pub struct AsyncTaskQueue {
368    pending: std::collections::VecDeque<(usize, String)>,
369    completed: std::collections::HashMap<usize, String>,
370    next_id: usize,
371}
372#[allow(dead_code)]
373impl AsyncTaskQueue {
374    /// Create an empty task queue.
375    pub fn new() -> Self {
376        Self {
377            pending: std::collections::VecDeque::new(),
378            completed: std::collections::HashMap::new(),
379            next_id: 0,
380        }
381    }
382    /// Enqueue a new task, returning its handle.
383    pub fn enqueue(&mut self, description: impl Into<String>) -> usize {
384        let id = self.next_id;
385        self.next_id += 1;
386        self.pending.push_back((id, description.into()));
387        id
388    }
389    /// Complete the next pending task with the given result.
390    pub fn complete_next(&mut self, result: impl Into<String>) -> Option<usize> {
391        if let Some((id, _)) = self.pending.pop_front() {
392            self.completed.insert(id, result.into());
393            Some(id)
394        } else {
395            None
396        }
397    }
398    /// Get the result of a completed task.
399    pub fn result_of(&self, id: usize) -> Option<&str> {
400        self.completed.get(&id).map(String::as_str)
401    }
402    /// Number of pending tasks.
403    pub fn pending_count(&self) -> usize {
404        self.pending.len()
405    }
406    /// Number of completed tasks.
407    pub fn completed_count(&self) -> usize {
408        self.completed.len()
409    }
410    /// Check if a task has completed.
411    pub fn is_complete(&self, id: usize) -> bool {
412        self.completed.contains_key(&id)
413    }
414}
415/// An IO action with its expected type.
416#[derive(Debug, Clone)]
417pub struct IoAction {
418    /// Kind of action.
419    pub kind: IoActionKind,
420    /// Result type of the action (the `α` in `IO α`).
421    pub result_type: Expr,
422}
423impl IoAction {
424    /// Create a new IO action.
425    pub fn new(kind: IoActionKind, result_type: Expr) -> Self {
426        Self { kind, result_type }
427    }
428    /// Create a `println` action.
429    pub fn println() -> Self {
430        Self::new(
431            IoActionKind::Println,
432            Expr::Const(Name::str("Unit"), vec![]),
433        )
434    }
435    /// Create a `readLine` action.
436    pub fn read_line() -> Self {
437        Self::new(
438            IoActionKind::ReadStdin,
439            Expr::Const(Name::str("String"), vec![]),
440        )
441    }
442    /// Create an `exit` action.
443    pub fn exit(code: i32) -> Self {
444        Self::new(
445            IoActionKind::Exit(code),
446            Expr::Const(Name::str("Empty"), vec![]),
447        )
448    }
449}
450/// An I/O error value.
451#[derive(Debug, Clone)]
452pub struct IoError {
453    /// Error kind.
454    pub kind: IoErrorKind,
455    /// Human-readable description.
456    pub message: String,
457}
458impl IoError {
459    /// Create a new IoError.
460    pub fn new(kind: IoErrorKind, message: impl Into<String>) -> Self {
461        Self {
462            kind,
463            message: message.into(),
464        }
465    }
466    /// Create a "not found" error.
467    pub fn not_found(path: &str) -> Self {
468        Self::new(IoErrorKind::NotFound, format!("file not found: {}", path))
469    }
470    /// Create a "permission denied" error.
471    pub fn permission_denied(path: &str) -> Self {
472        Self::new(
473            IoErrorKind::PermissionDenied,
474            format!("permission denied: {}", path),
475        )
476    }
477    /// Create an "unexpected EOF" error.
478    pub fn unexpected_eof() -> Self {
479        Self::new(IoErrorKind::UnexpectedEof, "unexpected end of file")
480    }
481    /// Create an "invalid data" error.
482    pub fn invalid_data(msg: impl Into<String>) -> Self {
483        Self::new(IoErrorKind::InvalidData, msg)
484    }
485}
486/// A Software Transactional Memory (STM) log.
487///
488/// Records reads and writes during a transaction for conflict detection.
489#[allow(dead_code)]
490pub struct StmLog {
491    /// Cells read during this transaction: (cell_id, value_at_read_time).
492    reads: Vec<(usize, i64)>,
493    /// Cells written during this transaction: (cell_id, new_value).
494    writes: Vec<(usize, i64)>,
495    /// Whether the transaction has been aborted.
496    aborted: bool,
497}
498#[allow(dead_code)]
499impl StmLog {
500    /// Create an empty STM log.
501    pub fn new() -> Self {
502        Self {
503            reads: Vec::new(),
504            writes: Vec::new(),
505            aborted: false,
506        }
507    }
508    /// Record a read.
509    pub fn record_read(&mut self, cell_id: usize, value: i64) {
510        self.reads.push((cell_id, value));
511    }
512    /// Record a write.
513    pub fn record_write(&mut self, cell_id: usize, value: i64) {
514        self.writes.push((cell_id, value));
515    }
516    /// Abort the transaction.
517    pub fn abort(&mut self) {
518        self.aborted = true;
519    }
520    /// Check if aborted.
521    pub fn is_aborted(&self) -> bool {
522        self.aborted
523    }
524    /// Check for write-write conflicts with another log.
525    pub fn conflicts_with(&self, other: &StmLog) -> bool {
526        for (wid, _) in &self.writes {
527            for (owid, _) in &other.writes {
528                if wid == owid {
529                    return true;
530                }
531            }
532        }
533        false
534    }
535    /// Number of reads recorded.
536    pub fn read_count(&self) -> usize {
537        self.reads.len()
538    }
539    /// Number of writes recorded.
540    pub fn write_count(&self) -> usize {
541        self.writes.len()
542    }
543}
544/// A descriptor for an IO action being elaborated.
545///
546/// Used by the elaborator to track what IO operations are being performed.
547#[derive(Debug, Clone, PartialEq)]
548pub enum IoActionKind {
549    /// Read from standard input.
550    ReadStdin,
551    /// Write to standard output.
552    WriteStdout,
553    /// Write to standard error.
554    WriteStderr,
555    /// Open a file for reading.
556    OpenRead(String),
557    /// Open a file for writing.
558    OpenWrite(String),
559    /// Close a file.
560    Close,
561    /// Print a line.
562    Println,
563    /// Flush a writer.
564    Flush,
565    /// Sleep for a duration (milliseconds).
566    Sleep(u64),
567    /// Exit with a code.
568    Exit(i32),
569}
570/// A simple in-memory buffered reader over a byte slice.
571///
572/// Simulates buffered line reading for I/O processing within the elaborator.
573pub struct BufferedReader {
574    /// Internal data buffer.
575    data: Vec<u8>,
576    /// Current read position.
577    pos: usize,
578    /// Buffer capacity.
579    capacity: usize,
580}
581impl BufferedReader {
582    /// Create a new buffered reader with default capacity (8 KiB).
583    pub fn new(data: Vec<u8>) -> Self {
584        Self {
585            capacity: data.len(),
586            data,
587            pos: 0,
588        }
589    }
590    /// Create a buffered reader from a string.
591    #[allow(clippy::should_implement_trait)]
592    pub fn from_str(s: &str) -> Self {
593        Self::new(s.as_bytes().to_vec())
594    }
595    /// Read a single byte, or `None` if at end.
596    pub fn read_byte(&mut self) -> Option<u8> {
597        if self.pos < self.data.len() {
598            let b = self.data[self.pos];
599            self.pos += 1;
600            Some(b)
601        } else {
602            None
603        }
604    }
605    /// Read the next line (up to and including `\n`).
606    ///
607    /// Returns `None` when the buffer is exhausted.
608    pub fn read_line(&mut self) -> Option<String> {
609        if self.pos >= self.data.len() {
610            return None;
611        }
612        let start = self.pos;
613        while self.pos < self.data.len() && self.data[self.pos] != b'\n' {
614            self.pos += 1;
615        }
616        if self.pos < self.data.len() {
617            self.pos += 1;
618        }
619        String::from_utf8(self.data[start..self.pos].to_vec()).ok()
620    }
621    /// Read all remaining content as a string.
622    pub fn read_to_string(&mut self) -> Result<String, IoError> {
623        let remaining = &self.data[self.pos..];
624        self.pos = self.data.len();
625        String::from_utf8(remaining.to_vec())
626            .map_err(|_| IoError::invalid_data("invalid UTF-8 sequence"))
627    }
628    /// Read exactly `n` bytes.
629    ///
630    /// Returns an error if fewer than `n` bytes remain.
631    pub fn read_exact(&mut self, n: usize) -> Result<Vec<u8>, IoError> {
632        if self.pos + n > self.data.len() {
633            return Err(IoError::unexpected_eof());
634        }
635        let slice = self.data[self.pos..self.pos + n].to_vec();
636        self.pos += n;
637        Ok(slice)
638    }
639    /// Peek at the next byte without consuming it.
640    pub fn peek(&self) -> Option<u8> {
641        self.data.get(self.pos).copied()
642    }
643    /// Check if the reader is at end of data.
644    pub fn is_eof(&self) -> bool {
645        self.pos >= self.data.len()
646    }
647    /// Return the number of bytes remaining.
648    pub fn remaining(&self) -> usize {
649        self.data.len().saturating_sub(self.pos)
650    }
651    /// Reset the reader to the beginning.
652    pub fn reset(&mut self) {
653        self.pos = 0;
654    }
655    /// Skip `n` bytes.
656    pub fn skip(&mut self, n: usize) {
657        self.pos = (self.pos + n).min(self.data.len());
658    }
659    /// Return the buffer capacity.
660    pub fn capacity(&self) -> usize {
661        self.capacity
662    }
663    /// Collect all lines into a vector.
664    pub fn lines(&mut self) -> Vec<String> {
665        let mut result = Vec::new();
666        while let Some(line) = self.read_line() {
667            result.push(line);
668        }
669        result
670    }
671}
672/// IO environment variable registry (mock).
673///
674/// Simulates `std::env::var` for testing IO elaboration.
675#[allow(dead_code)]
676#[derive(Debug, Default, Clone)]
677pub struct EnvRegistry {
678    vars: std::collections::HashMap<String, String>,
679}
680impl EnvRegistry {
681    /// Create an empty registry.
682    #[allow(dead_code)]
683    pub fn new() -> Self {
684        Self::default()
685    }
686    /// Set an environment variable.
687    #[allow(dead_code)]
688    pub fn set(&mut self, key: &str, val: &str) {
689        self.vars.insert(key.to_string(), val.to_string());
690    }
691    /// Get an environment variable.
692    #[allow(dead_code)]
693    pub fn get(&self, key: &str) -> Option<&str> {
694        self.vars.get(key).map(String::as_str)
695    }
696    /// Remove an environment variable.
697    #[allow(dead_code)]
698    pub fn remove(&mut self, key: &str) {
699        self.vars.remove(key);
700    }
701    /// List all variable names.
702    #[allow(dead_code)]
703    pub fn keys(&self) -> Vec<&str> {
704        self.vars.keys().map(String::as_str).collect()
705    }
706}
707/// A session channel that enforces a simple send/receive protocol.
708///
709/// The protocol is represented as a stack of expected actions.
710/// Each action is either `Send` or `Recv`.
711#[allow(dead_code)]
712#[derive(Debug, Clone, PartialEq)]
713pub enum SessionAction {
714    /// Expect a send action.
715    Send,
716    /// Expect a receive action.
717    Recv,
718}
719/// A simple in-memory buffered writer.
720///
721/// Accumulates output and allows flushing to a target.
722pub struct BufferedWriter {
723    /// Accumulated output bytes.
724    buffer: Vec<u8>,
725    /// Flush threshold in bytes (auto-flush when exceeded).
726    flush_threshold: usize,
727    /// Total bytes written (including past flushes).
728    total_written: usize,
729}
730impl BufferedWriter {
731    /// Create a new buffered writer with a given flush threshold.
732    pub fn new(flush_threshold: usize) -> Self {
733        Self {
734            buffer: Vec::new(),
735            flush_threshold,
736            total_written: 0,
737        }
738    }
739    /// Create a buffered writer with default threshold (4 KiB).
740    pub fn default_threshold() -> Self {
741        Self::new(4096)
742    }
743    /// Write bytes to the buffer.
744    ///
745    /// Auto-flushes (clears the buffer) if the threshold is exceeded.
746    pub fn write_bytes(&mut self, data: &[u8]) {
747        self.buffer.extend_from_slice(data);
748        self.total_written += data.len();
749        if self.buffer.len() >= self.flush_threshold {
750            self.flush();
751        }
752    }
753    /// Write a string to the buffer.
754    pub fn write_str(&mut self, s: &str) {
755        self.write_bytes(s.as_bytes());
756    }
757    /// Write a string followed by a newline.
758    pub fn writeln(&mut self, s: &str) {
759        self.write_str(s);
760        self.write_bytes(b"\n");
761    }
762    /// Flush the buffer (clear it, simulating a write to an underlying sink).
763    pub fn flush(&mut self) {
764        self.buffer.clear();
765    }
766    /// Get the current buffer contents as a string.
767    pub fn as_str(&self) -> Result<&str, IoError> {
768        std::str::from_utf8(&self.buffer)
769            .map_err(|_| IoError::invalid_data("buffer contains invalid UTF-8"))
770    }
771    /// Get the number of buffered (unflushed) bytes.
772    pub fn buffered_len(&self) -> usize {
773        self.buffer.len()
774    }
775    /// Get the total number of bytes written (including flushed).
776    pub fn total_written(&self) -> usize {
777        self.total_written
778    }
779    /// Check if the buffer is empty.
780    pub fn is_empty(&self) -> bool {
781        self.buffer.is_empty()
782    }
783}
784/// A chain of IO actions represented as a list of action descriptors.
785///
786/// Used during elaboration to validate that sequenced IO operations have
787/// compatible types.
788#[derive(Debug, Default)]
789pub struct IoActionPipeline {
790    /// Ordered list of IO actions in this pipeline.
791    actions: Vec<IoAction>,
792}
793impl IoActionPipeline {
794    /// Create an empty pipeline.
795    pub fn new() -> Self {
796        Self::default()
797    }
798    /// Add an action to the pipeline.
799    pub fn push(&mut self, action: IoAction) {
800        self.actions.push(action);
801    }
802    /// Get the number of actions.
803    pub fn len(&self) -> usize {
804        self.actions.len()
805    }
806    /// Check if the pipeline is empty.
807    pub fn is_empty(&self) -> bool {
808        self.actions.is_empty()
809    }
810    /// Get all actions.
811    pub fn actions(&self) -> &[IoAction] {
812        &self.actions
813    }
814    /// Check if any action is an exit.
815    pub fn has_exit(&self) -> bool {
816        self.actions
817            .iter()
818            .any(|a| matches!(a.kind, IoActionKind::Exit(_)))
819    }
820    /// Get the last action's result type (the overall result type of the pipeline).
821    pub fn result_type(&self) -> Option<&Expr> {
822        self.actions.last().map(|a| &a.result_type)
823    }
824    /// Remove all actions.
825    pub fn clear(&mut self) {
826        self.actions.clear();
827    }
828}