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(§ion.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}