helix_core/compiler/
bundle.rs

1use crate::compiler::{Compiler, CompileError};
2use crate::compiler::{
3    binary::{HelixBinary, DataSection},
4    optimizer::OptimizationLevel, serializer::BinarySerializer,
5};
6use crate::codegen::Instruction;
7use std::path::{Path, PathBuf};
8use std::fs;
9use std::collections::{HashMap, HashSet, VecDeque};
10pub struct Bundler {
11    include_patterns: Vec<String>,
12    exclude_patterns: Vec<String>,
13    follow_imports: bool,
14    tree_shake: bool,
15    verbose: bool,
16}
17impl Bundler {
18    pub fn new() -> Self {
19        Self {
20            include_patterns: vec!["*.hlx".to_string()],
21            exclude_patterns: Vec::new(),
22            follow_imports: true,
23            tree_shake: false,
24            verbose: false,
25        }
26    }
27    pub fn include(mut self, pattern: &str) -> Self {
28        self.include_patterns.push(pattern.to_string());
29        self
30    }
31    pub fn exclude(mut self, pattern: &str) -> Self {
32        self.exclude_patterns.push(pattern.to_string());
33        self
34    }
35    pub fn with_imports(mut self, follow: bool) -> Self {
36        self.follow_imports = follow;
37        self
38    }
39    pub fn with_tree_shaking(mut self, enable: bool) -> Self {
40        self.tree_shake = enable;
41        self
42    }
43    pub fn verbose(mut self, enable: bool) -> Self {
44        self.verbose = enable;
45        self
46    }
47    pub fn bundle_directory<P: AsRef<Path>>(
48        &self,
49        directory: P,
50        optimization_level: OptimizationLevel,
51    ) -> Result<HelixBinary, CompileError> {
52        let directory = directory.as_ref();
53        if self.verbose {
54            println!("Bundling directory: {}", directory.display());
55        }
56        let files = self.collect_files(directory)?;
57        if files.is_empty() {
58            return Err(CompileError::IoError("No HELIX files found".to_string()));
59        }
60        if self.verbose {
61            println!("Found {} files to bundle", files.len());
62        }
63        self.bundle_files(&files, optimization_level)
64    }
65    pub fn bundle_files(
66        &self,
67        files: &[PathBuf],
68        optimization_level: OptimizationLevel,
69    ) -> Result<HelixBinary, CompileError> {
70        let mut bundle = BundleBuilder::new();
71        let compiler = Compiler::new(optimization_level);
72        for file in files {
73            if self.verbose {
74                println!("  Processing: {}", file.display());
75            }
76            let source = fs::read_to_string(file)
77                .map_err(|e| CompileError::IoError(
78                    format!("Failed to read {}: {}", file.display(), e),
79                ))?;
80            let binary = compiler.compile_source(&source, Some(file))?;
81            bundle.add_file(file.clone(), binary);
82        }
83        if self.follow_imports {
84            self.resolve_dependencies(&mut bundle)?;
85        }
86        if self.tree_shake {
87            self.apply_tree_shaking(&mut bundle)?;
88        }
89        let merged = bundle.build()?;
90        if self.verbose {
91            println!("Bundle created successfully");
92            println!("  Total size: {} bytes", merged.size());
93        }
94        Ok(merged)
95    }
96    fn collect_files(&self, directory: &Path) -> Result<Vec<PathBuf>, CompileError> {
97        let mut files = Vec::new();
98        for entry in fs::read_dir(directory)
99            .map_err(|e| CompileError::IoError(
100                format!("Failed to read directory: {}", e),
101            ))?
102        {
103            let entry = entry
104                .map_err(|e| CompileError::IoError(
105                    format!("Failed to read entry: {}", e),
106                ))?;
107            let path = entry.path();
108            if path.is_file() {
109                let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
110                if self.should_include(file_name) {
111                    files.push(path);
112                }
113            } else if path.is_dir() && self.follow_imports {
114                let mut sub_files = self.collect_files(&path)?;
115                files.append(&mut sub_files);
116            }
117        }
118        Ok(files)
119    }
120    fn should_include(&self, file_name: &str) -> bool {
121        for pattern in &self.exclude_patterns {
122            if self.matches_pattern(file_name, pattern) {
123                return false;
124            }
125        }
126        for pattern in &self.include_patterns {
127            if self.matches_pattern(file_name, pattern) {
128                return true;
129            }
130        }
131        false
132    }
133    fn matches_pattern(&self, file_name: &str, pattern: &str) -> bool {
134        if pattern.contains('*') {
135            let parts: Vec<&str> = pattern.split('*').collect();
136            if parts.len() == 2 {
137                let prefix = parts[0];
138                let suffix = parts[1];
139                return file_name.starts_with(prefix) && file_name.ends_with(suffix);
140            }
141        }
142        file_name == pattern
143    }
144    fn resolve_dependencies(
145        &self,
146        bundle: &mut BundleBuilder,
147    ) -> Result<(), CompileError> {
148        for (path, binary) in &bundle.files {
149            let mut deps = HashSet::new();
150            let serializer = BinarySerializer::new(false);
151            if let Ok(ir) = serializer.deserialize_to_ir(&binary) {
152                for instruction in &ir.instructions {
153                    if let Instruction::ResolveReference { ref_type: _, index } = instruction {
154                        if *index > 1000 {
155                            deps.insert(PathBuf::from("external.hlx"));
156                        }
157                    }
158                }
159            }
160            bundle.dependencies.insert(path.clone(), deps);
161        }
162        if let Some(cycle) = self.detect_circular_dependencies(&bundle.dependencies) {
163            return Err(
164                CompileError::ParseError(
165                    format!("Circular dependency detected: {:?}", cycle),
166                ),
167            );
168        }
169        Ok(())
170    }
171    fn detect_circular_dependencies(
172        &self,
173        deps: &HashMap<PathBuf, HashSet<PathBuf>>,
174    ) -> Option<Vec<PathBuf>> {
175        for (start, _) in deps {
176            let mut visited = HashSet::new();
177            let mut path = VecDeque::new();
178            path.push_back(start.clone());
179            if self.has_cycle_from(start, deps, &mut visited, &mut path) {
180                return Some(path.into_iter().collect());
181            }
182        }
183        None
184    }
185    fn has_cycle_from(
186        &self,
187        node: &PathBuf,
188        deps: &HashMap<PathBuf, HashSet<PathBuf>>,
189        visited: &mut HashSet<PathBuf>,
190        path: &mut VecDeque<PathBuf>,
191    ) -> bool {
192        if visited.contains(node) {
193            return true;
194        }
195        visited.insert(node.clone());
196        if let Some(node_deps) = deps.get(node) {
197            for dep in node_deps {
198                path.push_back(dep.clone());
199                if self.has_cycle_from(dep, deps, visited, path) {
200                    return true;
201                }
202                path.pop_back();
203            }
204        }
205        visited.remove(node);
206        false
207    }
208    fn apply_tree_shaking(
209        &self,
210        bundle: &mut BundleBuilder,
211    ) -> Result<(), CompileError> {
212        use std::collections::HashSet;
213        for (path, binary) in &mut bundle.files {
214            let serializer = BinarySerializer::new(false);
215            let mut ir = serializer
216                .deserialize_to_ir(&binary)
217                .map_err(|e| CompileError::SerializationError(e.to_string()))?;
218            let mut used_agents = HashSet::new();
219            let mut used_workflows = HashSet::new();
220            let mut used_contexts = HashSet::new();
221            for (_id, crew) in &ir.symbol_table.crews {
222                for agent_id in &crew.agent_ids {
223                    used_agents.insert(*agent_id);
224                }
225            }
226            for workflow in ir.symbol_table.workflows.values() {
227                if let Some(pipeline) = &workflow.pipeline {
228                    for workflow_id in pipeline {
229                        used_workflows.insert(*workflow_id);
230                    }
231                }
232            }
233            for instruction in &ir.instructions {
234                if let Instruction::DeclareContext(id) = instruction {
235                    used_contexts.insert(id);
236                }
237            }
238            let unused_agents: Vec<u32> = ir
239                .symbol_table
240                .agents
241                .keys()
242                .filter(|id| !used_agents.contains(id))
243                .cloned()
244                .collect();
245            for id in unused_agents {
246                ir.symbol_table.agents.remove(&id);
247            }
248            let unused_workflows: Vec<u32> = ir
249                .symbol_table
250                .workflows
251                .keys()
252                .filter(|id| !used_workflows.contains(id))
253                .cloned()
254                .collect();
255            for id in unused_workflows {
256                ir.symbol_table.workflows.remove(&id);
257            }
258            let unused_contexts: Vec<u32> = ir
259                .symbol_table
260                .contexts
261                .keys()
262                .filter(|id| !used_contexts.contains(id))
263                .cloned()
264                .collect();
265            for id in unused_contexts {
266                ir.symbol_table.contexts.remove(&id);
267            }
268            *binary = serializer
269                .serialize(ir, Some(path))
270                .map_err(|e| CompileError::SerializationError(e.to_string()))?;
271        }
272        Ok(())
273    }
274}
275impl Default for Bundler {
276    fn default() -> Self {
277        Self::new()
278    }
279}
280struct BundleBuilder {
281    files: HashMap<PathBuf, HelixBinary>,
282    dependencies: HashMap<PathBuf, HashSet<PathBuf>>,
283}
284impl BundleBuilder {
285    fn new() -> Self {
286        Self {
287            files: HashMap::new(),
288            dependencies: HashMap::new(),
289        }
290    }
291    fn add_file(&mut self, path: PathBuf, binary: HelixBinary) {
292        self.files.insert(path, binary);
293    }
294    #[allow(dead_code)]
295    fn add_dependency(&mut self, from: PathBuf, to: PathBuf) {
296        self.dependencies.entry(from).or_insert_with(HashSet::new).insert(to);
297    }
298    fn build(self) -> Result<HelixBinary, CompileError> {
299        if self.files.is_empty() {
300            return Err(CompileError::IoError("No files in bundle".to_string()));
301        }
302        let mut merged = self.files.values().next().unwrap().clone();
303        for binary in self.files.values().skip(1) {
304            Self::merge_binary(&mut merged, binary)?;
305        }
306        merged.metadata.extra.insert("bundle".to_string(), "true".to_string());
307        merged
308            .metadata
309            .extra
310            .insert("bundle_files".to_string(), self.files.len().to_string());
311        merged.checksum = merged.calculate_checksum();
312        Ok(merged)
313    }
314    fn merge_binary(
315        target: &mut HelixBinary,
316        source: &HelixBinary,
317    ) -> Result<(), CompileError> {
318        Self::merge_symbol_tables(&mut target.symbol_table, &source.symbol_table);
319        for section in &source.data_sections {
320            let existing = target
321                .data_sections
322                .iter_mut()
323                .find(|s| {
324                    std::mem::discriminant(&s.section_type)
325                        == std::mem::discriminant(&section.section_type)
326                });
327            if let Some(existing_section) = existing {
328                Self::merge_sections(existing_section, section)?;
329            } else {
330                target.data_sections.push(section.clone());
331            }
332        }
333        Ok(())
334    }
335    fn merge_symbol_tables(
336        target: &mut crate::compiler::binary::SymbolTable,
337        source: &crate::compiler::binary::SymbolTable,
338    ) {
339        for string in &source.strings {
340            if !target.strings.contains(string) {
341                let id = target.strings.len() as u32;
342                target.strings.push(string.clone());
343                target.string_map.insert(string.clone(), id);
344            }
345        }
346        for (name, id) in &source.agents {
347            if !target.agents.contains_key(name) {
348                target.agents.insert(name.clone(), *id);
349            }
350        }
351        for (name, id) in &source.workflows {
352            if !target.workflows.contains_key(name) {
353                target.workflows.insert(name.clone(), *id);
354            }
355        }
356        for (name, id) in &source.contexts {
357            if !target.contexts.contains_key(name) {
358                target.contexts.insert(name.clone(), *id);
359            }
360        }
361        for (name, id) in &source.crews {
362            if !target.crews.contains_key(name) {
363                target.crews.insert(name.clone(), *id);
364            }
365        }
366    }
367    fn merge_sections(
368        target: &mut DataSection,
369        source: &DataSection,
370    ) -> Result<(), CompileError> {
371        target.data.extend_from_slice(&source.data);
372        target.size += source.size;
373        Ok(())
374    }
375}
376#[cfg(test)]
377mod tests {
378    use super::*;
379    #[test]
380    fn test_bundler_creation() {
381        let bundler = Bundler::new();
382        assert_eq!(bundler.include_patterns, vec!["*.hlx"]);
383        assert!(bundler.exclude_patterns.is_empty());
384        assert!(bundler.follow_imports);
385        assert!(! bundler.tree_shake);
386    }
387    #[test]
388    fn test_pattern_matching() {
389        let bundler = Bundler::new();
390        assert!(bundler.matches_pattern("config.hlx", "*.hlx"));
391        assert!(bundler.matches_pattern("test.hlx", "*.hlx"));
392        assert!(! bundler.matches_pattern("config.txt", "*.hlx"));
393        assert!(bundler.matches_pattern("exact.hlx", "exact.hlx"));
394    }
395    #[test]
396    fn test_bundler_builder() {
397        let bundler = Bundler::new()
398            .include("*.hlx")
399            .exclude("test_*.hlx")
400            .with_tree_shaking(true)
401            .verbose(true);
402        assert_eq!(bundler.include_patterns.len(), 2);
403        assert_eq!(bundler.exclude_patterns.len(), 1);
404        assert!(bundler.tree_shake);
405        assert!(bundler.verbose);
406    }
407}