← board

Rewrite tkhtmlview in NilPy

Why NilPy rather than fixing the Pascal

lib/pcl/tkhtmlview.pas was written as though Pascal had keyword arguments (text_.configure(yscrollcommand := bar.set_)), which it does not, so the unit has never compiled — identically on pinned, so this is not a regression. The two options were to give Pascal named parameters, or to write the file in a language that already has them.

The decisive argument: named parameters are not standard Pascal, so no existing Pascal code will ever use them — the only consumers would be pxx-authored wrappers of Python-shaped APIs, and those can simply be Python. Reinforcing it: Pascal has its own GUI RTL that works entirely differently, so the typical consumer of a Tk-shaped API is Python code anyway.

Secondary benefits: it is a real test case for NilPy as a library language (as opposed to a program language, which is all it is exercised as today), and the module's identity is Python — its own header quotes from tkhtmlview import HTMLScrolledText.

Scope

398 lines. No Pascal consumer exists: the only mentions of tkhtmlview outside the file are comments in lib/pcl/tkinter.pas and lib/rtl/markdown.pas. Its sole consumer is a NilPy import, which is what makes the swap clean.

Verified available for the port:

Blocked on one prerequisite

[[feature-nilpy-import-a-py-module-from-the-library-path]]. A .py shipped in lib/** is currently unreachable — NilPy finds a .py module only as a SIBLING of the importing file; the fall-through chain resolves .pas only. Measured, with the identical file working as a sibling and failing from lib/pcl/. Without that fix the port would land and not be importable, so the "pure Track B, zero surprises" framing holds only after it.

Guidance for the port

Gate

make lib-test / make demos green; a NilPy program importing HTMLScrolledText compiles and renders; pascal26 SongFormatter.py builds (its Track N half is already fixed, so this is the last blocker — see [[bug-nilpy-songformatter-no-longer-compiles-set-callback-and-get-arity]]). Build with $(PXX_STABLE); never rebuild the compiler under Track B.

2026-08-10 (Track B) — port ATTEMPTED; blocked at its core, on a second thing

Claimed and started. The prerequisite this ticket already names ([[feature-nilpy-import-a-py-module-from-the-library-path]]) was re-verified by probe first, not read off the board: a .py in lib/pcl/ still fails import while the identical file as a sibling prints from-lib-pcl. So placement is still blocked — but that one was known, and it only stops the file from being shipped, not from being written. Development proceeded as a sibling, which works.

A second blocker was found, and this one stops the code itself: [[bug-nilpy-text-class-name-binds-the-rtl-file-record]]. NilPy binds the class name Text to lib/rtl/textfile.pas's Text = record — Pascal's FILE type — in exactly the two positions a widget library needs: an instance attribute and a base class. Canvas and Scrollbar in the same positions are fine, which is the control that makes it a name collision rather than a façade defect.

Both candidate designs die on it, which is why this is not writable around:

Per the platonic-code rule the natural spelling stays and the ticket is filed; no half-port was committed. A one-way-wired scrollbar would look finished and scroll wrongly, which is the silent-failure trade this repo keeps refusing.

blocked-by: now carries both.

Banked so the port is short once unblocked

2026-08-15 (Track B) — PORTED. lib/pcl/tkhtmlview.py, the .pas deleted

Both blockers this ticket had waited on were re-verified by probe before anything was written, not read off the board: class Panel(tk.Frame) with self.text.configure(yscrollcommand=self.bar.set) + self.bar.config(command= self.text.yview) — the exact three lines both candidate designs died on last time — compiles and runs on pinned v339 / f11e0ed9816edc1d57ef8ee6e6ab0e5b9885db6c.

Shape: Frame containing a Text + Scrollbar, which is the old .pas's design and what the header prose already documents ("packed inside a frame of its own... geometry calls go to the FRAME"). Real tkhtmlview subclasses ScrolledText, whose trick is copying the Frame's geometry methods onto the Text; that is not reproducible through this façade and buys nothing here. The header paragraph survives verbatim, as the ticket asked.

lib/pcl/tkhtmlview.pas is deleted, and tools/lib_units_compile.py's KNOWN_BROKEN is now empty — tkhtmlview was the only entry it ever held, and that list was written with "must only ever shrink" on it.

Verified — with CPython as the oracle, not just against itself

examples/tk/htmlview.npy builds the widget, feeds it a document and reads the Text widget back, asserting the RENDERED TEXT (headings, inline runs, the bullet, entities, <pre> indentation, the link, set_html replacing rather than appending). It runs unchanged under CPython on real tkinter, and the two outputs are byte-identical. That is the nilsh pattern, and it is the check that a self-consistent-but-wrong renderer cannot pass.

Wired into make lib-test inside the existing guarded xvfb block (tk-nilpy), plus the CPython diff as its own guarded step. make lib-test GREEN on stable v339, with both new lines present in the log: tk-nilpy: ok and lib-test: tkhtmlview renders identically under CPython.

Two NilPy bugs found, as this ticket predicted, and both are quiet ones

Per the platonic-code rule neither was worked around by reshaping the library. The scanners are written with find/slicing/iteration rather than while i < len(s): s[i], which is the more Pythonic spelling on its own merits and is what CPython parity is measured against; and every widget construction precedes every conditional, which is the natural order anyway. Both tickets record that the port happens not to trip them, so neither is marked as blocking.

A renderer bug fixed on the way, that the .pas would have shipped

Whitespace was collapsed per text run instead of per document. The space in <strong>bold</strong> and <em>italic</em> is the leading character of its own run, so it was dropped and the words ran together as boldand italic. The .pas had the identical logic and nobody ever saw it, because the file never compiled. _collapse now takes keep_leading, which is false only at the start of a line — where HTML drops it too. Asserted in the example.

And a Track A regression found by checking the real consumer

~/songformatter/SongFormatter.py now gets past its from tkhtmlview import HTMLScrolledText — the port works — and dies further in, inside lib/rtl/configparser.pas, on a name that has nothing to do with either: [[bug-a-tkinters-text-class-captures-the-rtl-text-record-in-other-units]]. Five lines of pure Pascal reproduce it (uses tkinter, configparser;), so it is not a NilPy issue and it is not this ticket's to fix. This ticket's own gate line about SongFormatter is therefore satisfied as far as it reaches: tkhtmlview is no longer what stops it.

lib/pcl/tkinter.pas's TkTextWidget / NewText pair stays — nothing uses it now, but the flat-namespace problem it works around is still there for the next Pascal caller, and the A ticket above is the same disease. Its comment was updated to say so rather than to keep naming a file that is now Python.

Log