Edge Rewrite
// HTMLRewriter · presentation

This page was redesigned at the edge.

Cloudflare fetched the original article and streamed it through HTMLRewriter to apply an entirely new visual system without rebuilding the source page.

// request.cf · coarse context

A page that knows where it met you.

Only coarse request metadata is shown. This demo does not display or persist visitor IP addresses.

Country
US
Cloudflare location
CMH
Connection
HTTP/2
Language
Not provided

Ray ID: a22b30488e507fd8

Jump to content

Module talk:Sandbox/Grufo

Page contents not supported in other languages.
Add topic
From Wikipedia, the free encyclopedia
Latest comment: 9 days ago by Grufo in topic newChild emulation

frameSpoof

[edit]

@Uzume: Thank you for your work on this. It looks really promising. I am still very interested in it, and perhaps we can think about implementing it. I also had an alternative idea a few weeks ago for working around the 100 child frames limitation, although I have not yet had time to test it. It is also much more primitive than your solution; it is essentially just a “divide and conquer” idea. But maybe it can give you further thoughts.

As I understand it, the limitation is per module: if Module:A calls 99 modules using frame:newChild(), then each of those 99 modules can, in turn, call up to 99 modules themselves (yielding 9801 modules), and so on. Assuming this understanding is correct, one possible mechanism would be the following. Suppose the user asks Params to call Module:XXX a very large number of times; then:

  1. Params calls Module:XXX 98 times (using frame:newChild() 98 times).
  2. Params then calls Module:Params/helper once (again via frame:newChild()); Params/helper, in turn, calls Module:XXX another 98 times.
  3. Params/helper then calls Params/helper (itself) once (also using frame:newChild())—I don't know whether this would run into problems related to module recursion or scoping (I suspect it might); in that case we should imagine using Params/helper2—which in turn calls Module:XXX 98 times (and calls frame:newChild() as many times).
  4. And so on.

In this way we would effectively process the calls in blocks of 98 newChild() invocations at a time. It would obviously need to be tested, since I may be making one or more incorrect assumptions about how Scribunto accounts for child frames.

In general, though, I would still prefer your solution, since it has already proved to be faster than frame:newChild().

Happy hacking! --Grufo (talk) 22:18, 27 June 2026 (UTC)Reply

@Grufo: Something like that might be possible. The way I see it, it isn't specifically a per module limitation, however, I believe the issue stems from the fact that there is no actual method to free a frame. frame:newChild creates a new child frame but from Scribunto Lua there is no method to tell the system you are now done with the aforementioned child frame. That said, I believe the system will cleanup everything including the created child frames when the current frame is exited by returning from the #invoke. What you could perhaps do is to craft a new call to #invoke to a helper module that does a smaller limited portion of the full workload (say limited to 50 iterations using frame:newChild) and call that off the parent frame repeatedly with frame:callParserFunction('#invoke', args) so it looks like it was directly called from the parent template. This would in turn allow the helper module to create new child frames off its same parent that should allow it to emulate #invoke calls all from the same parent template frame. If you constructed it correctly, you could accept jobs that appeared to require many hundreds of such new child frames by farming them out to many smaller jobs that did portions of the job. Now, I know there is a significant overhead to calling #invoke otherwise you would just consider directly calling it each time for each iteration (which in someways does exactly what I specified above just with one invocation per iteration instead of 50 or something) but batching them should significantly reduce the overhead. No computer system is infinite but you might be able to stretch the resources by using them in a smarter fashion. Probably the most straightforward means to crafting this helper module interface would be to encapsulate all the arguments needed for 50 #invoke calls and pass that to the helper module that would then emulate each call. Then you can call the helper module an arbitrary number of times to get as many iterations as you wanted within the boundaries of system resources. Frankly I would not limit how many iterations the helper module accepted and just control that from the caller's side. This way the helper module could just remain somewhat dumb, parsing out packets of #invoke arguments passed to it and emulating them with frame:newChild while its caller could craft the packets and dividing up bigger jobs into multiple calls of the helper. Now that I think about it, a single module could be created to do all this work: the caller code could be a Scribunto library package while #invoke calls to the same module could handle the dumb client emulations. Putting it all in a single module helps guarantee the caller and client side always match. It is an interesting problem set. —Uzume (talk) 03:16, 28 June 2026 (UTC)Reply
@Uzume: What you just described does seem to work. The following example tells Params to invoke itself 200 times using frame:callParserFunction('#invoke', args), and in each of those 200 invocations the child Params process invokes {{#invoke:string|len}} 99 times using frame:newChild(). In total this results in 19800 calls to frame:newChild(), and it works. It is only a proof of concept; eventually the mechanism would need to be implemented directly in Lua. You can try it in a preview window:
{{#invoke:params|new|
	imposing|200|xxx|
	filling_the_gaps|
	mapping_by_magic|#invoke|blindly|13|
		params|new|
		imposing|99|Hello|
		filling_the_gaps|
		mapping_by_invoking|string|len|
		setting|i|, |
		list_values|
	setting|i|, |
	list_values
}}
Contrary to my expectations, instead, doing the same thing using frame:newChild() for both the parent and the child does not work. The following example therefore fails, despite involving only 2550 total calls to frame:newChild() (50 × 50 + 50):
{{#invoke:params|new|
	imposing|50|xxx|
	filling_the_gaps|
	mapping_by_invoking|params|new|blindly|11|
		imposing|50|Hello|
		filling_the_gaps|
		mapping_by_invoking|string|len|
		setting|i|, |
		list_values|
	setting|i|, |
	list_values
}}
I am still more fascinated by your frameSpoof solution, though, because I think it has potential applications beyond Params and it runs fast, even faster than frame:newChild() (people can indeed build expensive pipelines using Params, so optimization should not be underestimated). --Grufo (talk) 04:50, 28 June 2026 (UTC)Reply
@Grufo: Yes, your 50 × 50 + 50 attempt should not work as I see it as you never cleanup the frames created by frame:newChild. With template and module invocations (and some parser functions too; remember #invoke is a parser function), MediaWiki also creates a child frame and then passes the frame to the new parsing environment effectively executing inside the new frame. When the environment that is using the frame exits, MediaWiki cleans up the frame it is using or in fact all the frames that the environment might be using if it is using more than one. There is a limitation on the number of total frames to limit recursion depth. This means a page can only call another page as a template going down 99 times before it has to come back; the limit is really 100 but the base page is the first frame and you can do nothing without it so effectively it is a limit on 99 recursions. —Uzume (talk) 07:22, 28 June 2026 (UTC)Reply
Parser functions are allowed but not required to use the frame parser. A good example of such is #invoke although it does not use such for parsing the first two arguments (the module name and the function name; yes you can define a function named = and even call it without escaping anything). A good example of a parser function that does not use the frame parser at all is #select—notice, even though it does parse out equal signs in its arguments, the order of such is crucial to its meaning. The frame parser makes so such requirement and this is why args are not guaranteed to be in the same order as the invocation. The frame parser actually fully parses the parameter names and thus knows the order but it does not retain this information so there is no way to consume it later on. Remember templates have no means to even request such information—all they can do is ask for ask for an argument by parameter key and find out if there is anything there. This actually allows the arguments themselves to be expanded only on-demand. A template that never requests the argument of some parameter means that argument is never actually expanded. This means constructions like {{{x|long complex expression with many sub-expansions}}} never get expanded if |x=some value is defined, etc. This was all designed this way in the name of performance even though many Scribunto modules immediately call pairs on frame.args effectively causing every argument to be expanded. Param checks for unused params is powerful but certainly not free. —Uzume (talk) 07:22, 28 June 2026 (UTC)Reply
As for frameSpoof vs. a similar naive emulation using purely frame:newChild, it is about the quality of the emulation and what you want to optimize vs. the amount of development work to create and maintain things. Remember even frameSpoof falls back to using frame:newChild to support things like frame:preprocess which would otherwise likely be impossible to emulate correctly. —Uzume (talk) 07:22, 28 June 2026 (UTC)Reply
Thank you, Uzume. I am coming up with some ideas on how to implement it via #invoke + frame:newChild(). It is less trivial than it first appears, because a helper module called via #invoke cannot receive custom objects, only strings in the form of wikitext parameters. Fortunately, that helper module in turn only needs to pass wikitext parameters to the final module, but some parsing will still be necessary. For instance, Params should be able to tell the helper module something like: “Please call function XXX from Module:YYY, always passing the following twenty parameters unchanged, but iterate over these ten values for the parameter named foobar,” because that is essentially what Params already does internally. Moreover, since all my current problems ultimately stem from the need to modify only the args and title fields of a child frame, I would also like to explore whether this could be addressed upstream by opening a task on Phabricator. In the meantime, if you have the time, I would be very interested in hearing your thoughts on the user interface of Params (setting aside its implementation for a while). From what I have seen, you are among the editors who have examined the module in the greatest depth. I have come across a few advanced uses of Params—for example Template:Infobox network service provider/sandbox, User:Thatgaypigeon/TemplateData param, and User:SkSlick/templates/Titled Except—but, apart from DefaultFree, I have very little idea how those users feel about the experience of writing with it. I think evaluating the interface is something that should be done slowly, because it is quite rich and there is no rush. --Grufo (talk) 12:00, 28 June 2026 (UTC)Reply
@Grufo: Well, actually, I frankly have not really studied Module:Params that much and I admit I do not really understand how to use it very well. I do find it to be quite difficult to understand and have not tried that hard to learn it. On the other hand, I have studied some of the code in some small pieces. Due to its AfD here at EN-WP, I would suggest you make a Module:Sandbox copy of that module and its parts but I am aware you are less concerned about that due to copies you have on other wikis like LA-WP. —Uzume (talk) 01:07, 29 June 2026 (UTC)Reply
Thank you even more for supporting it at TfD then, Uzume. I am not that worried about backups, eventually it will be more annoying finding a new place for the documentation. Params is used in more than 60 Wikimedia projects, it truly powers Latin Wikipedia, and people have imported it in totally unrelated wikis (like Hollow Knight Fandom's wiki, Sarna, etc.—although I don't know how up to date these forks are), so I expect the module will be around for quite some time. Unfortunately on enwiki this module was born unlucky. Some of the participants at TfD wanted to orphan it already months ago without even knowing what it was (#1, #2); others became skeptical after a friendly exchange in which I showed that the (at the time) buggy Module:Pagelist could be rewritten in a few lines of wikitext using Params and the criticism has largely continued since then (#1 #2); and maybe the last one might have felt that their verbosity in the Module: namespace was being threatened by this module (I give up understanding this last case). However, if even you find it difficult to understand how to use it very well, then I am sure I bear a good share of the responsibility. Perhaps part of the problem is that English is not my native language. If you want to really appreciate what Params can do, you will need to look at the templates at lawiki that use it (although there might be the language barrier). Most of the examples are fairly simple, but there are a handful of substantial ones that enwiki has not managed to create yet. The module is the main reason we can afford to have very few other modules over there, and yet a very functional wiki. Let's see how this TfD will turn out. --Grufo (talk) 02:46, 29 June 2026 (UTC)Reply
Since frameSpoof degrades to frame:newChild, I wonder if it would make sense to implement a batch invoker module that worked like we discussed above but instead of directly using frame:newChild in the dumb packet parser we used frameSpoof instead. This would mean even in the most degraded situation it would take a true #invoke hit only once every batch (I am currently targeting 50 as the default so we do not fry the entire recursion stack but I would likely make this configurable) and a bunch of frameSpoof calls that might end up as frame:newChild calls. This would add the batcher and the #invoke overhead to frameSpoof which might otherwise be extremely light but it would also almost guarantee that things would run properly even when they degraded to using frame:newChild. —Uzume (talk) 01:07, 29 June 2026 (UTC)Reply
After thinking a bit more about using #invoke + frame:newChild(), I realized it cannot be done—at least not without implementing a fairly sophisticated parser. The main problem, besides the input, is the output: in fact, Module:Params/helper, invoked via #invoke, would not only need to invoke Module:XXX for each parameter, but also keep the output of each invocation separate. It can certainly do that internally, but it cannot pass those results back to Params except as a single serialized string, because a module invoked via #invoke cannot return a Lua table. Going back to frameSpoof, I was wondering: for frame:preprocess, could it simply fall back to the original frame supplied to Params instead of creating new child frames? --Grufo (talk) 13:46, 28 June 2026 (UTC)Reply
@Grufo: Yes, the return values will need to be packaged and reverse parsed on the way out. Incidentally, Lua functions can return multiple values and technically Scribunto also supports this for functions called via #invoke, however, the multiple values are not particularly interesting as Scribunto just converts them to strings and concatenates them all together via table.concat and the separator cannot be selected (always the default empty string). So one can do something like return '|a', '|b', '|c' and the frame above it will receive the string '|a|b|c' or like return 1, 2, 3 and receive '123'. You can see where I emulate this at the function call and at the function return. I am confident it can be done but it is certainly challenging and perhaps even daunting. Then again my frameSpoof based #invoke emulator is hardly trivial—I suspect you haven't really wrapped your head around it and do not understand it fully since you never really asked me that many questions about its implementation. Probably the trickiest part to think about with the batch invoker would be how to handle errors thrown by the target module. Do we terminate the batch? How does one package the error message and stack for consumption at the return point in order to pass it back to the client module? Already I am doing a bit of hand waving in my frameSpoof based #invoke emulator as errors thrown by the target module will likely result in what looks very weird for the error stack at the client because I do not really handle that (and it likely depends on what the target module does and whether it causes a frame:newChild call or not; I have not really considered/investigated that much beyond realizing the potential issue). —Uzume (talk) 02:30, 29 June 2026 (UTC)Reply
I think you need to consider how to design the batcher API. Consider your code that loops generating arguments for each invocation. It currently expects to send those off one at a time and receive results one at a time in the same fashion as frame:callParserFunction('#invoke', args) handles such. Now consider a batcher API that also receives these requests but now instead of returning results one at a time, it waits until it is told the batch is complete and then executes them in bundles and returns all the results en masse. The batcher has all the arguments for all the invocations unrolled (think "loop unrolling"). It can package those up into strings bundled and pass those repeatedly across the true #invoke interface and parse out the results coming back from each bundle. The bundle handler would receive these bundles one at a time, rip the arguments back apart and attempt to execute them with whatever #invoke emulator it wants to (i.e., based purely upon frame:newChild or a more advanced frameSpoof, etc.). After completing the execution of the entire bundle it would package up all the results and return them to the bundler so it could in turn gather results from all the bundles and return them via its batch interface. I hope I am explaining this adequately. —Uzume (talk) 02:30, 29 June 2026 (UTC)Reply
@Uzume: Indeed, I have not yet understood frameSpoof in full (although I understand it much better than before). I had another look at it a few weeks ago, and that is where I got the “divide and conquer” idea. I can also see that you have put a great deal of thought into handling the edge cases, although those are always a never-ending story. Concerning #invoke, yes, I think the most annoying part will be the serialization and parsing of strings and building the batcher. In principle it is all doable, but I wonder whether the performance trade-off will actually be worth it. At some point, the cost of packaging and unpackaging the arguments and return values may outweigh the savings from reducing the number of #invoke calls (although I suspect it will still be much faster). I also wonder whether a simpler approach might end up being better: keep using frame:newChild() normally, but have Params keep an internal counter and, after (say) 98 child frames have been created, brutally and permanently switch over to #invoke for the remaining calls (in this way neither a string parser nor a batcher API is ever needed). Grufo (talk) 03:13, 29 June 2026 (UTC)Reply
@Grufo: I really do not like that simple but somewhat brain-dead idea. Certainly using frame:newChild is a very sticky wicket because they cannot be easily disposed of and they are limited. But there is really no good way to emulate #invoke without such (and even that has some caveats). As it is, I am doing some crazy things to get around the most common cases with frameSpoof. —Uzume (talk) 03:37, 29 June 2026 (UTC)Reply
@Uzume: Probably I did not explain the brain-dead example very well :) At this point we have four possibilities:
  1. frame:newChild()
  2. frameSpoof
  3. #invoke + parser and complicate stuff (still theoretical)
  4. permanent #invoke
I did not want to say that we should prefer #1 to #2, instead I wanted to say that since at some point also frameSpoof will eventually fail, we can keep a counter and then (also in the frameSpoof case) permanently switch over to #invoke for the remaining calls. This proposed counter would be alternative to #3 (the still-theoretical stuff that in theory should never break), not to frameSpoof. I hope I made myself clear now. --Grufo (talk) 03:51, 29 June 2026 (UTC)Reply
@Grufo: I definitely understood. I just deemed it "brain-dead" because it was basically something that depended on frame:newChild and then fell back to real or what you called permanent #invoke because it never cleans up the child frames. I would characterize this as:
  1. "permanent" #invoke
  2. "permanent" newChild
  3. "minimal" newChild via frameSpoof
  4. "bundled" newChild with cleanup via "theoretical" batch interface
  5. "measured" newChild with "permanent" #invoke fallback
#1 is the best but has resource and performance issues. #2 is simplistic and straightforward but has a frame limit. #3 is fast but complex; it avoids the frame limit in many cases but not always. #4 is also complex and is as yet largely undefined but has potential to get around the frame limit; employs either #2 or #3. #5 also employs #2 or #3 but attempts to count created frames and then just falls back to #1 when it estimates the limit is imminent. Only #1, #4 or #5 can really get around hitting the frame limit. —Uzume (talk) 05:44, 29 June 2026 (UTC)Reply
@Uzume: I would divide the last one into two:
  1. “permanent” #invoke
  2. “permanent” newChild
  3. “minimal” newChild via frameSpoof
  4. “bundled” newChild with cleanup via “theoretical” batch interface
  5. “measured” newChild with “permanent” #invoke fallback
  6. frameSpoof with “measured” newChild with “permanent” #invoke fallback
For Params I would prefer #6 or otherwise #5. --Grufo (talk) 22:22, 29 June 2026 (UTC)Reply
@Grufo: Okay, well technically #4 can have versions that employ either #2 or #3 so it could theoretically be split too. I think I would simplify the nomenclature of #6 as something more like: "measured" "minimal" newChild via frameSpoof with "permanent" #invoke fallback. I am not sure about the term "permanent". I think "simplistic" but be more descriptive. To that end, #6 does seem quite powerful. It avoids newChild possibly never using such but if it is needed it degrades to #invoke when the frame count gets inadequately high. It would be fairly easy to implement as my #invoke emulator API that employs frameSpoof can already use direct #invoke when the noemu flag is passed. I could get rid of that and instead always use that code when the static newChild count is over a certain threshold. —Uzume (talk) 03:34, 30 June 2026 (UTC)Reply
@Uzume: Yes, I thought that #6 with your current code should be relatively easy to implement. There would still be a theoretical case in which it breaks, which is when the invoked module repeatedly calls newChild() and that goes undetected by your metatable—however that should be possible to solve if every descendant frame object (until you switch to using #invoke permanently) is always your metatable, even if Module:A invokes Module:B which invokes Module:C, etc.: very theoretical edge case, but super interesting challenge :) --Grufo (talk) 10:35, 30 June 2026 (UTC)Reply
@Grufo: I am not sure I understand your description of this edge case. First of all—read your mail: there are no metatables (except for args just like a real frame does). It is true that our use of newChild limits the target module in how many frames it can use. This is true of it either directly using newChild itself or if it indirectly creates subframes via say frame:expandTemplate, frame:preprocess, etc. Remember the frame limit is originally there for recursion reasons. If the target module uses the MediaWiki parser and the parser needs to create frames to do its job that is where the limit was originally created. It is not a Scribunto limitation but a MediaWiki frame limitation. Although it would be nice if Scribunto gave us a means to destroy a frame created with newChild! Maybe we should post a Phabricator ticket for this. The trick of course would be that we should not be able to destroy frames available to Scribunto that are not created by newChild so just adding a destroy field to the frame might not be the answer. Also I do not think destroying frames in the MediaWiki parser is a trivial task as I am sure you know there is a non-trivial overhead to creating them. —Uzume (talk) 12:22, 30 June 2026 (UTC)Reply
@Grufo: I think what you were trying to say was to build a better emulator for frame:newChild that includes target module usages in the child frame tally used by the rest of the emulation code. That might be possible but it does nothing to avoid the target module from creating many frames via the MediaWiki parser itself—and I do not think we should even consider trying that anyway. —Uzume (talk) 12:30, 30 June 2026 (UTC)Reply
@Uzume: I had read this line in a hurry last time and I thought that frameSpoof is a metatable, when actually only the args field is a metatable, as you correctly pointed out (also I just read your email). I should reframe it then: I think that to be more unbreakable frameSpoof should be a metatable. It should never return real frame objects. When the invoked module requests newChild() from frameSpoof, the latter should do a real newChild() call and increase the counter, but then return a new frameSpoof metatable built around that new real child frame, otherwise we risk losing track of how many child frames there are. If the invoked module exceeds the limit there is nothing we can do, but if the invoked module creates, let's say, 20 child frames and then returns, then we are able to detect that we are close to the limit (e.g. 95 child frames in total) and switch to #invoke. I don't know if this is possible, I did not think about all edge cases, but by instinct I would guess it is. --Grufo (talk) 12:47, 30 June 2026 (UTC)Reply
P.S. Actually a metatable wouldn't even be needed, all we need is to change the behavior of the wrappers around newChild(), preprocess(), etc. --Grufo (talk) 12:53, 30 June 2026 (UTC)Reply
@Grufo: Exactly! frameSpoof does not need to be a metatable and none of my code ever returns a reference to the child frame and in fact that would not be very useful if it did. Remember the target module thinks it has a frame object. I cannot just ask it to change its perspective and update its frame object reference on the fly as that would require all the target modules to know something about this process breaking the concept of an emulation to begin with. —Uzume (talk) 13:06, 30 June 2026 (UTC)Reply
Actually, instead of keeping a count of all the created child frames, we could actually keep a list of all the created child frames and then just use the size of the list to estimate the number of outstanding frames. I am not sure if is valuable to actually keep a list of such references but the point is it is possible. —Uzume (talk) 13:06, 30 June 2026 (UTC)Reply
I think a reference count is way simpler than a list. Once we reach the limit we cannot force the invoked modules to switch to #invoke, only Params can force itself to switch to #invoke, so I don't see a reason to keep a list, because I don't see what we could do about it. --Grufo (talk) 13:12, 30 June 2026 (UTC)Reply
@Grufo: Well that depends on what we do with the list. If we keep the list and also keep the arguments we used to create each child frame, we could try to optimize things. For example if we need to create a child frame with specific arguments and we already did that, we could just use the existing one and not have to create another new child frame. Basically caching them all and avoiding redundant calls. Of course, at that point if the module and function key are also the same we might be able to fully memoize calls to the target modules by just keeping the return values too—of course that also implies there are no side effects we cannot capture. Based upon how Params work, I am not sure if caching child frames is highly useful but the point is if there is redundancy this could be exploited. Remember, even if we let our child frame references go out of scope and Lua garbage collects the objects, MediaWiki has not released its resources related to these frames so the limit is still imposed. —Uzume (talk) 13:37, 30 June 2026 (UTC)Reply
@Uzume: If you think that a list could be useful in any way, that of course changes things. I don't know if a list could have any influence on it, but on this note, this is where your frameSpoof could be useful beyond Params. In the future, once it is complete, we can imagine tracking down all the modules that use newChild() and see if maintainers are willing to use your frameSpoof. In that way we can imagine a way to coordinate when to permanently switch to using #invoke across modules. But maybe this is too far reaching. --Grufo (talk) 13:47, 30 June 2026 (UTC)Reply
@Uzume: Re “Maybe we should post a Phabricator ticket for this”: I am totally on board with that! --Grufo (talk) 13:22, 30 June 2026 (UTC)Reply
@Grufo: As for your question about supporting frame:preprocess without frame:newChild (or a real frame:callParserFunction('#invoke', args)), I do not see how that would work. What if the target module does something like frame:preprocess('{{{1|xyzzy}}}')? This requires the MediaWiki parser to know what |1= is. How is it going to know what that is if we do not provide it via frame:newChild? —Uzume (talk) 03:16, 29 June 2026 (UTC)Reply
Indeed, that was a stupid thing to ask. I am so used to the frame object in Params, which is always the parent frame (the module's frame gets forgotten immediately, except for its arguments), that my mind slipped. --Grufo (talk) 03:33, 29 June 2026 (UTC)Reply
@Grufo: Yes, and it is more complex than you might think. Remember there are other ways to access the frame too like what if the target module did: frame:preprocess('{{#invoke:module|funckey}}') or frame:callParserFunction('#invoke', {'module', 'funckey'})? And what if module module then tried to access the args from its parent frame? Yes, that is right, it would be the frame created by newChild! —Uzume (talk) 12:53, 30 June 2026 (UTC)Reply

newChild emulation

[edit]

@Grufo: I redesigned my frameSpoof code here along the lines as I laid out in my email and I had a weird epiphany. In Module:Params you were/are using frame:newChild to emulate #invoke. I was considering what other frame fields I could emulate and realized I could emulate newChild with frameSpoof. After that it became obvious, I could develop my own newChild emulator and just write my #invoke emulator to use that in much the same way as you were doing in Module:Params with the real frame:newChild. Anyway, the point was to emulate newChild field of the emulated frame. This means it will effectively be recursive and allows the target module to call frame:newChild in an emulated way that dynamically falls back to a real frame:newChild call. This allows any target module that uses that for the poor man's #invoke emulation that is often seen in code to also be emulated so long as its target is well behaved too (i.e., doesn't use fields that are not emulated). This thought process came about when I was thinking about frame:newChild reference counting and I was concerned about how to handle the target module calling frame:newChild and then I realized the target module can likely only get to frame:newChild via my emulator anyway so why not emulate it too. One minor issue might be the parent frame. I only emulate it by returning our own parent frame and if a target module were to call frame:newChild off that (probably a useful paradigm because that often continues to provide access to the template args), I would not have a way to track it via reference counting. I now am considering how to emulate frame:getParent differently instead of just passing a real frame object. It would be easier as I do not need to consider using frame:newChild to emulate it. How to leverage the shared code between the two...hmm? —Uzume (talk) 02:53, 13 July 2026 (UTC)Reply

Hi Uzume. Well thought! Indeed, emulating frame:getParent would seem to remove the last remaining access point to a real frame. There will still be the issue of frame:preprocess, which will force the creation of a real child frame, but there is probably little we can do about that. On that note, though, I was thinking about a possible workaround, although I suspect it would be difficult to implement cleanly. If you look at Module:For nowiki, that module gives the impression of creating N child frames, each with different {{{i}}} and {{{1}}} parameters, but that is really just an illusion: preprocess() is always run on the same frame, and {{{i}}} and {{{1}}} are not actual frame parameters but string substitutions performed manually in Lua. I was wondering whether we could emulate our own frame:preprocess in a similar way. I do expect, however, that it would be a difficult path, full of strange edge cases. --Grufo (talk) 09:07, 13 July 2026 (UTC)Reply
@Grufo: Yes, I do not think I want to seriously consider frame:preprocess. Have you had a look at Module:WikitextParser (which grew out Module:Transcluder; notice this is now also deleted locally), Module:Wikitext Parsing, etc.? Considering a full wikitext parser is non-trivial and pretty foolhardy in Scribunto during a page render when the real parser is active. Wikitext is context sensitive. One day you should look at the PEG tokenizer used in Parsoid; they forked their own tool just to build it, see WikiPEG (Q140167502) based upon PEG.js which is now Peggy. On the plus-side, one can now use Parsoid from JavaScript and it uses the same parser that MediaWiki is moving to. For things like Module:For nowiki, I wish they would have used a different syntax than trying to leverage the MW-wikitext triple brace parameter notation. I would have preferred the $ notation from MW-message strings. Then it would be very clear what {{{$1}}} does (substitute the value and then let the parser use that value as a parameter). Incidentally, I have considered emulating frame:callParserFunction—at least a very notched variant that just does #invoke and punts for the rest anyway. —Uzume (talk) 12:07, 13 July 2026 (UTC)Reply
@Uzume: I have been aware for a while that Module:WikitextParser exists, but I never really looked into it. I am pretty sure that replacing frame:preprocess is less trivial than it might appear. That said, we would not actually need a full parser; we would only need to handle the triple-bracket notation and then pass the whole thing to the real parser (i.e. to frame:preprocess). Basically, we would need to do what Module:For nowiki already does (but trying to do a better job). The algorithm used there is also not optimal, because it always runs gsub on the same string. A better approach would be to split the string once, fill the placeholder elements with different parameters as needed, and then concat the whole thing. I did this with parse_placeholder_string() in Params. For example, when the for_each function iterates through all parameters, it calls parse_placeholder_string() only once. It then simply places different strings into a table for each parameter, without ever performing string substitution (i.e. without using gsub). P.S. I agree that using {{{1}}} and {{{i}}} was not the best idea for Module:For nowiki. Unfortunately, that notation has recently been copied into new modules as well. Params uses $# and $@ for names and values (e.g. {{#invoke:params|for_each|Arg name: $#, Arg value: $@}}); I avoided using $1 and $2 because that would create confusion with preload parameters, which might themselves be strings that we would not want to interfere with. --Grufo (talk) 12:45, 13 July 2026 (UTC)Reply
@Grufo: Well, I recently made some optimizations in Module:WikitextParser but I am not a fan of its "generate a list" and then "process the list" paradigm. FYI: its main author is the same as mw:Synchronizer. I mentioned this on its talk page but I would have liked to have seen the module's list generating APIs as iterator factory APIs instead. I have not looked at all of Module:Params but seems to be something could be considered for its improvement as well. —Uzume (talk) 09:19, 14 July 2026 (UTC)Reply
In the case of Module:Params though I believe it would likely only affect internal APIs and mostly for implementing what its documentation calls "modifiers" that affect the list of parent frame.args based key–value pairs fed to its "functions". Basically, instead of mutating the key–value pairs in ctx.params and having your "functions" use that, have all your "functions" directly use parent frame.args (or even another source) but have your "modifiers" stack filtering iterators. I notice some of its code does some of this with ctx.iterfunc but that seems quite limited to pairs or ipairs at the moment and technically those are iterator factories and not the iterator and iteration contexts. —Uzume (talk) 09:19, 14 July 2026 (UTC)Reply
I am not sure how hacking frame:preprocess in such a way helps anything. It seems like you are trying to accomplish something other than emulating how a real frame would do such things. —Uzume (talk) 09:19, 14 July 2026 (UTC)Reply
@Uzume: The more I think about emulating frame:preprocess this way, the more edge cases I find (which therefore already affect the current version of Module:For nowiki). For instance, if {{#invoke:foo|bar}} were as follows,
p.bar = function (frame)
	args = frame:getParent().args
	return 'Name is ' .. args.i .. ', value is ' .. args[1] .. '.'
end
then the following code would not work as expected:
{{#invoke:for nowiki|template|<br />|<nowiki>Hello {{#invoke:foo|bar}} world.</nowiki>}}
--Grufo (talk) 20:38, 14 July 2026 (UTC)Reply
@Grufo: I am not sure what you mean. What is your expectation? Based upon the code you provided, I assume there is a module named foo and it is presumably called from some template that is called with parameters |i= and |1= otherwise the .. concatenation would fail when those parameter lookups return nil. In looking at for nowiki, if the template called that as you provided instead of directly calling {{#invoke:foo|bar}} as you mentioned earlier, then for nowiki would pass the parent frame to its doLoop along with its arguments the separator and the code string. There should be at least one numeric argument |1= or the original direct call to module foo would have also failed. There is no "magic" {{{i}}} or {{{1}}} in the provided code string so the substitution does nothing while the frame:preprocess calls module foo on the parent frame and one should effectively get Hello Names is {{{i}}}, value is {{{1}}}. world. for whatever values of |i= and |1= were provided to the base template. Since the base template was called with only a single numeric argument, the loop runs only once and the concatenation adds no separators (i.e., the provided |<br /> goes no where). I ask again, what is your expectation? —Uzume (talk) 08:37, 15 July 2026 (UTC)Reply
@Uzume: Indeed. I'll try to explain myself better. I would expect these two lines to return an identical text:
Name is {{{i}}}, value is {{{1}}}

{{#invoke:foo|bar}}
However that is not the case with Module:For nowiki. To see it better, imagine two templates, {{Test1}} and {{Test2}}, containing the following wikitext.
Source code of template {{Test1}}:
{{#invoke:params|
	mapping_by_calling|Call wikitext|
		names_and_values_as|i|1|
		let|sourceCode|<nowiki>Name is {{{i}}}, value is {{{1}}} // {{#invoke:foo|bar}}</nowiki>|
	setting|i|<br />|
	list_values
}}
Source code of template {{Test2}}:
{{#invoke:for nowiki|template|<br />|<nowiki>Name is {{{i}}}, value is {{{1}}} // {{#invoke:foo|bar}}</nowiki>}}
The <nowiki>...</nowiki> code parsed via Params + {{Call wikitext}} is truly preprocessed in its own frame, whereas the one parsed by {{#invoke:for nowiki|template}} is preprocessed in an emulated frame, in which {{{i}}} and {{{1}}} are not real parameters, but only string substitutions. And so you get the following results.
{{Test1|One|Two|Three|Four}}:
Name is 1, value is One // Name is 1, value is One.
Name is 2, value is Two // Name is 2, value is Two.
Name is 3, value is Three // Name is 3, value is Three.
Name is 4, value is Four // Name is 4, value is Four.
{{Test2|One|Two|Three|Four}}:
Name is 1, value is One // Lua error in Module:Test at line 127: attempt to concatenate field 'i' (a nil value).
Name is 2, value is Two // Lua error in Module:Test at line 127: attempt to concatenate field 'i' (a nil value).
Name is 3, value is Three // Lua error in Module:Test at line 127: attempt to concatenate field 'i' (a nil value).
Name is 4, value is Four // Lua error in Module:Test at line 127: attempt to concatenate field 'i' (a nil value).
That of {{Test1}} should be the expected behavior when you want to emulate frame:preprocess. --Grufo (talk) 09:28, 15 July 2026 (UTC)Reply
@Grufo: Well...yes but Module:Call wikitext is an entirely different beast from Module:For nowiki. It would be unrealistic to expect them to do similar things. The former uses frame:preprocess with a fixed code string on a generated frame:newChild frame (albeit nearly identical to the module's current frame or the template's frame if called from there as you are doing in Params) whereas the latter uses preprocess on a fixed parent frame with generated strings based upon a provided "template" code. The former calls preprocess once while the latter calls it in a loop. Short of them both using preprocess, I do not know why anyone would expect them to do similar things. If you changed foo to read args from the current frame instead and then modified the code generation "template" in the call to For nowiki to pass their "magic" values to the #invoke of foo, you could generate something similar. It is a matter of who is doing the loop. In the params case (where I am assuming mapping_by_calling uses frame:expandTemplate to do its job), it is changing the args passed to {{call wikitext}} so of course that creates new frame objects with different args values that the processor can see. I think you would get a similar result if you jumped the template and used the #invoke and mapping_by_magic. In one case you are using params to loop while changing the args whereas in the other the args are fixed but the code is changed in a loop according to a "template". —Uzume (talk) 11:51, 15 July 2026 (UTC)Reply
@Grufo: Consider this: How would you implement a new function for params called something like preprocess_for_each—without calling a template or invoking a module? Wouldn't that look something like Module:For nowiki? Now if you did that, it would be unfair to expect that to do the same thing as call_for_each—right? The problem is your understanding and expectations. Frankly, I think that is what doomed params at en-WP: understanding and expectations. They did not understand it and had incorrect expectations (I do not know what it does so it must be broken). It is funny too. There are some *really* hairy wikitext templates about but nobody ditches them because only two people understand them. —Uzume (talk) 12:00, 15 July 2026 (UTC)Reply
@Uzume: The reason I am interested in exploring how to emulate frame:preprocess is not because I want to add a preprocess_for_each function to Params, but because I am wondering whether it could be a viable option for frameSpoof, so that it does not always have to create a real child frame when code calls frame:preprocess. That said, if I were somehow forced to implement a preprocess_for_each function in Params… I wouldn't. :-) Just joking. I would probably do something ugly:
  1. add an undocumented Scribunto entry point to Params (say library.undocumented);
  2. have preprocess_for_each repeatedly call #invoke (inside a loop) with Params and undocumented as the first two parameters, followed by the other parameters;
  3. collect the results and return the concatenated output.
As ugly as that would be, I do not really see a better alternative. That is probably one of the reasons why I have always opposed adding preprocess() capabilities to Params. The other reason is that people who genuinely need preprocessing already have viable options: they can use {{Call wikitext}}, as I did earlier, or {{Expand wikitext}}, like DefaultFree did here. In general, if there is already a good solution, I would rather avoid making Params larger. I was not really criticizing Module:For nowiki. I think that module is appropriately simple for what it is trying to accomplish, and the example I gave is probably just a theoretical edge case that nobody will ever hit in practice. But if we want to find a way to emulate frame:preprocess for frameSpoof, that's a totally different thing; the edge cases that are reasonably just theoretical in Module:For nowiki can easily become real use cases here. I know I can work around my example by changing {{#invoke:foo|bar}} so that it uses a different frame, and by explicitly passing {{{i}}} and {{{1}}} as parameters. However, that only works if I control Module:Foo and can modify its code. A general-purpose frame:preprocess emulation should ideally also handle the cases where Module:Foo is unmodifiable and expects to read its parameters from the parent frame. Those are the kinds of edge cases I try to think about when evaluating whether an approach is sufficiently general. As for Params on enwiki, I generally agree that it was widely misunderstood (which makes it even stranger that people who do not understand something feel free to argue for its deletion). I am not sure, however, that I would say it was doomed. Its deletion had several contributing factors, including a bit of bad luck, the fact that it was summer in the northern hemisphere, and me being the main author :-) --Grufo (talk) 13:55, 15 July 2026 (UTC)Reply
@Grufo: I was not trying to get you to implement anything like preprocess_for_each (in the for_each docs I think you might have called it expand_for_each as something not offered) but more use it as a thought exercise. I believe I mentioned "without calling a template or invoking a module" so I am a tad confused why you though using any sort of #invoke was interesting for implementing such a theoretical params function. The way I think of it was just to add a frame:preprocess after the more normal part of for_each. That seemed like a normal extension due to the examples mentioning syntax like {{#invoke:params|sequential|for_each|{{foobar|$#|$@}}}} in call_for_each docs. If expand_for_each/preprocess_for_each allowed <nowiki>...</nowiki> escaping, it would seem to be very similar to Module:For nowiki. As for the possibility of re-implementing preprocess, I do not see how this discussion is relevant unless you are just trying to come to terms with how it works. —Uzume (talk) 17:14, 15 July 2026 (UTC)Reply
@Uzume: It depends on what degree of complexity you want to delegate to the preprocess functionality. The simplest (and most “natural”) form would look like this: i.e. a simple mapping_by_expanding modifier that takes no arguments, it only expands the wikitext already stored in the parameters (to play with $# and $@ you would have to do ...|mapping_by_mixing|I am playing with $# and $@|mapping_by_expanding|...—in that short experiment mw.text.unstripNoWiki() is missing, because I was still thinking whether to create a separate mapping_by_nowiki_unstripping modifier). In my previous answer instead I assumed I was forced to create preprocess functionality + passing additional parameters, very similar to how call_for_each and mapping_by_calling behave. As for implementing preprocess for frameSpoof, I am still in the brainstorming phase and still skeptical that it can be done! --Grufo (talk) 17:41, 15 July 2026 (UTC)Reply
@Grufo: I really do not see how any of that would realistically work. What about things like: mw.getCurrentFrame():getParent():preprocess('<noinclude>1</noinclude>') == '' or even something really simple like: frame:preprocess('{{NUMBEROFFILES}}')? The point is it is a moot prospect. —Uzume (talk) 23:18, 15 July 2026 (UTC)Reply
@Uzume: I am also skeptical, but if we copy (almost) verbatim what Module:For nowiki does (unlike Module:For nowiki we could create and reuse one single real child frame with no parameters and always call preprocess() on that), {{NUMBEROFFILES}} would work as expected, and mw.getCurrentFrame():getParent():preprocess('<noinclude>1</noinclude>') == '' should work as expected too. What would not work is the real inspection of that child frame with no parameters (i.e. frame:getParent().args and frame:getParent():getTitle() from a module called via {{#invoke:...}}—that is why earlier I discussed that edge case). In short, frameSpoof's own preprocess() would do something like string substitution and then reusable_child_frame:preprocess(substituted_text). In all of this, reusable_child_frame would be used only for preprocess (otherwise it always remains private). --Grufo (talk) 23:40, 15 July 2026 (UTC)Reply
@Grufo: I hear you. You are suggesting we do our own magic pre-preprocessing, substituting parameter markup for our fake "args" followed by a real call to frame:preprocess on some other real frame. By don't see the benefit if that frame necessitates a call to frame:newChild, however I do see what you are saying. That said, MediaWiki supports some crazy syntax. I am not convinced we could even emulate that well enough to handle or own magic pre-preprocessing except perhaps in the most rudimentary ways. I am not sure I see the benefit. —Uzume (talk) 03:25, 16 July 2026 (UTC)Reply
@Uzume: Exactly. Now, if that were the only edge case, then the only way around it would be to replace not only all triple-brace expressions, but also every occurrence of {{#invoke:...}} with {{#invoke:interceptor|...}}, where Module:Interceptor would be our interceptor module. If that were not complicated enough, wikitext makes it extremely difficult to determine when string substitution is actually appropriate. For nowiki handles this pretty terribly; for example, the following should always remain unexpanded,
<pre>{{{1|}}}</pre>
yet inside For nowiki it gets expanded anyway:
{{#invoke:for nowiki|template||<nowiki><pre>{{{1|}}}</pre></nowiki>}}
We would run into a similar problem with {{#invoke:...}} too, which would be replaced with {{#invoke:interceptor|...}} in contexts where it should not be. On top of all this, I have not even mentioned the difficulty of implementing Module:Interceptor, which I would rate as medium to high, since we would need to pass both the parent's parameters and the child's parameters in the same invocation. I'd pass. --Grufo (talk) 06:32, 16 July 2026 (UTC)Reply
@Grufo: I do not really think it is very worthwhile to seriously consider this avenue. But I agree that the way for nowiki handles things is deceptive. That is exactly why I would have preferred it did not use triple brace syntax to begin with. If we leverage params notation we could implement a expand_for_each that acts like for nowiki but used things like <nowiki>prefix{{{$#}}}{{{1}}}and{{{i}}}{{{$@}}}suffix</nowiki>. I am not advocating for a expand_for_each of course but that notation makes it clear that the triple braces are not something that params touches but to feed it to frame:preprocess while the $# and $@ were things that were changed by params. —Uzume (talk) 07:16, 16 July 2026 (UTC)Reply
@Uzume: My only attempt to add frame:preprocess to Params, the one I showed you earlier, was actually quite acceptable to me design-wise, and extremely simple. In the end, though, I decided not to add it to Params for several reasons. The biggest issue is how to insert the code to be preprocessed. You either use things like {{((}} and {{))}} (which make the code unreadable), or constructs like <noinclude />{<noinclude />{Foo}<noinclude />}<noinclude /> (which are also unreadable), or you use <nowiki>...</nowiki> (which is semantically confusing—wasn't the whole point of <nowiki>...</nowiki> to prevent any evaluation?—more importantly, the nowiki tag introduces edge cases: what if someone actually wanted to insert a <nowiki>...</nowiki> with its literal meaning, namely “please skip this part”? That is why I wondered whether unstripping should instead be done via its own mapping_by_nowiki_unstripping modifier, and only when explicitly requested). Params is very feature-rich, but it is not complicated; its algorithms are surprisingly linear and essentially free of edge cases. Yet, as soon as I tried to accommodate frame:preprocess, I felt I was entering a labyrinth. I also liked, for once, delegating something to other modules such as Module:Expand wikitext instead of having to do everything in-house. --Grufo (talk) 07:54, 16 July 2026 (UTC)Reply
@Grufo: I am not sure but I believe there may be another issue with unstripping <nowiki />. Originally it was possible to unstrip any strip marker but they explicitly removed that. I do know when MW strips it creates a marker that has a number and it eventually puts stuff back without expansion near the end. When you unstrip, I am not sure what happens to that. You consume the stripped state early and remove the marker. I am sure someone has thought of this (and why they removed the ability to unstrip the other markers; you can still kill them though) so it probably isn't as bad as it sounds but it still makes one wonder. I have never taken the time to research it deeply. The way I see it <nowiki /> prevents wikitext parser expansion. A Scribunto module can use that and pass stuff to the parser based upon that unparsed strip state. —Uzume (talk) 08:28, 16 July 2026 (UTC)Reply

@Uzume: Regarding unstrip/kill, my guess would be that there are three different possibilities and Module:Unstrip handles all three. On a different note, I noticed you read a few sections of Params' documentation. English isn't my native language, so I'm sure the documentation contains a few mistakes. If you happen to find any, please be bold! --Grufo (talk) 08:58, 16 July 2026 (UTC)Reply

@Grufo: Well, I was more reading the code but the docs help to figure out what it is trying to do. You mentioned reviewing la:Modulus:Vicidata and that got me thinking about reviewing parts of mw:Module:Params—possibly by pushing those parts through an AI review process which would also help me understand such. It won't likely be fast but it could be interesting. The docs could go through a similar process. —Uzume (talk) 09:43, 16 July 2026 (UTC)Reply
@Uzume: That would be lovely and you will have fun! I only did that once, during the TfD. One suggestion for the AI workflow: first submit the whole documentation, then a few substantial templates from lawiki (the AI speaks Latin!), asking whether it understands the code and can explain what it does, and finally the module's code. A few suggestions concerning big lawiki templates:
Self-contained:
Templates that use subtemplates:
After that, the AI should be more or less able to sketch new templates using Params. --Grufo (talk) 15:54, 16 July 2026 (UTC)Reply
@Grufo: At your behest, I started doing that with la:Modulus:Vicidata. I might make some minor changes. I shall consider the much larger mw:Module:Params later. Right now I am just using Google search with the "AI Mode" button (they have a general Gemini web app too but I have had some issues with it vs. "AI Mode" search) but I am not sure how large of a prompts I can actually give it however I can reiterate with many prompts without issue (provided the chat does not get too long and page starts to slow down to the large number of DOM elements in the chat session tab of my browser; sometimes I can sort of fix that with a context migration prompt copied into a new session). I do not plan to try use a paid AI although it might be interesting to literally translate the module I am reviewing and use it in an English site. —Uzume (talk) 14:53, 18 July 2026 (UTC)Reply
@Uzume: Well done! (P.S. I'd suggest not introducing a function similar to munda_textum() into Params; text trimming is performed so frequently there that introducing a separate function would only add call overhead for very little benefit). And yes, definitely don't use a paid AI! A few weeks ago I used ChatGPT with a registered free account to review Params. Having an account lets you upload files, so I uploaded the whole documentation as a .txt file containing the wikitext source. Then I pasted the more complicated lawiki templates I mentioned earlier, and finally, at ChatGPT's request, I uploaded Params itself as a .lua file (I wasn't really interested in reviewing the code back then, I was only interested to see if the AI could understand those complicated templates, and, with my surprise, it understood them extremely well). The proprietas function in Modulus:Vicidata could probably benefit from a design review, because it grew rather organically as I gradually discovered more about Wikidata's properties (and I have the feeling I have not discovered everything yet). --Grufo (talk) 15:22, 18 July 2026 (UTC)Reply
@Grufo: I think you will find the overhead of munda_textum to be extremely small. Its cost is one conditional and if the argument is a string, then one tail call to string.match. Most of the usages of the match were already in conditionals or conditional short-circuit operators (i.e., and or or). Often that means as many or fewer conditionals and the call overhead is elided. The only real overhead might be to call to the type global vs. just assuming it is a string or nil and direct comparison to the latter. The global overhead can be moved to a local via caching above the function. And all of this is clearly in the realm of micro-optimization and very likely not significant compared to other overhead like calling #invoke in the first place. I highly doubt introducing something like this to params would add much overhead and would most certainly help with maintenance and possibly understanding too (as is often true with encapsulation). —Uzume (talk) 08:16, 19 July 2026 (UTC)Reply
@Uzume: You're probably right. Although, with Params, you'll notice that many of these trimming operations actually differ from one another, so a few different trimming functions will be needed rather than just one. If you ever venture into that module in depth, however, I'd like you to do so from the perspective of someone trying to understand what kinds of templates can be written with it and how it simplifies things, before looking at how the code is implemented (asking ChatGPT to interpret those very complicated lawiki templates would be the best starting point, and then moving on to smaller templates that can be understood immediately without any AI). --Grufo (talk) 08:27, 19 July 2026 (UTC)Reply
@Grufo: Most of the uses of the direct match also resulted in multiple indexing of the args table too, so I would not be surprised if the function was less overhead vs. the earlier code (and there are some ways to squeeze out a little more from the function if you really wanted to). —Uzume (talk) 08:33, 19 July 2026 (UTC)Reply
@Uzume: Yes, with Modulus:Vicidata I did not care too much about micro-optimizations, since the very big bottleneck is the actual Wikidata access. --Grufo (talk) 08:35, 19 July 2026 (UTC)Reply
@Grufo: I once suggested moving params from a pipeline that processed lists to one that processed iterators (I think I was comparing similar issues with Module:WikitextParser). I still think it could benefit from such an overhaul; though it might be hard to retrofit such without a major rewrite or AI assistance. The hardest part with AI assist is one really needs to check their work and too many users of such are sloppy and in a hurry to do that. —Uzume (talk) 08:55, 19 July 2026 (UTC)Reply
@Uzume: Using iterator-based transformations would reduce the range of operations that can be performed. As you correctly noticed, I do already use iterators in a few places, not only with pairs or ipairs, but more generally with what I call “flag modifiers”. For example, when you write {{#invoke:params|non-sequential|count}}, the non-sequential modifier simply sets a flag telling count that it can count by doing “total number − #ctx.params” (no parameters are actually ever removed). However, there is only so much that can be done with this approach. The current model does not only filter parameters: modifiers can fundamentally transform their structure. For example, a substack can end up containing more parameters than its parent, which would not naturally fit an iterator-based model. --Grufo (talk) 09:17, 19 July 2026 (UTC)Reply
@Grufo: I am not sure why that does not fit an iterator model. An iterator can magically create whatever data it wants to—including adding data from one or more other iterators based upon whatever criteria it wants (i.e., it can skip values, remap them to different values, etc.). —Uzume (talk) 14:25, 19 July 2026 (UTC)Reply
@Uzume: I would need a concrete example. For instance, in this snippet taken from the old version of {{Wrapper}}, how would all the steps in the pipeline work if what is saved each time is not the current table of parameters?
{{#invoke:params|new|
	pulling|omitting|
	entering_substack|
		detaching_substack|
		reinterpreting|omitting|trim_all|splitter_string|{{{separator|//}}}|setter_string||
		mixing_names_and_values|$@|<tr><td><code style{{=}}"white-space: preserve nowrap; word-break: keep-all;"><s>&#124;<span style{{=}}"color: #767600;">$@</span>&#61;</s></code></td><td>''(undefined)''</td></tr>|
	leaving_substack|
	pulling|except|
	reinterpreting|except|trim_all|splitter_string|{{{separator|//}}}|setter_string|{{{setter|->}}}|
	snapshotting|entering_substack|
		with_value_matching||strict|
		detaching_substack|
		mapping_by_mixing|''(empty string)''|
	leaving_substack|
	mapping_by_magic|#tag|values_only_as|2|let|1|syntaxhighlight|let|lang|wikitext|let|inline|true|
	flushing|
	mapping_by_mixing|<tr><td><code style{{=}}"white-space: preserve nowrap; word-break: keep-all;">&#124;<span style{{=}}"color: #767600;">$#</span>&#61;</code></td><td>$@</td></tr>|
	flushing|
	setting|h/f| {{#ifeq:{{{passing-through|+}}}|{{{passing-through|-}}}
		| The {{#if:{{{passing-through|}}}|other p|p}}arameters passed are managed as follows:
		| However, the following are exceptions:
	}}<tabl{{#if:{{{table-class|/}}}
		| e class{{=}}"{{{table-class|wikitable}}}"
		| e
	}} style{{=}}"margin-left: auto; margin-right: auto;"><tr><th>Parameter passed to {{Tl|1={{{1}}}}}</th><th>Value</th></tr>|</table>|
	all_sorted|
	list_values
}}
--Grufo (talk) 15:33, 19 July 2026 (UTC)Reply
@Grufo: Well, I am not sure I ever felt params was that complex in and of itself but it does introduce a dense notation that can be hard to understand and get right. That said, the alternative is often even more complex and fraught with fragility due to repeated pasting of wikitext macro style coding. So though, it could be argued params made the resulting wikitext hard to understand, I would argue that it was likely considerably less complicated to understand. I think the naysayers real objection was they needed to learn something new. —Uzume (talk) 08:41, 19 July 2026 (UTC)Reply
@Uzume: I was actually very surprised by how well the AI could understand {{Formula in nuntiis systematis adhibita}}. That template performs quite a few dependency and protection-level checks under the hood. More importantly, if someone told me that this template (pure wikitext) was in any way understandable, I'd feel like they were making fun of me. A few years ago, I introduced a bug into that template and have never been able to fix it. It got to the point where I preferred to deprecate it and create a new one from scratch using Params. --Grufo (talk) 09:04, 19 July 2026 (UTC)Reply