RERANKED 58 → 75 on 2026-08-18, and the reason inverts the campaign's standing assumption. A Track B re-measure at pinned v352 (
0d2087d6…, pinb14da0847) found the missing-module batch had already shipped — what remains is 8 files, 4 of them behind one Track U decision, while the LANGUAGE walls are 32.yieldalone is 18 files — more than every missing-module row combined. Track N is the bottleneck for this ladder again; the earlier "Track N is no longer the bottleneck" read was measured before those shims landed and is superseded.DISPATCH CONSTRAINT — this is SCHEDULED work, not queue work. Despite now topping Track N, it must NOT be handed to a long-running session just because
nextranks it first. Its remaining failure mode is silent stack corruption inPyEmitParamSpills, not a compile error, and that is the worst possible thing to chase on a thin context. It is smaller than the body below claims (the generator engine is built and proven for Pascal —slgen.pas, stackless, working today), it is NOT blocked ondecide-how-a-compiled-def-carries-its-signature-when-boxed, and the diagnosis is banked so a fresh session starts from it. Give it a fresh session with room.
yield only works inside a for — a while-loop generator does not compile
def gen(n: int):
i = 0
while i < n:
yield i # error: undefined variable (yield)
i = i + 1
The same generator written with for i in range(n): yield i also fails today,
so the working surface is narrower than the "for-in/yield" support suggests —
worth establishing exactly which shape does work before starting.
yield reported as an "undefined variable" says it is not being recognised as
a statement at all in this position, so the parse arm is keyed to a context
rather than the keyword.
Found by sweeping generator/ternary/unpacking constructs against CPython.
Recon 2026-07-31 — bigger than the title suggests, not attempted
Measured, not assumed: grep -n "'yield'" compiler/pyparser.inc returns ZERO
matches. yield is not recognised ANYWHERE in the NilPy frontend today — not
"works inside for, fails inside while" as the title implies, but
unimplemented full stop. Confirmed directly: for i in range(n): yield i
(the shape the title says already works) ALSO fails with the same "undefined
variable (yield)" — matching the ticket body's own footnote, not the title.
CurProcIsGenerator/CurGenSelfSym/GenTryDepth and the "stackful
coroutine body" machinery mentioned around PyParseDef (pyparser.inc) exist
for a Pascal-only ; generator; directive — there is no NilPy-side wiring
to it, automatic or otherwise. So this isn't a parse-context bug to
relocate; it's full Python generator support to build: recognising a
yield-containing def as a generator automatically, giving it suspend/
resume semantics (there IS a coroutine backend to potentially reuse, just
not connected), and the iteration protocol (for x in gen(), list(gen()))
on top. Sized like the other bigger, dedicated-pass features in this
backlog (feature-nilpy-lambda-compiled-closure, closure-ABI items), not
like a quick bug fix. Not attempted this session — retitling the fork
correctly (unimplemented feature, not a narrow context bug) is the useful
output of this recon.
Gate
make test-nilpy + self-host byte-identical, plus generators driven by
while, by for ... in range, and by for ... in <list>, each consumed by a
for loop and by list().
Corpus evidence 2026-08-18 (frank2-7e, Track N)
From the ladder A/B in [[feature-nilpy-thirdparty-libraries-as-targets]], at HEAD
c7974b6af. The 2026-07-31 recon is confirmed exactly — re-measured before
adding anything, because this ticket's own title had already been wrong once:
| shape | result |
|---|---|
def g(): for x in [1,2]: yield x |
undefined variable (yield) |
the same with an if/elif chain before the yield |
same |
the same as a METHOD (def __iter__(self)) |
same |
So no yield shape works, for-driven included. The title remains wrong and
the recon section is right; anyone reading only the title will size this as a
context bug.
Reach — this is the number for ranking. The ladder's first-wall table counts
undefined variable (yield) at 3 files, which badly understates it: a first-wall
table only ever sees the file's FIRST error, and most yield users stop on an
earlier wall today.
Files that USE yield, in the ladder corpora (67 .py total):
| corpus | files | use yield |
|---|---|---|
| html5lib | 52 | 21 |
| tinycss2 | 10 | 1 |
| webencodings | 5 | 1 |
| total | 67 | 23 |
23 of 67 — but 9 of those are html5lib/tests/**. Corrected after compiling
each of the 23 and reading its first wall: the decision-relevant figure is
14 library files, not 23. Stating both because the raw 23 was published first
and is the more flattering number; the test files are real users of yield but
nobody's build is gated on them.
html5lib is a streaming tokeniser/filter/serialiser pipeline, so generators are
its spine rather than a convenience — _tokenizer.py, serializer.py and every
filters/*.py are generator-based. No amount of shim work reaches those files.
The three whose FIRST wall is yield today, i.e. the files this feature alone
would unblock:
html5lib/filters/inject_meta_charset.pyhtml5lib/filters/optionaltags.pyhtml5lib/filters/whitespace.py
The other 11 library users sit behind a module shim first (sys, xml_dom,
xml_etree_elementtree, xml_sax_saxutils, genshi_core) — so yield and the
Track B shim work compound: neither alone opens the pipeline, and the shims
landing without yield would move 11 files onto this wall rather than past it.
That is the argument for ranking it alongside the shims rather than behind them.
Recorded as evidence only. Ranking is the coordinator's and the user's call —
prio: deliberately untouched.
Reranked 35 -> 58 by the coordinator, 2026-08-18 — on corpus evidence
Rank follows evidence rather than preceding it, so this moved only once the files were
named. Three corpus files stop on yield and nothing else
(html5lib/filters/inject_meta_charset.py, optionaltags.py, whitespace.py), and 11
more reach it once the module shims land.
Placed at 58 rather than at the shim ticket's 60 deliberately: the shims unblock more files outright, but these two compound and neither alone opens html5lib's pipeline. Scheduling them in the same window is the point of the number, not the ordering between them. See [[feature-b-module-shims-for-the-html5lib-corpus]].
Title warning, raised by frank2-7e and left in place rather than fixed here: this
ticket's title has already been wrong once, and it is still wrong. The 2026-07-31 recon
was re-measured 2026-08-18 and is exactly right — no yield shape works, including
the for-driven and method forms the title implies are fine. Anyone sizing this from the
title alone will read it as a context bug and under-scope it. Retitling is a hygiene
call left for whoever takes it.
SCOPED 2026-08-18 (frank2-7e) — strategy de-risked, then PARKED UNSTARTED
Claimed, measured, not started. No compiler file was touched. The measurement below removes the main unknown so the next session does not have to decide the strategy first.
Confirmed a third time: nothing exists on the NilPy side
grep -c "'yield'" compiler/pyparser.inc -> 0, and the NilPy lexer has no
yield token either. The 2026-07-31 recon stands: yield is unimplemented
outright, not broken in a particular context. The TITLE still says otherwise --
flagged, not retitled unilaterally.
What DOES exist, and it is a lot
A complete Pascal generator feature, in two strategies that deliberately share a consumption interface:
- stackful --
; generator;+lib/rtl/coroutine, a real 64 KB heap stack per live generator (CO_STACK_BYTES), switched byCoSwitch. - stackless -- a state-machine transform in
parser.inc(SLCheckEligible,MAX_GEN_STATES = 1024) overlib/rtl/slgen.pas, persisting locals in slots.
defs.inc records the key property: "Share CURRENT/DONE offsets with the
stackful layout so for-in reads the value/done flag identically for either
strategy; only the advance step differs." And for x in Gen(args) already has
its desugar in parser.inc.
So the runtime, the state machine, slot persistence and the iteration protocol all exist. What is missing is NilPy-side wiring, not generators.
THE STRATEGY QUESTION IS SETTLED -- by measurement, not by argument
The stackless transform refuses yield inside a try, inside a with, and in
an if/while/for/case condition; it allows yield at top level and in
those constructs' bodies. The obvious worry is that real Python generators wrap
a yield in try/finally, which would force the stackful path -- 64 KB per live
generator, in a tokeniser pipeline that keeps several open at once.
Measured over every non-test generator in the ladder corpora (html5lib,
tinycss2, webencodings) using CPython's own ast module:
generator functions (excluding tests/**) |
19 |
total yield sites |
76 |
generators with a yield inside try/with |
0 |
Zero. Stackless covers the entire corpus, so this can be built on the cheap path with no 64 KB stack per generator. That was the single biggest open risk and it is now closed.
What the corpus needs is the METHOD form, not the module-level one
The files whose first wall is yield are html5lib filters, and each is the same
shape:
class Filter(base.Filter):
def __iter__(self):
for token in base.Filter.__iter__(self):
yield token
A generator method, returned from __iter__, consumed by for t in filter_instance. So the tempting first increment -- a module-level def with
yield driven by for x in gen() -- appears nowhere in this corpus and would
move zero files. Whoever lands that half should report it as groundwork, not
as ladder movement. (Same trap as the first-wall/reach split already recorded on
the campaign: work that moves files ONTO the next wall is progress-shaped.)
Implementation order
- NilPy lexer: a
yieldtoken (none today). pyparser: a pre-pass marks adefwhose body containsyieldas a generator -- Python decides this at compile time from the body, which matches the existingPyScanLopre-passes over a module's token span.- Lower the marked def onto the stackless transform. This lands in
parser.inc(SLCheckEligibleand the state-machine builder live there) -- a shared file, so it needs the A/P slot declared. - The iteration protocol: a generator object returned from a call, and
for x in <it>over it. This is what the corpus needs and the largest piece. - The module-level
for x in gen()form then falls out.
Why parked rather than started
A dedicated pass, as the recon said and as the corpus evidence confirms: 19
generator functions, a full iteration protocol, landing in shared files. Steps
1-3 move no corpus file on their own, so a partial landing would be a
feature-shaped commit with nothing to show plus a new half-state in
parser.inc.
Returned to backlog/ rather than unfinished/ deliberately: nothing is
half-applied, so the queue should rank it as available work, not as a lock.
Open question for whoever takes it
Generators are stateful objects. If step 4 needs a callable value to carry
state, read decide-how-a-compiled-def-carries-its-signature-when-boxed FIRST
-- it is the same representation question, still with the human, and
pre-empting its ruling would be wasted work in a lifetime-sensitive area.
Coordinator note 2026-08-18 — this ticket now DEPENDS ON A PENDING DECISION at step 4
Recorded here rather than as a blocked-by, because steps 1-3 are genuinely independent
and blocking the whole ticket would hide available work. But step 4 (the iteration
protocol, the largest piece) may need a callable to carry state, which is the same
representation question as
[[decide-how-a-compiled-def-carries-its-signature-when-boxed]] (U, p88, with the
human).
Read that ruling before starting step 4, or you will build the wrong object twice. That decision now gates three items: the p88 procedural-value bug, the p85 def-rebinding bug, and this step.
The dispatch trap, and it is the day's recurring shape
The corpus needs the generator METHOD form — def __iter__ with yield, consumed as
for t in filter_instance, which is what the html5lib filters are. The tempting first
increment, a module-level def driven by for x in gen(), appears NOWHERE in these
corpora and would move ZERO files.
So a session could land the lexer token, generator marking and the stackless lowering, have a genuinely working feature, and move the ladder not at all. If a future report says "yield landed", ask WHICH SHAPE before believing any count moved. Same first-wall-understates-reach trap as [[feedback_measuring_a_thing_is_not_filing_it]], wearing a different hat.
Settled by measurement, so it need not be asked again
The strategy question was the biggest open risk and is now closed. Two generator
strategies already exist for Pascal and share a consumption interface by design
(defs.inc: both share CURRENT/DONE offsets "so for-in reads the value/done flag
identically for either strategy; only the advance step differs").
- Stackful costs a real 64 KB heap stack per LIVE generator — brutal for a tokeniser pipeline holding several open at once.
- Stackless is a state machine over
lib/rtl/slgen.pasbut REFUSESyieldinside atryorwith.
The textbook worry is that real Python generators wrap yields in try/finally. Measured
across every non-test generator in the ladder corpora using CPython's own ast:
19 generator functions, 76 yield sites, ZERO with a yield inside try/with. Stackless
covers the entire corpus.
Title: still says "outside a for loop", implying the for-driven form works. No shape
works — NilPy has no yield handling at all (0 hits in pyparser.inc, no lexer token).
That title has now misdirected three separate reads and is worth a rename by whoever owns
the campaign.
Coordinator re-estimate 2026-08-18 (human challenge) — the generator ENGINE is already built
Rene asked whether yield was not already implemented for Pascal and therefore mostly mechanical. Measured, and the answer is yes — this ticket's framing as a "dedicated-pass feature" overstates the core.
Verified end to end at HEAD, not read from a comment:
function Count(n: Int64): Int64; generator; stackless;
begin i := 0; while i < n do begin yield i; i := i + 1; end; end;
for x in Count(4) do WriteLn(x); -> 0 1 2 3
What already exists: the yield keyword, the ; generator; / stackless; markers,
AN_YIELD (defs.inc:279), IR_YIELD (defs.inc:554), both runtimes —
lib/rtl/coroutine.pas (stackful, 64 KB heap stack per live generator) and
lib/rtl/slgen.pas (stackless state machine, no context switch, no heap stack, every
target, zero per-target asm) — and a shared CURRENT/DONE layout so for-in reads either
strategy identically.
What does NOT exist: any NilPy side at all. No lexer token, no parser handling. The earlier "0 hits" measurement was right.
So the honest shape is wiring a frontend to a working engine, not building generators.
Steps 1-3 (lexer token, marking a def that contains yield as a generator routine,
lowering yield to AN_YIELD) are largely mechanical against machinery that is already
proven, and the stackless strategy is already known to cover the whole corpus (19
generator functions, 76 yield sites, zero inside try/with).
Where it is genuinely NOT mechanical
The corpus needs the iteration protocol, not the function form:
class Filter:
def __iter__(self):
for t in self.source: yield t
for token in filter_instance: ...
Pascal's model is for x in Gen(args) — a generator FUNCTION called in a for-in. Python's
is an OBJECT whose __iter__ returns a generator. Bridging that is real work, but it is
iteration-protocol plumbing over a finished engine, not generator implementation.
The boxed-def dependency should be RE-MEASURED before it is believed
This ticket records that step 4 "may want a callable to carry state" and therefore waits
on [[decide-how-a-compiled-def-carries-its-signature-when-boxed]]. That deserves a
check rather than inheritance: a stackless generator's state lives in a HEAP RECORD
(SL_OFF_*, slgen.pas), not in the callable, and __iter__ returning that instance does
not obviously require a boxed def to carry anything. If the dependency does not survive
measurement, this ticket is not gated on the human at all — which matters, because it is
the board's largest row at 14 files.
Whoever takes it: measure that first, in an hour, before planning around a decision that may not apply.
Correction to the section above: the ITERATION PROTOCOL also already exists
I wrote that bridging Python's for t in obj to Pascal's for x in Gen(args) was "the
genuinely NOT mechanical" part. That was wrong — I had not checked whether Pascal has
the object form. It does.
parser.inc:19428 implements the FPC structural enumerator protocol:
for X in C do BODY where C has GetEnumerator
-> __e := C.GetEnumerator;
try while __e.MoveNext do begin X := __e.Current; BODY end;
Verified end to end at HEAD with a hand-written class — for v in c do WriteLn(v) over a
TColl.GetEnumerator returning a TEnum with MoveNext/Current prints 10 20 30.
So Python's __iter__/__next__ maps onto machinery that is already built and proven,
essentially one-to-one: __iter__ -> GetEnumerator, __next__ -> MoveNext + Current
(with StopIteration as the False return). That is the piece I had flagged as the hard part,
and it is a naming/adaptation job over an existing protocol.
And the "stackless is deferred" memory is about ASYNC, not generators
Worth stating because it is an easy misread of defs.inc. The "STACKLESS (later)" note
sits on AN_AWAIT (54), the async/await marker — not on AN_YIELD (52). Generator
stackless is DONE: ; generator; stackless; compiles and runs today (verified: prints
0 1 2 3), and slgen.pas is a complete state-machine runtime. Nothing about generators
is deferred.
Revised estimate
Every engine this needs is built and proven for Pascal: the yield keyword, both generator
strategies, and the object-enumerator for-in protocol. NilPy has none of it wired. So this
is frontend wiring against three finished subsystems, not a dedicated pass — closer to
the .npy lexer/parser work than to an object-model change, and the "dedicated-pass"
framing in this ticket (mine and the parking session's) should not be inherited.
The boxed-def dependency is now doubly suspect: generator state lives in a heap record, and the consumption side is an enumerator object. Measure before planning around it.
ATTEMPTED AND PARKED 2026-08-18 (frank2-7e, Track A+N) — wired end to end, parked on a frame-layout crash
First session to actually build this rather than scope it. A NilPy generator now
compiles end to end and prints its first value, then segfaults. Reverted to a
clean tree — nothing is half-applied in compiler/**, make compiler/pascal26
converges at HEAD. Banking the wiring and the exact blocker.
THE DEPENDENCY DOES NOT SURVIVE MEASUREMENT — this ticket is NOT gated
Measured, as the coordinator asked. decide-how-a-compiled-def-carries-its-signature-when-boxed
does not gate this. Reason: generator methods are refused by the engine
outright, so the boxed-callable route was never on the table — and the route that
DOES work needs no callable value at all.
stackless generator/async methods are not supported (v1)
for-in: unsupported iterable expression
The lifted route works today. A generator method lowered to a free function taking the receiver, consumed as a plain for-in, verified end to end at HEAD:
function Filt__iter__(self: TF): Integer; generator; stackless;
...
for v in Filt__iter__(f) do WriteLn(v);
Verified including the exact html5lib nested-filter shape — a generator iterating
another generator, with continue in the body: prints 10 20 40 50. Also
verified: stackless generator over Variant elements (1 2 done), which is
what NilPy values are, and the object-enumerator protocol (10 20 30).
So the implementation order in the sections above is sound and step 4 needs no ruling from Track U. Anyone inheriting the "read the decision first" note can drop it.
The wiring, as implemented (all in pyparser.inc except one shared hunk)
Every step below was reached and worked; listing them so the next session re-applies rather than re-derives.
- No new lexer token needed.
yieldarrives as an identifier and is handled at statement position, next to thepassarm. PyDefBodyHasYield(bodyStart)— token scan over the def's span, with anestedDefDepthcounter so a nesteddef's yield does not mark the outer one. (Python decides generator-ness from the body at compile time; this matches it.)PyApplyGeneratorABI(bodyStart)— the stackless ABI isfunction(__genself: Pointer): Boolean, so the 10PyHdr*arrays shift down by one,__genself/tyPointeris injected at index 0,PyHdrNParams+1,PyHdrRetType := tyBoolean,PyHdrIsProc := False.PyPullSlgenIfGenerators— scans the module for a statement-startyieldand issuesParseUsesUnit('slgen'); must be called at BOTHPyPreScanImportssites or the unit is not in scope.- Generator flags set before the locals pre-pass, not after — the pre-pass
trial-parses the body, and with the context unset
yieldis refused there first. This cost a cycle; it is the non-obvious ordering constraint. for x in gen()routing, plus a shared-file change (declared, A/P slot held):ParseForInGeneratorASTinparser.incis Pascal-only — it doesExpect(tkDo)+ParseStatementAST. Made frontend-aware:if isNilPy then Expect(tkColon) + PyParseSuite. NotePyParseSuite, notPyParseBlock— the latter givesunexpected token/expected expressionon the NEWLINE/INDENT..DEDENT block.
The blocker, localised to one procedure
The program prints the first yielded value, then segfaults with a corrupted
stack (bt shows garbage frames). Localised by experiment, not by reading:
prologue emitters guarded with if not isGen |
result |
|---|---|
PyEmitParamSpills + PyInitVariantLocals + EmitManagedLocalsZeroInit |
no crash, but infinite loop printing the first value — state never advances |
re-enable PyEmitParamSpills alone |
crash returns |
So PyEmitParamSpills (pyparser.inc:27513) is both the frame-corrupter and
necessary for state to persist. It spills incoming argument registers into each
param's frame slot — correct for an ordinary def, wrong here: in the stackless
step ABI only __genself arrives in a register, and the declared params 1..n are
persistent instance slots the transform restores itself, so spilling them
writes register garbage over live state every step.
The next thing to try (untested, this is where I stopped): make the spill
loop generator-aware — spill __genself at pointer width and skip indices 1..n
entirely — then re-test whether PyInitVariantLocals and
EmitManagedLocalsZeroInit can be restored (they must NOT re-zero persistent
slots on resume either, so expect the same shape of answer for both).
Why parked
Per the standing instruction: it did not land green, so bank and park rather than iterate. The remaining work is x86-64 frame-layout integration between NilPy's prologue and the stackless step ABI — bounded and now precisely located, but real, and the failure mode is silent stack corruption rather than a compile error.
Returned to backlog/, not unfinished/: the tree is clean and nothing is
half-applied, so the queue should rank this as available work rather than hold a
lock on it. Same call the previous parking session made, for the same reason.
LANDED 2026-08-18 (Track A+N, sole-A confirmed) — generators work; four commits
Built, not scoped. Four commits on master:
| sha | what |
|---|---|
c59a57ff4 |
yield in a def, consumed by for x in gen(args) |
1d84a4bef |
generator METHODS + the three call spellings + tuple targets |
78842fec8 |
the parameter limit lifted 4 -> 6 (SL_MAX_PARAMS) |
f132f1f7e |
fix: a Pascal var argument is not a Nil Python variant (a RED I caused) |
The banked analysis was right and saved the session. The engine is built, the strategy question was settled, and the dependency on [[decide-how-a-compiled-def-carries-its-signature-when-boxed]] does not exist — re-measured, as the coordinator note asked. Nothing here needed a Track U ruling.
What works, verified against CPython
31 differential probes, 30 byte-identical to CPython, 1 a clean compile error
(the unsupported value form below). Shapes covered: while-driven, for-over-list,
for-over-range, string and integer elements, parameters, tuple yield, tuple loop
targets, generators consuming generators, generator METHODS, and the full html5lib
filter shape (a subclass whose __iter__ yields, chaining to Base.__iter__(self)).
Consumption spellings routed to the shared desugar: gen(args), obj.gen(args),
Cls.gen(self, args).
Corpus, stated honestly. All three files whose first wall was yield
(filters/whitespace.py, optionaltags.py, inject_meta_charset.py) now compile,
and yield no longer appears as any file's wall. The html5lib non-test compile
count is 7 of 33; the parameter lift moved three treewalkers onto their NEXT
wall (an open-ended slice details[1:], a *-unpack, missing modules) without
changing the count. That is progress-shaped without being progress, exactly as this
ticket's own dispatch-trap note warns — recorded rather than reported as movement.
What it took that was NOT in the banked plan
The banked "next thing to try" was PyEmitParamSpills. It was not the problem —
the spills are correct, and the Pascal path spills a stackless generator's params
too. Three other things were:
EmitManagedLocalCleanupreleases the generator's live state. A stackless step returns at every yield through the ordinary epilogue, so a Nil Python generator freed the very objects the instance still pointed at. Silent: a generator walkingfor t in xsgot a dangling list on its SECOND step and the loop simply ENDED, one element in, with no error. Pascal never saw it because its class locals are not refcounted. This is the crash the earlier session localised to the prologue — the prologue was innocent, the EPILOGUE was not.- Variant and string LOCALS could not persist across a yield at all
(
AssignStacklessSlotsrefused them outright). They are what a Nil Python generator body is MADE of, and it cannot be solved frontend-side: the ones that trip it are hidden desugar temps minted mid-body, which no promotion pass can see. They now persist by BLOB copy (variant, 16 bytes) or raw word (string) — bitwise, which is what keeps them refcount-neutral. - The hoist queue is dropped by the desugar. A list literal is built by hoisted
statements, so
g(["a"])handed the generator None whiled = ["a"]; g(d)worked. Parked and spliced ahead of the instance setup now.
Two silent bugs found by measuring, both layout-dependent
PyDefAtHasYieldconsumed ONE newline after a header, not a run, so a BLANK LINE between a method'sdefand its first statement made the member pre-pass and the real parse disagree about generator-ness — the silent ABI mismatchpyparser.incwarns about throughout. That warning is now checked: the two passes are compared and a disagreement is a compile error, not a segfault.FindUClassis case-INSENSITIVE (Pascal heritage, correct there; Python is not).w = W(...)thenfor a in w.slider():matched the RECEIVERwagainst the CLASSW, took the unboundCls.method(self)route, and passed no receiver — the generator ran against a garbageself. It reproduced only when a variable and a class differed by case alone, so renaming either made it vanish. Worth grepping for siblings: any other Nil Python path that resolves a name throughFindUClasshas the same hazard.
What does NOT work — filed, not hidden
- A generator as a VALUE:
g = gen(),list(gen()), passing one as an argument,next(g),for t in <object>through the__iter__protocol. All are compile ERRORS today, not wrong answers — the injected receiver makes every such call an arity mismatch. Filed as [[feature-nilpy-a-generator-as-a-first-class-value]]. yieldinsidetryorwith— the stackless transform's own limit. Measured as ZERO occurrences across the ladder corpora, so it blocks nothing today.- More than 6 parameters (
SL_MAX_PARAMS), bounded by riscv32's eight-word call limit rather than by anything about generators. The fix that removes the limit entirely is to stop making the declared params PARAMETERS: they are already persistent instance slots, so a step function taking only__genselfneeds no pads at all. Noted in the follow-up ticket. - A bounded LEAK, deliberate and recorded rather than papered over. A generator's managed locals are no longer released at step exit (that is fix 1 above), so they are dropped only when the instance is — which today means never. Same for the heap cell each variant argument travels in. One-off per generator instance, not per yield or per step. Filed as [[bug-nilpy-a-generator-instance-leaks-its-locals-and-argument-cells]].
Log
- 2026-08-18 — resolved, commit cbe88d992.