fix(checker): track literal character conditions - #445
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
ℹ️ No blocking issues — one edge-case observation inline.
Reviewed changes
- Known-string tracking: new
literal_values.rsmodule addsScope::known_strings+literal_values_unknownwith journal undo (Undo::KnownString/KnownStrings,Mark.literal_values_unknown), sod <- "hello"; if (d)reports RY001 through ordinary assignments and identifier aliases (condition_string_literalnow resolvesExpr::Identvia the scope). - Invalidation points: calls with unknown effects, complex-target writes, binop/S3 dispatch, branch merges (statements, expressions, pipes, cloned-scope), loop entry/exit, closures, and per-name
Scope::insertall discard the facts — soundness holds on every shape I could reach, including short-circuit clone/merge paths, sibling branches, and parenthesized-RHS assignments (which failordinary_assignmentand wipe the map). - Tests/oracle: 5 focused test fns including an exact-count + span-pinned sibling-branch leak test and an oracle fixture pinning R's exact
"argument is not interpretable as logical"error; revertinginfer/mod.rsto base fails the 3 positive tests, so they genuinely pin the fix.
Validated: 769 lib tests pass, clippy -D warnings clean, fmt --check clean. CHANGELOG entry matches the sibling ### Fixed style and cites #425.
openai-compatible/glm-5.3 | 𝕏
There was a problem hiding this comment.
ℹ️ No blocking issues — the one open thread from the prior review remains the only outstanding item.
Reviewed changes
- Guard short-circuit for unrelated assignments: both
known_stringcomputation sites ininfer/mod.rsnow call the cheapcondition_string_literallookup first and only run the expensive guards (ordinary_assignmentsource scan plus twooperator_reboundbinding scans) when a literal candidate exists, with theliteral_values_unknown/effects_unknowncheck hoisted into a.filter. Behavior-preserving —set_known_string's internal guard still re-checks the flags aftertry_assign_value— and the flag-blocked and plain shapes behave identically to the prior commit under CLI probing.
Re-validated on the new head: 769 lib tests pass, clippy -D warnings clean, fmt --check clean. The informational active-binding thread from the prior review (S3-operator/replacement-function route) is unchanged by this commit and remains open for the author to weigh; nothing else new.
openai-compatible/glm-5.3 | 𝕏
There was a problem hiding this comment.
ℹ️ No critical issues — the flagged routes are fixed; one residual invalidation route inline.
Reviewed changes
- Method-dispatch invalidation: new
Scope::invalidate_literal_values_for_dispatchruns atinfer_binopentry (both operands), after operand inference for unary operators, and after base inference for index extraction — invalidating literal facts whenever an operand's class is unknown or non-empty or its mode is opaque/union, so S3 method dispatch (+.flag,[.foo,$.bar, unary-.foo) can no longer leave a later plain assignment tracked. The skip case (provably empty class, concrete mode) matches R, where internal generics only dispatch on objects with a class attribute. - Masked operators and replacement writes: the
Expr::BinOparm now invalidates literal values whenhas_explicit_operator_maskreports a shadowing binding (the same predicate that gatesinfer_custom_operator), andtry_assign_value's complex-target arm sets the literal flag alongside the ops flag before any replacement-function route runs. - Short-circuit merge propagation:
merge_condition_assignmentsnow propagatesliteral_values_unknownfrom the cloned&&/||RHS scope back to the real scope, replacing the previous reliance on unconditional invalidation insidetry_s3_binop_dispatch. - Tests/oracle: new
method_and_replacement_effects_block_later_literal_claimscovers three active-binding-via-method shapes (S3 withclass(d) <-, replacement`activate<-`, purebase::structureclassing) plus a plain-numeric control that keeps the warning, andliteral_conditions_after_methods.Rjoins the oracle fixtures (auto-discovered viaread_dir).
Validated: 770 lib tests pass, clippy --all-targets -D warnings clean, fmt --check clean; reverting this commit's three source files to 72118c5 fails the new test, so it genuinely pins the fix; CLI probes confirm the originally flagged S3 route and the replacement route are now silent while the plain control still warns. The prior review's active-binding thread is addressed and resolved.
openai-compatible/glm-5.3 | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Promise-forcing invalidation:
infer_identifier's parameter/unbound-name arm (mod.rs:1716-1719) now callsinvalidate_literal_values()alongsideinvalidate_ops_environment(), closing the last active-binding route —f <- function(x) { x; e <- "hello"; if (e) 1L }no longer warns when a caller's forced argument can install an active binding. The invalidation is order-sensitive as it should be: an assignment made before the parameter read keeps its fact (f <- function(x) { e <- "hello"; if (e) 1L; x }still warns — probe-verified), and reading an unbound name wipes prior facts, which matches the established"d <- 'hello'; mutate(); if (d)"model (a runtime-resolved name may be an active binding; a genuinely unbound one errors in R, leaving the rest dead and RY010 fired). - Tests/oracle: new
forced_promises_block_later_literal_claimscovers parameter-read, unbound-read, and the oracle fixture — falsifiable (fails withinfer/mod.rsreverted to372dd19), andliteral_conditions_after_promises.Rpins R's acceptance of the default-promise active-binding shape via# oracle: must-pass. The sibling-branch test now bindsflag <- TRUEfirst, so it no longer relies on reading an unknown binding being effect-free.
Validated on the new head: 771 lib tests pass, clippy -p ry-checker --all-targets -- -D warnings clean, fmt --check clean. The open thread from the prior review is addressed and resolved.
openai-compatible/glm-5.3 | 𝕏
sims1253
left a comment
There was a problem hiding this comment.
This PR extends the RY001 character-condition rule from direct string literals to bindings with known values: ordinary <-/= assignments of string literals (or of identifiers already holding a known string — so aliases, backticked names, and chained assignment work) are recorded in a new per-scope known_strings map, and condition_string_literal consults it when classifying if/while/if-expression conditions. The facts participate in the scope journal with LIFO undo, and are deliberately discarded on any unknown call, promise forcing, possible S3 dispatch, non-identifier write, loop entry/exit, branch merge, and deferred execution — sound reasoning, since a call can install an active binding whose setter swallows the later assignment (I verified with Rscript: with makeActiveBinding("e", ...), e <- "hello" succeeds silently and if (e) passes on the getter's value).
Findings
-
[minor]
crates/ry-checker/src/infer/mod.rs:87-93—if (d <- "hello") 1Landwhile (d <- "hello")still pass silently, though R errors withargument is not interpretable as logical(verified with Rscript).condition_string_literalmatchesExpr::String/Expr::Identbut not an expression-position assignment, and the resulting character-scalar type is classified coercible. Since the assign arm (crates/ry-checker/src/infer/mod.rs:1992-2005) already records the fact for a laterif (d)while inferring the condition, recursing into the RHS of anAssignbinop here would close the gap nearly for free. (flag && (d <- "hello")is the same family via the cloned-scope path incrates/ry-checker/src/infer/binop.rs:664-676, which merges only binding types back.) -
[minor]
crates/ry-checker/src/infer/mod.rs:766-767andcrates/ry-checker/src/infer/cloned_scope_reference.rs:61-67— everyifmerge clears all known strings, including names neither branch touches: ind <- "hello"; if (flag) x <- 1L; if (d) 1Lthe second condition stays silent. The journal already records per-nameUndo::KnownStringentries and per-name assignment deltas (the capture loop skips them atcrates/ry-checker/src/scope_journal.rs:447-448), so retaining facts for names with no assignment or fact change in either branch looks feasible. This errs conservative and matches the unchanged 472-package corpus — raising it as a precision opportunity, not a defect. -
[nit]
crates/ry-checker/src/infer/mod.rs:1935-1940—&format!("{symbol}")allocates per BinOp while literal facts are live, and for arithmetic/comparison opsinfer_custom_operator(crates/ry-checker/src/infer/custom_operator.rs:69-80) immediately re-runs the identicalhas_explicit_operator_masklookup with static&strs. A static quoted-symbol match (as there) plus reusing that result would avoid both the allocation and the duplicate lookup. The!scope.literal_values_unknownguard keeps the cost bounded in practice since the first unknown call makes it sticky. -
[question]
crates/ry-checker/src/infer/mod.rs:1995—<<-is excluded from tracking (*op == BinOpKind::Assignonly) and additionally invalidates everything via theordinary_assignmentfailure path. That looks correct (superassign writes an outer frame and<<-can be masked), but the PR description lists only calls/writes/loops/merges — is the blanket invalidation on every<<-intended as part of this change's contract? -
[nit]
crates/ry-checker/src/lib.rs:313-314— the two newScopefields (literal_values_unknown,known_strings) are the only undocumented fields in the struct; the sticky-flag vs. cleared-map distinction is explained in the PR description but deserves a doc comment where the neighboring fields all have one.
Overall
The invalidation lattice is the hard part of this change and it checks out: journal undo for the new entries is LIFO-correct and pairs with the Mark flag restore (crates/ry-checker/src/scope_journal.rs:461-468 and :524), the dispatch gate in crates/ry-checker/src/literal_values.rs:11-18 only skips provably classless operands (S3 Ops dispatch requires a class attribute; environments/S4 are Mode::Opaque, so indexing them invalidates), and the accepted-string boundaries match R behavior I verified with Rscript (if ("NA") and whitespace-padded spellings error, the eight exact spellings pass, NA_character_ correctly deferred to #354). Test coverage — aliases, backticks, chaining, sibling-branch isolation, closures, superassignment, custom <-/+/replacement masks, forced promises, and oracle fixtures asserting exact error messages — is thorough.
This review was generated with AI assistance.

value <- "hello"; if (value) 1Lwas silent even though R rejects the condition. Track decoded character literals through ordinary assignments and identifier aliases, then apply the existing condition check to their values.Keep these facts in the scope journal so branch rollback restores the right value. Writes, loops, branch merges, and deferred execution discard them. Calls with unknown effects also prevent later assignments from claiming a literal value, because a call can install an active binding. Tests cover accepted strings, aliases, backticks, chained assignment, sibling branches, closures, superassignment, and custom binding behavior.
Closes #425. Depends on #442 for the condition family's documented warning severity. Known NA constants remain tracked in #354, as described in #425.
Validation: workspace tests, Clippy with warnings denied, formatting, and the full R oracle pass. The oracle confirms accepted and rebound strings and the exact error for the rejected alias. The fixed 472-package corpus retains all 3,106 baseline diagnostics with no additions or removals; the regression is covered by focused fixtures.
The method-effect follow-up also passes a fresh workspace build, full R oracle, and all eight release performance tests. Its fixed 472-package comparison retains all 3,106 baseline diagnostics, with zero additions and zero removals.