Skip to main content

celox_macros/
lib.rs

1#![allow(clippy::disallowed_methods)] // proc-macro build context is an application boundary
2
3mod generator;
4
5use proc_macro::TokenStream;
6use std::env;
7use std::fs;
8use std::path::PathBuf;
9use syn::LitStr;
10use veryl_analyzer::ir::Ir;
11use veryl_analyzer::{Analyzer, Context};
12use veryl_metadata::Metadata;
13use veryl_parser::Parser;
14
15#[proc_macro]
16pub fn veryl_test(input: TokenStream) -> TokenStream {
17    let project_path_lit = match syn::parse::<LitStr>(input.clone()) {
18        Ok(lit) => lit,
19        Err(err) => return err.to_compile_error().into(),
20    };
21    let project_path_str = project_path_lit.value();
22    let span = project_path_lit.span();
23
24    macro_rules! exit_with_error {
25        ($msg:expr) => {{
26            let err_msg = $msg;
27            return quote::quote_spanned! { span => compile_error!(#err_msg); }.into();
28        }};
29    }
30
31    // 1. Find Veryl.toml
32    let manifest_dir = match env::var("CARGO_MANIFEST_DIR") {
33        Ok(dir) => dir,
34        Err(_) => exit_with_error!("CARGO_MANIFEST_DIR is not set"),
35    };
36    let base_dir = PathBuf::from(manifest_dir);
37    let target_dir = base_dir.join(project_path_str);
38
39    let metadata_path = match Metadata::search_from(&target_dir) {
40        Ok(path) => path,
41        Err(_) => exit_with_error!(&format!("Failed to find Veryl.toml from {:?}", target_dir)),
42    };
43
44    let mut metadata = match Metadata::load(&metadata_path) {
45        Ok(meta) => meta,
46        Err(e) => exit_with_error!(&format!("Failed to load Veryl.toml: {}", e)),
47    };
48
49    // 2. Run Analysis
50    veryl_analyzer::symbol_table::clear();
51    veryl_analyzer::attribute_table::clear();
52    let mut ir = Ir::default();
53    let mut context = Context::default();
54    let analyzer = Analyzer::new(&metadata);
55    analyzer.clear();
56
57    let paths = match metadata.paths::<PathBuf>(&[], false, true) {
58        Ok(p) => p
59            .into_iter()
60            .filter(|path| !path.example)
61            .collect::<Vec<_>>(),
62        Err(e) => exit_with_error!(&format!("Failed to gather paths: {}", e)),
63    };
64
65    // Veryl CLI dependency logic
66    let mut table = fxhash::FxHashMap::default();
67    for path in &paths {
68        table.insert(path.src.clone(), path);
69    }
70
71    let mut prj_namespace = veryl_analyzer::namespace::Namespace::new();
72    prj_namespace.push(veryl_parser::resource_table::insert_str(
73        &metadata.project.name,
74    ));
75
76    let candidate_symbols: Vec<_> = veryl_analyzer::type_dag::connected_components()
77        .into_iter()
78        .filter(|symbols| symbols[0].namespace.included(&prj_namespace))
79        .flatten()
80        .collect();
81
82    let mut used_paths = fxhash::FxHashMap::default();
83    for symbol in &candidate_symbols {
84        if let veryl_parser::veryl_token::TokenSource::File { path, .. } = symbol.token.source {
85            let path = PathBuf::from(format!("{path}"));
86            if let Some(x) = table.remove(&path) {
87                used_paths.insert(path, x);
88            }
89        }
90    }
91
92    let mut sorted_paths = vec![];
93    for path in veryl_analyzer::type_dag::toposort_file() {
94        let path = PathBuf::from(format!("{path}"));
95        if let Some(x) = used_paths.remove(&path) {
96            sorted_paths.push(x.clone());
97        }
98    }
99
100    for path in used_paths.into_values() {
101        sorted_paths.push(path.clone());
102    }
103
104    // Now sorted_paths still might not include $std if we only sorted candidate_symbols from AST DAG.
105    // Actually, `metadata.paths(..., true, true)` includes stdlib.
106    // The problem in the macro was passing `false` for load_std. Let's just use `paths` if we don't care about the rigorous emit order. Wait, the first pass must populate the symbol table before sort.
107
108    let mut parsed_files = Vec::new();
109    for path_set in paths {
110        let code = match fs::read_to_string(&path_set.src) {
111            Ok(c) => c,
112            Err(e) => {
113                exit_with_error!(&format!("Failed to read {}: {}", path_set.src.display(), e))
114            }
115        };
116        parsed_files.push((path_set, code));
117    }
118
119    let mut parsers = Vec::new();
120    for (path_set, code) in &parsed_files {
121        let parser = match Parser::parse(code, &path_set.src) {
122            Ok(p) => p,
123            Err(_) => exit_with_error!(&format!("Failed to parse {}", path_set.src.display())),
124        };
125        let errors = analyzer.analyze_pass1(&path_set.prj, &parser.veryl);
126        if !errors.is_empty() {
127            let msgs: Vec<String> = errors.iter().map(|e| format!("{e}")).collect();
128            exit_with_error!(&format!("Analysis pass1 errors: {}", msgs.join("; ")));
129        }
130        parsers.push(parser);
131    }
132    let errors = Analyzer::analyze_post_pass1();
133    if !errors.is_empty() {
134        let msgs: Vec<String> = errors.iter().map(|e| format!("{e}")).collect();
135        exit_with_error!(&format!("Analysis post-pass1 errors: {}", msgs.join("; ")));
136    }
137
138    for (i, (_path_set, _code)) in parsed_files.iter().enumerate() {
139        let parser = &parsers[i];
140        let errors = analyzer.analyze_pass2(&parser.veryl, &mut context, Some(&mut ir));
141        if !errors.is_empty() {
142            let msgs: Vec<String> = errors.iter().map(|e| format!("{e}")).collect();
143            exit_with_error!(&format!("Analysis pass2 errors: {}", msgs.join("; ")));
144        }
145    }
146    let errors = Analyzer::analyze_post_pass2(&ir);
147    if !errors.is_empty() {
148        let msgs: Vec<String> = errors.iter().map(|e| format!("{e}")).collect();
149        exit_with_error!(&format!("Analysis post-pass2 errors: {}", msgs.join("; ")));
150    }
151
152    // 3. Generate TokenStream
153    let expanded = generator::generate_project(&ir);
154
155    TokenStream::from(expanded)
156}