Skip to main content

dolang_compile/
lib.rs

1#![deny(warnings)]
2
3pub(crate) mod ast;
4pub(crate) mod cfg;
5pub(crate) mod constant;
6pub mod diag;
7pub(crate) mod elab;
8pub(crate) mod emit;
9pub(crate) mod flow;
10pub(crate) mod lex;
11pub(crate) mod lower;
12pub(crate) mod origin;
13pub(crate) mod parse;
14pub(crate) mod sig;
15pub mod source;
16pub(crate) mod sym;
17
18use std::{
19    error,
20    fmt::{self, Display},
21    io::{self, Write},
22    mem,
23    ops::ControlFlow,
24    path::Path,
25};
26
27use crate::{ast::visit, lex::Comment};
28
29use self::{
30    ast::visit::{Node, NodeKind},
31    elab::Elaborater,
32    emit::Emitter,
33    lex::Lexer,
34    lower::Lowerer,
35    parse::Parser,
36    source::{Diags, File},
37};
38
39pub use ast::{Context, visit::Token};
40
41use dolang_util::intern::{self, BinTable};
42
43use ast::Res;
44
45const STD_PRELUDE: &[&str] = &[
46    "array", "bin", "bool", "dbg", "dict", "float", "func", "int", "module", "range", "record",
47    "set", "str", "sym", "tuple", "type",
48];
49
50#[derive(Debug)]
51enum ErrorInfo<B> {
52    Fail,
53    Io(io::Error),
54    Break(B),
55}
56
57/// Kind of compilation error.
58#[derive(Debug)]
59#[non_exhaustive]
60pub enum ErrorKind {
61    /// Compilation failed; consult diagnostics.
62    Fail,
63    /// I/O error emitting bytecode.
64    Io,
65    /// Diagnostic or token visitor stopped execution.
66    Break,
67}
68
69/// Compile error
70#[derive(Debug)]
71pub struct Error<B>(ErrorInfo<B>);
72
73impl<B> Error<B> {
74    /// Get kind of error
75    pub fn kind(&self) -> ErrorKind {
76        match &self.0 {
77            ErrorInfo::Fail => ErrorKind::Fail,
78            ErrorInfo::Io(_) => ErrorKind::Io,
79            ErrorInfo::Break(_) => ErrorKind::Break,
80        }
81    }
82
83    /// Get underlying [`io::Error`], if applicable
84    pub fn as_io(&self) -> Option<&io::Error> {
85        match &self.0 {
86            ErrorInfo::Io(error) => Some(error),
87            _ => None,
88        }
89    }
90
91    /// Get underlying `B`, if applicable
92    pub fn as_break(&self) -> Option<&B> {
93        match &self.0 {
94            ErrorInfo::Break(b) => Some(b),
95            _ => None,
96        }
97    }
98}
99
100impl<B> From<io::Error> for Error<B> {
101    fn from(value: io::Error) -> Self {
102        Error(ErrorInfo::Io(value))
103    }
104}
105
106impl<B: Display> Display for Error<B> {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        match &self.0 {
109            ErrorInfo::Fail => "compilation failed".fmt(f),
110            ErrorInfo::Io(e) => e.fmt(f),
111            ErrorInfo::Break(b) => b.fmt(f),
112        }
113    }
114}
115
116impl<B: error::Error> error::Error for Error<B> {
117    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
118        match &self.0 {
119            ErrorInfo::Fail => None,
120            ErrorInfo::Io(error) => Some(error),
121            ErrorInfo::Break(b) => b.source(),
122        }
123    }
124}
125
126impl<B> From<parse::Error> for Error<B> {
127    fn from(_: parse::Error) -> Self {
128        Self(ErrorInfo::Fail)
129    }
130}
131
132impl<B> From<elab::Error> for Error<B> {
133    fn from(_: elab::Error) -> Self {
134        Self(ErrorInfo::Fail)
135    }
136}
137
138impl<B> From<lower::Error> for Error<B> {
139    fn from(_: lower::Error) -> Self {
140        Self(ErrorInfo::Fail)
141    }
142}
143
144/// Diagnostic emitter
145pub trait EmitDiag {
146    /// Information supplied when stopping compilation
147    type Break;
148
149    /// Emit diagnostic
150    fn emit(&mut self, diag: diag::Diag) -> ControlFlow<Self::Break>;
151}
152
153/// Callback function as diagnostic emitter
154impl<F, B> EmitDiag for F
155where
156    F: FnMut(diag::Diag) -> ControlFlow<B>,
157{
158    type Break = B;
159
160    fn emit(&mut self, diag: diag::Diag) -> ControlFlow<Self::Break> {
161        self(diag)
162    }
163}
164
165/// Origin of a resolved identifier
166pub use diag::Origin;
167
168/// Token emitter
169pub trait EmitToken {
170    /// Information supplied when stopping analysis
171    type Break;
172
173    /// Emit token
174    fn emit(
175        &mut self,
176        token: Token,
177        span: diag::Span,
178        origin: Option<diag::Origin>,
179        context: Context,
180    ) -> ControlFlow<Self::Break>;
181}
182
183/// Callback function as token emitter
184impl<F, B> EmitToken for F
185where
186    F: FnMut(Token, diag::Span, Option<diag::Origin>, Context) -> ControlFlow<B>,
187{
188    type Break = B;
189
190    fn emit(
191        &mut self,
192        token: Token,
193        span: diag::Span,
194        origin: Option<diag::Origin>,
195        context: Context,
196    ) -> ControlFlow<Self::Break> {
197        self(token, span, origin, context)
198    }
199}
200
201struct VisitAdapter<'a, 'e, B> {
202    file: &'a File<'a>,
203    origintab: &'a origin::Table,
204    emit: &'e mut dyn EmitToken<Break = B>,
205}
206
207struct CallAdapter<'a, 'b, 'e, B> {
208    parent: &'b mut VisitAdapter<'a, 'e, B>,
209    seen_arg0: bool,
210}
211
212struct CallIdentAdapter<'a, 'b, 'e, B>(&'b mut VisitAdapter<'a, 'e, B>);
213
214impl<'a, 'b, 'e, B> visit::Visit for CallIdentAdapter<'a, 'b, 'e, B> {
215    type Break = B;
216
217    fn node<T: Node + ?Sized>(&mut self, node: &T) -> ControlFlow<Self::Break> {
218        node.accept(self.0)
219    }
220
221    fn token(
222        &mut self,
223        _token: Token,
224        span: source::Span,
225        origin: Option<origin::Id>,
226    ) -> ControlFlow<Self::Break> {
227        self.0
228            .emit_token(Token::Variable, span, origin, Context::Call)
229    }
230}
231
232struct MethodAdapter<'a, 'b, 'e, B>(&'b mut VisitAdapter<'a, 'e, B>);
233
234impl<'a, 'b, 'e, B> visit::Visit for MethodAdapter<'a, 'b, 'e, B> {
235    type Break = B;
236
237    fn node<T: Node + ?Sized>(&mut self, node: &T) -> ControlFlow<Self::Break> {
238        node.accept(self.0)
239    }
240
241    fn token(
242        &mut self,
243        token: Token,
244        span: source::Span,
245        origin: Option<origin::Id>,
246    ) -> ControlFlow<Self::Break> {
247        let context = match token {
248            Token::Field => Context::Call,
249            _ => Context::None,
250        };
251        self.0.emit_token(token, span, origin, context)
252    }
253}
254
255impl<'a, 'b, 'e, B> visit::Visit for CallAdapter<'a, 'b, 'e, B> {
256    type Break = B;
257
258    fn node<T: Node + ?Sized>(&mut self, node: &T) -> ControlFlow<Self::Break> {
259        if self.seen_arg0 {
260            node.accept(self.parent)
261        } else {
262            self.seen_arg0 = true;
263            match node.kind() {
264                NodeKind::Index | NodeKind::Call => node.accept(&mut CallAdapter {
265                    parent: self.parent,
266                    seen_arg0: false,
267                }),
268                NodeKind::Ident => node.accept(&mut CallIdentAdapter(self.parent)),
269                NodeKind::Field => node.accept(&mut MethodAdapter(self.parent)),
270                _ => node.accept(self.parent),
271            }
272        }
273    }
274
275    fn token(
276        &mut self,
277        token: Token,
278        span: source::Span,
279        origin: Option<origin::Id>,
280    ) -> ControlFlow<Self::Break> {
281        self.parent.emit_token(token, span, origin, Context::None)
282    }
283}
284
285fn convert_span(file: &File, span: source::Span) -> diag::Span {
286    let coords = file.coord_span(span);
287    diag::Span::new(
288        diag::Pos::new(span.start as usize, coords.start.line, coords.start.column),
289        diag::Pos::new(span.end as usize, coords.end.line, coords.end.column),
290    )
291}
292
293fn convert_origin(file: &File, internal: &origin::Origin) -> Option<diag::Origin> {
294    match internal {
295        origin::Origin::ImportItem { module, item, name } => Some(diag::Origin::ImportItem {
296            module: convert_span(file, *module),
297            item: convert_span(file, *item),
298            name: convert_span(file, *name),
299        }),
300        origin::Origin::ImportModule { module, name } => Some(diag::Origin::ImportModule {
301            module: convert_span(file, *module),
302            name: convert_span(file, *name),
303        }),
304        origin::Origin::PreludeModule { module, name } => Some(diag::Origin::PreludeModule {
305            module: module.clone(),
306            name: name.clone(),
307        }),
308        origin::Origin::PreludeItem { module, item, name } => Some(diag::Origin::PreludeItem {
309            module: module.clone(),
310            item: item.clone(),
311            name: name.clone(),
312        }),
313        origin::Origin::Class { span } => Some(diag::Origin::Class {
314            span: convert_span(file, *span),
315        }),
316        origin::Origin::Def { span, class } => Some(diag::Origin::Def {
317            span: convert_span(file, *span),
318            class: class.map(|s| convert_span(file, s)),
319        }),
320        origin::Origin::Bind { span, class } => Some(diag::Origin::Bind {
321            span: convert_span(file, *span),
322            class: class.map(|s| convert_span(file, s)),
323        }),
324        origin::Origin::Param { span } => Some(diag::Origin::Param {
325            span: convert_span(file, *span),
326        }),
327        origin::Origin::SelfParam { span } => Some(diag::Origin::SelfParam {
328            span: convert_span(file, *span),
329        }),
330        origin::Origin::Synthetic | origin::Origin::Repl => None,
331    }
332}
333
334impl<'a, 'e, B> VisitAdapter<'a, 'e, B> {
335    fn emit_token(
336        &mut self,
337        token: Token,
338        span: source::Span,
339        origin: Option<origin::Id>,
340        context: Context,
341    ) -> ControlFlow<B> {
342        let diag_span = convert_span(self.file, span);
343        let diag_origin = origin.and_then(|id| convert_origin(self.file, &self.origintab[id]));
344        self.emit.emit(token, diag_span, diag_origin, context)
345    }
346}
347
348impl<'a, 'e, B> visit::Visit for VisitAdapter<'a, 'e, B> {
349    type Break = B;
350
351    fn node<T: Node + ?Sized>(&mut self, node: &T) -> ControlFlow<Self::Break> {
352        if matches!(node.kind(), NodeKind::Call) {
353            node.accept(&mut CallAdapter {
354                parent: self,
355                seen_arg0: false,
356            })
357        } else {
358            node.accept(self)
359        }
360    }
361
362    fn token(
363        &mut self,
364        token: Token,
365        span: source::Span,
366        origin: Option<origin::Id>,
367    ) -> ControlFlow<Self::Break> {
368        self.emit_token(token, span, origin, Context::None)
369    }
370}
371
372/// Compilation mode
373#[non_exhaustive]
374#[derive(Clone, PartialEq, Eq)]
375pub enum Mode<'a> {
376    /// Compile as script: return value is value of final statement or early return
377    Script,
378    /// Compile as module: return value is a module of top-level bindings, or that of early return
379    Module { name: &'a str },
380    /// Compile in REPL mode:
381    /// - Return value is a module of top-level bindings, including private bindings (e.g. imports)
382    /// - Early return is disallowed at top level
383    /// - Value of final statement is bound to `_`
384    Repl,
385}
386
387#[derive(Debug)]
388pub(crate) struct PreludeItem {
389    item: String,
390    bind: String,
391    res: Option<Res>,
392}
393
394#[derive(Debug)]
395pub(crate) enum PreludeImport {
396    Items {
397        module: String,
398        items: Vec<PreludeItem>,
399    },
400    ModuleAsIs {
401        module: String,
402        bind: String,
403        res: Option<Res>,
404        insert: bool,
405    },
406    ModuleRenamed {
407        module: String,
408        bind: String,
409        res: Option<Res>,
410    },
411}
412
413/// Prelude configurer
414pub struct Prelude<'a, 'b> {
415    compiler: &'b mut Compiler<'a>,
416}
417
418impl<'a, 'b> Prelude<'a, 'b> {
419    fn module_name_first(module: &str) -> &str {
420        if let Some((first, _)) = module.split_once(".") {
421            first
422        } else {
423            module
424        }
425    }
426
427    /// Clear the prelude, including any default imports
428    pub fn clear(self) -> Self {
429        self.compiler.prelude.clear();
430        self
431    }
432
433    /// Imports the named module, as by:
434    /// ```do
435    /// import module
436    /// ```
437    pub fn import_module(self, module: impl Into<String>) -> Self {
438        let module = module.into();
439        let bind = Self::module_name_first(&module).to_owned();
440
441        self.compiler.prelude.push(PreludeImport::ModuleAsIs {
442            module,
443            bind,
444            res: None,
445            insert: false,
446        });
447        self
448    }
449
450    /// Imports the named module under a different name, as by:
451    /// ```do
452    /// import module: name
453    /// ```
454    pub fn import_module_with_name(
455        self,
456        module: impl Into<String>,
457        name: impl Into<String>,
458    ) -> Self {
459        self.compiler.prelude.push(PreludeImport::ModuleRenamed {
460            module: module.into(),
461            bind: name.into(),
462            res: None,
463        });
464        self
465    }
466
467    /// Import items from a module.  Returns a builder object to configure individual items.
468    pub fn import_items(self, module: impl Into<String>) -> Items<'a, 'b> {
469        self.compiler.prelude.push(PreludeImport::Items {
470            module: module.into(),
471            items: Vec::new(),
472        });
473        Items {
474            compiler: self.compiler,
475        }
476    }
477}
478
479/// Item import builder.
480#[must_use]
481pub struct Items<'a, 'b> {
482    compiler: &'b mut Compiler<'a>,
483}
484
485impl<'a, 'b> Items<'a, 'b> {
486    /// Imports the given item, as by:
487    /// ```do
488    /// import module:
489    ///   - item
490    /// ```
491    pub fn item(self, item: impl Into<String>) -> Self {
492        let item = item.into();
493        match self.compiler.prelude.last_mut().unwrap() {
494            PreludeImport::Items { items, .. } => items.push(PreludeItem {
495                item: item.clone(),
496                bind: item,
497                res: None,
498            }),
499            _ => unreachable!(),
500        };
501        self
502    }
503
504    /// Imports the given items, as by:
505    /// ```do
506    /// import module:
507    ///   - item
508    ///   - ...
509    /// ```
510    pub fn items(mut self, items: impl IntoIterator<Item = impl Into<String>>) -> Self {
511        for item in items.into_iter() {
512            self = self.item(item);
513        }
514        self
515    }
516
517    /// Imports the given item under a different name, as by:
518    /// ```do
519    /// import module:
520    ///   item: name
521    /// ```
522    pub fn item_with_name(self, item: impl Into<String>, name: impl Into<String>) -> Self {
523        match self.compiler.prelude.last_mut().unwrap() {
524            PreludeImport::Items { items, .. } => items.push(PreludeItem {
525                item: item.into(),
526                bind: name.into(),
527                res: None,
528            }),
529            _ => unreachable!(),
530        };
531        self
532    }
533
534    /// Finish item imports.
535    ///
536    /// Calling this method may be necessary to ensure changes take effect.
537    pub fn commit(self) -> Prelude<'a, 'b> {
538        Prelude {
539            compiler: self.compiler,
540        }
541    }
542}
543
544/// Compiles Do source to bytecode.
545pub struct Compiler<'a> {
546    file: File<'a>,
547    origintab: origin::Table,
548    symtab: sym::Table,
549    bintab: BinTable,
550    consttab: constant::Table,
551    packtab: sig::PackTable,
552    unpacktab: sig::UnpackTable,
553    mode: Mode<'a>,
554    prelude: Vec<PreludeImport>,
555}
556
557impl<'a> Compiler<'a> {
558    /// Create new compiler instance
559    ///
560    /// # Arguments
561    /// - `path`: The path of the source file; used in backtraces
562    /// - `content`: The source as a byte slice
563    pub fn new(path: &'a Path, content: &'a [u8]) -> Self {
564        let mut this = Self {
565            file: File::new(path, content),
566            origintab: Default::default(),
567            symtab: sym::Table::new(),
568            bintab: BinTable::new(),
569            consttab: constant::Table::new(),
570            packtab: sig::PackTable::new(),
571            unpacktab: sig::UnpackTable::new(),
572            mode: Mode::Script,
573            prelude: Default::default(),
574        };
575        this.prelude()
576            .import_module("std")
577            .import_module("strand")
578            .import_items("std")
579            .items(STD_PRELUDE.iter().copied())
580            .commit();
581        this
582    }
583
584    /// Change compilation mode
585    ///
586    /// Default: [`Mode::Script`]
587    pub fn mode(&mut self, mode: Mode<'a>) -> &mut Self {
588        self.mode = mode;
589        self
590    }
591
592    /// Configure a prelude, a collection of standard imports which are injected into the code.
593    ///
594    /// Note that prelude imports which are not referenced by the code are omitted from compilation, even
595    /// if importing them would have side effects.
596    pub fn prelude(&mut self) -> Prelude<'a, '_> {
597        Prelude { compiler: self }
598    }
599
600    fn feed_comments<B>(
601        &self,
602        tokens: &mut dyn EmitToken<Break = B>,
603        comments: impl IntoIterator<Item = source::Span>,
604    ) -> Result<(), Error<B>> {
605        for comment in comments.into_iter() {
606            let content = self.file.str(comment);
607            let slice = content.trim_end();
608            if let ControlFlow::Break(b) = tokens.emit(
609                Token::Comment,
610                convert_span(
611                    &self.file,
612                    source::Span {
613                        start: comment.start,
614                        end: comment.start + slice.len() as u32,
615                    },
616                ),
617                None,
618                ast::Context::None,
619            ) {
620                return Err(Error(ErrorInfo::Break(b)));
621            }
622        }
623        Ok(())
624    }
625
626    fn run<D: EmitDiag>(
627        mut self,
628        diags: &mut D,
629        tokens: Option<&mut dyn EmitToken<Break = D::Break>>,
630        write: Option<&mut dyn Write>,
631        ignore_errors: bool,
632    ) -> Result<(), Error<D::Break>> {
633        let mut prelude = mem::take(&mut self.prelude);
634        let mut ds = Diags::new();
635        let mut comments = vec![];
636        let comment = if tokens.is_some() {
637            Some((&mut |span| comments.push(span)) as &mut dyn Comment)
638        } else {
639            None
640        };
641        let mut parser = self.parser(&ds, comment);
642        let ast = parser.parse(ignore_errors);
643        self.drain_diags(&mut ds, diags)?;
644        let mut ast = ast?;
645        #[cfg(feature = "debug")]
646        if let Err(e) = self.export_ast_dot(&ast, false) {
647            eprintln!("AST DOT export failed: {e}")
648        }
649        let mut elab = self.elaborater(&ds);
650        let res = elab.elaborate(&mut ast, &mut prelude, ignore_errors);
651        #[cfg(feature = "debug")]
652        if let Err(e) = self.export_ast_dot(&ast, true) {
653            eprintln!("Resolved AST DOT export failed: {e}")
654        }
655        self.drain_diags(&mut ds, diags)?;
656        res?;
657        self.prelude = prelude;
658        if let Some(tokens) = tokens {
659            if let ControlFlow::Break(e) = ast.accept(&mut VisitAdapter {
660                file: &self.file,
661                origintab: &self.origintab,
662                emit: tokens,
663            }) {
664                return Err(Error(ErrorInfo::Break(e)));
665            }
666            self.feed_comments(tokens, comments)?;
667        }
668        if let Some(write) = write {
669            let mut lowerer = self.lowerer();
670            let graph = lowerer.run(&ast)?;
671            #[cfg(feature = "debug")]
672            {
673                // Export DOT file if environment variable is set
674                if let Ok(output) = std::env::var("DO_EXPORT_DOT")
675                    && let Err(e) = self.export_cfg_dot(&graph, output)
676                {
677                    eprintln!("dot export failed: {e}");
678                }
679            }
680            let mut emitter = self.emitter(&graph);
681            Ok(emitter.emit(write)?)
682        } else {
683            Ok(())
684        }
685    }
686
687    /// Analyze source code, generating diagnostics and semantic tokens.
688    /// Ignores errors to the greatest extent possible, other than a request
689    /// by either emitter to stop analysis.  The two emitters must share a common
690    /// `Break` type.
691    ///
692    /// The precise order of emitted tokens is not specified, but will be approximately in
693    /// textual order.  The precise interleaving of diagnostics and tokens is not specified.
694    ///
695    /// # Arguments
696    /// - `diags`: Where to send generated diagnostics.
697    /// - `tokens`: Where to send semantic tokens.
698    ///
699    /// # Errors
700    /// - [`ErrorKind::Fail`]: Compilation failed due to at least one fatal error.  This should
701    ///   nearly never occur unless the source code is too severely malformed to process.
702    /// - [`ErrorKind::Break`]: Compilation was stopped by the diagnostic emitter.
703    pub fn analyze<D: EmitDiag, T: EmitToken<Break = D::Break>>(
704        self,
705        diags: &mut D,
706        tokens: &mut T,
707    ) -> Result<(), Error<D::Break>> {
708        self.run(diags, Some(tokens), None, true)
709    }
710
711    /// Compile the source
712    ///
713    /// # Arguments
714    /// - `write`: Where to write bytecode.
715    /// - `diags`: Where to send generated diagnostics.
716    ///
717    /// # Errors
718    /// - [`ErrorKind::Fail`]: Compilation failed due to at least one fatal error.
719    /// - [`ErrorKind::Break`]: Compilation was stopped by the diagnostic emitter.
720    /// - [`ErrorKind::Io`]: Writing bytecode failed with an [`io::Error`].
721    pub fn compile<E: EmitDiag>(
722        self,
723        write: &mut impl Write,
724        diags: &mut E,
725    ) -> Result<(), Error<E::Break>> {
726        self.run(diags, None, Some(write), false)
727    }
728
729    fn drain_diags<E: EmitDiag>(
730        &self,
731        diags: &mut Diags,
732        emit: &mut E,
733    ) -> Result<(), Error<E::Break>> {
734        for diag in diags.drain() {
735            if let ControlFlow::Break(b) = emit.emit(diag.resolve(self)) {
736                return Err(Error(ErrorInfo::Break(b)));
737            }
738        }
739        Ok(())
740    }
741
742    fn parser<'b>(
743        &'b mut self,
744        diags: &'b Diags,
745        comment: Option<&'b mut dyn Comment>,
746    ) -> Parser<'b> {
747        Parser::new(Lexer::new(&self.file, diags, comment), &self.file, diags)
748    }
749
750    fn elaborater<'b>(&'b mut self, diags: &'b Diags) -> Elaborater<'b> {
751        Elaborater::new(
752            self.mode.clone(),
753            &self.file,
754            &mut self.bintab,
755            &mut self.symtab,
756            &mut self.origintab,
757            diags,
758        )
759    }
760
761    fn lowerer(&mut self) -> Lowerer<'_> {
762        Lowerer {
763            mode: self.mode.clone(),
764            file: &self.file,
765            symtab: &mut self.symtab,
766            bintab: &mut self.bintab,
767            consttab: &mut self.consttab,
768            packtab: &mut self.packtab,
769            unpacktab: &mut self.unpacktab,
770            origintab: &self.origintab,
771            prelude: &self.prelude,
772            sentinel_const: None,
773        }
774    }
775
776    fn emitter<'b>(&'a self, graph: &'b cfg::Graph) -> Emitter<'b>
777    where
778        'a: 'b,
779    {
780        Emitter {
781            file: &self.file,
782            graph,
783            bintab: &self.bintab,
784            symtab: &self.symtab,
785            consttab: &self.consttab,
786            packtab: &self.packtab,
787            unpacktab: &self.unpacktab,
788            debugbintab: Default::default(),
789            mode: self.mode.clone(),
790        }
791    }
792}
793
794#[cfg(feature = "debug")]
795impl Compiler<'_> {
796    /// Generate a graphviz DOT representation of an AST
797    fn ast_to_dot<N: Node + ?Sized>(&self, ast: &N, writer: &mut impl Write) -> io::Result<()> {
798        use crate::ast::{dot::DotVisitor, visit::Visit};
799        use dot_writer::DotWriter;
800
801        let mut writer = DotWriter::from(writer);
802        writer.set_pretty_print(true);
803        let mut digraph = writer.digraph();
804        let mut visitor = DotVisitor::new(&mut digraph, self);
805        match visitor.node(ast) {
806            ControlFlow::Continue(()) => Ok(()),
807            ControlFlow::Break(e) => Err(e),
808        }
809    }
810
811    /// Export AST to a DOT file based on the DO_EXPORT_DOT environment variable
812    /// Similar to the CFG export functionality
813    fn export_ast_dot<N: Node + ?Sized>(&self, ast: &N, res: bool) -> io::Result<()> {
814        use std::{
815            fs,
816            path::{self, Component},
817        };
818
819        if let Ok(output) = std::env::var("DO_EXPORT_DOT") {
820            let src = path::absolute(self.file.path())?;
821            let cwd = std::env::current_dir()?;
822            let rel = src.strip_prefix(&cwd).unwrap_or(&src);
823            let comps: Vec<_> = rel
824                .components()
825                .filter_map(|c| {
826                    if let Component::Normal(c) = c {
827                        Some(c.to_string_lossy())
828                    } else {
829                        None
830                    }
831                })
832                .collect();
833            let name = comps.join("_");
834            fs::create_dir_all(&output)?;
835            let out = std::path::Path::new(&output)
836                .join(&name)
837                .with_extension(if res { "res.dot" } else { "ast.dot" });
838            let mut file = fs::File::create(&out)?;
839
840            self.ast_to_dot(ast, &mut file)?;
841            eprintln!("AST DOT exported to: {}", out.display());
842        }
843
844        Ok(())
845    }
846
847    fn export_cfg_dot(&mut self, graph: &cfg::Graph, output: String) -> Result<(), io::Error> {
848        use std::{
849            fs,
850            path::{self, Component},
851        };
852        let src = path::absolute(self.file.path())?;
853        let cwd = std::env::current_dir()?;
854        let rel = src.strip_prefix(cwd).unwrap_or(&src);
855        let comps: Vec<_> = rel
856            .components()
857            .filter_map(|c| {
858                if let Component::Normal(c) = c {
859                    Some(c.to_string_lossy())
860                } else {
861                    None
862                }
863            })
864            .collect();
865        let name = comps.join("_");
866        fs::create_dir_all(&output)?;
867        let out = Path::new(&output).join(&name).with_extension("cfg.dot");
868        let mut file = fs::File::create(&out)?;
869        graph.dot(self, &mut file)
870    }
871}