fusabi_host/
compile.rs

1//! Compilation APIs for Fusabi source and bytecode.
2
3use std::collections::HashMap;
4use std::path::Path;
5
6use crate::error::{Error, Result};
7
8/// Options for compilation.
9#[derive(Debug, Clone, Default)]
10pub struct CompileOptions {
11    /// Optimization level (0-3).
12    pub opt_level: u8,
13    /// Whether to include debug information.
14    pub debug_info: bool,
15    /// Whether to strip symbols.
16    pub strip: bool,
17    /// Target Fusabi version.
18    pub target_version: Option<String>,
19    /// Custom compiler flags.
20    pub flags: HashMap<String, String>,
21    /// Source file name (for error messages).
22    pub source_name: Option<String>,
23}
24
25impl CompileOptions {
26    /// Create new compile options.
27    pub fn new() -> Self {
28        Self::default()
29    }
30
31    /// Set optimization level.
32    pub fn with_opt_level(mut self, level: u8) -> Self {
33        self.opt_level = level.min(3);
34        self
35    }
36
37    /// Enable debug information.
38    pub fn with_debug_info(mut self) -> Self {
39        self.debug_info = true;
40        self
41    }
42
43    /// Enable symbol stripping.
44    pub fn with_strip(mut self) -> Self {
45        self.strip = true;
46        self
47    }
48
49    /// Set target version.
50    pub fn with_target_version(mut self, version: impl Into<String>) -> Self {
51        self.target_version = Some(version.into());
52        self
53    }
54
55    /// Add a custom flag.
56    pub fn with_flag(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
57        self.flags.insert(key.into(), value.into());
58        self
59    }
60
61    /// Set source name for error messages.
62    pub fn with_source_name(mut self, name: impl Into<String>) -> Self {
63        self.source_name = Some(name.into());
64        self
65    }
66
67    /// Create options optimized for development.
68    pub fn development() -> Self {
69        Self {
70            opt_level: 0,
71            debug_info: true,
72            strip: false,
73            target_version: None,
74            flags: HashMap::new(),
75            source_name: None,
76        }
77    }
78
79    /// Create options optimized for production.
80    pub fn production() -> Self {
81        Self {
82            opt_level: 2,
83            debug_info: false,
84            strip: true,
85            target_version: None,
86            flags: HashMap::new(),
87            source_name: None,
88        }
89    }
90}
91
92/// Metadata extracted from compiled bytecode.
93#[derive(Debug, Clone, Default)]
94pub struct Metadata {
95    /// Fusabi language version used.
96    pub language_version: String,
97    /// Compiler version.
98    pub compiler_version: String,
99    /// Original source name.
100    pub source_name: Option<String>,
101    /// Compilation timestamp.
102    pub compiled_at: Option<u64>,
103    /// Required capabilities declared in the script.
104    pub required_capabilities: Vec<String>,
105    /// Exported functions.
106    pub exports: Vec<ExportInfo>,
107    /// Imported modules.
108    pub imports: Vec<ImportInfo>,
109    /// Custom metadata entries.
110    pub custom: HashMap<String, String>,
111}
112
113/// Information about an exported function.
114#[derive(Debug, Clone)]
115pub struct ExportInfo {
116    /// Function name.
117    pub name: String,
118    /// Parameter count.
119    pub param_count: usize,
120    /// Whether the function is async.
121    pub is_async: bool,
122    /// Documentation comment if available.
123    pub doc: Option<String>,
124}
125
126/// Information about an imported module.
127#[derive(Debug, Clone)]
128pub struct ImportInfo {
129    /// Module name.
130    pub module: String,
131    /// Imported items (or "*" for all).
132    pub items: Vec<String>,
133    /// Version constraint if specified.
134    pub version: Option<String>,
135}
136
137impl Metadata {
138    /// Check if a capability is required.
139    pub fn requires_capability(&self, cap: &str) -> bool {
140        self.required_capabilities.iter().any(|c| c == cap)
141    }
142
143    /// Get an export by name.
144    pub fn get_export(&self, name: &str) -> Option<&ExportInfo> {
145        self.exports.iter().find(|e| e.name == name)
146    }
147
148    /// Check if a module is imported.
149    pub fn imports_module(&self, module: &str) -> bool {
150        self.imports.iter().any(|i| i.module == module)
151    }
152}
153
154/// Result of compilation.
155#[derive(Debug, Clone)]
156pub struct CompileResult {
157    /// Compiled bytecode.
158    pub bytecode: Vec<u8>,
159    /// Extracted metadata.
160    pub metadata: Metadata,
161    /// Compilation warnings.
162    pub warnings: Vec<CompileWarning>,
163    /// Compilation statistics.
164    pub stats: CompileStats,
165}
166
167/// A compilation warning.
168#[derive(Debug, Clone)]
169pub struct CompileWarning {
170    /// Warning message.
171    pub message: String,
172    /// Source location if available.
173    pub location: Option<SourceLocation>,
174    /// Warning code.
175    pub code: Option<String>,
176}
177
178/// Source location for diagnostics.
179#[derive(Debug, Clone)]
180pub struct SourceLocation {
181    /// Line number (1-indexed).
182    pub line: usize,
183    /// Column number (1-indexed).
184    pub column: usize,
185    /// Source file name.
186    pub file: Option<String>,
187}
188
189/// Statistics about compilation.
190#[derive(Debug, Clone, Default)]
191pub struct CompileStats {
192    /// Source size in bytes.
193    pub source_bytes: usize,
194    /// Bytecode size in bytes.
195    pub bytecode_bytes: usize,
196    /// Number of functions.
197    pub function_count: usize,
198    /// Compilation time in milliseconds.
199    pub compile_time_ms: u64,
200}
201
202/// Compile Fusabi source code to bytecode.
203///
204/// # Arguments
205///
206/// * `source` - The Fusabi source code
207/// * `options` - Compilation options
208///
209/// # Returns
210///
211/// A `CompileResult` containing bytecode, metadata, and diagnostics.
212pub fn compile_source(source: &str, options: &CompileOptions) -> Result<CompileResult> {
213    let start = std::time::Instant::now();
214
215    // Validate source isn't empty
216    if source.trim().is_empty() {
217        return Err(Error::compilation("empty source"));
218    }
219
220    // Simulate compilation - in real implementation would call fusabi_frontend
221    let bytecode = generate_bytecode(source, options)?;
222    let metadata = extract_metadata(source, options);
223    let warnings = check_warnings(source);
224
225    let compile_time = start.elapsed();
226
227    Ok(CompileResult {
228        bytecode: bytecode.clone(),
229        metadata,
230        warnings,
231        stats: CompileStats {
232            source_bytes: source.len(),
233            bytecode_bytes: bytecode.len(),
234            function_count: 1,
235            compile_time_ms: compile_time.as_millis() as u64,
236        },
237    })
238}
239
240/// Compile a Fusabi source file to bytecode.
241///
242/// # Arguments
243///
244/// * `path` - Path to the source file (.fsx)
245/// * `options` - Compilation options
246///
247/// # Returns
248///
249/// A `CompileResult` containing bytecode, metadata, and diagnostics.
250pub fn compile_file(path: &Path, options: &CompileOptions) -> Result<CompileResult> {
251    // Check file extension
252    let extension = path.extension().and_then(|e| e.to_str());
253    if extension != Some("fsx") && extension != Some("fusabi") {
254        return Err(Error::compilation(format!(
255            "expected .fsx or .fusabi file, got: {}",
256            path.display()
257        )));
258    }
259
260    // Read source
261    let source = std::fs::read_to_string(path)?;
262
263    // Compile with source name
264    let options = options
265        .clone()
266        .with_source_name(path.display().to_string());
267
268    compile_source(&source, &options)
269}
270
271/// Validate bytecode without executing.
272///
273/// # Arguments
274///
275/// * `bytecode` - The bytecode to validate
276///
277/// # Returns
278///
279/// Metadata if valid, error if invalid.
280pub fn validate_bytecode(bytecode: &[u8]) -> Result<Metadata> {
281    // Check minimum size
282    if bytecode.len() < 16 {
283        return Err(Error::invalid_bytecode("bytecode too short"));
284    }
285
286    // Check magic number
287    if &bytecode[0..4] != b"FZB\x00" {
288        return Err(Error::invalid_bytecode("invalid magic number"));
289    }
290
291    // Check version
292    let version = bytecode[4];
293    if version > 1 {
294        return Err(Error::invalid_bytecode(format!(
295            "unsupported bytecode version: {}",
296            version
297        )));
298    }
299
300    // Extract metadata from bytecode
301    Ok(Metadata {
302        language_version: "0.18.0".to_string(),
303        compiler_version: "0.18.0".to_string(),
304        source_name: None,
305        compiled_at: None,
306        required_capabilities: Vec::new(),
307        exports: Vec::new(),
308        imports: Vec::new(),
309        custom: HashMap::new(),
310    })
311}
312
313/// Extract metadata from existing bytecode.
314pub fn extract_bytecode_metadata(bytecode: &[u8]) -> Result<Metadata> {
315    validate_bytecode(bytecode)
316}
317
318// Internal helper functions
319
320fn generate_bytecode(source: &str, options: &CompileOptions) -> Result<Vec<u8>> {
321    // Generate fake bytecode for simulation
322    // Real implementation would use fusabi_frontend
323
324    let mut bytecode = Vec::new();
325
326    // Magic number: "FZB\0"
327    bytecode.extend_from_slice(b"FZB\x00");
328
329    // Version byte
330    bytecode.push(1);
331
332    // Flags byte
333    let mut flags = 0u8;
334    if options.debug_info {
335        flags |= 0x01;
336    }
337    if options.strip {
338        flags |= 0x02;
339    }
340    flags |= (options.opt_level & 0x03) << 4;
341    bytecode.push(flags);
342
343    // Reserved bytes
344    bytecode.extend_from_slice(&[0u8; 10]);
345
346    // Source hash (simplified)
347    let hash = simple_hash(source);
348    bytecode.extend_from_slice(&hash.to_le_bytes());
349
350    // Placeholder for actual bytecode
351    // In real impl, this would be the compiled instructions
352    bytecode.extend_from_slice(source.as_bytes());
353
354    Ok(bytecode)
355}
356
357fn extract_metadata(source: &str, options: &CompileOptions) -> Metadata {
358    let mut metadata = Metadata {
359        language_version: "0.18.0".to_string(),
360        compiler_version: env!("CARGO_PKG_VERSION").to_string(),
361        source_name: options.source_name.clone(),
362        compiled_at: Some(
363            std::time::SystemTime::now()
364                .duration_since(std::time::UNIX_EPOCH)
365                .map(|d| d.as_secs())
366                .unwrap_or(0),
367        ),
368        required_capabilities: Vec::new(),
369        exports: Vec::new(),
370        imports: Vec::new(),
371        custom: HashMap::new(),
372    };
373
374    // Parse source for metadata hints (simplified)
375    for line in source.lines() {
376        let line = line.trim();
377
378        // Check for capability declarations
379        if line.starts_with("@require ") {
380            let cap = line.trim_start_matches("@require ").trim();
381            metadata.required_capabilities.push(cap.to_string());
382        }
383
384        // Check for imports
385        if line.starts_with("import ") {
386            let module = line.trim_start_matches("import ").trim();
387            metadata.imports.push(ImportInfo {
388                module: module.to_string(),
389                items: vec!["*".to_string()],
390                version: None,
391            });
392        }
393
394        // Check for function exports
395        if line.starts_with("export fn ") || line.starts_with("pub fn ") {
396            let rest = line
397                .trim_start_matches("export fn ")
398                .trim_start_matches("pub fn ");
399            if let Some(paren) = rest.find('(') {
400                let name = rest[..paren].trim();
401                metadata.exports.push(ExportInfo {
402                    name: name.to_string(),
403                    param_count: 0, // Would need proper parsing
404                    is_async: rest.contains("async"),
405                    doc: None,
406                });
407            }
408        }
409    }
410
411    metadata
412}
413
414fn check_warnings(source: &str) -> Vec<CompileWarning> {
415    let mut warnings = Vec::new();
416
417    for (line_num, line) in source.lines().enumerate() {
418        // Check for TODO comments
419        if line.contains("TODO") || line.contains("FIXME") {
420            warnings.push(CompileWarning {
421                message: "unresolved TODO/FIXME comment".to_string(),
422                location: Some(SourceLocation {
423                    line: line_num + 1,
424                    column: 1,
425                    file: None,
426                }),
427                code: Some("W001".to_string()),
428            });
429        }
430
431        // Check for unused variable hints
432        if line.contains("let _") {
433            warnings.push(CompileWarning {
434                message: "unused variable".to_string(),
435                location: Some(SourceLocation {
436                    line: line_num + 1,
437                    column: 1,
438                    file: None,
439                }),
440                code: Some("W002".to_string()),
441            });
442        }
443    }
444
445    warnings
446}
447
448fn simple_hash(s: &str) -> u64 {
449    // Simple FNV-1a hash for simulation
450    let mut hash: u64 = 0xcbf29ce484222325;
451    for byte in s.bytes() {
452        hash ^= byte as u64;
453        hash = hash.wrapping_mul(0x100000001b3);
454    }
455    hash
456}
457
458#[cfg(test)]
459mod tests {
460    use super::*;
461
462    #[test]
463    fn test_compile_source() {
464        let result = compile_source("42", &CompileOptions::default()).unwrap();
465
466        assert!(!result.bytecode.is_empty());
467        assert!(result.bytecode.starts_with(b"FZB\x00"));
468        assert_eq!(result.stats.source_bytes, 2);
469    }
470
471    #[test]
472    fn test_compile_empty_source() {
473        let result = compile_source("", &CompileOptions::default());
474        assert!(matches!(result, Err(Error::Compilation(_))));
475
476        let result = compile_source("   ", &CompileOptions::default());
477        assert!(matches!(result, Err(Error::Compilation(_))));
478    }
479
480    #[test]
481    fn test_compile_options_builder() {
482        let opts = CompileOptions::new()
483            .with_opt_level(2)
484            .with_debug_info()
485            .with_source_name("test.fsx");
486
487        assert_eq!(opts.opt_level, 2);
488        assert!(opts.debug_info);
489        assert_eq!(opts.source_name, Some("test.fsx".to_string()));
490    }
491
492    #[test]
493    fn test_compile_options_presets() {
494        let dev = CompileOptions::development();
495        assert_eq!(dev.opt_level, 0);
496        assert!(dev.debug_info);
497
498        let prod = CompileOptions::production();
499        assert_eq!(prod.opt_level, 2);
500        assert!(prod.strip);
501    }
502
503    #[test]
504    fn test_validate_bytecode() {
505        let result = compile_source("42", &CompileOptions::default()).unwrap();
506        let metadata = validate_bytecode(&result.bytecode).unwrap();
507
508        assert_eq!(metadata.language_version, "0.18.0");
509    }
510
511    #[test]
512    fn test_validate_invalid_bytecode() {
513        assert!(validate_bytecode(b"invalid").is_err());
514        assert!(validate_bytecode(b"FZB").is_err()); // Too short
515        assert!(validate_bytecode(b"XXX\x00").is_err()); // Wrong magic
516    }
517
518    #[test]
519    fn test_metadata_extraction() {
520        let source = r#"
521@require fs:read
522import json
523
524export fn main() {
525    // TODO: implement
526}
527"#;
528
529        let result = compile_source(source, &CompileOptions::default()).unwrap();
530
531        assert!(result.metadata.requires_capability("fs:read"));
532        assert!(result.metadata.imports_module("json"));
533        assert!(result.metadata.get_export("main").is_some());
534    }
535
536    #[test]
537    fn test_compile_warnings() {
538        let source = "// TODO: fix this";
539        let result = compile_source(source, &CompileOptions::default()).unwrap();
540
541        assert!(!result.warnings.is_empty());
542        assert!(result.warnings[0].message.contains("TODO"));
543    }
544
545    #[test]
546    fn test_compile_file_wrong_extension() {
547        let result = compile_file(Path::new("test.txt"), &CompileOptions::default());
548        assert!(matches!(result, Err(Error::Compilation(_))));
549    }
550}