pytv/
convert.rs

1use crate::Config;
2use crate::FileOptions;
3use std::error::Error;
4use std::io::{Result as IoResult, Write};
5use std::path;
6use std::result::Result;
7
8/// Represents a converter that converts PyTV script to Python script to generate Verilog.
9///
10/// It is also possible to run the Python script after conversion and optionally delete it after running it.
11/// It contains methods for converting code and managing input/output files.
12#[derive(Debug, Default)]
13pub struct Convert {
14    config: Config,
15    file_options: FileOptions,
16    vars: Option<Vec<(String, String)>>,
17    preamble_py: Option<String>,
18}
19
20#[derive(Debug, PartialEq)]
21enum LineType {
22    Verilog,
23    PythonInline,
24    PythonBlock(bool), // 'false' if in first line ('/*!'), 'true' otherwise
25    None,
26}
27
28impl Default for LineType {
29    fn default() -> Self {
30        Self::None
31    }
32}
33
34impl Convert {
35    /// Creates a new `Convert` instance with the given configuration and file options.
36    pub fn new(
37        config: Config,
38        file_options: FileOptions,
39        vars: Option<Vec<(String, String)>>,
40        preamble_py: Option<String>,
41    ) -> Convert {
42        Convert {
43            config,
44            file_options,
45            vars,
46            preamble_py,
47        }
48    }
49
50    /// Creates a new `Convert` instance by parsing command line arguments.
51    pub fn from_args() -> Convert {
52        let (config, file_options, vars, preamble_py) = Config::from_args();
53        Convert::new(config, file_options, vars, preamble_py)
54    }
55
56    /// Opens the input file and reads its contents as a string.
57    fn open_input(&self) -> IoResult<String> {
58        std::fs::read_to_string(&self.file_options.input)
59    }
60
61    /// Opens the output Python file and returns a file handle.
62    ///
63    /// Note: This will overwrite the existing file.
64    pub fn open_output(&self) -> IoResult<std::fs::File> {
65        std::fs::File::create(&self.output_python_file_name())
66    }
67
68    /// Generates the output file name based on the input file name and configuration.
69    fn output_file_name(&self) -> String {
70        self.file_options
71            .output
72            .clone()
73            .unwrap_or_else(|| {
74                if self.file_options.output.is_some() {
75                    return self.file_options.input.clone();
76                }
77                let mut output = self.file_options.input.clone();
78                // change extension to .v if there is extension
79                let ext = path::Path::new(&output).extension().unwrap_or_default();
80                if ext.is_empty() {
81                    output.push_str(".v");
82                } else {
83                    output = output.replace(ext.to_str().unwrap(), "v");
84                }
85                output
86            })
87            .replace("\\", "/")
88    }
89
90    /// Generates the output Python file name based on the input file name and configuration.
91    fn output_python_file_name(&self) -> String {
92        self.output_file_name() + ".py"
93    }
94
95    /// Generates the output instantiation file name based on the input file name and configuration.
96    fn output_inst_file_name(&self) -> String {
97        self.output_file_name() + ".inst"
98    }
99
100    fn switch_line_type(&self, line_type: &mut LineType, line: &str) {
101        let trimmed_line = line.trim_start();
102        *line_type = match line_type {
103            LineType::PythonBlock(_not_first_line) => {
104                if trimmed_line == "*/" {
105                    LineType::None // end of PythonBlock does nothing
106                } else {
107                    LineType::PythonBlock(true)
108                }
109            }
110            _ => {
111                if trimmed_line.starts_with(&format!("/*{}", self.config.magic_comment_str)) {
112                    LineType::PythonBlock(false)
113                } else if trimmed_line.starts_with(&format!("//{}", self.config.magic_comment_str))
114                {
115                    LineType::PythonInline
116                } else {
117                    LineType::Verilog
118                }
119            }
120        }
121    }
122
123    /// Pre-processes a line of code by trimming trailing whitespace and replacing tabs with spaces.
124    fn pre_process_line(&self, line: &str) -> String {
125        line.trim_end().replace(
126            "\t",
127            str::repeat(" ", self.config.tab_size as usize).as_str(),
128        )
129    }
130
131    /// Escapes special characters in a line of Verilog code.
132    fn escape_verilog(&self, line: &str) -> String {
133        let mut escaped_line = String::with_capacity(line.len());
134        for c in line.chars() {
135            match c {
136                '\'' => escaped_line.push_str("\\'"), // we use single quote for print
137                '{' => escaped_line.push_str("{{"),
138                '}' => escaped_line.push_str("}}"),
139                _ => escaped_line.push(c),
140            }
141        }
142        escaped_line
143    }
144
145    /// Applies a regular expression to a line of code based on the template regex in the configuration.
146    fn apply_verilog_regex(&self, line: &str) -> String {
147        self.config
148            .template_re
149            .replace_all(line, format!("{{$1}}").as_str())
150            .to_string()
151    }
152
153    pub(crate) fn apply_protected_verilog_regex(&self, line: &str) -> String {
154        self.config
155            .template_re
156            .replace_all(
157                line,
158                format!("__LEFT_BRACKET__{{$1}}__RIGHT_BRACKET__").as_str(),
159            )
160            .to_string()
161    }
162
163    /// Runs the Python code to generate verilog.
164    ///
165    /// The command `python3` (`python` for Windows) should be available to call.
166    pub fn run_python(&self) -> IoResult<()> {
167        let py_file = self.output_python_file_name();
168        let v_file = self.output_file_name();
169        let v_file_f = std::fs::File::create(&v_file)?;
170        #[cfg(not(target_family = "windows"))]
171        let python_cmd = "python3";
172        #[cfg(target_family = "windows")]
173        let python_cmd = "python";
174        let output = std::process::Command::new(python_cmd)
175            .arg(&py_file)
176            .stdout(v_file_f)
177            .output()?;
178        if !output.status.success() {
179            return Err(std::io::Error::new(
180                std::io::ErrorKind::Other,
181                format!(
182                    "Python script failed with exit code: {}\n{}",
183                    output.status.code().unwrap_or(-1),
184                    String::from_utf8_lossy(&output.stderr)
185                ),
186            ));
187        } else {
188            if self.config.delete_python {
189                std::fs::remove_file(&py_file)?;
190            }
191        }
192        Ok(())
193    }
194
195    #[cfg(not(feature = "inst"))]
196    fn process_python_line<W: Write>(
197        &self,
198        line: &str,
199        py_indent_prior: usize,
200        stream: &mut W,
201    ) -> Result<()> {
202        writeln!(stream, "{}", utf8_slice::from(&line, py_indent_prior))
203    }
204
205    #[cfg(feature = "macro")]
206    fn print_macros<W: Write>(&self, stream: &mut W) -> Result<(), Box<dyn Error>> {
207        let output_verilog_file_name = &self.output_file_name();
208        let output_inst_file_name = &self.output_inst_file_name();
209        let verilog_path = path::Path::new(output_verilog_file_name);
210        let inst_path = path::Path::new(output_inst_file_name);
211        writeln!(
212            stream,
213            concat!(
214                "# PyTV macros:\n",
215                "OUTPUT_VERILOG_FILE_PATH = '{}'\n",
216                "OUTPUT_VERILOG_FILE_NAME = '{}'\n",
217                "OUTPUT_VERILOG_FILE_STEM = '{}'\n",
218                "OUTPUT_INST_FILE_PATH = '{}'\n",
219                "OUTPUT_INST_FILE_NAME = '{}'\n\n",
220            ),
221            output_verilog_file_name,
222            verilog_path.file_name().unwrap().to_str().unwrap(),
223            verilog_path.file_stem().unwrap().to_str().unwrap(),
224            output_inst_file_name,
225            inst_path.file_name().unwrap().to_str().unwrap(),
226        )?;
227        Ok(())
228    }
229
230    fn update_py_indent_space(&self, line: &str, py_indent_space: usize) -> usize {
231        if !line.is_empty() {
232            let re = regex::Regex::new(r":\s*(#|$)").unwrap();
233            line.chars().position(|c| !c.is_whitespace()).unwrap_or(0)
234                + if re.is_match(line) {
235                    self.config.tab_size as usize
236                } else {
237                    0usize
238                }
239        } else {
240            py_indent_space
241        }
242    }
243
244    /// Converts the code and writes the converted code to the given stream.
245    pub fn convert<W: Write>(&self, mut stream: W) -> Result<(), Box<dyn Error>> {
246        let mut first_py_line = false;
247        let mut py_indent_prior = 0usize;
248        let mut py_indent_space = 0usize;
249        let magic_string_len = 2 + self.config.magic_comment_str.len();
250        #[cfg(feature = "inst")]
251        let mut within_inst = false;
252        #[cfg(feature = "inst")]
253        let mut inst_indent_space = 0usize;
254        let mut inst_str = String::new();
255        #[cfg(feature = "inst")]
256        // print user-defined variables
257        if let Some(vars) = &self.vars {
258            if !vars.is_empty() {
259                writeln!(stream, "# User-defined variables:")?;
260                for (name, value) in vars {
261                    writeln!(stream, "{} = {}", name, value)?;
262                }
263                writeln!(stream)?;
264            }
265        }
266        // load preamble
267        if let Some(preamble_py) = &self.preamble_py {
268            // read from file and write to stream
269            let preamble_py = std::fs::read_to_string(preamble_py)?;
270            writeln!(stream, "# Preamble:\n{}", preamble_py)?;
271        }
272        writeln!(
273            stream,
274            concat!(
275                "# PyTV utility functions:\n",
276                "_inst_file = open('{}', 'w')\n",
277                "def _inst_var_map(tuples):\n",
278                "    s = ['%s: %s\\n' % tuple for tuple in tuples]\n",
279                "    return '    '.join(s)\n\n",
280                "def _verilog_ports_var_map(tuples, first_port):\n",
281                "    s = ['  .%s(%s)' % tuple for tuple in tuples]\n",
282                "    return ('' if first_port else ',\\n') + ',\\n'.join(s)\n\n",
283                "def _verilog_vparams_var_map(tuples, first_vparam):\n",
284                "    s = ['\\n  .%s(%s)' % tuple for tuple in tuples]\n",
285                "    return ('#(' if first_vparam else ',') + ','.join(s)\n",
286            ),
287            self.output_inst_file_name()
288        )?;
289        #[cfg(feature = "macro")]
290        self.print_macros(&mut stream)?;
291        let mut line_type = LineType::default();
292        // parse line by line
293        for line in self.open_input()?.lines() {
294            let line = self.pre_process_line(&line);
295            self.switch_line_type(&mut line_type, line.as_str());
296            match line_type {
297                LineType::PythonBlock(true) => {
298                    py_indent_space = self.update_py_indent_space(&line, py_indent_space);
299                    #[cfg(feature = "inst")]
300                    self.process_python_line(
301                        &line,
302                        0,
303                        &mut stream,
304                        &mut within_inst,
305                        &mut inst_str,
306                        &mut inst_indent_space,
307                    )?;
308                    #[cfg(not(feature = "inst"))]
309                    self.process_python_line(&line, 0, &mut stream)?;
310                }
311                LineType::PythonInline => {
312                    let line = utf8_slice::from(line.trim_start(), magic_string_len);
313                    if !first_py_line && !line.is_empty() {
314                        first_py_line = true;
315                        py_indent_prior =
316                            line.chars().position(|c| !c.is_whitespace()).unwrap_or(0);
317                    }
318                    if !utf8_slice::till(&line, py_indent_prior).trim().is_empty() {
319                        Err(format!(
320                            "Python line should start with {} spaces.\nUnexpected line: {}",
321                            py_indent_prior, &line
322                        ))?;
323                    }
324                    py_indent_space =
325                        self.update_py_indent_space(&line, py_indent_space) - py_indent_prior;
326                    #[cfg(feature = "inst")]
327                    self.process_python_line(
328                        &line,
329                        py_indent_prior,
330                        &mut stream,
331                        &mut within_inst,
332                        &mut inst_str,
333                        &mut inst_indent_space,
334                    )?;
335                    #[cfg(not(feature = "inst"))]
336                    self.process_python_line(&line, py_indent_prior, &mut stream)?;
337                }
338                LineType::Verilog => {
339                    let line = self.apply_verilog_regex(self.escape_verilog(&line).as_str());
340                    writeln!(stream, "{}print(f'{line}')", " ".repeat(py_indent_space))?;
341                }
342                _ => {}
343            }
344        }
345        #[cfg(feature = "inst")]
346        writeln!(stream, "_inst_file.close()")?;
347        Ok(())
348    }
349
350    /// Converts the code and writes the converted code to a file.
351    ///
352    /// With default `Config`, the output will be a Python file.
353    pub fn convert_to_file(&self) -> Result<(), Box<dyn Error>> {
354        let out_f = self.open_output()?;
355        self.convert(out_f)?;
356        if self.config.run_python {
357            self.run_python()?;
358        }
359        Ok(())
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366
367    #[test]
368    fn test_pre_process_line() {
369        let convert = Convert::default();
370        dbg!(convert.config.tab_size);
371        assert_eq!(convert.pre_process_line("hello\they"), "hello    hey");
372    }
373
374    #[test]
375    fn test_escape_verilog() {
376        let convert = Convert::default();
377        assert_eq!(convert.escape_verilog("hello'world"), "hello\\'world");
378        assert_eq!(convert.escape_verilog("string {foo}"), "string {{foo}}");
379        assert_eq!(
380            convert.escape_verilog("string {{bar}}"),
381            "string {{{{bar}}}}"
382        );
383        assert_eq!(convert.escape_verilog("\"em"), "\"em");
384    }
385
386    #[test]
387    fn test_apply_verilog_regex() {
388        let convert = Convert::default();
389        assert_eq!(
390            convert.apply_verilog_regex("hello `world`"),
391            "hello {world}"
392        );
393        assert_eq!(
394            convert.apply_verilog_regex("hello `world` `bar`"),
395            "hello {world} {bar}"
396        );
397        assert_eq!(
398            convert.apply_verilog_regex("`timescale 1ns / 1ps"),
399            "`timescale 1ns / 1ps"
400        );
401    }
402
403    #[test]
404    fn test_switch_line_type() {
405        let mut line_type = LineType::default();
406        let convert = Convert::default();
407        convert.switch_line_type(&mut line_type, "assign a = b;");
408        assert_eq!(line_type, LineType::Verilog);
409        convert.switch_line_type(&mut line_type, "//! num = 2 ** n;");
410        assert_eq!(line_type, LineType::PythonInline);
411        convert.switch_line_type(&mut line_type, "   //! num = num + 1;");
412        assert_eq!(line_type, LineType::PythonInline);
413        convert.switch_line_type(&mut line_type, "/*!");
414        assert_eq!(line_type, LineType::PythonBlock(false));
415        convert.switch_line_type(&mut line_type, "num = 2 ** n;");
416        assert_eq!(line_type, LineType::PythonBlock(true));
417        convert.switch_line_type(&mut line_type, "*/");
418        assert_eq!(line_type, LineType::None);
419        convert.switch_line_type(&mut line_type, "// Verilog comment");
420        assert_eq!(line_type, LineType::Verilog);
421    }
422}