Skip to content

Commit 23543bb

Browse files
committed
feat: add short-circuit for || and &&
1 parent 2e8bd9c commit 23543bb

6 files changed

Lines changed: 361 additions & 7 deletions

File tree

docs/scripting.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,9 @@ let complex = (x + y) / (a - b);
236236

237237
- `&&` (logical AND), `||` (logical OR)
238238
- Operands are treated as booleans with "non-zero is true" semantics
239-
- Current implementation evaluates both sides (no short-circuit yet)
239+
- `||` and `&&` use short-circuit evaluation
240+
- `||`: if LHS is true, RHS is not evaluated
241+
- `&&`: if LHS is false, RHS is not evaluated
240242

241243
Boolean values
242244

docs/zh/scripting.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,9 @@ let complex = (x + y) / (a - b);
288288

289289
- `&&`(逻辑与)、`||`(逻辑或)
290290
- 操作数按“非零为真”处理
291-
- 当前实现为“非短路”:左右两侧都会被求值
291+
- `||``&&` 均采用短路求值:
292+
- `||`:左侧为真则不再计算右侧
293+
- `&&`:左侧为假则不再计算右侧
292294

293295
示例
294296

ghostscope-compiler/src/ebpf/expression.rs

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,161 @@ impl<'ctx> EbpfContext<'ctx> {
7878
}
7979
Expr::SpecialVar(name) => self.handle_special_variable(name),
8080
Expr::BinaryOp { left, op, right } => {
81+
// Implement short-circuit for logical OR (||) and logical AND (&&)
82+
if matches!(op, BinaryOp::LogicalOr) {
83+
// Evaluate LHS to boolean (non-zero => true)
84+
let lhs_val = self.compile_expr(left)?;
85+
let lhs_int = match lhs_val {
86+
BasicValueEnum::IntValue(iv) => iv,
87+
_ => {
88+
return Err(CodeGenError::TypeError(
89+
"Logical OR requires integer operands".to_string(),
90+
))
91+
}
92+
};
93+
let lhs_zero = lhs_int.get_type().const_zero();
94+
let lhs_bool = self
95+
.builder
96+
.build_int_compare(
97+
inkwell::IntPredicate::NE,
98+
lhs_int,
99+
lhs_zero,
100+
"lor_lhs_nz",
101+
)
102+
.map_err(|e| CodeGenError::Builder(e.to_string()))?;
103+
104+
// Prepare control flow blocks
105+
let curr_block = self.builder.get_insert_block().ok_or_else(|| {
106+
CodeGenError::LLVMError("No current basic block".to_string())
107+
})?;
108+
let func = curr_block
109+
.get_parent()
110+
.ok_or_else(|| CodeGenError::LLVMError("No parent function".to_string()))?;
111+
let rhs_block = self.context.append_basic_block(func, "lor_rhs");
112+
let merge_block = self.context.append_basic_block(func, "lor_merge");
113+
114+
// If lhs is true, jump directly to merge (short-circuit)
115+
self.builder
116+
.build_conditional_branch(lhs_bool, merge_block, rhs_block)
117+
.map_err(|e| CodeGenError::LLVMError(e.to_string()))?;
118+
119+
// RHS path: compute boolean only if needed
120+
self.builder.position_at_end(rhs_block);
121+
let rhs_val = self.compile_expr(right)?;
122+
let rhs_int = match rhs_val {
123+
BasicValueEnum::IntValue(iv) => iv,
124+
_ => {
125+
return Err(CodeGenError::TypeError(
126+
"Logical OR requires integer operands".to_string(),
127+
))
128+
}
129+
};
130+
let rhs_zero = rhs_int.get_type().const_zero();
131+
let rhs_bool = self
132+
.builder
133+
.build_int_compare(
134+
inkwell::IntPredicate::NE,
135+
rhs_int,
136+
rhs_zero,
137+
"lor_rhs_nz",
138+
)
139+
.map_err(|e| CodeGenError::Builder(e.to_string()))?;
140+
// Capture the actual block where RHS computation ended
141+
let rhs_end_block = self.builder.get_insert_block().ok_or_else(|| {
142+
CodeGenError::LLVMError("No current basic block after RHS".to_string())
143+
})?;
144+
self.builder
145+
.build_unconditional_branch(merge_block)
146+
.map_err(|e| CodeGenError::LLVMError(e.to_string()))?;
147+
148+
// Merge: phi of i1: true from LHS-true, RHS bool from rhs_block
149+
self.builder.position_at_end(merge_block);
150+
let i1 = self.context.bool_type();
151+
let phi = self
152+
.builder
153+
.build_phi(i1, "lor_phi")
154+
.map_err(|e| CodeGenError::LLVMError(e.to_string()))?;
155+
let one = i1.const_int(1, false);
156+
phi.add_incoming(&[(&one, curr_block), (&rhs_bool, rhs_end_block)]);
157+
return Ok(phi.as_basic_value());
158+
} else if matches!(op, BinaryOp::LogicalAnd) {
159+
// Evaluate LHS to boolean (non-zero => true)
160+
let lhs_val = self.compile_expr(left)?;
161+
let lhs_int = match lhs_val {
162+
BasicValueEnum::IntValue(iv) => iv,
163+
_ => {
164+
return Err(CodeGenError::TypeError(
165+
"Logical AND requires integer operands".to_string(),
166+
))
167+
}
168+
};
169+
let lhs_zero = lhs_int.get_type().const_zero();
170+
let lhs_bool = self
171+
.builder
172+
.build_int_compare(
173+
inkwell::IntPredicate::NE,
174+
lhs_int,
175+
lhs_zero,
176+
"land_lhs_nz",
177+
)
178+
.map_err(|e| CodeGenError::Builder(e.to_string()))?;
179+
180+
// Prepare control flow: if lhs is true, evaluate rhs; else short-circuit to false
181+
let curr_block = self.builder.get_insert_block().ok_or_else(|| {
182+
CodeGenError::LLVMError("No current basic block".to_string())
183+
})?;
184+
let func = curr_block
185+
.get_parent()
186+
.ok_or_else(|| CodeGenError::LLVMError("No parent function".to_string()))?;
187+
let rhs_block = self.context.append_basic_block(func, "land_rhs");
188+
let merge_block = self.context.append_basic_block(func, "land_merge");
189+
190+
// If lhs is true, go compute rhs; else jump to merge with false
191+
self.builder
192+
.build_conditional_branch(lhs_bool, rhs_block, merge_block)
193+
.map_err(|e| CodeGenError::LLVMError(e.to_string()))?;
194+
195+
// RHS path
196+
self.builder.position_at_end(rhs_block);
197+
let rhs_val = self.compile_expr(right)?;
198+
let rhs_int = match rhs_val {
199+
BasicValueEnum::IntValue(iv) => iv,
200+
_ => {
201+
return Err(CodeGenError::TypeError(
202+
"Logical AND requires integer operands".to_string(),
203+
))
204+
}
205+
};
206+
let rhs_zero = rhs_int.get_type().const_zero();
207+
let rhs_bool = self
208+
.builder
209+
.build_int_compare(
210+
inkwell::IntPredicate::NE,
211+
rhs_int,
212+
rhs_zero,
213+
"land_rhs_nz",
214+
)
215+
.map_err(|e| CodeGenError::Builder(e.to_string()))?;
216+
let rhs_end_block = self.builder.get_insert_block().ok_or_else(|| {
217+
CodeGenError::LLVMError("No current basic block after RHS".to_string())
218+
})?;
219+
self.builder
220+
.build_unconditional_branch(merge_block)
221+
.map_err(|e| CodeGenError::LLVMError(e.to_string()))?;
222+
223+
// Merge: phi(i1) with false from LHS=false path, RHS bool from rhs path
224+
self.builder.position_at_end(merge_block);
225+
let i1 = self.context.bool_type();
226+
let phi = self
227+
.builder
228+
.build_phi(i1, "land_phi")
229+
.map_err(|e| CodeGenError::LLVMError(e.to_string()))?;
230+
let zero = i1.const_zero();
231+
phi.add_incoming(&[(&rhs_bool, rhs_end_block), (&zero, curr_block)]);
232+
return Ok(phi.as_basic_value());
233+
}
234+
235+
// Default eager evaluation for other binary ops
81236
let left_val = self.compile_expr(left)?;
82237
let right_val = self.compile_expr(right)?;
83238
self.compile_binary_op(left_val, op.clone(), right_val)

ghostscope-compiler/src/script/ast.rs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -212,19 +212,39 @@ pub fn infer_type(expr: &Expr) -> Result<VarType, String> {
212212
return Ok(VarType::Bool);
213213
}
214214

215-
// Logical operations expect boolean operands and return boolean
215+
// Logical operations return boolean; allow Int literals as truthy (non-zero)
216216
if matches!(*op, BinaryOp::LogicalAnd | BinaryOp::LogicalOr) {
217-
if left_type != VarType::Bool || right_type != VarType::Bool {
218-
return Err("Logical operations require boolean operands".to_string());
217+
match (left_type, right_type) {
218+
(VarType::Bool, VarType::Bool)
219+
| (VarType::Bool, VarType::Int)
220+
| (VarType::Int, VarType::Bool)
221+
| (VarType::Int, VarType::Int) => return Ok(VarType::Bool),
222+
_ => {
223+
return Err("Logical operations require boolean or integer operands"
224+
.to_string())
225+
}
219226
}
220-
return Ok(VarType::Bool);
221227
}
222228

223229
Ok(left_type)
224230
} else {
225231
// If there are variable references, assume type compatibility to let parsing pass
226232
// Actual type checking will be done in code generation phase
227-
Ok(VarType::Int)
233+
if matches!(*op, BinaryOp::LogicalAnd | BinaryOp::LogicalOr)
234+
|| matches!(
235+
*op,
236+
BinaryOp::Equal
237+
| BinaryOp::NotEqual
238+
| BinaryOp::LessThan
239+
| BinaryOp::LessEqual
240+
| BinaryOp::GreaterThan
241+
| BinaryOp::GreaterEqual
242+
)
243+
{
244+
Ok(VarType::Bool)
245+
} else {
246+
Ok(VarType::Int)
247+
}
228248
}
229249
}
230250
}

ghostscope/tests/complex_types_execution.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,87 @@ trace complex_types_program.c:25 {
490490
Ok(())
491491
}
492492

493+
#[tokio::test]
494+
async fn test_or_short_circuit_avoids_null_deref() -> anyhow::Result<()> {
495+
init();
496+
497+
// Start complex_types_program (Debug)
498+
let binary_path =
499+
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
500+
let mut prog = Command::new(&binary_path)
501+
.stdout(Stdio::null())
502+
.stderr(Stdio::null())
503+
.spawn()?;
504+
let pid = prog
505+
.id()
506+
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
507+
tokio::time::sleep(Duration::from_millis(500)).await;
508+
509+
// The RHS would deref c.friend_ref, which is NULL for 'a' iterations.
510+
// Since LHS is true, RHS must not be evaluated and no null-deref error should appear.
511+
let script = r#"
512+
trace update_complex {
513+
print (1 || *c.friend_ref);
514+
}
515+
"#;
516+
517+
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 3, pid).await?;
518+
let _ = prog.kill().await;
519+
assert_eq!(exit_code, 0, "stderr={} stdout={}", stderr, stdout);
520+
521+
assert!(
522+
stdout.contains("true"),
523+
"Expected true result. STDOUT: {}",
524+
stdout
525+
);
526+
assert!(
527+
!stdout.contains("<error: null pointer dereference>"),
528+
"Short-circuit should avoid null-deref RHS. STDOUT: {}",
529+
stdout
530+
);
531+
Ok(())
532+
}
533+
534+
#[tokio::test]
535+
async fn test_and_short_circuit_avoids_null_deref() -> anyhow::Result<()> {
536+
init();
537+
538+
// Start complex_types_program (Debug)
539+
let binary_path =
540+
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
541+
let mut prog = Command::new(&binary_path)
542+
.stdout(Stdio::null())
543+
.stderr(Stdio::null())
544+
.spawn()?;
545+
let pid = prog
546+
.id()
547+
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
548+
tokio::time::sleep(Duration::from_millis(500)).await;
549+
550+
// LHS is false, RHS would deref c.friend_ref which can be NULL. Short-circuit must avoid RHS.
551+
let script = r#"
552+
trace update_complex {
553+
print (0 && *c.friend_ref);
554+
}
555+
"#;
556+
557+
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 3, pid).await?;
558+
let _ = prog.kill().await;
559+
assert_eq!(exit_code, 0, "stderr={} stdout={}", stderr, stdout);
560+
561+
assert!(
562+
stdout.contains("false"),
563+
"Expected false result. STDOUT: {}",
564+
stdout
565+
);
566+
assert!(
567+
!stdout.contains("<error: null pointer dereference>"),
568+
"Short-circuit should avoid null-deref RHS. STDOUT: {}",
569+
stdout
570+
);
571+
Ok(())
572+
}
573+
493574
#[tokio::test]
494575
async fn test_address_of_and_comparisons_local() -> anyhow::Result<()> {
495576
init();

0 commit comments

Comments
 (0)