Skip to content

Parser: fully qualify the declarations mentioned in default arguments - #41

Open
Fedr wants to merge 1 commit into
masterfrom
qualify-default-arg-decls
Open

Parser: fully qualify the declarations mentioned in default arguments#41
Fedr wants to merge 1 commit into
masterfrom
qualify-default-arg-decls

Conversation

@Fedr

@Fedr Fedr commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Problem

Stmt::printPretty() prints a DeclRefExpr only with the qualifiers that were written in the source — PrintingPolicy::FullyQualifiedName affects types, not value references. Since the printed default argument is inserted into the generated bindings, which live outside of the original scope, any unqualified name breaks the build of the generated code.

The existing workaround (default_arguments_need_using_namespace + using namespace in the generated wrappers) rescues namespace-scope names, but cannot rescue class-scope names: a using namespace can't re-enter a class scope. Practical example from MeshLib (MeshLib#6507):

class BitSet
{
    static constexpr size_t npos = (size_t)-1;
    IndexType find_next( IndexType n, IndexType upTo = npos ) const; // `npos` emitted unqualified
};

generated MRCMesh/MRBitSet.cpp failed with error: 'npos' was not declared in this scope on every platform, and the header had to spell the default as MR::BitSet::npos to work around it.

Fix

DefaultArgument::as_cpp_expression is now printed via a clang::PrinterHelper hook that prints every DeclRefExpr as the fully qualified name of the referenced declaration (with a leading ::), plus its explicit template arguments if any. The default printing is kept for declarations that can't be named from the global scope:

  • template parameters,
  • function-local declarations (e.g. variables of lambdas written directly in the default argument),
  • anything enclosed in an unnamed scope (anonymous namespaces, unnamed enums/classes) — for the unnamed-enum-constant case the using namespace workaround still applies, so I left default_arguments_need_using_namespace untouched.

original_spelling is intentionally unchanged (still as-written) — it's used for comments and the nullptr-uselessness heuristics.

Drive-by: fixed the newline-removal loop, which did new_str = ' ' (clobbering the accumulated string) instead of new_str += ' '.

Effect on generated code

// before
(n ? *n : static_cast<int>(limit))                    // does not compile without `using namespace`
(b ? ... : static_cast<MR::CSharp::E1>(MR::CSharp::E1::b))
// after
(n ? *n : static_cast<int>(::MR::StaticFuncs::DefaultArgsMentioningMembers::limit))
(b ? ... : static_cast<MR::CSharp::E1>(::MR::CSharp::E1::b))

Tests

Added DefaultArgsMentioningMembers to test/input/MR/test_static_memberr.h and test/input_py/MR/1.h: a static constexpr member, a nested enum constant, and a compound expression (limit + 1 - A::x), all referenced without qualification from default arguments.

Verified locally (Windows, MSYS2 CLANG64, clang 22.1.4):

  • A/B diff of make_c.sh output between stock and patched parser shows only the intended qualification changes (in test_csharp, test_simple_types, test_static_memberr, test_std_optional, test_std_shared_ptr, test_std_unique_ptr sources, same for the fixed-typedefs variants);
  • all changed generated sources compile with clang++ -fsyntax-only -pedantic-errors.

Note: I did not commit regenerated test/output_* goldens — on Windows they come out platform-flavored (e.g. std_vector_long / std_vector_unsigned_long are replaced by long long variants, and the final link of libbleh fails for pre-existing platform reasons), so they need the usual regeneration on Linux. Sorry for the extra step.

Unrelated observation from testing: when the parse ends with a fatal error (e.g. standard headers not found), the parser still proceeds and then dies with Too many template instantiation iterations! on the broken AST — bailing out early on Diags.hasErrorOccurred() would give a clearer failure. Happy to file/fix separately if useful.

Stmt::printPretty() prints DeclRefExprs only with the qualifiers written in
the source (PrintingPolicy::FullyQualifiedName only affects types), so e.g.
a default argument `= npos` written inside `class MR::BitSet` was emitted
as bare `npos` into the generated bindings, which are outside of the
original scope. The existing `using namespace` workaround can't help for
class-scope members like this one.

Now `DefaultArgument::as_cpp_expression` is printed with a PrinterHelper
that replaces every DeclRefExpr with the fully qualified name of the
referenced declaration (`::MR::BitSet::npos`), unless it's a template
parameter, function-local, or enclosed in an unnamed scope, in which cases
the old behavior is kept. `original_spelling` is unchanged (as written).

Also fixes a bug in the newline-removal loop, which replaced the entire
accumulated string with a single space (`new_str = ' '`) instead of
appending it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant