1use std::collections::HashMap;
4use std::path::Path;
5
6use crate::error::{Error, Result};
7
8#[derive(Debug, Clone, Default)]
10pub struct CompileOptions {
11 pub opt_level: u8,
13 pub debug_info: bool,
15 pub strip: bool,
17 pub target_version: Option<String>,
19 pub flags: HashMap<String, String>,
21 pub source_name: Option<String>,
23}
24
25impl CompileOptions {
26 pub fn new() -> Self {
28 Self::default()
29 }
30
31 pub fn with_opt_level(mut self, level: u8) -> Self {
33 self.opt_level = level.min(3);
34 self
35 }
36
37 pub fn with_debug_info(mut self) -> Self {
39 self.debug_info = true;
40 self
41 }
42
43 pub fn with_strip(mut self) -> Self {
45 self.strip = true;
46 self
47 }
48
49 pub fn with_target_version(mut self, version: impl Into<String>) -> Self {
51 self.target_version = Some(version.into());
52 self
53 }
54
55 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 pub fn with_source_name(mut self, name: impl Into<String>) -> Self {
63 self.source_name = Some(name.into());
64 self
65 }
66
67 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 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#[derive(Debug, Clone, Default)]
94pub struct Metadata {
95 pub language_version: String,
97 pub compiler_version: String,
99 pub source_name: Option<String>,
101 pub compiled_at: Option<u64>,
103 pub required_capabilities: Vec<String>,
105 pub exports: Vec<ExportInfo>,
107 pub imports: Vec<ImportInfo>,
109 pub custom: HashMap<String, String>,
111}
112
113#[derive(Debug, Clone)]
115pub struct ExportInfo {
116 pub name: String,
118 pub param_count: usize,
120 pub is_async: bool,
122 pub doc: Option<String>,
124}
125
126#[derive(Debug, Clone)]
128pub struct ImportInfo {
129 pub module: String,
131 pub items: Vec<String>,
133 pub version: Option<String>,
135}
136
137impl Metadata {
138 pub fn requires_capability(&self, cap: &str) -> bool {
140 self.required_capabilities.iter().any(|c| c == cap)
141 }
142
143 pub fn get_export(&self, name: &str) -> Option<&ExportInfo> {
145 self.exports.iter().find(|e| e.name == name)
146 }
147
148 pub fn imports_module(&self, module: &str) -> bool {
150 self.imports.iter().any(|i| i.module == module)
151 }
152}
153
154#[derive(Debug, Clone)]
156pub struct CompileResult {
157 pub bytecode: Vec<u8>,
159 pub metadata: Metadata,
161 pub warnings: Vec<CompileWarning>,
163 pub stats: CompileStats,
165}
166
167#[derive(Debug, Clone)]
169pub struct CompileWarning {
170 pub message: String,
172 pub location: Option<SourceLocation>,
174 pub code: Option<String>,
176}
177
178#[derive(Debug, Clone)]
180pub struct SourceLocation {
181 pub line: usize,
183 pub column: usize,
185 pub file: Option<String>,
187}
188
189#[derive(Debug, Clone, Default)]
191pub struct CompileStats {
192 pub source_bytes: usize,
194 pub bytecode_bytes: usize,
196 pub function_count: usize,
198 pub compile_time_ms: u64,
200}
201
202pub fn compile_source(source: &str, options: &CompileOptions) -> Result<CompileResult> {
213 let start = std::time::Instant::now();
214
215 if source.trim().is_empty() {
217 return Err(Error::compilation("empty source"));
218 }
219
220 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
240pub fn compile_file(path: &Path, options: &CompileOptions) -> Result<CompileResult> {
251 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 let source = std::fs::read_to_string(path)?;
262
263 let options = options
265 .clone()
266 .with_source_name(path.display().to_string());
267
268 compile_source(&source, &options)
269}
270
271pub fn validate_bytecode(bytecode: &[u8]) -> Result<Metadata> {
281 if bytecode.len() < 16 {
283 return Err(Error::invalid_bytecode("bytecode too short"));
284 }
285
286 if &bytecode[0..4] != b"FZB\x00" {
288 return Err(Error::invalid_bytecode("invalid magic number"));
289 }
290
291 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 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
313pub fn extract_bytecode_metadata(bytecode: &[u8]) -> Result<Metadata> {
315 validate_bytecode(bytecode)
316}
317
318fn generate_bytecode(source: &str, options: &CompileOptions) -> Result<Vec<u8>> {
321 let mut bytecode = Vec::new();
325
326 bytecode.extend_from_slice(b"FZB\x00");
328
329 bytecode.push(1);
331
332 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 bytecode.extend_from_slice(&[0u8; 10]);
345
346 let hash = simple_hash(source);
348 bytecode.extend_from_slice(&hash.to_le_bytes());
349
350 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 for line in source.lines() {
376 let line = line.trim();
377
378 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 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 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, 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 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 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 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()); assert!(validate_bytecode(b"XXX\x00").is_err()); }
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}