Skip to content

Update TclX for Tcl 9 API compatibility - #27

Open
zolk3ri wants to merge 37 commits into
flightaware:BCK-13016-tcl9-upgradefrom
zolk3ri:tcl9-api-compat
Open

zolk3ri wants to merge 37 commits into
flightaware:BCK-13016-tcl9-upgradefrom
zolk3ri:tcl9-api-compat

Conversation

@zolk3ri

@zolk3ri zolk3ri commented May 3, 2026

Copy link
Copy Markdown

The current TclX implementation depends on several Tcl 8-era APIs and compatibility assumptions that no longer hold with Tcl 9. This PR updates TclX for Tcl 9 API compatibility, allowing it to build, load, and pass the main test suite against Tcl 9.1a2.

Problem Description

  • TclX fails to compile against Tcl 9 because many Tcl APIs now use Tcl_Size instead of int
  • TclX still uses Tcl 8-era APIs and types that are no longer available in Tcl 9
  • Package initialization requests Tcl 8 compatibility, which prevents loading under Tcl 9
  • Legacy TclX index expressions such as len, len-3, end-1, and arithmetic expressions like 3*2 no longer work with Tcl 9’s stricter index parsing
  • Some variable assignment paths use flags that cause changed behavior under Tcl 9
  • Library conversion does not handle Tcl 9-style tclIndex entries using source -encoding
  • Some legacy TclX library helpers need small command-syntax compatibility updates for Tcl 9

The original implementation was written for Tcl 8 and assumes APIs and parser behavior that have changed in Tcl 9. This PR updates those assumptions while preserving TclX’s historical command behavior where practical.

Implementation Details

This PR updates TclX internals for Tcl 9 while keeping the public TclX command behavior compatible with the existing tests.

Key implementation changes:

  • Updated string, list, and object length handling to use Tcl_Size
  • Updated package initialization for Tcl 9 compatibility
  • Replaced removed legacy math function registration with Tcl-level compatibility commands
  • Restored TclX legacy relative index expression handling
  • Fixed variable assignment behavior affected by Tcl 9 parsing flag changes
  • Fixed chmod mode parsing so leading-zero mode strings keep TclX’s historical octal behavior
  • Fixed convlib handling of Tcl 9 tclIndex entries using source -encoding
  • Fixed read_file ... nonewline handling for Tcl 9’s read -nonewline syntax
  • Fixed dup handling for Tcl 9 channel names such as file5
  • Updated tests/build behavior so the Tcl 9 main test suite can run reliably

Design Decisions

  1. Preserve TclX compatibility:

    • TclX historically accepts index expressions such as len-3, end-1, and 3*2
    • Tcl 9’s stricter parsing rejects some of these forms directly
    • The compatibility code restores TclX’s older behavior instead of changing command semantics
  2. Keep the port conservative:

    • The goal is not to redesign TclX
    • Changes are limited to API compatibility, parser behavior compatibility, and test-suite reliability
    • Existing TclX public behavior is preserved where practical

Testing

Tested with Tcl 9.1a2.

The TclX main test suite passes:

Total   953
Passed  947
Skipped 6
Failed  0

The current TclX implementation depends on several Tcl 8-era
APIs and compatibility assumptions that no longer hold with Tcl
9. This commit updates TclX for Tcl 9 API compatibility,
allowing it to build, load, and pass the main test suite
against Tcl 9.1.
@resuna

resuna commented May 4, 2026

Copy link
Copy Markdown
Member

The CI builds against Tcl8, that will need to be updated for this branch.

@zolk3ri

zolk3ri commented May 4, 2026 •

Copy link
Copy Markdown
Author

The CI builds against Tcl8, that will need to be updated for this branch.

Oh, I missed it. My bad. I will work on it.

zolk3ri added 3 commits May 5, 2026 00:27
Build Tcl 9.1a1 from the core-9-1-a1 branch in Linux and macOS
GitHub Actions instead of using Tcl 8.6.

The workflows now clone the Tcl source branch, build and
install Tcl during CI, locate the installed tclConfig.sh and
tclsh paths dynamically, verify the Tcl patchlevel, and pass
the discovered Tcl configuration directory to configure.
Stop adding ppa:ubuntu-toolchain-r/test in the Linux CI
workflow.

Ubuntu Noble provides gcc-11 and g++-11 through the standard
repositories, so the external PPA is unnecessary and can cause
CI failures when Launchpad is unreachable.
Add stricter Tcl version validation to the Linux and macOS CI
workflows.

The workflows now source the discovered tclConfig.sh, combine
TCL_VERSION and TCL_PATCH_LEVEL to verify the expected Tcl
9.1a1 version, and keep the tclsh patchlevel check using
EXPECTED_TCL_VERSION. This ensures CI does not accidentally
configure against a different Tcl installation.

The workflow YAML files were also formatted with yamlfmt.
@zolk3ri

zolk3ri commented May 4, 2026 •

Copy link
Copy Markdown
Author

OK, I updated the CI workflows to build against Tcl 9.1a1.

Linux passes locally with act, and both workflow files pass actionlint. I could not test the macOS workflow locally because act cannot emulate GitHub’s macOS runners, so that should be verified by GitHub Actions.

I also removed the obsolete Ubuntu toolchain PPA and added stricter checks to ensure CI uses the expected Tcl installation.

The workflow YAML files were also formatted with yamlfmt.

zolk3ri added 3 commits May 5, 2026 16:20
The macOS GitHub Actions runner does not provide autoreconf by
default, which caused the Mac CI configure step to fail before
the build started.

Since the generated configure script is checked in again, run
it directly instead of regenerating it in CI.
Drop unused helper functions added for Tcl 9 compatibility in
the chmod, list, and string command implementations.

The list and string commands already use the existing TclX
index parsing paths where needed, preserving TclX arithmetic
index expression semantics. The chmod mode parser was also
unused, so keeping it only required compiler-specific unused
annotations without changing behavior.

This removes dead code and avoids carrying compatibility
wrappers that have no callers.
Tcl 9 uses Tcl_Size for string lengths returned by Tcl object
APIs, so local variables and helper parameters that receive
those lengths should use Tcl_Size as well.

Update the TclX_EchoObjCmd length variable and the string helper
input-length parameters, while keeping ExpandString's existing
int output-length interface for its callers.
@zolk3ri
zolk3ri force-pushed the tcl9-api-compat branch from d4ca069 to 0bf3a5a Compare May 5, 2026 14:27
@zolk3ri

zolk3ri commented May 5, 2026

Copy link
Copy Markdown
Author

Mac CI should work now. :)

The macOS CI failure happened because autoreconf is not available on the runner. Since configure is committed, the workflow now runs it directly instead of regenerating it during the macOS build.

@resuna

resuna commented May 5, 2026 •

Copy link
Copy Markdown
Member

Both Mac and Linux tests claim it passes but:

Mac:

dup.test
make: *** [test] Segmentation fault: 11
Error: Process completed with exit code 2.

Linux:

dup.test
make: *** [Makefile:232: test] Segmentation fault (core dumped)
Error: Process completed with exit code 2.

@zolk3ri

zolk3ri commented May 5, 2026

Copy link
Copy Markdown
Author

Both Mac and Linux tests claim it passes but:

Mac:

dup.test
make: *** [test] Segmentation fault: 11
Error: Process completed with exit code 2.

Linux:

dup.test
make: *** [Makefile:232: test] Segmentation fault (core dumped)
Error: Process completed with exit code 2.

I am working on it as we speak. :)

zolk3ri added 2 commits May 5, 2026 19:42
Add a smoke test step to the Linux and macOS CI workflows after
the build step and before the full test suite.

The smoke test loads TclX from the build directory via
TCLLIBPATH and sets TCLX_LIBRARY so the extension can find the
TclX script library before installation. It verifies the
package and runtime versions, prints the loaded shared library,
and asserts results from a small set of TclX string commands.

This gives CI an early hard failure if the built TclX library
cannot be loaded or basic commands return unexpected results,
even where the broader test suite is allowed to continue on
error.
Use Tcl_GetIntFromObj when parsing numeric dup arguments as
file descriptors instead of converting through
Tcl_GetObjType("int").

Tcl object type names are internal details and
Tcl_GetObjType("int") is not a safe public conversion path.
Under Tcl 9 this can return NULL, causing Tcl_ConvertToType to
segfault when dup handles an invalid numeric file descriptor
such as dup 100.

Using Tcl_GetIntFromObj avoids that internal type lookup and
lets the invalid file descriptor path report the expected POSIX
EBADF error.

Keep the existing channel-first lookup so numeric-looking
channel names are not mistaken for file descriptors.
@zolk3ri

zolk3ri commented May 5, 2026 •

Copy link
Copy Markdown
Author

Both Mac and Linux tests claim it passes but:

Mac:

dup.test
make: *** [test] Segmentation fault: 11
Error: Process completed with exit code 2.

Linux:

dup.test
make: *** [Makefile:232: test] Segmentation fault (core dumped)
Error: Process completed with exit code 2.

Thanks, that was a real issue in dup.test.

I tracked it to the dup 100 case. The code was using Tcl_ConvertToType with Tcl_GetObjType("int") to parse numeric file descriptor arguments. Under Tcl 9 that internal object type lookup can return NULL, causing the segfault.

I changed it to use Tcl_GetIntFromObj instead, so the invalid descriptor path now reports the expected POSIX EBADF error instead of crashing.

I also updated the CI smoke test to exercise the build-tree TclX before installation more cleanly. It now sets TCLX_LIBRARY so the loaded extension can find the build-tree tclx.tcl script library, and it asserts the results of a few basic TclX commands instead of only printing them.

After rebuilding locally, dup.test now passes through dup-5.9 and completes successfully with no segfault.

@zolk3ri

zolk3ri commented May 6, 2026 •

Copy link
Copy Markdown
Author

If this PR lands, I will follow up with a separate cleanup PR to fix the compiler warnings it exposes and add the necessary range checks (posssibly, will have to think about it) and related defensive checks. :)

@zolk3ri
zolk3ri marked this pull request as draft May 7, 2026 00:42
@zolk3ri

zolk3ri commented May 7, 2026

Copy link
Copy Markdown
Author

I fixed another Tcl 9 compatibility issue in this branch: profile was still relying on Tcl 8-era command dispatch assumptions.

The profile fix restores the saved Tcl_CmdInfo before dispatch, adds Tcl 9 objProc2 handling, avoids direct Tcl object internals access in the tailcall check, detects Tcl procedures by their delete hook, and updates the Tcl 9 profile stack expectations.

Tested after a clean rebuild with Tcl 9.1a2:

make clean
make
TCLLIBPATH="$PWD" TCLX_LIBRARY="$PWD/library" tclsh9.1 tests/all.tcl

Result:

Tests ended at Thu May 07 15:13:01 CEST 2026
all.tcl:        Total   1015    Passed  1004    Skipped 11      Failed  0
Sourced 0 Test Files.
Number of tests skipped for each constraint:
        7       pcOnly
        4       tclx_test_eval

@zolk3ri
zolk3ri marked this pull request as ready for review May 7, 2026 13:22
@resuna

resuna commented May 7, 2026

Copy link
Copy Markdown
Member

I tracked it to the dup 100 case. The code was using Tcl_ConvertToType with Tcl_GetObjType("int") to parse numeric file descriptor arguments. Under Tcl 9 that internal object type lookup can return NULL, causing the segfault.

I changed it to use Tcl_GetIntFromObj instead, so the invalid descriptor path now reports the expected POSIX EBADF error instead of crashing.

Good call, TclGetIntFromObj is the idiomatic API.

@resuna

resuna commented May 7, 2026

Copy link
Copy Markdown
Member

It looks like it's still building a libtclX8.6.

@zolk3ri

zolk3ri commented May 7, 2026 •

Copy link
Copy Markdown
Author

I pushed two follow-up commits after rechecking Tcl 8.6 compatibility:

  • 2bd5891 adds a Tcl 8.6 compatibility shim for Tcl_Size / TCL_SIZE_MODIFIER, so the Tcl 9-oriented size cleanups still compile against Tcl 8.6.
  • f182f97 adjusts lmatch-2.3 to accept the Tcl 8.6 regexp diagnostic text while preserving the Tcl 9 expectation.

I also verified the branch against Tcl 8.6.17 with a plain ./configure, followed by make and make test.

The Tcl 8.6 interpreter used for that run was:

/usr/bin/tclsh8.6 (8.6.17)

Result:

Tests ended at Thu May 07 16:12:46 CEST 2026
all.tcl:        Total   1712    Passed  1701    Skipped 11      Failed  0
Sourced 0 Test Files.
Number of tests skipped for each constraint:
        7       pcOnly
        4       tclx_test_eval
====== SUCCESS =====

Most importantly, regarding the library name question:

It looks like it's still building a libtclX8.6.

I rechecked the Tcl 9 build configuration. When configured explicitly against my Tcl 9.1a2 build, the generated Makefile contains:

PKG_LIB_FILE    = libtclx8.6.so
PACKAGE_VERSION = 8.6
TCL_BIN_DIR     = /home/zolk3ri/.local/pkg/tcl-9.1/lib
TCL_SRC_DIR     = /home/zolk3ri/lab/sourcegate/tcl/tcl-git

So the build is using Tcl 9 headers/configuration, but the produced library name still comes from the TclX package version (8.6), not from the configured Tcl core version.

Would you prefer this compatibility branch to keep producing libtclx8.6.so, or should the library/package naming be adjusted for Tcl 9 builds? If the latter is desired, one possible approach would be to derive the generated library name from the Tcl version selected by configure, but I did not want to make that packaging change without confirmation.

@resuna

resuna commented May 7, 2026 •

Copy link
Copy Markdown
Member

It should build a library that reflects the Tcl version it's built with, no? Am I missing something?

Shoudln't the TclX package version match the Tcl version?

@zolk3ri

zolk3ri commented May 8, 2026 •

Copy link
Copy Markdown
Author

It should build a library that reflects the Tcl version it's built with, no? Am I missing something?

Shoudln't the TclX package version match the Tcl version?

Yes, that makes sense. You are not missing anything, I believe.

I had initially treated the existing 8.6 value as a fixed TclX package version, so I kept it stable while doing the Tcl 9 compatibility work. Looking at it again, though, I think that was the wrong interpretation: this value appears to be intended to follow the Tcl version used for the build.

So I will update the branch so the generated TclX package/library version follows the Tcl version used for the build. In other words, a Tcl 8.6 build should continue to produce/provide Tclx 8.6, while a Tcl 9.x build should produce/provide the matching Tclx 9.x package/library.

I will make sure the generated pkgIndex.tcl, the shared library name, and package provide Tclx ... all use the same build Tcl version, then retest with both Tcl 8.6 and Tcl 9.x.

If there is also a separate TclX release-version convention I should preserve, please let me know, but my current understanding is that the package/library version should follow the Tcl build version. Am I understanding that correctly?

@zolk3ri
zolk3ri marked this pull request as draft May 8, 2026 05:38
@resuna

resuna commented May 8, 2026 •

Copy link
Copy Markdown
Member

Yes, you are understanding it correctly. TclX post-4 major/minor tracks the Tcl version, and patch tracks the Tcl version where possible. It started as "4" and out of sync because Tcl3 hadn't been updated in so long.

@zolk3ri

zolk3ri commented May 9, 2026 •

Copy link
Copy Markdown
Author

Yes, you are understanding it correctly. TclX post-4 major/minor tracks the Tcl version, and patch tracks the Tcl version where possible. It started as "4" and out of sync because Tcl3 hadn't been updated in so long.

Thanks, that clarification helped.

I pushed two commits addressing this:

  • tests now require the Tclx package version matching the running Tcl major/minor version instead of hard-coding Tclx 8.4/8.6
  • configure now derives the TclX package/library version from the detected Tcl version after loading tclConfig.sh

I also want to point out why the PR stats look larger than the actual functional change. One commit removes legacy form-feed characters (^L) from the generic C sources:

  • 9f6b827 Remove form-feed separators from generic sources

That cleanup accounts for a substantial part of the changed-line count, but it is mechanical formatting cleanup only. It does not intentionally change TclX behavior. I can revert this commit if you would rather keep this PR limited to the functional changes.

I retested both Tcl 8.6 and Tcl 9.1 builds after the versioning changes.

The resulting behavior is now:

  • Tcl 8.6 build:

    • provides Tclx 8.6
    • builds libtclx8.6.so
    • generates pkgIndex.tcl requiring Tclx 8.6
    • infox version reports the Tcl 8.6 patchlevel-derived full version
  • Tcl 9.1 build:

    • provides Tclx 9.1
    • builds libtclx9.1.so
    • generates pkgIndex.tcl requiring Tclx 9.1
    • infox version reports the Tcl 9.1 patchlevel-derived full version

Both test runs completed successfully with zero failures.

Does this now match the versioning behavior you had in mind?

@zolk3ri
zolk3ri marked this pull request as ready for review May 9, 2026 15:42
TclX.n describes infox version as the TclX version combined
with the base Tcl version, which was fine while the two
matched. Now that the TclX version is fixed, build the string
at runtime from the compiled-in TclX version and the running
interpreter's tcl_patchLevel, rather than the single value
returned before.
The interpreter's call frame level is int in some Tcl builds
and Tcl_Size in others, so a hardcoded %d or %ld warns on
whichever build has the other type. Format it with
TCL_SIZE_MODIFIER and cast the value to Tcl_Size so the
specifier and the argument always agree.
@zolk3ri
zolk3ri marked this pull request as ready for review September 3, 2026 11:52
@zolk3ri

zolk3ri commented Sep 3, 2026

Copy link
Copy Markdown
Author

I am sorry for sitting on this so long! Hopefully version handling is sorted now with TclX package version and infox version reporting a fixed 9.0 regardless of Tcl core, with a test covering it.

I left the .so name following the Tcl version (libtclx8.6.so vs libtclx9.1.so). My understanding is that using different names avoids a collision if both builds end up being installed in the same place. A single libtclx9.0.so for both Tcl 8 and Tcl 9 would only be safe if the two builds are always installed in separate prefixes. Maybe that is how it is set up at FlightAware, but TclX is built elsewhere too. Please correct me if I am misunderstanding how this is handled.

If you still want me to change this, I have already made the changes locally and can push them.

The TclX release version is independent of the version used to
name its installed package directory. tclXinit.c was still
passing TCLX_PACKAGE_VERSION to tcl_findLibrary, causing
installed builds to look for tclx9.0 instead of the directory
actually containing the script library.

Use TCLX_LIB_DIR_VERSION for the tcl_findLibrary lookup while
retaining TCLX_PACKAGE_VERSION for TclX package registration
and version reporting.
Tcl 9 no longer provides case, and its compatibility
replacement does not support the syntax used by these tests. As
a result, the file scan tests stopped early without reporting a
failure.

Replace the three remaining uses with switch, using exact
matching since none of the patterns contain wildcards.
Nothing in TclX uses case anymore, and Tcl 9 has removed it in
favour of switch. The compatibility command does not reproduce
case's syntax or matching rules, so leaving it absent avoids
silently taking the wrong branch.
@zolk3ri
zolk3ri marked this pull request as draft September 3, 2026 19:09
Tcl 9 reworded the regular expression compilation error.
scanmatch passes that message through unchanged, so ask the
interpreter for it rather than hard coding one version's
wording.
auto_reset deletes every command listed in the autoload index,
including itself, and these tests then point auto_path at
directories it cannot be reloaded from. The first reset
therefore left the file unable to reset again, and it stopped
partway through on every Tcl version.
These tests fail identically under Tcl 8.6 and Tcl 9 and were
never reached before, because the file stopped early on the
first reset. They cover package index handling, not Tcl version
compatibility.
The test runner caught errors raised while sourcing a test file
but only printed the message, so tests below the error were
never counted and the run finished reporting success. Count
those errors towards the exit status and print the error stack
with them.
Tcl hands back a result object from expression evaluation that
the caller owns. It was never released, so every index
expression leaked one object.
@zolk3ri
zolk3ri marked this pull request as ready for review September 4, 2026 18:54
@zolk3ri

zolk3ri commented Sep 5, 2026 •

Copy link
Copy Markdown
Author

Just a note on the scope of the earlier commits for completeness' sake. :)

The changes are a mix of regressions I introduced earlier in this branch and Tcl 9 compatibility fixes. The one worth calling out is the script library lookup, which was searching for tclx9.0 instead of the directory the files are actually installed into, so an installed build could not
find tclx.tcl on either Tcl version.

That said, 3 commits concern test infrastructure. The test runner was suppressing errors while sourcing test files, which meant ~700 tests were not actually being run under Tcl 9. Fixing this also exposed 8 package index tests in tcllib.test that fail identically under Tcl 8.6 and Tcl 9, so I marked them knownBug rather than change behavior outside the scope of this branch. They still run with -constraints knownBug and I am happy to open a separate issue for them, if you would like.

Both builds now run the same 1715 (out of 1734) tests with no failures, and the suite's exit status means something again!

@zolk3ri
zolk3ri marked this pull request as draft October 10, 2026 00:50
@resuna

resuna commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

Why did you convert it to a draft again after Chris approved it?

Also, can you change it to merge to BCK-13016-tcl9-upgrade?

@zolk3ri

zolk3ri commented Oct 12, 2026

Copy link
Copy Markdown
Author

Why did you convert it to a draft again after Chris approved it?

Also, can you change it to merge to BCK-13016-tcl9-upgrade?

Hey! I took another look and I realized it had a few issues (although relatively minor). I thought I should have it resolved in this PR.

Yes, of course! I will have it done by the end of this week.

@resuna

resuna commented Oct 13, 2026

Copy link
Copy Markdown
Member

@zolk3ri
zolk3ri changed the base branch from master to BCK-13016-tcl9-upgrade October 16, 2026 07:12
Tcl 9.1 is the first to compile uplevel into bytecode and the
profiler does not trace compiled commands. profile-3.2 and
profile-3.4 failed on Tcl 9.0, which still runs it as a
command. Choose the expectations without uplevel from 9.1 on.

tcltest 2.6 in Tcl 9.1.0 runs each test body from
tcltest::TestOnce. The stack filter matched Eval and RunTest
only with their leading colons and the profiler records
TestOnce without them. Match all three with or without the
colons.
cmdtrace prints the real level of each command but stops
indenting at level 20. Test bodies run two levels deeper under
tcltest 2.6 in Tcl 9.1.0. The body of the if in DoStuff then
reached level 21 and cmdtrace printed it at the same depth as
the if.
tclx.tcl read tclx_library and set auto_index and auto_path by
their plain names inside namespace eval. Tcl 9 no longer
resolves such names to existing globals. The check for
tclx_library failed and the setup never ran. buildpackageindex
could not be autoloaded and the library directory never reached
auto_path.
CI built only Tcl 9.1a1, where the tests that failed on the
9.0.4 and 9.1.0 releases passed. Tcl 8.6 and 9.0 went untested.
Pick tclsh by the version tclConfig.sh reports, which also
finds tclsh8.6.

tclsh reads the smoke test from a here document in the system
encoding. In a C locale the accented letter in the clength
check would count as two characters. Use the escape \u00e9
there instead.
Both workflows ran only for pull requests into master. Tcl 9
work goes into BCK-13016-tcl9-upgrade.
@zolk3ri

zolk3ri commented Oct 16, 2026 •

Copy link
Copy Markdown
Author

I had only been testing against Tcl 8.6 and a 9.1 alpha. Running the branch against the released 8.6.18, 9.0.4 and 9.1.0 showed failures on the last two. Two profile tests failed on 9.0.4. On 9.1.0 it was 22 profile tests and four cmdtrace tests. The first two new commits fix those tests. The third fixes tclx.tcl, which skipped part of its setup on every Tcl 9 and left buildpackageindex unable to autoload. All three releases now pass.

The fourth commit makes CI build against those three releases instead of 9.1a1. On the alpha those tests pass, which is how CI missed the failures. Since Update CI to build against Tcl 9.1a1 the workflows on this branch also built neither 8.6 nor 9.0.

The fifth runs both workflows for pull requests into any branch. They only ran for those into master and this one now targets BCK-13016-tcl9-upgrade.

I also removed my last commit which assumed TclProcDeleteProc was missing from the stub table. It is in the internal stub table and a stubs build reaches it through the table without linking against it.

A fix for dup at offsets of 2 GiB or more and one for the random profile-10 failures will follow as separate PRs.

Both bugs are older than this branch. Should those go to master or to BCK-13016-tcl9-upgrade?

@zolk3ri
zolk3ri marked this pull request as ready for review October 17, 2026 22:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants