Skip to main content

inauguration/
bytecode_compiler.rs

1use crate::bytecode::{BytecodeModule, Value, module_to_text, text_to_module};
2use crate::parser_registry::{self, ParserCli};
3use crate::sil_to_bytecode;
4use crate::vm::BytecodeVM;
5use std::fs;
6use std::path::Path;
7
8#[derive(Debug, Clone)]
9pub struct BytecodeCompileOutput {
10    pub module: BytecodeModule,
11    pub sil: String,
12    pub identity: crate::core_ir::ModuleIdentityReport,
13}
14
15pub fn compile_source_path(
16    path: &Path,
17    module_id: &str,
18    parser: ParserCli,
19) -> Result<BytecodeCompileOutput, String> {
20    let resolved = parser_registry::resolve_parser_id(path, parser);
21    let Some(mut module) =
22        parser_registry::parse_with_resolved(resolved, path).map_err(|err| err.to_string())?
23    else {
24        return Err("bytecode compiler requires a Core IR frontend; Swift SIL emit is not supported by this path".to_string());
25    };
26    // Desugar classes to structs before typecheck
27    crate::lower_core::desugar_module(&mut module);
28    crate::family_typecheck::typecheck_resolved(&resolved, &module)?;
29
30    if std::env::var("IN_TYPECHECK").is_ok() {
31        let strict = std::env::var("IN_TYPECHECK").as_deref() == Ok("strict");
32        if let Err(errors) = crate::typecheck::TypeChecker::new().check_module(&module) {
33            for err in &errors {
34                eprintln!("[typecheck] {:?}", err);
35            }
36            if strict {
37                return Err("typecheck-failed".to_string());
38            }
39        }
40    }
41
42    let identity = module.identity_report(module_id);
43    let sil = crate::compiler::driver::lower_unified_module(&module, &identity.effective_module_id);
44    let artifact = crate::hybrid_sil::parse_textual_sil(&sil);
45    let mut module = sil_to_bytecode::lower_sil_to_bytecode(&artifact)?;
46    module.identity = Some(identity.clone());
47    module.package_exports = crate::package_runtime::collect_package_exports_for_source(path);
48    Ok(BytecodeCompileOutput {
49        module,
50        sil,
51        identity,
52    })
53}
54
55pub fn write_bytecode_module(module: &BytecodeModule, out_path: &Path) -> Result<(), String> {
56    if let Some(parent) = out_path.parent()
57        && !parent.as_os_str().is_empty()
58    {
59        fs::create_dir_all(parent).map_err(|err| format!("create output dir: {err}"))?;
60    }
61    fs::write(out_path, module_to_text(module)).map_err(|err| format!("write bytecode: {err}"))
62}
63
64pub fn read_bytecode_module(path: &Path) -> Result<BytecodeModule, String> {
65    let text = fs::read_to_string(path).map_err(|err| format!("read bytecode: {err}"))?;
66    text_to_module(&text).map_err(|err| format!("parse bytecode: {err}"))
67}
68
69pub fn run_bytecode_module(module: BytecodeModule) -> Result<Value, String> {
70    let mut vm = BytecodeVM::new(module);
71    vm.run()
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use std::time::{SystemTime, UNIX_EPOCH};
78
79    fn temp_file(name: &str) -> std::path::PathBuf {
80        std::env::temp_dir().join(format!(
81            "inauguration-bytecode-{}-{}-{name}",
82            std::process::id(),
83            SystemTime::now()
84                .duration_since(UNIX_EPOCH)
85                .unwrap()
86                .as_nanos()
87        ))
88    }
89
90    #[test]
91    fn compiles_in_source_to_runnable_bytecode() {
92        let path = temp_file("main.in");
93        fs::write(
94            &path,
95            "fn helper(value: Int) -> Int { return value; }\nfn main() -> Int { return helper(7); }\n",
96        )
97        .unwrap();
98
99        let output = compile_source_path(&path, "App", ParserCli::Auto).unwrap();
100        assert!(
101            output.sil.contains("function_ref @helper"),
102            "{}",
103            output.sil
104        );
105        assert!(
106            output
107                .module
108                .functions
109                .iter()
110                .any(|function| function.name == "main")
111        );
112        assert_eq!(run_bytecode_module(output.module).unwrap(), Value::Int(7));
113
114        fs::remove_file(path).unwrap();
115    }
116
117    #[test]
118    fn compile_output_and_artifact_carry_core_identity_metadata() {
119        let source_path = temp_file("identity.in");
120        let bytecode_path = temp_file("identity.bca");
121        fs::write(
122            &source_path,
123            "package agents.video;\nmodule agents.video.main;\nfn helper(value: Int) -> Int { return value; }\nfn main() -> Int { return helper(9); }\n",
124        )
125        .unwrap();
126
127        let output = compile_source_path(&source_path, "App", ParserCli::Auto).unwrap();
128        assert_eq!(output.identity.package.as_deref(), Some("agents.video"));
129        assert_eq!(output.identity.module.as_deref(), Some("agents.video.main"));
130        assert_eq!(output.identity.requested_module_id, "App");
131        assert_eq!(output.identity.effective_module_id, "agents.video.main");
132        let module_identity = output.module.identity.as_ref().expect("module identity");
133        assert_eq!(module_identity.package.as_deref(), Some("agents.video"));
134        assert_eq!(module_identity.module.as_deref(), Some("agents.video.main"));
135        assert_eq!(module_identity.requested_module_id, "App");
136        assert_eq!(module_identity.effective_module_id, "agents.video.main");
137        assert!(output.sil.contains("sil @helper"), "{}", output.sil);
138        assert!(
139            !output.sil.contains("sil @agents.video.main.helper"),
140            "{}",
141            output.sil
142        );
143        assert_eq!(
144            run_bytecode_module(output.module.clone()).unwrap(),
145            Value::Int(9)
146        );
147
148        write_bytecode_module(&output.module, &bytecode_path).unwrap();
149        let roundtrip = read_bytecode_module(&bytecode_path).unwrap();
150        assert_eq!(roundtrip.entry_point, "main");
151        let roundtrip_identity = roundtrip.identity.as_ref().expect("roundtrip identity");
152        assert_eq!(roundtrip_identity.package.as_deref(), Some("agents.video"));
153        assert_eq!(
154            roundtrip_identity.module.as_deref(),
155            Some("agents.video.main")
156        );
157        assert_eq!(roundtrip_identity.requested_module_id.as_str(), "App");
158        assert_eq!(
159            roundtrip_identity.effective_module_id.as_str(),
160            "agents.video.main"
161        );
162        assert_eq!(run_bytecode_module(roundtrip).unwrap(), Value::Int(9));
163
164        fs::remove_file(source_path).unwrap();
165        fs::remove_file(bytecode_path).unwrap();
166    }
167
168    #[test]
169    fn compiles_in_modulo_to_runnable_bytecode() {
170        let path = temp_file("modulo.in");
171        fs::write(&path, "fn main() -> Int { return 7 % 4; }\n").unwrap();
172
173        let output = compile_source_path(&path, "App", ParserCli::Auto).unwrap();
174        assert_eq!(run_bytecode_module(output.module).unwrap(), Value::Int(3));
175
176        fs::remove_file(path).unwrap();
177    }
178
179    #[test]
180    fn compiles_javascript_class_method_to_runnable_bytecode() {
181        let path = temp_file("counter.js");
182        fs::write(
183            &path,
184            r#"
185class Counter {
186  value = 0;
187  constructor(start) {
188    this.value = start;
189  }
190  inc() {
191    return this.value + 1;
192  }
193}
194function answer() {
195  const c = new Counter(41);
196  return c.inc();
197}
198function main() {
199  return answer();
200}
201"#,
202        )
203        .unwrap();
204
205        let output = compile_source_path(&path, "App", ParserCli::Auto).unwrap();
206        assert_eq!(run_bytecode_module(output.module).unwrap(), Value::Int(42));
207
208        fs::remove_file(path).unwrap();
209    }
210
211    #[test]
212    fn compiles_typescript_class_method_to_runnable_bytecode() {
213        let path = temp_file("counter.ts");
214        fs::write(
215            &path,
216            r#"
217class Counter {
218  value: number;
219  constructor(start: number) {
220    this.value = start;
221  }
222  inc(): number {
223    return this.value + 1;
224  }
225}
226function answer(): number {
227  const c = new Counter(41);
228  return c.inc();
229}
230function main(): number {
231  return answer();
232}
233"#,
234        )
235        .unwrap();
236
237        let output = compile_source_path(&path, "App", ParserCli::Auto).unwrap();
238        assert_eq!(run_bytecode_module(output.module).unwrap(), Value::Int(42));
239
240        fs::remove_file(path).unwrap();
241    }
242
243    #[test]
244    fn rejects_unresolved_identifier_before_lowering() {
245        let path = temp_file("unresolved-ident.in");
246        fs::write(&path, "fn main() -> Int { return missing; }\n").unwrap();
247
248        let err = compile_source_path(&path, "App", ParserCli::Auto)
249            .expect_err("unresolved identifiers should fail before bytecode lowering");
250        assert!(
251            err.contains("unresolved identifier `missing` in `main`"),
252            "unexpected error: {err}"
253        );
254
255        fs::remove_file(path).unwrap();
256    }
257
258    #[test]
259    fn writes_and_reads_bytecode_artifact() {
260        let source_path = temp_file("artifact.in");
261        let bytecode_path = temp_file("artifact.bca");
262        fs::write(&source_path, "fn main() -> void {}\n").unwrap();
263
264        let output = compile_source_path(&source_path, "App", ParserCli::Auto).unwrap();
265        write_bytecode_module(&output.module, &bytecode_path).unwrap();
266        let roundtrip = read_bytecode_module(&bytecode_path).unwrap();
267        assert_eq!(roundtrip.entry_point, output.module.entry_point);
268        run_bytecode_module(roundtrip).unwrap();
269
270        fs::remove_file(source_path).unwrap();
271        fs::remove_file(bytecode_path).unwrap();
272    }
273
274    #[test]
275    fn bytecode_artifact_preserves_string_literals_with_spaces() {
276        let source_path = temp_file("string-space.in");
277        let bytecode_path = temp_file("string-space.bca");
278        fs::write(
279            &source_path,
280            "fn main() -> String { return \"hello world\"; }\n",
281        )
282        .unwrap();
283
284        let output = compile_source_path(&source_path, "App", ParserCli::Auto).unwrap();
285        assert_eq!(
286            run_bytecode_module(output.module.clone()).unwrap(),
287            Value::String("hello world".to_string())
288        );
289        write_bytecode_module(&output.module, &bytecode_path).unwrap();
290        let roundtrip = read_bytecode_module(&bytecode_path).unwrap();
291        assert_eq!(
292            run_bytecode_module(roundtrip).unwrap(),
293            Value::String("hello world".to_string())
294        );
295
296        fs::remove_file(source_path).unwrap();
297        fs::remove_file(bytecode_path).unwrap();
298    }
299
300    #[test]
301    fn compiles_array_literal_and_index_access() {
302        let path = temp_file("array-index.in");
303        fs::write(
304            &path,
305            "fn main() -> Int { let xs: [Int] = [2, 5, 8]; return xs[1]; }\n",
306        )
307        .unwrap();
308
309        let output = compile_source_path(&path, "App", ParserCli::Auto).unwrap();
310        assert_eq!(run_bytecode_module(output.module).unwrap(), Value::Int(5));
311
312        fs::remove_file(path).unwrap();
313    }
314
315    #[test]
316    fn compiles_array_index_assignment() {
317        let path = temp_file("array-index-assign.in");
318        fs::write(
319            &path,
320            "fn main() -> Int { let xs: [Int] = [2, 5, 8]; xs[1] = 9; return xs[1]; }\n",
321        )
322        .unwrap();
323
324        let output = compile_source_path(&path, "App", ParserCli::Auto).unwrap();
325        assert_eq!(run_bytecode_module(output.module).unwrap(), Value::Int(9));
326
327        fs::remove_file(path).unwrap();
328    }
329
330    #[test]
331    fn compiles_struct_method_call() {
332        let path = temp_file("method-call.in");
333        fs::write(
334            &path,
335            r#"
336struct Point {
337  Int x
338  Int y
339
340  fn sum() -> Int {
341    return self.x + self.y;
342  }
343}
344
345fn main() -> Int {
346  let p: Point = Point { x: 2, y: 5 };
347  return p.sum();
348}
349"#,
350        )
351        .unwrap();
352
353        let output = compile_source_path(&path, "App", ParserCli::Auto).unwrap();
354        assert_eq!(run_bytecode_module(output.module).unwrap(), Value::Int(7));
355
356        fs::remove_file(path).unwrap();
357    }
358
359    #[test]
360    fn branch_assignment_updates_variable_value() {
361        let path = temp_file("branch-assign.in");
362        fs::write(
363            &path,
364            "fn main() -> Int { let x: Int = 0; if true { x = 9; } return x; }\n",
365        )
366        .unwrap();
367
368        let output = compile_source_path(&path, "App", ParserCli::Auto).unwrap();
369        assert_eq!(run_bytecode_module(output.module).unwrap(), Value::Int(9));
370
371        fs::remove_file(path).unwrap();
372    }
373
374    #[test]
375    fn while_loop_updates_variable_until_condition_is_false() {
376        let path = temp_file("while-loop.in");
377        fs::write(
378            &path,
379            "fn main() -> Int { let x: Int = 0; while x < 3 { x = x + 1; } return x; }\n",
380        )
381        .unwrap();
382
383        let output = compile_source_path(&path, "App", ParserCli::Auto).unwrap();
384        assert_eq!(run_bytecode_module(output.module).unwrap(), Value::Int(3));
385
386        fs::remove_file(path).unwrap();
387    }
388
389    #[test]
390    fn while_loop_skips_body_when_condition_is_false() {
391        let path = temp_file("while-skip.in");
392        fs::write(
393            &path,
394            "fn main() -> Int { let x: Int = 5; while x < 3 { x = x + 1; } return x; }\n",
395        )
396        .unwrap();
397
398        let output = compile_source_path(&path, "App", ParserCli::Auto).unwrap();
399        assert_eq!(run_bytecode_module(output.module).unwrap(), Value::Int(5));
400
401        fs::remove_file(path).unwrap();
402    }
403
404    #[test]
405    fn match_statement_selects_single_arm() {
406        let path = temp_file("match.in");
407        fs::write(
408            &path,
409            r#"
410fn choose(tag: Int) -> Int {
411  let out: Int = 0;
412  match tag {
413    1 { out = 10; }
414    _ { out = 20; }
415  }
416  return out;
417}
418fn main() -> Int { return choose(1); }
419"#,
420        )
421        .unwrap();
422
423        let output = compile_source_path(&path, "App", ParserCli::Auto).unwrap();
424        assert_eq!(run_bytecode_module(output.module).unwrap(), Value::Int(10));
425
426        fs::remove_file(path).unwrap();
427    }
428
429    #[test]
430    fn string_equality_compares_string_values() {
431        let path = temp_file("string-eq.in");
432        fs::write(
433            &path,
434            "fn main() -> Bool { return \"left\" == \"right\"; }\n",
435        )
436        .unwrap();
437
438        let output = compile_source_path(&path, "App", ParserCli::Auto).unwrap();
439        assert_eq!(
440            run_bytecode_module(output.module).unwrap(),
441            Value::Bool(false)
442        );
443
444        fs::remove_file(path).unwrap();
445    }
446
447    #[test]
448    fn struct_field_access_returns_scalar() {
449        let path = temp_file("struct-field.in");
450        fs::write(
451            &path,
452            "struct Point { Int x; Int y }\nfn main() -> Int { let p: Point = Point { x: 2, y: 5 }; return p.y; }\n",
453        )
454        .unwrap();
455
456        let output = compile_source_path(&path, "App", ParserCli::Auto).unwrap();
457        assert_eq!(run_bytecode_module(output.module).unwrap(), Value::Int(5));
458
459        fs::remove_file(path).unwrap();
460    }
461
462    #[test]
463    fn direct_struct_field_access_returns_scalar() {
464        let path = temp_file("direct-struct-field.in");
465        fs::write(
466            &path,
467            "struct Point { Int x; Int y }\nfn main() -> Int { return Point { x: 2, y: 5 }.y; }\n",
468        )
469        .unwrap();
470
471        let output = compile_source_path(&path, "App", ParserCli::Auto).unwrap();
472        assert_eq!(run_bytecode_module(output.module).unwrap(), Value::Int(5));
473
474        fs::remove_file(path).unwrap();
475    }
476
477    #[test]
478    fn runs_string_bool_and_if_return_bytecode() {
479        let path = temp_file("agent.in");
480        fs::write(
481            &path,
482            "fn ready(flag: Bool) -> String { if flag { return \"ready\"; } return \"no\"; }\nfn main() -> String { return ready(true); }\n",
483        )
484        .unwrap();
485
486        let output = compile_source_path(&path, "App", ParserCli::Auto).unwrap();
487        assert_eq!(
488            run_bytecode_module(output.module).unwrap(),
489            Value::String("ready".to_string())
490        );
491
492        fs::remove_file(path).unwrap();
493    }
494}