Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion flux-ftl/src/bin/flux-mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,9 @@ fn build_tools() -> Vec<Tool> {
"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"]
}),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
133 changes: 130 additions & 3 deletions flux-ftl/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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.
Expand All @@ -125,6 +133,8 @@ pub enum OutputFormat {
ObjectFile,
Assembly,
LlvmIr,
/// LLVM bitcode output (used for LTO pipelines).
Bitcode,
}

/// Successful codegen output.
Expand Down Expand Up @@ -180,6 +190,8 @@ impl Default for CodegenConfig {
target,
opt_level: OptLevel::None,
output_format: OutputFormat::ObjectFile,
emit_debug_info: false,
lto: false,
}
}
}
Expand All @@ -193,6 +205,8 @@ impl CodegenConfig {
target,
opt_level: OptLevel::None,
output_format: OutputFormat::ObjectFile,
emit_debug_info: false,
lto: false,
}
}
}
Expand All @@ -216,8 +230,19 @@ impl OptLevel {
pub fn codegen(program: &Program, config: &CodegenConfig) -> Result<CodegenResult, CodegenError> {
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,
Expand All @@ -231,6 +256,11 @@ pub fn codegen(program: &Program, config: &CodegenConfig) -> Result<CodegenResul
optimizer::optimize_llvm_function(&generator.module, main_fn, llvm_opt_level);
}

// Run LTO (module-level) passes if requested
if config.lto {
generator.run_lto_passes();
}

generator.finish()
}

Expand Down Expand Up @@ -283,7 +313,7 @@ impl<'ctx, 'prog> CodeGenerator<'ctx, 'prog> {
"generic",
"",
config.opt_level.to_inkwell(),
RelocMode::Default,
RelocMode::PIC,
CodeModel::Default,
)
{
Expand Down Expand Up @@ -2114,6 +2144,99 @@ impl<'ctx, 'prog> 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<Module<'_>> = 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
// ------------------------------------------------------------------
Expand All @@ -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()?
}
Expand All @@ -2149,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(|| {
Expand Down
15 changes: 13 additions & 2 deletions flux-ftl/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Comment on lines +207 to +208

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Hallo! Um die Notwendigkeit von #[allow(clippy::too_many_arguments)] zu vermeiden und die Wartbarkeit zu verbessern, könnten Sie die Argumente für den build-Befehl in einer eigenen Struktur zusammenfassen. Dies würde die cmd_build-Funktion übersichtlicher machen und die Kopplung reduzieren.

Sie könnten eine BuildArgs-Struktur definieren und diese im Commands::Build-Enum verwenden:

#[derive(clap::Args, Debug)]
struct BuildArgs {
    file: String,
    #[arg(short, long)]
    output: Option<String>,
    #[arg(long, default_value = "2")]
    opt_level: u8,
    #[arg(long, default_value = "host")]
    target: String,
    #[arg(long)]
    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,
}

#[derive(clap::Subcommand, Debug)]
enum Commands {
    // ... andere Befehle
    Build(BuildArgs),
    // ...
}

Die cmd_build-Funktion würde dann eine Referenz auf BuildArgs entgegennehmen:

fn cmd_build(args: &BuildArgs) -> ExitCode {
    // Verwenden Sie hier args.file, args.opt_level etc.
    // ...
}

Und der Aufruf in main würde entsprechend angepasst werden:

// in main()
match cli.command {
    // ...
    Some(Commands::Build(ref build_args)) => cmd_build(build_args),
    // ...
}

let flux_target = match FluxTarget::parse(target_str) {
Ok(t) => t,
Err(e) => {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
90 changes: 90 additions & 0 deletions flux-ftl/tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment on lines +255 to +256

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Hallo! Für eine robustere Handhabung von temporären Dateien in Tests empfehle ich die Verwendung des tempfile-Crates. NamedTempFile oder tempdir stellen sicher, dass die temporären Dateien auch im Falle eines Panics zuverlässig gelöscht werden, was die Tests stabiler macht.

Dies würde den Code vereinfachen und die manuelle Erstellung und Löschung von Dateipfaden überflüssig machen.

Ein Beispiel, wie dieser Test mit tempfile aussehen könnte:

use tempfile::NamedTempFile;

#[test]
fn build_with_debug_info() {
    let out_file = NamedTempFile::new().expect("failed to create temp file");
    let out_path = out_file.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");

    // out_file und die zugehörige temporäre Datei werden hier automatisch entfernt.
}

Diese Empfehlung gilt auch für den build_with_lto-Test.


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"
);
}
Loading
Loading