Skip to content

Implement x4::with_local<T>[...] and x4::with_local<T, ID>[...]#73

Merged
saki7 merged 2 commits intoboostorg:developfrom
saki7:locals
Oct 16, 2025
Merged

Implement x4::with_local<T>[...] and x4::with_local<T, ID>[...]#73
saki7 merged 2 commits intoboostorg:developfrom
saki7:locals

Conversation

@saki7
Copy link
Collaborator

@saki7 saki7 commented Oct 11, 2025

Part of #1

This PR implements x4::with_local<T>[...] and x4::with_local<T, ID>[...], similar to that of Spirit.Qi's qi::locals<...>.

Naming

x4::locals

This is not appropriate.

In Qi era, the name locals was idiomatic solely because it was an extra parameter given to the rule nonterminal:

qi::rule<char const*, qi::locals<char>> r;

When it comes to X4, x4::rule have no place for such parameter, because X4's rule is a more minimalist entity that consists of only the rule's tag type and the attribute type. So it should be implemented as a directive. Therefore, the name "locals" is no longer appropriate and we need to choose the best name.

x4::local

This is OK, but if we carefully think about its semantics then it sounds awkward because it is very vague what "local" it does point to. What I'm trying to say here is that the term "local" itself is vague, but when it's used in a directive, it gets increasingly weird:

auto p = x4::local<int>[
    some_parser
];

What "local int" does it refer to? How can some_parser be relevant to some "local int"? Is the underlying parser "local int"? Does the underlying parser contain some "local int"? Is that an inner variable or an outer variable?

We could insist that it IS a local variable similar to that of Qi, but that's a very arbitrary definition and none of the above questions can be answered clearly.

x4::with_local

This is the preferred naming in this PR.

Context design

Initially I implemented x4::with_local<ID, T>[...], where the ID parameter is mandatory and must be supplied by the users, similar to x4::with<ID>(var)[p]. However, there are many use cases where only a single local variable is needed for grammar definition.

The current design is to default the ID type to x4::contexts::local_var, which refers to always the innermost variable created by potentially nested invocation of with_local parsers. This PR also provides a convenient accessor x4::_local_var(ctx) that fetches the corresponding variable. In this scenario, simply x4::with_local<T>[...] (with ID param omitted) can be utilized.

For more complicated scenario, users can supply arbitrary context types: x4::with_local<T, A_ID>[x4::with_local<U, B_ID>[...]]. This way, any number of local variables can be defined and all of them can be fetched simultaneously by specifying corresponding tag like x4::get<A_ID>(ctx) or x4::get<B_ID>(ctx).

Multiple local variables in a single directive

Multiple local variables in a single directive is not supported in X4 by design.

The old feature in Qi was able to have multiple local variables specified in a single chunk:

qi::rule<char const*, qi::locals<int, double, std::string>> r;

However this required users to access the variable using a surrogate key, i.e., qi::_a, qi::_b, qi::_c, and so on. In my experience, this was an unbearable nightmare because it was very unintuitive to correlate the sequential index within the grammar definition, as the real index is buried deep inside the rule's declaration.

If we keep that design then we would have this in X4:

auto p = x4::with_locals<int, double, std::string>[
    some_directive[
        x4::with_locals<std::vector<float>, std::optional<std::string>, bool>[
            some_parser[([](auto&& ctx) {
                /* ??? */ a = x4::_a(ctx);
                /* ??? */ b = x4::_b(ctx);
                /* ??? */ c = x4::_c(ctx);
            })]
        ]
    ][([](auto&& ctx) {
        /* ??? */ a = x4::_a(ctx);
        /* ??? */ b = x4::_b(ctx);
        /* ??? */ c = x4::_c(ctx);
    })]
];

I consider this very unpractical.

As a solution we can simply supply some predefined struct with arbitrary number of elements:

struct SomeData
{
    int i;
    double d;
    std::string s;
};

auto p = x4::with_local<SomeData>[
    eps[([](auto&& ctx) {
        auto& [i, d, s] = x4::_local_var(ctx);
    })]
];

This way it's much cleaner, and of course, std::tuple<...> can also be used for this purpose.

@saki7 saki7 self-assigned this Oct 11, 2025
@saki7 saki7 added the enhancement New feature or request label Oct 11, 2025
@saki7 saki7 marked this pull request as ready for review October 11, 2025 05:58
@saki7
Copy link
Collaborator Author

saki7 commented Oct 11, 2025

@yaito3014 Could you review this PR?

@saki7 saki7 mentioned this pull request Oct 11, 2025
Copy link
Contributor

@yaito3014 yaito3014 left a comment

Choose a reason for hiding this comment

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

LGTM overall, please just resolve chores on test code.

@saki7 saki7 merged commit 889ee32 into boostorg:develop Oct 16, 2025
13 checks passed
@saki7 saki7 deleted the locals branch October 16, 2025 21:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants