From fb6c20e50c3e546485e138a342d503c3af015f56 Mon Sep 17 00:00:00 2001 From: Marco Doehler Date: Sat, 14 Mar 2026 19:47:42 +0000 Subject: [PATCH 1/2] =?UTF-8?q?Phase=2022:=20Extended=20Codegen=20?= =?UTF-8?q?=E2=80=94=20DWARF=20debug=20info,=20LTO=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add emit_debug_info and lto fields to CodegenConfig (both default false) - DWARF debug info: module flags (Debug Info Version, Dwarf Version), DIBuilder with compile unit, subroutine type, and main subprogram - LTO: module-level IPO passes (inlining, global DCE, constant merge, dead arg elimination, IPSCCP, merge functions, internalize) - Add Bitcode variant to OutputFormat for LTO pipeline integration - CLI: --debug-info and --lto flags on build subcommand - MCP: debug_info and lto parameters on flux_build tool - Tests: 7 new codegen tests, 3 new CLI tests covering all new features Co-Authored-By: Claude Haiku 4.5 --- flux-ftl/src/bin/flux-mcp.rs | 8 +- flux-ftl/src/codegen.rs | 129 +++++++++++++++++++++++++++++++- flux-ftl/src/main.rs | 15 +++- flux-ftl/tests/cli_tests.rs | 90 ++++++++++++++++++++++ flux-ftl/tests/codegen_tests.rs | 108 ++++++++++++++++++++++++++ 5 files changed, 346 insertions(+), 4 deletions(-) diff --git a/flux-ftl/src/bin/flux-mcp.rs b/flux-ftl/src/bin/flux-mcp.rs index b8a9e67..4acb5af 100644 --- a/flux-ftl/src/bin/flux-mcp.rs +++ b/flux-ftl/src/bin/flux-mcp.rs @@ -174,7 +174,9 @@ fn build_tools() -> Vec { "ftl_source": { "type": "string" }, "output_path": { "type": "string", "description": "Path for output executable" }, "target": { "type": "string", "enum": ["host", "x86_64", "aarch64", "riscv64", "wasm32"], "default": "host" }, - "opt_level": { "type": "integer", "enum": [0, 1, 2, 3], "default": 2 } + "opt_level": { "type": "integer", "enum": [0, 1, 2, 3], "default": 2 }, + "debug_info": { "type": "boolean", "description": "Emit DWARF debug information", "default": false }, + "lto": { "type": "boolean", "description": "Enable Link-Time Optimization", "default": false } }, "required": ["ftl_source", "output_path"] }), @@ -379,6 +381,8 @@ fn handle_flux_build(stdout: &std::io::Stdout, id: Value, args: &Value) { let target_str = args.get("target").and_then(|v| v.as_str()).unwrap_or("host"); let opt_level_num = args.get("opt_level").and_then(|v| v.as_u64()).unwrap_or(2) as u8; + let debug_info = args.get("debug_info").and_then(|v| v.as_bool()).unwrap_or(false); + let lto = args.get("lto").and_then(|v| v.as_bool()).unwrap_or(false); let flux_target = match FluxTarget::parse(target_str) { Ok(t) => t, @@ -425,6 +429,8 @@ fn handle_flux_build(stdout: &std::io::Stdout, id: Value, args: &Value) { output_format: OutputFormat::ObjectFile, target_triple: flux_target.resolved_triple(), target: flux_target, + emit_debug_info: debug_info, + lto, }; let cg_result = match codegen::codegen(optimized_ast, &config) { diff --git a/flux-ftl/src/codegen.rs b/flux-ftl/src/codegen.rs index aaf2524..917d5d4 100644 --- a/flux-ftl/src/codegen.rs +++ b/flux-ftl/src/codegen.rs @@ -20,7 +20,11 @@ use std::collections::HashMap; use inkwell::context::Context; -use inkwell::module::Module; +use inkwell::debug_info::{ + AsDIScope, DWARFEmissionKind, DWARFSourceLanguage, DIFlags, DIFlagsConstants, +}; +use inkwell::module::{FlagBehavior, Module}; +use inkwell::passes::PassManager; use inkwell::targets::{ CodeModel, FileType, InitializationConfig, RelocMode, Target, TargetMachine, TargetTriple, }; @@ -108,6 +112,10 @@ pub struct CodegenConfig { pub opt_level: OptLevel, /// Desired output format. pub output_format: OutputFormat, + /// Emit DWARF debug information into the output. + pub emit_debug_info: bool, + /// Enable Link-Time Optimization (LTO) passes. + pub lto: bool, } /// Optimization level mapping. @@ -125,6 +133,8 @@ pub enum OutputFormat { ObjectFile, Assembly, LlvmIr, + /// LLVM bitcode output (used for LTO pipelines). + Bitcode, } /// Successful codegen output. @@ -180,6 +190,8 @@ impl Default for CodegenConfig { target, opt_level: OptLevel::None, output_format: OutputFormat::ObjectFile, + emit_debug_info: false, + lto: false, } } } @@ -193,6 +205,8 @@ impl CodegenConfig { target, opt_level: OptLevel::None, output_format: OutputFormat::ObjectFile, + emit_debug_info: false, + lto: false, } } } @@ -216,8 +230,19 @@ impl OptLevel { pub fn codegen(program: &Program, config: &CodegenConfig) -> Result { let context = Context::create(); let mut generator = CodeGenerator::new(&context, program, config)?; + + // Set up DWARF debug info if requested + if config.emit_debug_info { + generator.setup_debug_info(); + } + generator.emit_program()?; + // Attach debug subprogram to main and finalize debug info + if config.emit_debug_info { + generator.finalize_debug_info(); + } + // Run LLVM optimization passes on the main function if opt_level > 0 let llvm_opt_level = match config.opt_level { OptLevel::None => 0u8, @@ -231,6 +256,11 @@ pub fn codegen(program: &Program, config: &CodegenConfig) -> Result CodeGenerator<'ctx, 'prog> { } } + // ------------------------------------------------------------------ + // DWARF debug info support + // ------------------------------------------------------------------ + + /// Set up module flags required for DWARF debug info emission. + fn setup_debug_info(&self) { + let debug_metadata_version = self.context.i32_type().const_int(3, false); + self.module.add_basic_value_flag( + "Debug Info Version", + FlagBehavior::Warning, + debug_metadata_version, + ); + + let dwarf_version = self.context.i32_type().const_int(4, false); + self.module.add_basic_value_flag( + "Dwarf Version", + FlagBehavior::Warning, + dwarf_version, + ); + } + + /// Create debug subprogram for the main function and finalize debug info. + fn finalize_debug_info(&self) { + let is_optimized = !matches!(self.config.opt_level, OptLevel::None); + + let (dibuilder, compile_unit) = self.module.create_debug_info_builder( + true, + DWARFSourceLanguage::C, + "flux_module.ftl", + ".", + "flux-ftl", + is_optimized, + "", + 0, + "", + DWARFEmissionKind::Full, + 0, + false, + false, + "", + "", + ); + + let subroutine_type = dibuilder.create_subroutine_type( + compile_unit.get_file(), + None, + &[], + DIFlags::PUBLIC, + ); + + let func_scope = dibuilder.create_function( + compile_unit.as_debug_info_scope(), + "main", + None, + compile_unit.get_file(), + 0, + subroutine_type, + true, + true, + 0, + DIFlags::PUBLIC, + is_optimized, + ); + + if let Some(main_fn) = self.module.get_function("main") { + main_fn.set_subprogram(func_scope); + } + + dibuilder.finalize(); + } + + // ------------------------------------------------------------------ + // LTO (Link-Time Optimization) passes + // ------------------------------------------------------------------ + + /// Run module-level LTO optimization passes. + fn run_lto_passes(&self) { + let mpm: PassManager> = PassManager::create(()); + + mpm.add_function_inlining_pass(); + mpm.add_global_dce_pass(); + mpm.add_global_optimizer_pass(); + mpm.add_constant_merge_pass(); + mpm.add_dead_arg_elimination_pass(); + mpm.add_ipsccp_pass(); + mpm.add_strip_dead_prototypes_pass(); + mpm.add_function_attrs_pass(); + mpm.add_merge_functions_pass(); + mpm.add_internalize_pass(true); + + mpm.run_on(&self.module); + } + // ------------------------------------------------------------------ // Finalize: produce IR text and optional object file // ------------------------------------------------------------------ @@ -2123,6 +2246,10 @@ impl<'ctx, 'prog> CodeGenerator<'ctx, 'prog> { let output_bytes = match self.config.output_format { OutputFormat::LlvmIr => llvm_ir.as_bytes().to_vec(), + OutputFormat::Bitcode => { + let buf = self.module.write_bitcode_to_memory(); + buf.as_slice().to_vec() + } OutputFormat::ObjectFile | OutputFormat::Assembly => { self.emit_machine_code()? } diff --git a/flux-ftl/src/main.rs b/flux-ftl/src/main.rs index 0eeb00a..29a2e90 100644 --- a/flux-ftl/src/main.rs +++ b/flux-ftl/src/main.rs @@ -47,6 +47,12 @@ enum Commands { bmc: bool, #[arg(long, default_value = "10")] bmc_depth: u32, + /// Emit DWARF debug information + #[arg(long)] + debug_info: bool, + /// Enable Link-Time Optimization + #[arg(long)] + lto: bool, }, Ir { file: String, @@ -198,7 +204,8 @@ fn cmd_compile(file: &str, output: Option<&str>) -> ExitCode { ExitCode::SUCCESS } -fn cmd_build(file: &str, output: Option<&str>, opt_level: u8, target_str: &str, bmc: bool, bmc_depth: u32) -> ExitCode { +#[allow(clippy::too_many_arguments)] +fn cmd_build(file: &str, output: Option<&str>, opt_level: u8, target_str: &str, bmc: bool, bmc_depth: u32, debug_info: bool, lto: bool) -> ExitCode { let flux_target = match FluxTarget::parse(target_str) { Ok(t) => t, Err(e) => { @@ -272,6 +279,8 @@ fn cmd_build(file: &str, output: Option<&str>, opt_level: u8, target_str: &str, output_format: OutputFormat::ObjectFile, target_triple: flux_target.resolved_triple(), target: flux_target, + emit_debug_info: debug_info, + lto, }; let cg_result = match codegen::codegen(optimized_ast, &config) { @@ -598,7 +607,9 @@ fn main() -> ExitCode { ref target, bmc, bmc_depth, - }) => cmd_build(file, output.as_deref(), opt_level, target, bmc, bmc_depth), + debug_info, + lto, + }) => cmd_build(file, output.as_deref(), opt_level, target, bmc, bmc_depth, debug_info, lto), Some(Commands::Ir { ref file, ref target }) => cmd_ir(file, target), Some(Commands::Generate { ref requirement, diff --git a/flux-ftl/tests/cli_tests.rs b/flux-ftl/tests/cli_tests.rs index 93a2072..42000bc 100644 --- a/flux-ftl/tests/cli_tests.rs +++ b/flux-ftl/tests/cli_tests.rs @@ -245,3 +245,93 @@ fn test_evolve_with_testdata() { "stderr should contain best fitness" ); } + +// --------------------------------------------------------------------------- +// Phase 22: --debug-info and --lto CLI flags +// --------------------------------------------------------------------------- + +#[test] +fn build_with_debug_info() { + let out_path = std::env::temp_dir().join("flux_cli_test_dbg"); + let _ = std::fs::remove_file(&out_path); + + let output = flux_cmd() + .args([ + "build", + "testdata/hello_world.ftl", + "--debug-info", + "-o", + out_path.to_str().unwrap(), + ]) + .output() + .expect("failed to execute"); + + assert!( + output.status.success(), + "build --debug-info failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(out_path.exists(), "executable was not created with --debug-info"); + + // Run the built executable to verify it still works + let run_output = Command::new(&out_path) + .output() + .expect("failed to run built binary"); + let stdout = String::from_utf8_lossy(&run_output.stdout); + assert_eq!(stdout, "Hello World\n"); + + let _ = std::fs::remove_file(&out_path); +} + +#[test] +fn build_with_lto() { + let out_path = std::env::temp_dir().join("flux_cli_test_lto"); + let _ = std::fs::remove_file(&out_path); + + let output = flux_cmd() + .args([ + "build", + "testdata/hello_world.ftl", + "--lto", + "-o", + out_path.to_str().unwrap(), + ]) + .output() + .expect("failed to execute"); + + assert!( + output.status.success(), + "build --lto failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(out_path.exists(), "executable was not created with --lto"); + + // Run the built executable to verify it still works + let run_output = Command::new(&out_path) + .output() + .expect("failed to run built binary"); + let stdout = String::from_utf8_lossy(&run_output.stdout); + assert_eq!(stdout, "Hello World\n"); + + let _ = std::fs::remove_file(&out_path); +} + +#[test] +fn build_help_shows_debug_info_and_lto() { + let output = flux_cmd() + .args(["build", "--help"]) + .output() + .expect("failed to execute"); + + assert!(output.status.success(), "build --help should succeed"); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("--debug-info"), + "help should mention --debug-info" + ); + assert!( + stdout.contains("--lto"), + "help should mention --lto" + ); +} diff --git a/flux-ftl/tests/codegen_tests.rs b/flux-ftl/tests/codegen_tests.rs index c6f4d10..a53f8d6 100644 --- a/flux-ftl/tests/codegen_tests.rs +++ b/flux-ftl/tests/codegen_tests.rs @@ -412,3 +412,111 @@ fn test_ir_host_target() { "IR must contain target triple directive" ); } + +// --------------------------------------------------------------------------- +// Phase 22: Debug Info and LTO tests +// --------------------------------------------------------------------------- + +#[test] +fn test_debug_info_compiles() { + let program = parse_file("testdata/hello_world.ftl"); + let config = CodegenConfig { + output_format: OutputFormat::LlvmIr, + emit_debug_info: true, + ..CodegenConfig::default() + }; + let result = codegen(&program, &config).expect("codegen with debug_info failed"); + assert!( + result.llvm_ir.contains("define i32 @main"), + "IR must define main with debug info enabled" + ); + // Debug info should add !dbg metadata or DICompileUnit + assert!( + result.llvm_ir.contains("!llvm.dbg") || result.llvm_ir.contains("DICompileUnit"), + "IR must contain debug info metadata" + ); +} + +#[test] +fn test_debug_info_object_file() { + let program = parse_file("testdata/hello_world.ftl"); + let config = CodegenConfig { + output_format: OutputFormat::ObjectFile, + emit_debug_info: true, + ..CodegenConfig::default() + }; + let result = codegen(&program, &config).expect("codegen with debug_info to object failed"); + assert!(result.output_bytes.len() > 4, "object file too small"); + assert_eq!( + &result.output_bytes[..4], + b"\x7fELF", + "output must be a valid ELF object file with debug info" + ); +} + +#[test] +fn test_lto_compiles() { + let program = parse_file("testdata/hello_world.ftl"); + let config = CodegenConfig { + output_format: OutputFormat::LlvmIr, + lto: true, + ..CodegenConfig::default() + }; + let result = codegen(&program, &config).expect("codegen with LTO failed"); + assert!( + result.llvm_ir.contains("define i32 @main"), + "IR must define main with LTO enabled" + ); +} + +#[test] +fn test_lto_object_file() { + let program = parse_file("testdata/hello_world.ftl"); + let config = CodegenConfig { + output_format: OutputFormat::ObjectFile, + lto: true, + ..CodegenConfig::default() + }; + let result = codegen(&program, &config).expect("codegen with LTO to object failed"); + assert!(result.output_bytes.len() > 4, "object file too small"); + assert_eq!( + &result.output_bytes[..4], + b"\x7fELF", + "output must be a valid ELF object file with LTO" + ); +} + +#[test] +fn test_bitcode_output() { + let program = parse_file("testdata/hello_world.ftl"); + let config = CodegenConfig { + output_format: OutputFormat::Bitcode, + ..CodegenConfig::default() + }; + let result = codegen(&program, &config).expect("codegen to bitcode failed"); + assert!(!result.output_bytes.is_empty(), "bitcode should not be empty"); + // LLVM bitcode starts with 'BC' magic + assert!( + result.output_bytes.len() > 2 + && (result.output_bytes[0] == b'B' && result.output_bytes[1] == b'C'), + "output must start with BC bitcode magic" + ); +} + +#[test] +fn test_debug_info_and_lto_combined() { + let program = parse_file("testdata/hello_world.ftl"); + let config = CodegenConfig { + output_format: OutputFormat::ObjectFile, + emit_debug_info: true, + lto: true, + ..CodegenConfig::default() + }; + let result = codegen(&program, &config).expect("codegen with debug_info+LTO failed"); + assert!(result.output_bytes.len() > 4, "object file too small"); + assert_eq!( + &result.output_bytes[..4], + b"\x7fELF", + "output must be a valid ELF with debug info and LTO" + ); +} From bd15a86946b42ad0ce44825f16a39c1d1839350a Mon Sep 17 00:00:00 2001 From: Marco Doehler Date: Sat, 14 Mar 2026 19:58:15 +0000 Subject: [PATCH 2/2] Fix: use RelocMode::PIC for PIE-compatible linking on modern Linux Co-Authored-By: Claude Haiku 4.5 --- flux-ftl/src/codegen.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flux-ftl/src/codegen.rs b/flux-ftl/src/codegen.rs index 917d5d4..a86f4a2 100644 --- a/flux-ftl/src/codegen.rs +++ b/flux-ftl/src/codegen.rs @@ -313,7 +313,7 @@ impl<'ctx, 'prog> CodeGenerator<'ctx, 'prog> { "generic", "", config.opt_level.to_inkwell(), - RelocMode::Default, + RelocMode::PIC, CodeModel::Default, ) { @@ -2276,7 +2276,7 @@ impl<'ctx, 'prog> CodeGenerator<'ctx, 'prog> { "generic", "", self.config.opt_level.to_inkwell(), - RelocMode::Default, + RelocMode::PIC, CodeModel::Default, ) .ok_or_else(|| {