← board

Why this is a ticket and not a line in done/

The fix that landed (dca30fbae, [[bug-n-a-list-bound-to-a-c-pointer-to-pointer-parameter-passes-the-object-pointer]]) closed the static spelling and left this half open ON PURPOSE, recorded under "Still open, deliberately" in the resolved ticket's body. That is a live question filed where ready and next never scan -- done/ is not ranked -- so it was invisible to the queue while reading as handled. Filed here so it can be ranked; the history stays in the resolved ticket.

Measured 2026-09-13 (frankS), at 9393ab277, live

One program, four spellings of the same call, /bin/echo as the readout (it prints argv[1..], so a wrong pointer shows up as a wrong line and a wrong terminator walks off the end). D execv returned means execv FAILED, i.e. argv was not an array of pointers:

execv("/bin/echo", [b"/bin/echo", b"C", b"three", b"words", None])
                                             ->  C three words      ok
argv = [...]; execv("/bin/echo", argv)       ->  C three words      ok
def go(args: list): execv("/bin/echo", args) ->  C three words      ok
def go(args):       execv("/bin/echo", args) ->  D execv returned   BROKEN

The boundary is the STATIC TYPE, and an annotation crosses it

The third row is the one the resolved ticket does not mention and it changes how this should be ranked. PyCoerceCallableArgsIn hoists tmp := pylist_cptrarray(lst) when the argument is statically TPyList, and an annotated parameter IS statically TPyList. So:

def f(args: list) is a working workaround costing one token. Anyone hitting this on real code -- lekkerzeilen is the live case, every C API taking an array of strings lands here -- can move today without waiting for this ticket. That was worth knowing and was not written down.

It also means the compiler is not missing the ability to BUILD the array; it is missing the ability to decide to at run time.

The open question is ownership

Unchanged from the resolved ticket, and it is the whole ticket:

pylist_cptrarray builds the array into a TPyBytes and the CALLER owns it, which is what makes the static path safe. The variant route is pyvar_cbuf, which returns a bare Pointer and can own nothing -- so deciding at run time to build an array puts the array's owner inside a function that has none. A scratch pool is wrong the moment two are live in one call. Options not yet costed:

Still unmeasured

str and array in this same position -- flagged as never measured when the static half landed, and still not measured here. Do not assume they behave like list.

Ownership

Unowned as of 2026-09-13. frankuser (who fixed the static half) explicitly declined it; frankS (who filed this) is on the bare-builtin-as-a-value group and is not taking it. Free to take.

Resolution

Fixed. pyvar_cptrarray(v) (pylib) returns the pointer array as a TPyBytes when v holds a tag-7 TPyList and nil otherwise; pyvar_cbuf_or(v, arr) returns pybytes_cbuf(arr) when arr is non-nil and pyvar_cbuf(v) otherwise. The frontend arm sits beside the static one in PyCoerceCallableArgsIn.

The ownership answer, stated as the thing that was actually blocking: the ticket asked where the array's owner lives when the decision is made at run time, and read the two as one question. They are not. Only the CONTENT is decided at run time; the OWNER can be decided statically, because allocating a caller-local temp costs nothing when it goes unused. So the decision did NOT have to move back to the call site -- the ALLOCATION did, which the call site can do unconditionally and without knowing the type.

Verified

Still unmeasured, and it stays that way

str and array in this same position -- unmeasured when the static half landed, unmeasured here. pyvar_cptrarray refuses anything that is not a tag-7 TPyList, so they take exactly the path they took before this fix.

Log