Fix analyzer blind spot for constant mutation via add/remove/clear - #674
Conversation
`add <value> to <name>` parses to `AddToListStatement`, which the analyzer never checked for constness, so a program mutating a constant five ways got only four `Cannot modify constant` reports. The same hole covers `remove ... from CONST` and `clear CONST`. Red evidence: `add_to_constant_is_rejected_on_its_own` (0 reports, expected 1), `every_mutation_form_of_a_constant_is_reported` (4, expected 5) and `list_mutation_statements_reject_constant_targets` (0, expected 3) all fail; `mutable_targets_are_still_accepted` already passes and guards the fix against over-reporting. Refs #671 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H9AnrAH4d8sTni73GWgkjR
`add <value> to <name>` parses to `AddToListStatement` rather than
`Assignment` — the target's type is unknown at parse time, so the interpreter
decides between arithmetic and list-append at runtime. The analyzer's
`AddToListStatement` arm only checked that the target name was defined, never
that it was writable, so `add 10 to MAX_SIZE` on a constant produced no
report. A program mutating one constant five ways got four reports instead of
five; the `add` line was the silent one. `remove ... from` and `clear` had the
same hole.
The analyzer now tracks constants explicitly in a `constant_bindings` set of
`SymbolBindingKey`s, populated when a `store new constant` declaration is
defined, and `report_constant_mutation` consults it from all three bare-name
mutation statements. Keying by binding rather than name keeps an inner-scope
shadow from being mistaken for the constant.
`SymbolKind::Variable { mutable: false }` deliberately is NOT the test: action
parameters, container-method parameters, loop variables, try/when error
bindings, predefined globals, and REPL parent-scope variables are all
registered immutable without being constants, and appending to a list
parameter has always been legal.
This also closes a runtime hole for constant lists. A constant number was
already rejected at runtime via `Environment::assign`, but `add`/`remove`/
`clear` on a constant list mutated the `Rc<RefCell<Vec<Value>>>` in place
without ever reassigning the binding, so the constant check was bypassed.
Docs: `Docs/03-language-basics/variables-and-types.md` claimed constants were
"planned for future versions" and `Docs/06-best-practices/naming-conventions.md`
that "true immutability is limited today" — both false since `store new
constant` shipped. Both now document the real syntax and the rejected mutation
forms, with two validated examples registered in the docs-examples manifest.
Verified: `cargo test --all --no-fail-fast` (150 suites, 0 failures), clippy
`-D warnings`, `cargo fmt --check`, 111 `TestPrograms/` programs against the
release binary, and `scripts/validate_docs_examples.py` (21/21).
Fixes #671
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9AnrAH4d8sTni73GWgkjR
📝 WalkthroughWalkthroughThe analyzer now tracks constants by binding identity and rejects scalar and supported list mutations. Documentation and executable examples describe the behavior. Regression tests cover scope propagation and mutable exceptions. CI and repository hygiene checks now preserve file integrity across platforms. ChangesConstant mutation analysis
Repository hygiene and CI setup
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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 |
# Conflicts: # History/dev-diary/2026/2026-07-31-issue-671-constant-mutation-reports.md # TestPrograms/docs_examples/_meta/validation_cache.json
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 29c9ddf46c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| A constant is read exactly like any other variable — the only difference is | ||
| that WFL refuses to modify it. Every mutation form is rejected before the | ||
| program runs, so you get the error at check time, not halfway through a run: |
There was a problem hiding this comment.
Limit the claim to mutation forms actually checked
push with ALLOWED_ROLES and "guest" still passes analysis and mutates the constant list: the analyzer's Statement::PushStatement arm only analyzes its expressions, while the interpreter pushes directly into the list without checking whether its binding is constant. Therefore the new claim that every mutation form is rejected before execution is false; either enforce constness for PushStatement targets or document this exception.
Useful? React with 👍 / 👎.
| A constant is read exactly like any other variable — the only difference is | ||
| that WFL refuses to modify it. Every mutation form is rejected before the | ||
| program runs, so you get the error at check time, not halfway through a run: |
There was a problem hiding this comment.
🟡 Documentation promises constants can never be changed, but one list-modifying statement still changes them
The constants documentation states that WFL refuses every attempt to modify a constant (Docs/03-language-basics/variables-and-types.md:294-296), yet the push with <list> and <value> form still modifies a constant list at run time without any complaint, so readers are told a guarantee the language does not provide.
Impact: A user can follow the documentation, rely on a constant list being unchangeable, and still have it silently modified at run time.
Why the claim is inaccurate: PushStatement bypasses the new analyzer check
The new analyzer check only covers AddToListStatement, RemoveFromListStatement and ClearListStatement (src/analyzer/mod.rs:2444-2484). Statement::PushStatement takes an arbitrary expression target and is only expression-analyzed (src/analyzer/mod.rs:2358-2361), and the interpreter pushes directly into the list's Rc<RefCell<Vec<Value>>> so Environment::assign's constant check never runs. The PR's own dev diary acknowledges this (Dev diary/2026-07-31-issue-671-constant-mutation-reports.md:182-188, "Follow-up not taken").
The same overclaim appears in Docs/06-best-practices/naming-conventions.md:210-212 ("WFL then rejects every attempt to modify it"). CLAUDE.md's mandatory "Docs Must Be Honest" rule requires docs to describe what actually ships and to explicitly mark gaps/planned behavior.
| A constant is read exactly like any other variable — the only difference is | |
| that WFL refuses to modify it. Every mutation form is rejected before the | |
| program runs, so you get the error at check time, not halfway through a run: | |
| A constant is read exactly like any other variable — the only difference is | |
| that WFL rejects the mutation forms below before the program runs, so you get | |
| the error at check time, not halfway through a run: |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/analyzer/mod.rs`:
- Around line 958-963: Update all scope-merge and binding-promotion paths around
the `Symbol` merges at the locations corresponding to lines 1143 and 1219 so
`constant_bindings` markers move from each child-scope key to the promoted
parent-scope key. Preserve the marker when resolving later mutations through the
parent binding, and add a regression program declaring a constant list in both
conditional branches before mutating it after the branch, exercising the actual
scope-merge boundary.
In `@tests/constant_mutation_analyzer_test.rs`:
- Around line 103-106: Update the test’s loop body so the loop variable entry is
the mutation target, while gathered is only the source collection. Iterate over
list values and invoke add, remove, and clear using entry as the target,
ensuring the test verifies loop bindings are excluded from constant_bindings.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: f12fff2a-91e3-44fe-a4a8-5ed2084ca15f
📒 Files selected for processing (9)
Dev diary/2026-07-31-issue-671-constant-mutation-reports.mdDocs/03-language-basics/variables-and-types.mdDocs/06-best-practices/naming-conventions.mdTestPrograms/docs_examples/_meta/manifest.jsonTestPrograms/docs_examples/_meta/validation_cache.jsonTestPrograms/docs_examples/basic_syntax/constants_01.wflTestPrograms/docs_examples/basic_syntax/constants_immutable_01.wflsrc/analyzer/mod.rstests/constant_mutation_analyzer_test.rs
`tests/diagnostics_fixtures_test.rs::constant_mutation_is_rejected_for_every_mutation_form` landed with #672 pinning `>= 4` reports and a comment pointing at #671. With the analyzer gap closed the fixture reports all five, so the assertion is now an exact `== 5`. Also updates the dev diary for the new repository layout: the file moved to `History/dev-diary/2026/`, and the prose now points at `tests/fixtures/diagnostics/constant_immutability.wfl` rather than the retired `syntax_test/` tree. Refs #671 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H9AnrAH4d8sTni73GWgkjR
This comment has been minimized.
This comment has been minimized.
Addresses three review findings on #674. Constant markers survive a branch merge. `IfStatement`/`SingleLineIf` re-define a binding created on both arms into the parent scope under a new binding key, and `pop_scope_promoting_except` does the same for promoted scopes — neither moved the `constant_bindings` entry. The symbol keeps `mutable: false`, so `change` still reported while `add`/`remove`/`clear` went silent: the #671 defect again, one scope up. Both paths now carry the marker, mirroring the existing `mutable: then && else` merge (constant on either arm means constant after). Before the fix, `add 1 to LIMIT` after such a branch escaped analysis entirely and was caught only at runtime; for a constant list it would not have been caught at all. Docs no longer overclaim. "WFL refuses to modify it" / "every mutation form is rejected" is false while `push with <list> and <value>` is unchecked. Both pages now enumerate the forms that are actually checked and carry an explicit known-gap callout pointing at #673, plus a note that a constant fixes the binding rather than the contents reached through an alias. The loop-variable negative test now tests its claim. It previously used `add entry to gathered`, where the loop variable is the value and `gathered` is the target. Rewritten so the loop variable is the mutation target, scoped to the bare-name statements this change touches: `subtract`/`multiply` on a loop variable do report, via the `Assignment` path's long-standing `mutable: false` check, which is pre-existing and out of scope. Also adds a `// CI-SKIP:` directive to `constants_immutable_01.wfl`. The CI "Run WFL Programs" sweep covers `TestPrograms/docs_examples/` (unlike run_integration_tests.sh) and expects exit 0, so the intentional error example failed it. The docs validator still checks it statically and asserts it fails semantic analysis. Refs #671, #673 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H9AnrAH4d8sTni73GWgkjR
|
Round 1 of review addressed in 1. Docs overclaimed — fixed (@chatgpt-codex-connector, @devin-ai-integration)Both correct, and this was the worst of the batch: I filed the
I also added the aliasing caveat, which neither reviewer raised but is the same class of overclaim: 2. Constant marker lost across a branch merge — fixed (@coderabbitai)Confirmed before fixing. On the previous head:
Both merge paths now carry the marker, mirroring the existing 3. Loop-variable test didn't test its claim — fixed (@coderabbitai)Right — Worth recording what that surfaced: with 4. CI:
|
| Job | Failure | Verified pre-existing |
|---|---|---|
Repository Hygiene (ubuntu-arm / windows) |
version-drift: vscode-extension/package-lock.json: 26.7.59 disagrees with Cargo.toml (26.7.60), and on Windows test_archive_valid_manifest_passes sha256 drift on Archive/old/plan.md (CRLF checkout) |
✅ reproduced on a clean origin/main worktree |
Integration Tests (windows) |
HYGIENE-ERROR: Python 3.11+ with tomllib is required — the Windows step uses python, the Linux step python3 |
✅ fails on main |
Run WFL Programs (windows) |
same Windows hygiene precondition | ✅ fails on main |
Happy to take any of those in a separate PR — the lock-file bump and the python→python3 step are both small — but they belong to #672's tooling, not to this change.
Verification on 4cc10ba
cargo test --all --no-fail-fast — 151 suites; cargo clippy --all-targets --all-features -- -D warnings; cargo fmt --all -- --check; CI-style TestPrograms/ sweep 142/0/49; validate_docs_examples.py 21/21.
One flake to flag honestly: interpreter::process_tests::test_capture_process_output failed once under the full parallel run in my sandbox and passed 4/4 in isolation. It is a subprocess-capture test unrelated to this change and green in CI; I did not touch it.
Generated by Claude Code
Review Follow-upThe changes in
No additional code changes were needed, so I did not create a commit or follow-up pull request. Testing
|
Three failures that predate this branch and reproduce on main's own run for 438780a. They are unrelated to #671 but block this PR from going green. 1. Windows jobs had no Python. `integration-tests` and `run-wfl-programs` call scripts/check_repo_hygiene.py as their final step but never ran actions/setup-python, so on Windows they got the runner's bundled `python` (pre-3.11) and died with "HYGIENE-ERROR: Python 3.11+ with tomllib is required" regardless of what the job actually tested — the WFL sweep (141 passed, 0 failed), the docs-example validation (21/21) and the web tests (3/3) had all passed by then. Both jobs now set up Python 3.x, as `repo-hygiene` and `bump-version` already did. Switching Windows to `python3` was the other option and was rejected: on Windows that name can resolve to the Microsoft Store app-execution alias. `nightly.yml` also runs Python on Windows without setup-python, but only calls bump_version.py, which is stdlib-only and needs no tomllib — left alone. 2. Fixture writes were not byte-exact. FixtureTree.write used Path.write_text, which applies the platform newline translation, so on Windows every "\n" landed as "\r\n". test_archive_valid_manifest_passes computes its expected sha256 from the LF form, so it could never match the file the checker hashed — green on Linux, red on Windows. It now writes the caller's exact UTF-8 bytes (which also stops the locale encoding leaking in), with test_fixture_write_is_byte_exact pinning the behaviour. 3. The extension lockfile was stale. vscode-extension/package-lock.json still read 26.7.59 against Cargo.toml's 26.7.60, failing the static gate with "version-drift". bump_version.py already knows to update both mirrors; the 26.7.60 bump landed without it. Set to the values that script would write. Verified: `python3 -m unittest discover -s tests/tooling` 33 passed; `check_repo_hygiene.py --mode static` now exits 0 (was 1 on a clean origin/main worktree). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H9AnrAH4d8sTni73GWgkjR
|
All three reproduce on main's own run for 1. Windows jobs never set up Python
Both jobs now set up Python 3.x, matching what
2.
|
The Windows Repository Hygiene job failed with sha256 drift on all 48 archived files at once. The manifest hashes are computed from the committed (LF) bytes, but check_repo_hygiene.py hashes the file as it sits in the working tree, and the Windows runner checks out with core.autocrlf=true — so every LF became CRLF and every hash missed. Confirmed rather than assumed: re-hashing each file's CRLF-converted bytes reproduces the exact "actual" digests from the CI log. This was latent, not new. The job previously died earlier, at the checker's unit tests; fixing those in the parent commit let it reach the static check and surface the next Windows-only failure behind it. Archived files are immutable blobs pinned by hash, so a root .gitattributes marks Archive/** as `-text`: no end-of-line translation on any platform, making the working tree byte-identical to the commit. Fixing the checkout rather than the checker keeps the hash a pin on repository content instead of on whatever the local platform happened to write, and leaves the checker's semantics alone. `.gitattributes` is added to the root allowlist in .repo-hygiene.toml. It is a legitimate root-level Git config file alongside the already-allowed .gitignore, and root is the only place it applies from — this is a gap in the allowlist, not an exception carved out to silence a violation. Verified by simulating the Windows checkout on Linux — clone with core.autocrlf=true, then run the checker: before: 48 archive-manifest drift violations, exit 1 after: clean, exit 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H9AnrAH4d8sTni73GWgkjR
|
What happened
Confirmed rather than assumed — re-hashing each file's CRLF-converted bytes reproduces the exact digests from the CI log:
This was latent on main, not introduced here: the job used to die earlier, at the unit tests, so it never reached the static check. Fixing those in The fixA root I chose to fix the checkout rather than the checker deliberately. Making the checker normalize line endings before hashing, or hash
Verification — real Red→Green, not a claimThe Windows checkout is reproducible on Linux: clone with Working-tree mode is also clean in that same CRLF checkout, and No Rust changed since Generated by Claude Code |
|
Tip For best results, initiate chat on the files or code changes.
The existing 🐇 ✅ ✏️ Learnings added
If you found this review helpful, would you consider giving us a shout-out on X? Thank you for using CodeRabbit! You are interacting with an AI system. |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
History/dev-diary/2026/2026-07-31-issue-671-constant-mutation-reports.md (1)
126-134: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winRisk classification conflicts with the stated guideline.
The diary reports "Risk class: R1" at Line 166, but the same document states at Lines 132-134 that "The one genuine behavior change is the constant-list case described above, which moves from "silently allowed" to "rejected at analysis time"." This is a backward-compatibility change. The coding guidelines classify backward compatibility changes as R3, not R1. Correct the risk classification or add a justification for why this specific change does not require R3 treatment despite the behavior change.
As per coding guidelines, "Classify risk R0–R3 before implementation; concurrency, cancellation, lifecycle, streaming, untrusted input, crypto/secrets, and backward compatibility are R3."
Also applies to: 166-168
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@History/dev-diary/2026/2026-07-31-issue-671-constant-mutation-reports.md` around lines 126 - 134, The diary’s risk classification conflicts with its documented backward-incompatible constant-list behavior. Update the risk classification at the “Risk class” entry to R3, or add an explicit guideline-based justification if retaining R1; ensure the classification accurately accounts for the change from silently allowing to rejecting constant-list mutation.Source: Coding guidelines
.github/workflows/ci.yml (2)
695-698: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winRun the static check after every retry bump.
The static check runs only before the first push attempt. If a concurrent merge rejects that push, Line 719 creates new generated output and the next iteration pushes it without this check. Run
scripts/check_repo_hygiene.py --mode staticafter each retry bump and before its next push.Proposed fix
git fetch origin "$BRANCH" git reset --hard "origin/$BRANCH" python scripts/bump_version.py --update-all + python scripts/check_repo_hygiene.py --mode static done🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/ci.yml around lines 695 - 698, Move or repeat the static hygiene check from the one-time pre-push step into the retry loop after the retry bump and generated-output update, before every subsequent push attempt. Ensure each iteration running after the conflict-handling logic invokes scripts/check_repo_hygiene.py --mode static immediately before pushing.
49-51: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winUpdate the Python setup action.
actions/setup-python@v4is reported as too old for the GitHub Actions runner. Useactions/setup-python@v5, as this workflow already does in the other jobs.Proposed fix
- - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5#!/bin/bash set -euo pipefail rg -n 'actions/setup-python@v4' .github/workflows actionlint .github/workflows/ci.yml🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/ci.yml around lines 49 - 51, Update the actions/setup-python action version from v4 to v5 in the workflow step shown in the diff. This aligns with the v5 version already in use in other jobs within the same workflow and addresses the compatibility issue with the GitHub Actions runner.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/constant_mutation_analyzer_test.rs`:
- Around line 124-154: Expand constant-mutation regression coverage to exercise
both promotion paths: add a try/when case that declares a constant and mutates
it in finally after pop_scope_promoting_except promotion, and include remove and
clear in the existing branch mutation list. Update the test comment to
accurately describe branch merging via promote_constant_marker and distinguish
it from when-scope promotion.
---
Outside diff comments:
In @.github/workflows/ci.yml:
- Around line 695-698: Move or repeat the static hygiene check from the one-time
pre-push step into the retry loop after the retry bump and generated-output
update, before every subsequent push attempt. Ensure each iteration running
after the conflict-handling logic invokes scripts/check_repo_hygiene.py --mode
static immediately before pushing.
- Around line 49-51: Update the actions/setup-python action version from v4 to
v5 in the workflow step shown in the diff. This aligns with the v5 version
already in use in other jobs within the same workflow and addresses the
compatibility issue with the GitHub Actions runner.
In `@History/dev-diary/2026/2026-07-31-issue-671-constant-mutation-reports.md`:
- Around line 126-134: The diary’s risk classification conflicts with its
documented backward-incompatible constant-list behavior. Update the risk
classification at the “Risk class” entry to R3, or add an explicit
guideline-based justification if retaining R1; ensure the classification
accurately accounts for the change from silently allowing to rejecting
constant-list mutation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 14b88491-3d65-44f0-b7dd-afeab9c5f5a5
⛔ Files ignored due to path filters (1)
vscode-extension/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (11)
.gitattributes.github/workflows/ci.yml.repo-hygiene.tomlDocs/03-language-basics/variables-and-types.mdDocs/06-best-practices/naming-conventions.mdHistory/dev-diary/2026/2026-07-31-issue-671-constant-mutation-reports.mdTestPrograms/docs_examples/basic_syntax/constants_immutable_01.wflsrc/analyzer/mod.rstests/constant_mutation_analyzer_test.rstests/diagnostics_fixtures_test.rstests/tooling/test_check_repo_hygiene.py
🚧 Files skipped from review as they are similar to previous changes (3)
- Docs/03-language-basics/variables-and-types.md
- Docs/06-best-practices/naming-conventions.md
- TestPrograms/docs_examples/basic_syntax/constants_immutable_01.wfl
Round 2 of review. Three of four findings were valid. Test comment was wrong and coverage was thin. The branch test named `pop_scope_promoting_except`, but a `check` merge goes through `promote_constant_marker` — different function. Corrected, and the test now also covers a constant *list* through `add`/`remove`/`clear`, the two forms with no `Assignment` fallback behind them. The other migration site is unreachable, and now says so. The reviewer asked for a try/when regression exercising `pop_scope_promoting_except`. That test cannot be written: a binding declared inside a try statement does not survive it at all — even when declared on every path, a later reference reports `Variable '<name>' is not defined` rather than resolving to a promoted key. Rather than write a test that does not exercise what it claims, this pins the premise (`try_scoped_declarations_do_not_escape_the_statement`) so the migration gets real coverage the day try-scoping changes, and documents at the call site why the code is kept: `constant_bindings` should not silently diverge from the four alias maps migrated for the same keys beside it. `outer_constants_survive_an_intervening_try` covers what a program can actually observe. Risk class R1 was wrong; it is R3. testing.md §5 puts backward compatibility in R3 outright and forbids lowering the class, and this change does alter behavior (`add`/`remove`/`clear` on a constant list: silently allowed → rejected at analysis). The diary now records R3, notes that §11.3 does not apply (no concurrency, lifecycle, streaming, untrusted input, or crypto), and lists the failure-path and negative coverage that does. Also bumps the two remaining `actions/setup-python@v4` uses to v5, matching the jobs fixed in the previous commit and clearing the Node 20 deprecation warning. Not taken: running the static hygiene check inside the bump-version push-retry loop. That job is pre-existing code this branch does not touch, and the concern (a concurrent merge racing the retry) is real but belongs in its own change rather than riding along with an analyzer fix. Verified: 151 test binaries, clippy -D warnings, fmt --check, tooling tests 33 passed, static hygiene exit 0, CI program sweep 142/0/49, docs examples 21/21. Refs #671 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H9AnrAH4d8sTni73GWgkjR
|
1. Test comment was wrong, coverage was thin — fixedCorrect catch. The test named
2. The try/when regression can't be written — and that's the findingYou asked for a Not "Cannot modify constant" — not defined. Same for I'd rather say that than write a test that passes while exercising nothing. What's there instead:
If you'd rather I delete the unreachable migration outright, say so — it's three lines and I have no attachment to it. 3. Risk class R1 → R3 — you're right, I got that wrong
4.
|
#675 landed the same three CI repairs this branch carried, so the merge drops mine in favour of the versions already on main: - ci.yml: main sets up Python 3.12 in the same two jobs. Taken as-is, and the v4→v5 setup-python bump this branch added is dropped with it — v4 works and the bump only silenced a deprecation warning, which is not worth diverging from main for. It can be its own change. - test_check_repo_hygiene.py: main's `write_text(content, newline="\n")` replaces this branch's `write_bytes(content.encode())`. Same effect for the fixtures; main's is already merged. The byte-exactness regression test added here is kept — it is additive and now pins main's fix. - package-lock.json: taken from main, which is at 26.7.61 after the latest bump. The .gitattributes approach is removed entirely. This branch stopped the archive-manifest CRLF drift by checking archived files out verbatim; main instead hashes the staged blob (`git show :path`) in the checker, which fixes the same failure at the other end and is already merged. Keeping both would be redundant, and mine required widening the root allowlist in .repo-hygiene.toml — a change that needed maintainer judgement and is now unnecessary. The allowlist is restored to main's. What remains from this branch is the #671 analyzer fix and its tests, docs, examples, and dev diary, plus one additive tooling test. Verified after the merge: tooling tests 33 passed, static hygiene exit 0, `fmt --check`, and both constant-mutation suites green (9 + 3). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H9AnrAH4d8sTni73GWgkjR
|
Conflicts resolved in What was dropped, and why
Worth noting we independently reached opposite ends of the same fix and both were sound; main's is arguably the better one, since it makes the hash platform-independent by construction rather than relying on checkout attributes staying correct. One thing kept
The PR is now focusedNo CI infrastructure left in the diff — which addresses the scope concern I raised when those changes went in. Verified after the merge151 test binaries · clippy Generated by Claude Code |
Summary
Fixes issue #671: the analyzer failed to report mutations of constants via
add ... to,remove ... from, andclearstatements. These statements parse toAddToListStatement,RemoveFromListStatement, andClearListStatement(notAssignment), and the analyzer never checked their targets for constness. A program mutating a constant five ways (viachange,add,subtract,multiply,divide) received only four error reports.Key Changes
Analyzer constant tracking (
src/analyzer/mod.rs):constant_bindings: HashSet<SymbolBindingKey>to track symbols declared withstore new constant. This is deliberately not themutable: falseflag, which is overloaded — action parameters, container-method parameters, loop variables,try/whenerror bindings, predefined globals, and REPL parent-scope variables are all immutable without being constants. Keyed by binding rather than by name so an inner-scope shadow is not mistaken for the constant.VariableDeclarationwhenis_constantis true.report_constant_mutation()to check bare-name mutation targets against the set and emit the same message as the assignment path, applied to theAddToListStatement,RemoveFromListStatement, andClearListStatementarms.checkis re-defined into the parent scope under a new binding key, sopromote_constant_markermoves the marker with it. Without this,changestill reported whileadd/remove/clearwent silent — Analyzer drops theadd ... to CONSTreport when combined with other constant mutations #671 again, one scope up.Documentation (
Docs/03-language-basics/variables-and-types.md,Docs/06-best-practices/naming-conventions.md):store new constantsyntax.push with <list> and <value>(push with CONST and valuemutates a constant list — expression write-targets escape the constness check #673) and a note that a constant fixes the binding, not the contents reached through an alias. Both were flagged in review as overclaims and corrected.Tests (
tests/constant_mutation_analyzer_test.rs,tests/diagnostics_fixtures_test.rs):addrejection, all five mutation forms, list mutations, branch-merge promotion (scalar and list), and negative assertions for parameters, loop variables, and container members.constant_mutation_is_rejected_for_every_mutation_formlanded with feat: implement the Repository Hygiene and Layout Policy (design 2026-07-28) #672 pinning>= 4reports and a comment pointing at Analyzer drops theadd ... to CONSTreport when combined with other constant mutations #671; with the gap closed it is now an exact== 5.Documentation examples (
TestPrograms/docs_examples/basic_syntax/):constants_01.wfl(executable) andconstants_immutable_01.wfl(error example, carries the// CI-SKIP:directive the CI sweep requires). Both registered in the manifest and validated.Dev Diary (
History/dev-diary/2026/2026-07-31-issue-671-constant-mutation-reports.md).Compatibility
No shipped program mutates a constant. The one genuine behavior change is constant lists:
add/remove/clearon one previously succeeded silently at runtime — the interpreter mutates theRc<RefCell<Vec<Value>>>in place, soEnvironment::assign's constant check was never reached — and is now rejected at analysis time. That is the documented meaning ofconstant, and no shipped example relied on it.Risk class: R3. An earlier revision said R1 on the grounds that this only adds a front-end diagnostic; that was wrong.
testing.md§5 places backward compatibility in R3 outright and forbids lowering the class, and the constant-list change above is a backward-compatibility change. §11.3 does not apply — no concurrency, cancellation, lifecycle, streaming, untrusted input, or crypto. The applicable evidence is failure-path and negative coverage, listed in the dev diary.Testing
Figures are for the current head, after merging
maintwice — #672 (Repository Hygiene and Layout Policy) and then #675 (its CI follow-ups).constant_mutation_analyzer_test.rsadded as a test-only Red commit (1be7188, ancestor of the Green29c9ddf).cargo test --all --no-fail-fast— 151 test binaries, 0 failures.cargo clippy --all-targets --all-features -- -D warnings,cargo fmt --all -- --check.TestPrograms/against the release binary — 142 passed, 0 failed, 49 skipped.python scripts/validate_docs_examples.py --force— 21/21.python3 -m unittest discover -s tests/tooling— 33 passed; hygiene static and working-tree both exit 0.CI is green on
ed3b36d: 17 checks passed, 1 correctly skipped (Bump Versiondoes not run on PRs).Note on scope
Intermediate revisions of this branch also carried CI repairs (Windows
setup-python, the extension lockfile version, and archive-manifest CRLF drift). #675 landed equivalent fixes on main while this was open, so the merge dropped all of them in favour of what is already merged — including removing the.gitattributesapproach this branch used, since main instead hashes the staged blob in the checker. The diff is analyzer-only again.Follow-up filed
#673 —
push with CONST and valuestill mutates a constant list.PushStatementcarries its target as anExpressionrather than a bare name, so the check added here has nothing to resolve; fixing it needs write-target resolution through expressions.https://claude.ai/code/session_01H9AnrAH4d8sTni73GWgkjR
``<img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open in Devin Review">``
Summary by CodeRabbit
New Features
add,remove, andclearmutations across supported scopes, including conditional branches.Bug Fixes
Documentation
Chores