Skip to main content

ppx_impl/
lib.rs

1use std::borrow::Cow;
2use std::path::Path;
3// use std::path::{Path, PathBuf};
4
5use concat_string::concat_string;
6use eval::eval;
7use itertools::Itertools;
8use thiserror::Error;
9
10#[cfg(feature = "vfs")]
11use vfs::VfsPath;
12
13#[cfg(not(feature = "vfs"))]
14type FeatPath = std::path::Path;
15#[cfg(feature = "vfs")]
16type FeatPath = vfs::VfsPath;
17
18fn read_to_string(input_file: &FeatPath) -> Result<String> {
19    #[cfg(not(feature = "vfs"))] {
20        return read_to_string_std(input_file);
21    }
22    #[cfg(feature = "vfs")] {
23        let mut result = String::new();
24        input_file.open_file()?.read_to_string(&mut result)
25            .map_err(|err| Error::IOError(err, input_file.as_str().into()))?;
26        return Ok(result);
27    }
28}
29
30fn read_to_string_std(input_file: &Path) -> Result<String> {
31    return std::fs::read_to_string(input_file)
32        .map_err(|err| Error::IOError(err, input_file.to_path_buf()));
33}
34
35#[derive(Error, Debug)]
36pub enum Error {
37    #[error("Invalid macro `{}` on line {}", .0, .1)]
38    InvalidMacro(String, usize),
39    #[error("Found extra parameters in #param macro on line {}", .0)]
40    ExtraParamsInMacro(usize, &'static str),
41    #[error("Not enough parameters passed to template file")]
42    NotEnoughParameters,
43    #[error("Unused parameters passed to template file")]
44    UnusedParameters,
45    #[error("Not enough parameters passed to function-like macro `{}`", .0)]
46    NotEnoughParametersMacro(String),
47    #[error("Too many parameters passed to function-like macro `{}`", .0)]
48    UnusedParametersMacro(String),
49    #[error("Invalid parameter name {} on line {}", .0, .1)]
50    InvalidParameterName(String, usize),
51    #[error("First parameter of #include should be a string on line {}", .0)]
52    FirstParamOfIncludeNotString(usize),
53    #[error("Invalid pragma '{}'", .0)]
54    InvalidPragma(String),
55    #[error("IOError while reading {}: {}", .1.display(), .0)]
56    IOError(std::io::Error, std::path::PathBuf),
57    #[error("Couldn't evaluate `#if` expression condition: {}", .0)]
58    ConditionEvaluationError(#[from] eval::Error),
59    #[error("`#if` condition doesn't evaluate to bool '{}'", .0)]
60    NonBooleanConditionResult(eval::Value),
61    #[error("Elif specified after else")]
62    ElifAfterElse,
63    #[cfg(feature = "vfs")]
64    #[error("VfsError: {}", .0)]
65    VfsError(#[from] vfs::VfsError),
66}
67
68type Result<T> = std::result::Result<T, Error>;
69
70/// Parses a file using the templating engine.
71///
72/// For an example, see [parse_string].
73///
74/// # Parameters
75/// - `input_file`: the file that is read
76/// - `base_dir`: all includes are resolved relative to this directory
77/// - `parameters`: if `input_file` contains any parameter macros, pass an iterator
78///                 to them here. Otherwise pass `std::iter::empty()`.
79pub fn parse<'a>(
80    input_file: impl AsRef<Path>,
81    base_dir: impl AsRef<Path>,
82    parameters: impl Iterator<Item = &'a str>,
83) -> Result<String> {
84    return parse_cow(input_file, base_dir, parameters);
85}
86
87/// Parse a file in a virtual filesystem using the templating engine.
88///
89/// # Examples
90///
91/// ```rust
92/// # use ppx_impl::parse_vfs;
93/// # use vfs::VfsPath;
94/// let root: VfsPath = vfs::MemoryFS::new().into();
95/// let path = root.join("test.txt").unwrap();
96///
97/// path.create_file().unwrap().write_all(b"#param A\nHello A!");
98/// let result = parse_vfs(path, root, ["world"].into_iter()).unwrap();
99/// assert_eq!(result, "Hello world!");
100/// ```
101#[cfg(feature = "vfs")]
102pub fn parse_vfs<'a>(
103    input_file: impl Into<VfsPath>,
104    base_dir: impl Into<VfsPath>,
105    parameters: impl Iterator<Item = &'a str>
106) -> Result<String> {
107    return parse_vfs_cow(input_file.into(), base_dir.into(), parameters);
108}
109
110/// Same as [parse], but the parameters iterator has an item of `impl Into<Cow<str>>`.
111/// This is so that an empty iterator can be passed to `parse`.
112pub fn parse_cow<'a, Iter, C>(
113    input_file: impl AsRef<Path>,
114    base_dir: impl AsRef<Path>,
115    parameters: Iter
116) -> Result<String>
117    where
118        Iter: Iterator<Item = C>,
119        C: Into<Cow<'a, str>>
120{
121    let content = read_to_string_std(input_file.as_ref())?;
122
123    return parse_string_cow(&content, base_dir, parameters);
124}
125
126#[cfg(feature = "vfs")]
127pub fn parse_vfs_cow<'a, Iter, C>(
128    input_file: impl Into<VfsPath>,
129    base_dir: impl Into<VfsPath>,
130    parameters: Iter
131) -> Result<String>
132    where
133        Iter: Iterator<Item = C>,
134        C: Into<Cow<'a, str>>
135{
136    let content = read_to_string(&input_file.into())?;
137
138    return parse_string_vfs_cow(&content, base_dir, parameters);
139}
140
141/// Parses a file using the templating engine.
142///
143/// # Parameters
144/// - `input`: the contents to process
145/// - `base_dir`: all includes are resolved relative to this directory
146/// - `parameters`: if `input` contains any parameter macros, pass an iterator
147///                 to them here. Otherwise pass `std::iter::empty()`.
148///
149/// # Example
150///
151/// ```rust
152/// # use ppx_impl::*;
153/// # #[cfg(not(feature = "vfs"))]
154/// let res = parse_string(
155///     "#define A 4\nThe answer is A",
156///     std::env::current_dir().unwrap(),
157///     std::iter::empty()
158/// ).unwrap();
159/// # #[cfg(not(feature = "vfs"))]
160/// assert_eq!(res, "The answer is 4");
161/// ```
162pub fn parse_string<'a>(
163    input: &str,
164    base_dir: impl AsRef<Path>,
165    parameters: impl Iterator<Item = &'a str>
166) -> Result<String> {
167    parse_string_cow(input, base_dir, parameters)
168}
169
170#[cfg(feature = "vfs")]
171pub fn parse_string_vfs<'a>(
172    input: &str,
173    base_dir: impl Into<VfsPath>,
174    parameters: impl Iterator<Item = &'a str>
175) -> Result<String> {
176    parse_string_vfs_cow(input, base_dir, parameters)
177}
178
179/// Same as [parse_string], but the parameters iterator has an item of `impl Into<Cow<str>>`.
180/// This is so that an empty iterator can be passed to `parse_string`.
181pub fn parse_string_cow<'a, Iter, C>(
182    input: &str,
183    base_dir: impl AsRef<Path>,
184    parameters: Iter
185) -> Result<String>
186    where
187        Iter: Iterator<Item = C>,
188        C: Into<Cow<'a, str>>
189{
190    #[cfg(not(feature = "vfs"))]
191    let base_dir = base_dir.as_ref();
192    #[cfg(feature = "vfs")]
193    let base_dir = VfsPath::from(vfs::PhysicalFS::new(base_dir));
194    parse_string_cow_impl(input, &base_dir, &mut parameters.map(|v| v.into()))
195}
196
197#[cfg(feature = "vfs")]
198pub fn parse_string_vfs_cow<'a, Iter, C>(
199    input: &str,
200    base_dir: impl Into<VfsPath>,
201    parameters: Iter
202) -> Result<String>
203    where
204        Iter: Iterator<Item = C>,
205        C: Into<Cow<'a, str>>
206{
207    parse_string_cow_impl(input, &base_dir.into(), &mut parameters.map(|v| v.into()))
208}
209
210fn parse_string_cow_impl<'a>(
211    input: &str,
212    base_dir: &FeatPath,
213    parameters: &mut dyn Iterator<Item = Cow<'a, str>>
214) -> Result<String> {
215    let mut replacements: Vec<(String, Cow<str>)> = vec![];
216    let mut fn_replacements: Vec<(String, Vec<String>, String)> = vec![];
217    let mut visited_sources: Vec<String> = vec![];
218
219    parse_string_cow_rec(input, None, base_dir, parameters, &mut replacements, &mut fn_replacements, &mut visited_sources)
220        .map(|str| str.unwrap())
221}
222
223fn parse_string_cow_rec<'a>(
224    input: &str,
225    path: Option<&str>,
226    base_dir: &FeatPath,
227    parameters: &mut dyn Iterator<Item = Cow<'a, str>>,
228    replacements: &mut Vec<(String, Cow<'a, str>)>,
229    fn_replacements: &mut Vec<(String, Vec<String>, String)>,
230    visited_sources: &mut Vec<String>,
231) -> Result<Option<String>> {
232    let mut out = String::new();
233
234    let mut cur_fn_replacement: Option<(String, Vec<String>, String)> = None;
235    let mut if_condition: Vec<(bool, bool, bool)> = vec![];
236
237    let max_lines = input.chars()
238        .filter(|c| *c == '\n')
239        .count();
240
241    for (line_num, line) in input.lines().enumerate() {
242        let mut line_chars = line.chars().skip_while(char::is_ascii_whitespace);
243        let start_char = line_chars.by_ref().next();
244
245        let mut macro_name = None;
246
247        if let Some((include, _has_included, _is_end)) = if_condition.last() {
248            if !include {
249                if start_char == Some('#') {
250                    macro_name = Some(line_chars.by_ref().take_while(|c| c.is_ascii_alphanumeric()).collect::<String>());
251                    let macro_name = macro_name.as_ref().unwrap();
252                    if macro_name == "endif" {
253                        if_condition.pop();
254                        continue;
255                    } else if !(macro_name == "else" || macro_name == "elif") {
256                        continue;
257                    }
258                } else {
259                    continue;
260                }
261            }
262        }
263
264        if let Some(cur_fn_repl) = cur_fn_replacement {
265            if line.ends_with("\\") {
266                cur_fn_replacement = Some((cur_fn_repl.0, cur_fn_repl.1, cur_fn_repl.2 + &line[..line.len()-1]))
267            } else {
268                fn_replacements.push((cur_fn_repl.0, cur_fn_repl.1, cur_fn_repl.2 + line));
269                cur_fn_replacement = None;
270            }
271            continue;
272        }
273
274        match start_char {
275            Some('#') => {
276                if macro_name.is_none() {
277                    macro_name = Some(line_chars.by_ref().take_while(|c| c.is_ascii_alphanumeric()).collect::<String>());
278                }
279                match macro_name.as_ref().unwrap().as_str() {
280                    "define" => {
281                        let mut is_last_bracket = false;
282                        let name = line_chars.by_ref()
283                            .skip_while(char::is_ascii_whitespace)
284                            .take_while(|c| {
285                                if *c == '(' {
286                                    is_last_bracket = true;
287                                }
288                                !c.is_ascii_whitespace() && *c != '('
289                            })
290                            .collect::<String>();
291
292                        if is_last_bracket {
293                            let params = line_chars.by_ref()
294                                .take_while(|c| *c != ')')
295                                .chunk_by(|c| *c == ',');
296                            let params = params
297                                .into_iter()
298                                .filter(|(b, _)| !b)
299                                .map(|(_, i)| i
300                                    .skip_while(char::is_ascii_whitespace)
301                                    .take_while(|c| !c.is_ascii_whitespace())
302                                    .collect::<String>())
303                                .collect::<Vec<String>>();
304
305                            let check_param_name = params.iter().find(|param| !param.chars().all(|c| c.is_alphanumeric() || c == '_'))
306                                .or(params.iter().find(|param| param.len() == 0 || param.chars().next().unwrap().is_numeric()));
307                            if let Some(param_name) = check_param_name {
308                                return Err(Error::InvalidParameterName(param_name.clone(), line_num).into())
309                            }
310
311                            let replacement = line_chars.by_ref().collect::<String>();
312
313                            if replacement.ends_with("\\") {
314                                cur_fn_replacement = Some((name, params, replacement[..replacement.len()-1].to_string()));
315                            } else {
316                                fn_replacements.push((name, params, replacement));
317                            }
318                        } else {
319                            let replacement = line_chars.collect::<Cow<str>>();
320                            replacements.push((name, replacement))
321                        }
322                    }, "include" => {
323                        let path = line_chars.by_ref()
324                            .skip_while(char::is_ascii_whitespace)
325                            .take_while(|c| !c.is_ascii_whitespace())
326                            .collect::<String>();
327
328                        if !(path.starts_with('"') && path.ends_with('"')) {
329                            return Err(Error::FirstParamOfIncludeNotString(line_num).into());
330                        }
331
332                        let path = &path[1..path.len()-1];
333
334                        let params = line_chars.by_ref()
335                            .chunk_by(|c| *c == ',');
336                        let mut params = params
337                            .into_iter()
338                            .filter(|(b, _)| !b)
339                            .map(|(_, i)| Cow::Owned(i.collect::<String>()));
340
341                        #[cfg(not(feature = "vfs"))]
342                        let file_path = base_dir.join(path);
343
344                        #[cfg(feature = "vfs")]
345                        let file_path = base_dir.join(path)?;
346
347                        let content = read_to_string(&file_path)?;
348
349                        match parse_string_cow_rec(&content, Some(path), &base_dir, &mut params, replacements, fn_replacements, visited_sources) {
350                            Ok(Some(res)) => {
351                                out += res.as_str();
352                                visited_sources.push(path.to_string());
353                            },
354                            Ok(None) => {},
355                            Err(err) => return Err(err),
356                        };
357                    }, "param" => {
358                        let param_name = line_chars.by_ref()
359                            .skip_while(|c| c.is_ascii_whitespace())
360                            .take_while(|c| !c.is_ascii_whitespace())
361                            .collect::<String>();
362
363                        if !line_chars.by_ref().all(|c| c.is_ascii_whitespace()) {
364                            return Err(Error::ExtraParamsInMacro(line_num, "param"));
365                        }
366
367                        let Some(param_value) = parameters.next() else {
368                            return Err(Error::NotEnoughParameters);
369                        };
370
371                        replacements.push((param_name, param_value));
372                    }, "pragma" => {
373                        let param_name = line_chars.by_ref()
374                            .skip_while(|c| c.is_ascii_whitespace())
375                            .take_while(|c| !c.is_ascii_whitespace())
376                            .collect::<String>();
377
378                        if !line_chars.by_ref().all(|c| c.is_ascii_whitespace()) {
379                            return Err(Error::ExtraParamsInMacro(line_num, "pragma"));
380                        }
381
382                        if param_name != "once" {
383                            return Err(Error::InvalidPragma(param_name));
384                        }
385
386                        if let Some(path) = path {
387                            if visited_sources.iter().find(|p| p.as_str() == path).is_some() {
388                                return Ok(None);
389                            }
390                        }
391                    }, "if" => {
392                        let condition = line_chars.collect::<String>();
393                        let condition = fn_replace(replace(&condition, &replacements), &fn_replacements)?;
394                        let res = eval(&condition)?;
395
396                        let Some(res) = res.is_boolean().then(|| res.as_bool().unwrap())
397                            .or_else(|| res.is_string().then(|| res.as_str().unwrap() == "true"))
398                            .or_else(|| res.is_i64().then(|| res.as_i64().unwrap() == 1))
399                            .or_else(|| res.is_u64().then(|| res.as_u64().unwrap() == 1))
400                        else {
401                            return Err(Error::NonBooleanConditionResult(res));
402                        };
403
404                        if_condition.push((res, res, false));
405                    }, "elif" => {
406                        if if_condition.last().map(|r| r.2).unwrap_or(false) {
407                            return Err(Error::ElifAfterElse);
408                        }
409
410                        let condition = line_chars.collect::<String>();
411                        let condition = fn_replace(replace(&condition, &replacements), &fn_replacements)?;
412                        let res = eval(&condition)?;
413
414                        let Some(res) = res.as_bool() else {
415                            return Err(Error::NonBooleanConditionResult(res));
416                        };
417
418                        let last_idx = if_condition.len() - 1;
419                        if_condition[last_idx].0 = res;
420                        if_condition[last_idx].1 |= res;
421                    }, "else" => {
422                        let last_idx = if_condition.len() - 1;
423                        if_condition[last_idx].0 = !if_condition[last_idx].1;
424                        if_condition[last_idx].2 = true;
425                    }, "endif" => {
426                        if_condition.pop();
427                    },
428                    _ => return Err(Error::InvalidMacro(macro_name.unwrap(), line_num)),
429                }
430            },
431            Some('\\') if (line_chars.next() == Some('#')) => {
432                out += fn_replace(replace(&line.replacen("\\#", "#", 1), &replacements), &fn_replacements)?.as_ref();
433                if line_num != max_lines {
434                    out += "\n";
435                }
436            },
437            _ => {
438                out += fn_replace(replace(line, &replacements), &fn_replacements)?.as_ref();
439                if line_num != max_lines {
440                    out += "\n";
441                }
442            },
443        }
444    }
445
446    if parameters.count() != 0 {
447        return Err(Error::UnusedParameters);
448    }
449
450    return Ok(Some(out));
451}
452
453/// Returns Some((start, end)) if start, end contains an identifier
454fn ident_range(str: &str, start: usize, end: usize) -> Option<(usize, usize)> {
455    if (start == 0 || str.chars().nth(start - 1).map(|c| !(c.is_alphanumeric() || c == '_')).unwrap_or(true))
456        && str.chars().nth(end).map(|c| !(c.is_alphanumeric() || c == '_')).unwrap_or(true)
457    {
458        return Some((start, end));
459    } else {
460        return None;
461    }
462}
463
464fn ident_or_paste_range(str: &str, start: usize, end: usize) -> Option<(usize, usize)> {
465    if start >= 2 && str.chars().skip(start - 2).take(2).all(|c| c == '#') {
466        let mut iter = str.chars().skip(end).take(2).tee();
467        // ##IDENT##
468        if iter.0.count() == 2 && iter.1.all(|c| c == '#') {
469            Some((start - 2, end + 2))
470        // ##IDENT
471        } else if str.chars().nth(end).map(|c| !(c.is_alphanumeric() || c == '_')).unwrap_or(true) {
472            Some((start - 2, end))
473        } else {
474            None
475        }
476    } else if start == 0 || str.chars().nth(start - 1).map(|c| !(c.is_alphanumeric() || c == '_')).unwrap_or(true) {
477        let mut iter = str.chars().skip(end).take(2).tee();
478        // IDENT##
479        if iter.0.count() == 2 && iter.1.all(|c| c == '#') {
480            Some((start, end + 2))
481        // IDENT
482        } else if str.chars().nth(end).map(|c| !(c.is_alphanumeric() || c == '_')).unwrap_or(true)  {
483            Some((start, end))
484        } else {
485            None
486        }
487    } else {
488        None
489    }
490}
491
492fn replace<'a>(line: &'a str, replacements: &Vec<(String, Cow<str>)>) -> Cow<'a, str> {
493    let mut out: Cow<str> = line.into();
494
495    for replacement in replacements {
496        out = replace_all(out, &replacement.0, replacement.1.as_ref(), ident_range);
497    }
498
499    return out;
500}
501
502fn fn_replace<'a>(line: Cow<'a, str>, replacements: &Vec<(String, Vec<String>, String)>) -> Result<Cow<'a, str>> {
503    let mut out: Cow<str> = line;
504
505    for replacement in replacements {
506        out = replace_all_fn(out, replacement.0.as_str(), replacement.2.as_str(), &replacement.1, ident_or_paste_range)?;
507    }
508
509    return Ok(out);
510}
511
512fn replace_all<'a>(str: Cow<'a, str>, to_match: &str, replacement: &str, predicate: impl Fn(&str, usize, usize) -> Option<(usize, usize)>) -> Cow<'a, str> {
513    let matches = str.match_indices(to_match).collect::<Vec<_>>();
514
515    let mut out: Option<Cow<str>> = None;
516    let mut end_idx = str.len();
517
518    for (idx, _) in matches.into_iter().rev() {
519        if let Some((start, end)) = predicate(str.as_ref(), idx, idx + to_match.len()) {
520            let following_str = &str[end..end_idx];
521            end_idx = start;
522            out = out.map(|m| concat_string!(replacement, following_str, m.as_ref()).into())
523                .or(Some(concat_string!(replacement, following_str).into()));
524        }
525    }
526
527    if end_idx != 0 {
528        out = out.map(|m| concat_string!(&str[0..end_idx], m.as_ref()).into())
529    }
530
531    return out.unwrap_or(str);
532}
533
534fn replace_all_fn<'a>(
535    str: Cow<'a, str>,
536    name: &str,
537    replacement: &str,
538    param_names: &Vec<String>,
539    predicate: impl Fn(&str, usize, usize) -> Option<(usize, usize)>,
540) -> Result<Cow<'a, str>> {
541    let matches = str.match_indices(name).collect::<Vec<_>>();
542
543    let mut out: Option<Cow<str>> = None;
544    let mut end_idx = str.len();
545
546    for (idx, _) in matches.into_iter().rev() {
547        let mut iter = str.chars();
548        if iter.by_ref().nth(idx + name.len()) != Some('(') {
549            continue;
550        }
551
552        let mut parens = 0;
553        let mut params = Vec::new();
554        let mut cur = String::new();
555        let mut param_len = 0;
556        for (i, c) in iter.enumerate() {
557            if c == ')' {
558                parens -= 1;
559                if parens == -1 {
560                    params.push(cur);
561                    cur = String::new();
562                    param_len = i;
563                    break;
564                }
565            }
566
567            // next param
568            if c == ',' && parens == 0 {
569                params.push(cur);
570                cur = String::new();
571                continue;
572            }
573
574            if c == '(' {
575                parens += 1;
576            }
577
578            cur.push(c);
579        }
580
581        let to_replace_len = name.len() + 2 + param_len;
582
583        let Some((start, end)) = predicate(str.as_ref(), idx, idx + to_replace_len) else {
584            continue;
585        };
586
587        if params.len() < param_names.len() {
588            return Err(Error::NotEnoughParametersMacro(name.to_string()));
589        } else if params.len() > param_names.len() {
590            return Err(Error::UnusedParametersMacro(name.to_string()))
591        }
592
593        let params = param_names.iter()
594            .zip(params.iter().map(|p| p.trim()));
595
596        let mut replacement = Cow::Borrowed(replacement);
597        for param in params {
598            replacement = replace_all(replacement, param.0, param.1, ident_or_paste_range);
599        }
600
601        let following_str = &str[end..end_idx];
602        end_idx = start;
603        out = out.map(|m| concat_string!(replacement, following_str, m.as_ref()).into())
604            .or(Some(concat_string!(replacement, following_str).into()));
605    }
606
607    if end_idx != 0 {
608        out = out.map(|m| concat_string!(&str[0..end_idx], m.as_ref()).into());
609    }
610
611    return Ok(out.unwrap_or(str));
612}