Skip to content

Commit 04a5d55

Browse files
committed
Use semantics of if coercion
1 parent c10f0c5 commit 04a5d55

File tree

2 files changed

+23
-36
lines changed

2 files changed

+23
-36
lines changed

crates/ark/src/console_debug.rs

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -744,11 +744,9 @@ mod tests_condition_output {
744744
/// so that typos in conditions cause a visible stop rather than a silently ignored
745745
/// breakpoint.
746746
fn eval_condition(condition: &str, envir: RObject) -> (bool, String, Option<String>) {
747-
// Call `.ark_eval_capture(CONDITION)` which captures warnings via
748-
// `withCallingHandlers` (R defers warnings by default) and returns
749-
// a list of (raw_result, warnings). We coerce to logical separately
750-
// so that coercion warnings are also captured.
751-
let code = format!("base::.ark_eval_capture(as.logical({{ {condition} }}))");
747+
// `if` coerces via `asLogicalNoNA` (not the generic `as.logical`)
748+
// and errors on NA, length != 1, and non-coercible types.
749+
let code = format!("base::.ark_eval_capture(if ({{ {condition} }}) TRUE else FALSE)");
752750

753751
let result = match harp::parse_eval0(&code, envir) {
754752
Ok(val) => val,
@@ -764,24 +762,13 @@ fn eval_condition(condition: &str, envir: RObject) -> (bool, String, Option<Stri
764762
let list_result = harp::list_get(result.sexp, 0);
765763
let list_conditions = harp::list_get(result.sexp, 1);
766764

767-
// Format conditions (warnings and messages) as a single string
768765
let conditions = format_captured_conditions(list_conditions);
769766

770-
let logical_result = RObject::view(list_result);
771-
match Option::<bool>::try_from(logical_result) {
772-
Ok(Some(val)) => (val, conditions, None),
773-
Ok(None) => (
774-
true,
775-
conditions,
776-
Some(String::from("Condition evaluated to NA, stopping")),
777-
),
778-
Err(_) => (
779-
true,
780-
conditions,
781-
Some(String::from(
782-
"Condition did not evaluate to scalar logical, stopping",
783-
)),
784-
),
767+
// `if` guarantees the result is TRUE or FALSE (never NA, never non-scalar),
768+
// so a failed conversion here is unexpected.
769+
match bool::try_from(RObject::view(list_result)) {
770+
Ok(val) => (val, conditions, None),
771+
Err(err) => (true, conditions, Some(format!("Error: {err}"))),
785772
}
786773
}
787774

crates/ark/tests/dap_breakpoints_conditional.rs

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ foo()
238238
/// Test that a condition that calls `stop()` reports the error cleanly.
239239
///
240240
/// The stderr output should show the user's error message without
241-
/// internal wrappers like `as.logical()` or R backtraces.
241+
/// internal wrappers or R backtraces.
242242
#[test]
243243
fn test_dap_conditional_breakpoint_stop_in_condition() {
244244
let frontend = DummyArkFrontend::lock();
@@ -266,19 +266,15 @@ foo()
266266

267267
frontend.recv_iopub_start_debug();
268268

269-
// Should show clean error without `as.logical()` wrapper or backtrace
269+
// Should show clean error without internal wrappers or backtrace
270270
frontend.assert_stream_stderr_contains("```breakpoint");
271271
frontend.assert_stream_stderr_contains("#> stop(\"oops\")");
272272
frontend.assert_stream_stderr_contains("Error: oops");
273273
frontend.assert_stream_stderr_contains("```");
274274

275-
// Must NOT contain internal wrappers
275+
// Must NOT contain backtrace noise
276276
let streams = frontend.drain_streams();
277277
let stderr = streams.stderr();
278-
assert!(
279-
!stderr.contains("as.logical"),
280-
"stderr should not expose as.logical() wrapper, got: {stderr}"
281-
);
282278
assert!(
283279
!stderr.contains("backtrace"),
284280
"stderr should not contain backtrace, got: {stderr}"
@@ -410,8 +406,10 @@ foo()
410406
frontend.recv_shell_execute_reply();
411407
}
412408

413-
/// Test that a condition returning a non-logical value (e.g. a number)
409+
/// Test that a condition returning a non-coercible value (e.g. a string)
414410
/// is treated as TRUE and the breakpoint fires.
411+
///
412+
/// `if` coercion produces R's native "argument is not interpretable as logical" error.
415413
#[test]
416414
fn test_dap_conditional_breakpoint_non_logical_condition_stops() {
417415
let frontend = DummyArkFrontend::lock();
@@ -427,7 +425,7 @@ foo()
427425
",
428426
);
429427

430-
// Condition evaluates to the string "hello", not a logical
428+
// `if ("hello")` errors: argument is not interpretable as logical
431429
let breakpoints = dap.set_conditional_breakpoints(&file.path, &[(3, "'hello'")]);
432430
assert_eq!(breakpoints.len(), 1);
433431

@@ -441,7 +439,7 @@ foo()
441439
frontend.recv_iopub_start_debug();
442440
frontend.assert_stream_stderr_contains("```breakpoint");
443441
frontend.assert_stream_stderr_contains("#> 'hello'");
444-
frontend.assert_stream_stderr_contains("Condition evaluated to NA, stopping");
442+
frontend.assert_stream_stderr_contains("Error: argument is not interpretable as logical");
445443
frontend.assert_stream_stderr_contains("```");
446444
// "Called from:" and "debug at" are filtered from console output
447445
frontend.drain_streams();
@@ -458,7 +456,9 @@ foo()
458456
}
459457

460458
/// Test that a condition returning a value not coercible to logical (e.g. an
461-
/// environment) errors during `as.logical()` and is treated as TRUE.
459+
/// environment) is treated as TRUE.
460+
///
461+
/// `if` coercion produces R's native "argument is of length zero" error.
462462
#[test]
463463
fn test_dap_conditional_breakpoint_non_coercible_condition_stops() {
464464
let frontend = DummyArkFrontend::lock();
@@ -474,7 +474,7 @@ foo()
474474
",
475475
);
476476

477-
// `as.logical(environment())` errors: cannot coerce type 'environment' to logical
477+
// `if (environment())` errors: argument is of length zero
478478
let breakpoints = dap.set_conditional_breakpoints(&file.path, &[(3, "environment()")]);
479479
assert_eq!(breakpoints.len(), 1);
480480

@@ -488,7 +488,7 @@ foo()
488488
frontend.recv_iopub_start_debug();
489489
frontend.assert_stream_stderr_contains("```breakpoint");
490490
frontend.assert_stream_stderr_contains("#> environment()");
491-
frontend.assert_stream_stderr_contains("Error:");
491+
frontend.assert_stream_stderr_contains("Error: argument is of length zero");
492492
frontend.assert_stream_stderr_contains("```");
493493
// "Called from:" and "debug at" are filtered from console output
494494
frontend.drain_streams();
@@ -520,7 +520,7 @@ foo()
520520
",
521521
);
522522

523-
// `0` coerces to FALSE via `as.logical()`
523+
// `0` coerces to FALSE (numeric zero is falsy, same as R's `if`)
524524
let breakpoints = dap.set_conditional_breakpoints(&file.path, &[(3, "0")]);
525525
assert_eq!(breakpoints.len(), 1);
526526
let bp_id = breakpoints[0].id;
@@ -555,7 +555,7 @@ foo()
555555
",
556556
);
557557

558-
// `42` coerces to TRUE via `as.logical()`
558+
// `42` coerces to TRUE (non-zero numeric is truthy, same as R's `if`)
559559
let breakpoints = dap.set_conditional_breakpoints(&file.path, &[(3, "42")]);
560560
assert_eq!(breakpoints.len(), 1);
561561

0 commit comments

Comments
 (0)