fix: extend a re-opened module instead of discarding the earlier block - #6206
fix: extend a re-opened module instead of discarding the earlier block#6206prql-bot wants to merge 2 commits into
module instead of discarding the earlier block#6206Conversation
prql-bot
left a comment
There was a problem hiding this comment.
Self-review, so this is a COMMENT rather than an approval.
The core change looks right — routing through RootModule::declare puts module on the same insertion path as every other statement kind, and the merge semantics match what insert_stmts_at_path already does across files. cargo test -p prqlc --test integration passes here (490 tests, 5 ignored), including every pre-existing snapshot, so the std placeholder still fills in as before.
Three things:
The residual asymmetry is worth a human call. enum m { Paid = 0 } then module m { let a = 5 } now merges, but the reverse order — module m { let a = 5 } then enum m { Paid = 0 } — still errors with duplicate declarations of m, asserted in enum_duplicate_of_existing_declaration, which this PR leaves in place. So the same two statements now compile or fail depending only on which comes first, and both outcomes are asserted a few lines apart in the same file. An enum builds a module, so if re-opening is the rule then fold_type_def_stmt wants the same treatment; if it isn't, the enum-then-module direction shouldn't merge either. This PR doesn't have to settle it, but it shouldn't stay implicit — happy to follow up whichever way you'd prefer.
Missing changelog entry. development.md asks for a line for any user-facing change, and #6150 — the same class of fix for import — got one under Language. This change adds two new compile errors for programs that previously compiled clean, so it needs one as well.
Some(false) duplicates declare's error. The arm hand-builds duplicate declarations of {ident} with stmt.span, which is byte-for-byte what declare already returns when the name is taken — two copies of one message, free to drift. Collapsing it into the None arm as _ produces the same message and span (the let m = 5 / module m snapshot is unchanged) and drops six lines.
Since this is a bot-authored PR with no human author to apply suggestions, I'm pushing the last two as a follow-up commit; the first is left for a maintainer.
`declare` already returns `duplicate declarations of {ident}` for a name
that's taken, so the explicit `Some(false)` arm was a second copy of the
same message and span.
|
Acknowledged — no further PRs on #6166 without an explicit ask, and that hold is now recorded in the repo's tend guidance rather than only in a thread. (Also answered on #6166, before I'd finished the trace below.) Why the existing guard didn't fire is worth recording, because #6212 as first written wouldn't have fired either. The 2026-08-18 nightly (run
|
|
Filed as max-sixty/tend#1013. Diagnosis, from the nightly's own session log (
Nothing keyed on the code the fix touches, which is the one handle that survives when the issue number doesn't exist yet: |
Nightly's Step 8 dedup fetched only open PRs, and only their titles, so both places a maintainer's rejection is recorded — the closed PR that carried it, and the comments on the issue tracking it — sat outside what the check ever read. A run could re-derive a finding from the code and reopen an approach already turned down, with nothing in context to say otherwise. The searches now live in `running-in-ci` (loaded by nightly, triage, and ci-fix alike, before any `gh pr create`), matched on the symbol or path the change would edit rather than an issue number a re-derived finding doesn't have. Verified with `pre-commit run --files` on both changed files. **This is a query fix, not a judgment rule** — worth stating up front, because #805 proposed the behavioral version of this and you closed it: _"My sense is that the models would see this principle as implicit and not require explicit guidance."_ I think that was right, and it doesn't cover this case. There, the dedup had already surfaced the rejection and the agent acted on it correctly without being told to; the rule was belt-and-suspenders. Here the rejection is never fetched at all, and no amount of inference recovers a fact that isn't in context. So what's added is three `gh` invocations and one sentence on what the result means, not a restatement of a principle the model already applies. <details><summary>What #1013 reported, and why the existing checks miss it</summary> Filed by PRQL's tend bot after a third repeat attempt at the same fix was closed ([PRQL/prql#6206](PRQL/prql#6206)). The prior two artifacts both said "don't fix this": [PRQL/prql#6147](PRQL/prql#6147), closed on the **approach** with the code reviewed as fine, and a note on [PRQL/prql#6166](PRQL/prql#6166) written by an earlier nightly run specifically so the next run would stop. Three gaps, all in the projection rather than the judgment: 1. `--state open` filters out the one state that carries a rejection. A closed PR on the same call site is the strongest available signal, and it's precisely what's excluded. 2. `--json number,title` carries no bodies or comments. The issue's title reads as an ordinary unclaimed bug; the hold is in its comments. A prior run's note was write-only by construction. 3. Matching by title or issue number never connects an attempt that predates the tracking issue. The reporter verified that `gh pr list --state all --search "fold_module_def_stmt"` returns all three attempts while `--search "6166"` omits the rejected one — hence searching by symbol. The pre-`gh pr create` dedup in `running-in-ci` does use `--state all`, so the closed PR is inside the list it returns, but that section is framed entirely around concurrent siblings. A weeks-old closed PR reads as history there, and nothing said to open it and find out why it closed. Per `/tend-ci-runner:triage`'s "Skill text fixes", I checked co-loaded skills first — no existing guidance anywhere in `plugins/` or `.claude/` covers reading a closed PR's rationale, so nothing is duplicated. </details> <details><summary>No regression test</summary> Documentation-only change to two skill files; the repo has no test coverage over skill prose. The `generator/tests` hits on "skills" are regtest outputs asserting skill *names* in generated workflow YAML, unaffected here. `pre-commit run --files` on both files passes trailing-whitespace, end-of-file-fixer, typos, the bang-backtick poison guard, and the install-tend reference-sync hook. </details> --- Closes #1013 — automated triage --------- Co-authored-by: tend-agent <270458913+tend-agent@users.noreply.github.com>
Declaring a
moduletwice at the same level kept only the last block and dropped the earlier one with no diagnostic, somodule m { let a = 5 }followed bymodule m { let b = 6 }leftm.aunresolvable.fold_module_def_stmtwas the one statement kind still inserting straight intoroot_mod.module, which overwrites; every other kind goes throughRootModule::declare. It now extends an existing module instead of replacing it, and reports a collision when the name holds something that isn't a module. Fixes #6166.Merge rather than error is the judgement call here, and it's worth a second opinion. The alternative — rejecting the second block outright, matching
let/type/import/enum— reads as more consistent, but it would make a single file stricter than the multi-file path, whereinsert_stmts_at_pathalready merges same-named module defs across files, and it would need a special case for the emptystdplaceholder thatModule::new_rootseeds and the standard library relies on overwriting. Merging keeps both behaviours consistent with no special case. Either way the silent discard is gone: a name declared by both blocks is now reported asduplicate declarations of m.a.Behaviour, before and after
module m { let a = 5 }module m { let b = 6 }Unknown name m.aenum m { Paid = 0 }module m { let a = 5 }module m { let a = 5 }module m { let a = 6 }duplicate declarations of m.alet m = 5module m { let a = 6 }letsilently droppedduplicate declarations of mThe last two rows are new diagnostics for what previously compiled clean.
Verification
module_reopened_is_extendedinerror_messages.rscovers all four rows; it fails onmainwithUnknown name m.a, which is the symptom reported in #6166. #6164's doc comment pointed at #6166 as the uncovered direction — that note is updated to point at the new test.cargo test -p prqlc -p prqlc-parserpasses (495 integration tests, +1 from this change), as docargo clippy -p prqlc --all-targetsandcargo fmt --check.task prqlc:pull-requestcouldn't run in the tend sandbox —cargo-instaisn't on the PATH there, which is what #6144 is about — so the snapshots here were written from actual compiler output rather than--accept.