Skip to content

fix: report duplicate declarations from enum definitions - #6164

Merged
kgutwin merged 2 commits into
mainfrom
fix/enum-duplicate-declarations
Aug 10, 2026
Merged

fix: report duplicate declarations from enum definitions#6164
kgutwin merged 2 commits into
mainfrom
fix/enum-duplicate-declarations

Conversation

@prql-bot

@prql-bot prql-bot commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

fold_type_def_stmt builds a module for an enum and pushed it in with Module::insert, which overwrites whatever is already under that name. Every other statement kind goes through RootModule::declare, which reports a duplicate instead. The result was order-dependent: the same pair of statements errored one way round and silently discarded a declaration the other.

enum Status { Paid = 0 }    let Status = 5
let Status = 5              enum Status { Paid = 0 }
→ duplicate declarations    → compiles; `Status` is now the enum

The same shape swallowed a whole module — module m { let a = 5 } followed by enum m { … } compiled clean, and m.a was simply gone. Moving to declare covers all of that, including enum vs enum.

Scope: this changes fold_type_def_stmt only, so the fix is one-directional where a module is the second declaration. enum m { … } followed by module m { … }, and two module m { … } blocks, both still overwrite silently — fold_module_def_stmt keeps its own Module::insert. Filed as #6166 rather than fixed here: the straight swap to declare fails 31 tests with duplicate declarations of std, because Module::new_root pre-seeds a std entry that load_std_lib's module def then collides with, so it needs its own change. fold_import_def_stmt is the third instance and is covered by #6150.

Two smaller silent drops inside the enum's own module, both from the same cause — names is a HashMap, so an insert that collides keeps only the last value:

  • enum Status { Paid = 0, Paid = 1 } compiled, and Status.Paid was 1. Now duplicate declarations of Status.Paid.
  • enum Status { _self = 0 } compiled, and the member vanished — NS_SELF is the key the enum's own type is stored under, and it's written after the members. _self is a legal identifier, so this was reachable from source. The type entry now goes in first and a member of that name is rejected: _self is a reserved name and cannot be an enum member.

enum landed in #6104 and hasn't been in a release, so nothing here changes behavior against a published version — that's also why there's no CHANGELOG entry, since the feature's own entry already covers the release. Happy to add one if you'd rather have it recorded.

The current_module_path push/pop went away because the only thing it fed was Ident::from_path(current_module_path), which reconstructed the ident already passed in; expand_expr is a free function and doesn't consult it.

Verification
  • cargo test -p prqlc -p prqlc-parser — green (lib 78 passed / 1 ignored, tests/integration 486 passed / 5 ignored, parser 97 passed).
  • cargo clippy -p prqlc --all-targets, cargo fmt --check -p prqlc — clean.
  • task prqlc:pull-request couldn't run here — it shells out to cargo insta, which isn't on the sandbox's PATH (ci: expose cargo-insta and cargo-nextest to the tend sandbox #6144). Ran cargo test over the same packages instead.
  • Reverted just stmt.rs with the tests in place: both enum_duplicate_of_existing_declaration and enum_duplicate_member fail, so they do gate the change.
  • Spot-checked that the std-lib enums still resolve — join y (==id) side:left and filter status == InvoiceStatus.Paid compile unchanged.

One rough edge worth flagging: the caret for a member-level error lands on the member's value, not its name (Paid = 0, Paid = 1 underlines the 1). alias is a bare Option<String> on Expr with no span of its own, so the value span is the closest anchor available without changing the AST.

@prql-bot prql-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One gap, and one note for merge ordering.

fold_module_def_stmt still takes the old path, so the enum-vs-module order-dependence only closes in one direction. The third case in enum_duplicate_of_existing_declaration asserts that module m { let a = 5 } followed by enum m { … } now errors. The reverse still compiles clean and silently discards the enum:

$ printf 'enum m { Paid = 0 }\nmodule m { let a = 5 }\nfrom t | filter x == m.Paid' | prqlc compile -
Error:
   ╭─[ :3:22 ]
   │
 3 │ from t | filter x == m.Paid
   │                      ───┬──
   │                         ╰──── Unknown name `m.Paid`

That's the same shape the description calls out — a whole declaration gone with no diagnostic — just with the surviving Module::insert on the other side. In fold_module_def_stmt:

self.root_mod
    .module
    .insert(ident, decl)
    .with_span(stmt.span)?;

Two module m { … } blocks collide the same way: module m { let a = 5 } then module m { let b = 6 } compiles, and m.a is gone. So "moving to declare covers all of it" holds for the let/type/enum axis but not for module defs, and the suite now asserts one direction of a still-symmetric bug. Routing module defs through declare is a separate breaking change and reads like its own PR next to #6150 rather than something to fold in here — but it might be worth a line in the description so the remaining half doesn't look covered.

#6150 edits fold_import_def_stmt in this same file. Different function, complementary change (it also adds a CHANGELOG.md entry, which this one skips on the grounds that enum is unreleased — that reasoning checks out, #6104 is still under [unreleased]). Just worth knowing they're queued together.

What I checked
  • cargo test -p prqlc --lib --tests — green (78 lib / 27 / 486 integration).
  • Enum inside a module still resolves: module m { enum Status { Paid = 0 } } + m.Status.Paidstatus = 0.
  • The _self guard is the right scope — of the NS_* constants, only NS_SELF lands in the enum's own module, so _infer, _param, std, this, that, main are all still usable as member names and resolve correctly (Status._infer → its value).
  • No new panic path: the pre-existing .expect("enum literal tuples should always have all fields with aliases") stays unreachable from source — literal_tuple() in the parser rejects an unaliased member with must specify an alias for this value, and enum_def always wraps a Tuple.
  • declare(ident, …, stmt.id, Vec::new()) builds the same Decl the old Decl { declared_at, kind, ..Default::default() } did (order: 0, empty annotations), with stmt.annotations still on the NS_SELF entry — so no annotation or ordering drift.
  • Dropping the current_module_path push/pop also removes a leak: the old code returned early from expand_expr(expr)? and from the insert without popping.
  • Enum name vs std lib name is unaffected (enum JoinSide { … } compiles) since std sits under its own module; import std.math + enum math { … } is a new error, consistent with #6150's direction.

@prql-bot

prql-bot commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator Author

Confirmed both against 8ab87569enum m { … } then module m { … } drops the enum, and two module m { … } blocks keep only the second (m.b resolves, m.a is gone).

Agreed it's its own change, and there's a concrete reason beyond "breaking": the straight swap to declare in fold_module_def_stmt fails 31 lib tests with duplicate declarations of std, because Module::new_root pre-seeds a std entry and load_std_lib's injected module def then collides with the placeholder. So it needs a way to distinguish filling a placeholder from a genuine source-level duplicate. Filed as #6166 with the repro and that finding.

Scoped the description accordingly, and added the same note to enum_duplicate_of_existing_declaration (0daf6cc3) so the second case doesn't read as covering both directions.

One thing the issue rules out: multi-file projects never reach this path, since insert_stmts_at_path merges a file's statements into an existing module-def stmt of the same name before the resolver runs. Only duplicates written in one source file collide.

@kgutwin
kgutwin merged commit a565284 into main Aug 10, 2026
38 checks passed
@kgutwin
kgutwin deleted the fix/enum-duplicate-declarations branch August 10, 2026 13:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants