-
Notifications
You must be signed in to change notification settings - Fork 97
Support for arbitrary range types #66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
I suppose, you can do it in the same way as Actually, I thought about generic range reflectors. But I postponed it till the main Jinja2 features would be implemented and debugged. And you should always keep in mind objects lifetime and prevent situation you run across recently. With arbitrary/generator that's even easier than with temporary containers. The second point: good implementation of this feature requires refactoring of the current |
I'm not sure, since you're handling the range there by index. The point of lazy ranges is that the iterator is visited only once, with the iteration going from begin to end. I would instead write a generic range for iteration adapter (For the |
Just figured out a way: With range-v3 we could use a transform view to take any range value and transform it into a jinja2::Value: template<typename Range>
ranges::v3::any_view<jinja2::Value> make_type_erased_range(const Range& range)
{
return {range | ranges::v3::transform([](const auto& item)
{
return jinja2::Reflect(item);
})};
} Now you can wrap a |
I see no easy way to avoid this till Jinja2 lists/tuples should support both index-based and range-based access concept. From another point of view, lists in bare Jinja2 could be generator-based (which can cause tricky errors in loops according to their issues tracker). The one of possible ways to allow index-based access only if underlying collection is support it.
You missed the interface definition. :) Personally I tends to implement .Net-based collection access style. I.e. introduce something like |
Here comes the full example of what I think Jinja2Cpp could be capable of with this generic range support: template<typename AstEntity>
using coro_t = boost::coroutine2::coroutine<type_safe::object_ref<const AstEntity>>;
template<typename AstEntity>
using yield_t = coro_t<AstEntity>::push_type;
template<typename AstEntity, typename Predicate>
void visit(yield_t<AstEntity>& yield, const cppast::cpp_entity& root, Predicate predicate)
{
cppast::visit(root, [&yield, predicate](const cppast::cpp_entity& entity, const cppast::cpp_visitor_info& info)
{
if(entity.kind() == AstEntity::kind() && predicate(root, entity, info))
{
yield(type_safe::cref(entity));
}
});
}
for(const auto& class_ : visit<cppast::cpp_class>(parser.parse("foo.hpp"), [](const auto& root, const auto& entity, const auto& info)
{
return entity.name() == "MyClass";
}))
{
std::cout << "Another class named \"MyClass\" found!\n";
}
template<>
struct TypeReflection<cppast::cpp_file>
{
...
{"classes", [](const cppast::cpp_file& file) { return jinja2::Reflect(visit<cppast::cpp_class>(file, ...)); }}
...
}; |
I think that's my main problem, that I've tried to see the full example of how a generic interface could be implemented and used to do TypeReflection for my custom container/range type. I think once I see it in full we will agree 100%.
That's perfectly fine for me, since it's basically a different POV of the same problem. Whether the range is accessed by a pair of iterators or by a Iterable interface is not the issue I think (Both could be translated from one to the other by an adapter). |
After reading your response in #67, now I see that the solution is not TypeReflection but implementing the container interface and specializing the reflector in |
Yep. |
I keep in mind this feature, but due to other activities (and issues) I'm planning to start implementing it not early than the beginning of November. Have to prepare for the conference talk. |
Conference talk? Where and when? |
In Minsk, at the Corehard++. Will introduce my Sutter's metaclasses out-of-compiler implementation. |
Whoa, that's awesome! It's one of the experiments I wanted to do with tinyrefl. Do you have link to the project or something? |
Implementation prototype is here: https://github.com/flexferrum/autoprogrammer/tree/metaclasses/src/generators/metaclasses And here the usage sample: |
I've been reading your sources in depth to try to implement this myself, but I've found that the for evaluation is much more complex than I thought :( |
Honestly, I took this particular part (statements evaluator) from clang constexpr evaluation module. But only this. And as far as I can understand (and clang guys wrote in the docs) it's impossible to implement such kind of interpreter over the libclang-c API. You need access to the full AST, not only reflected (inside libclang-c) and erasured subset. |
In case I would like to implement arbitrary range support myself, could you please provide me with a todo list or something? For me, the module/part most difficult to understand is how the template for statements are evaluated. A full explanation of the process would help a lot :) |
Hm... Ok. I'll try. 'From the stove', as we use to say. :) User-provided lists are described buy the two types: struct ListItemAccessor
{
virtual ~ListItemAccessor() {}
virtual size_t GetSize() const = 0;
virtual Value GetValueByIndex(int64_t idx) const = 0;
}; I.e. simple (primitive) index-based enumerator. After params preprocessing inside Jinja2Cpp both class ListAdapter
{
public:
ListAdapter() {}
explicit ListAdapter(ListAccessorProvider prov) : m_accessorProvider(std::move(prov)) {}
ListAdapter(const ListAdapter&) = default;
ListAdapter(ListAdapter&&) = default;
static ListAdapter CreateAdapter(InternalValueList&& values);
static ListAdapter CreateAdapter(const GenericList& values);
static ListAdapter CreateAdapter(const ValuesList& values);
static ListAdapter CreateAdapter(GenericList&& values);
static ListAdapter CreateAdapter(ValuesList&& values);
ListAdapter& operator = (const ListAdapter&) = default;
ListAdapter& operator = (ListAdapter&&) = default;
size_t GetSize() const
{
if (m_accessorProvider && m_accessorProvider())
{
return m_accessorProvider()->GetSize();
}
return 0;
}
InternalValue GetValueByIndex(int64_t idx) const;
bool ShouldExtendLifetime() const
{
if (m_accessorProvider && m_accessorProvider())
{
return m_accessorProvider()->ShouldExtendLifetime();
}
return false;
}
ListAdapter ToSubscriptedList(const InternalValue& subscript, bool asRef = false) const;
InternalValueList ToValueList() const;
class Iterator;
Iterator begin() const;
Iterator end() const;
private:
ListAccessorProvider m_accessorProvider;
};
https://github.com/flexferrum/Jinja2Cpp/blob/a146f5bdae19d6fe2eff165f03ccf20cde4ddce8/src/internal_value.h#L180
This type has iterator-based access and used in every place where list processing is involved (`for` loop, filters etc.). Type is actually kind of type-erasure and interacts with the underlying list via implementation of the `IListAccessor` interface which is similar to `ListItemAccessor`:
```c++
struct IListAccessor
{
virtual ~IListAccessor() {}
virtual size_t GetSize() const = 0;
virtual InternalValue GetValueByIndex(int64_t idx) const = 0;
virtual bool ShouldExtendLifetime() const = 0;
}; All desired wrappers created here: https://github.com/flexferrum/Jinja2Cpp/blob/a146f5bdae19d6fe2eff165f03ccf20cde4ddce8/src/internal_value.cpp#L193 There is also So, closer to
This is the basic logic. But actual behavior of
So, the full In this line: https://github.com/flexferrum/Jinja2Cpp/blob/a146f5bdae19d6fe2eff165f03ccf20cde4ddce8/src/statements.cpp#L25 input value is converted to the list. If failed, the the input list is preliminary filtered with Here: the That is how I was implemented it. As you see, there could be some problems with moving it to generic iterator/enumerator-based list processing. But in pandor/jinja these problems are solved. What can we do here I'll describe later. |
I see that these are part of Jinja2 standard variables available inside a for block. I will copy the full table here for reference:
|
(updated your comment and put marks into the third column) Ok, let's continue. As far, as I can understand, here is the python implementation of the In particularly, look at this piece of code: def length(self):
if self._length is None:
# if was not possible to get the length of the iterator when
# the loop context was created (ie: iterating over a generator)
# we have to convert the iterable into a sequence and use the
# length of that + the number of iterations so far.
iterable = tuple(self._iterator)
self._iterator = iter(iterable)
iterations_done = self.index0 + 2
self._length = len(iterable) + iterations_done
return self._length I. e. Actually, we can reproduce this functionality, but this will cause a lot of changes in the library core. It needs to:
struct IndexBasedAccessor
{
virtual Value GetValueByIndex(int64_t idx) const = 0;
};
struct Enumerator
{
virtual void Reset() = 0;
virtual bool MoveNext() = 0;
virtual Value GetCurrent() = 0;
};
struct ListItemAccessor
{
virtual ~ListItemAccessor() {}
virtual IndexBasedAccessor* GetIndexer() = 0;
virtual Enumerator* GetEnumerator() = 0;
virtual nonstd::optional<size_t> GetSize() = 0;
}; In this case
That are the minimum set of changes that I see as implementation of the generic ranges. |
some ideas:
I like this approach because every container/range (infinite ranges, vectors, maps, lists, etc) is handled as a generic range, which reduces special casing in the engine code. etc, etc |
@Manu343726 , I agree with you in generally, but disagree in particularly. Unfortunately, form the prospective of the original Jinja2 (and Python as well) dictionaries aren't sequences. For instance, this template: So, I think, we can't mix list's 'indexing' traits and map's 'indexing' traits, and lists and maps as well. It's better to keep then distinguish. That's not good news but it allows us to keep the compatibility with python Jinja2. Completely agree with the "string-range-variables" option. It will help to debug difficult cases. |
Oh, didn't know about that. I was trying to make integration of std and other containers as smooth as possible. So ranges only as iterable sequences, forget maps. |
Me too. Just checked it today. But we can have another option here. This will be a complicated path and (possible) error-prone, but more C++-ish. We can integrate such containers in case of unset 'python-jinja2-compatilibity-mode' option. |
Uh oh!
There was an error while loading. Please reload this page.
first of all, tag this as "feature-request" "discussion" etc, for the first time I'm not coming here with a bug report :P
I've been thinking about reflecting any kind of iterable range type. By range I mean any type where calls to std::begin() and std::end() are valid.
I see two advantages of implementing that interface:
Out of the box support for any container: As long as you're doing a foreach loop (which is the case in most of the template loops) it works. Of course we could provide a fake size() method that does std::distance(), for the uncommon case an user applies a length filter on the range.
Lazy ranges: This could be a game changer. By accepting arbitrary range types users could pass lazy ranges to the template, which means true zero-copy API binding. In my case, with @foonathan's cppast, a coroutine range implementing the AST visit comes to my mind.
The text was updated successfully, but these errors were encountered: