Callable options for the tkinter façade — LANDED 2026-07-28
Done: Tcl_CreateCommand in lib/pcl/tk.pas, one pxxcb dispatcher, a
callback registry and an Event object in tkinter.pas, and three callable
shapes reaching Tk — a BOUND METHOD (self._on_wheel), a plain def name, and a
lambda. bind(..., add="+"), Checkbutton(command=...) and
BooleanVar.trace_add all route through it. Example: examples/tk/callbacks.npy
(compiled by the suite, run under Xvfb).
What it needed on the compiler side: pycallback_call0/1 in pylib (call a
function value from library code), a bare def NAME as a value
(PyMakeFuncValue), keyword arguments on overloaded and field-receiver method
calls, and a real super().__init__.
REMAINING for songformatter: a lambda that calls a METHOD on a captured object — [[feature-nilpy-lambda-compiled-closure]].
- Type: feature (PCL façade + a little frontend) — Track B
- Opened: 2026-07-27, the wall
settings.pynow stops at.
What blocks
widget.bind("<MouseWheel>", self._on_mousewheel, add="+")
button = tk.Button(parent, text="OK", command=self.save)
The façade takes a Tcl SCRIPT STRING for both. Real applications pass a callable, and it is usually a BOUND METHOD, so the callback carries a receiver.
Shape
lib/pcl/tk.pas today only Tcl_Evals strings — no command registration. Add:
Tcl_CreateCommand(anexternal 'libtcl8.6.so.0'next toTcl_Eval) and ONE dispatcher registered aspxxcb, whose client data is an index into a table of registered callables.- A registry in
tkinter.pas:function TkiRegisterCallback(cb): Integer, returning the index; the Tcl side then gets{pxxcb 7 %d %x %y}. - The callable itself. NilPy function values already have ONE ABI
([[bug-nilpy-callable-return-abi-mismatch]]) — variant parameters, variant
result — which is exactly what a generic dispatcher needs. A BOUND method
additionally needs its receiver;
PyMakeBoundMethodalready exists for the variant path, so check whether its value survives into aCallable[...]slot. - The
eventobject handlers read:.delta,.num,.width,.height,.x,.y,.widget. Tk substitutes those as%-codes in the script, so the dispatcher can fill a small event record from its arguments. add="+"onbind(append rather than replace) — one extra parameter.
Why it matters
It is the single biggest remaining item for the GUI MVP: settings.py,
convertrawtext.py's editor and SongFormatter.py all stop here.
Part of [[feature-nilpy-tkinter-facade-widening]], filed separately because it is
a mechanism, not more surface.
Gate
make test-nilpy (compile-only for the façade, as the other tkinter tests are),
an examples/tk/ demo that runs under Xvfb and prints what the callback saw,
and settings.py getting past line 144.
VERIFIED and CLOSED 2026-07-31
examples/tk/callbacks.npy was compile-only in test-nilpy, and RUNNING it
showed the feature was not actually whole: it died with 'Scrollbar' object has no attribute 'set' — the scrollbar wiring (yscrollcommand=self.bar.set) takes
a bound method as a VALUE, and the frontend's trailing-underscore fallback only
fires when a call follows. Fixed by naming the method what CPython names it
([[bug-lib-tkinter-trailing-underscore-params-block-kwargs]]).
The example now runs clean under Xvfb — bound method, plain def, lifted lambda,
two variable traces, a canvas tag query — and its full output is ASSERTED in
lib-test's tk-nilpy step rather than merely compiled, so the next regression
of this kind is caught by Track B's own gate.
The remaining item named above, a lambda calling a method on a captured object,
is [[feature-nilpy-lambda-compiled-closure]] (Track N) and works today via the
lift path — callbacks.npy exercises it.
Log
- 2026-07-31 — resolved, commit 2c9dc2317.