diff --git a/e2e-tests/tests/script_execution.rs b/e2e-tests/tests/script_execution.rs index a118b45..fbc3cc3 100644 --- a/e2e-tests/tests/script_execution.rs +++ b/e2e-tests/tests/script_execution.rs @@ -882,6 +882,36 @@ trace calculate_something { Ok(()) } +#[tokio::test] +async fn test_top_level_non_trace_statement_is_rejected() -> anyhow::Result<()> { + init(); + ensure_global_cleanup_registered(); + + let script_content = r#" +print "orphan output"; +"#; + + println!("=== Top-level Non-trace Statement Test ==="); + + let (exit_code, stdout, stderr) = run_ghostscope_with_script(script_content, 5).await?; + + println!("Exit code: {exit_code}"); + println!("STDOUT: {stdout}"); + println!("STDERR: {stderr}"); + println!("=========================================="); + + assert_ne!( + exit_code, 0, + "top-level non-trace statements should fail. stdout={stdout} stderr={stderr}" + ); + assert!( + stderr.contains("top-level") && stderr.contains("trace"), + "expected a top-level trace error. stderr={stderr} stdout={stdout}" + ); + + Ok(()) +} + #[tokio::test] async fn test_format_mismatch() -> anyhow::Result<()> { init(); diff --git a/ghostscope-compiler/src/script/compiler.rs b/ghostscope-compiler/src/script/compiler.rs index 8d0a9fc..ca06688 100644 --- a/ghostscope-compiler/src/script/compiler.rs +++ b/ghostscope-compiler/src/script/compiler.rs @@ -114,6 +114,12 @@ impl<'a> AstCompiler<'a> { // AST will be saved immediately when we know the target details in generate_ebpf_for_target + if program.statements.is_empty() { + return Err(CompileError::Other( + "script must contain at least one top-level trace statement".to_string(), + )); + } + // Single-pass traversal: process each statement immediately // Continue processing even if some trace points fail let mut successful_trace_points = 0; @@ -195,8 +201,9 @@ impl<'a> AstCompiler<'a> { } } _ => { - warn!("Skipping non-trace statement: {:?}", stmt); - // TODO: Non-trace statements are ignored in current implementation + let message = Self::top_level_statement_error(stmt); + error!("{message}"); + return Err(CompileError::Other(message)); } } } @@ -263,6 +270,21 @@ impl<'a> AstCompiler<'a> { .filter(|path| !path.is_empty()) } + fn top_level_statement_error(statement: &Statement) -> String { + let kind = match statement { + Statement::Print(_) => "print", + Statement::Backtrace => "backtrace", + Statement::Expr(_) => "expression", + Statement::VarDeclaration { .. } | Statement::AliasDeclaration { .. } => "let", + Statement::If { .. } => "if", + Statement::Block(_) => "block", + Statement::TracePoint { .. } => "trace", + }; + format!( + "top-level {kind} statement is not allowed in a script file; put executable statements inside a trace block, for example: trace {{ ... }}" + ) + } + /// Process a trace point: resolve target + generate eBPF in one step fn process_trace_point( &mut self,