From 31b50472821c5cdb16944f193a62c1a006f49311 Mon Sep 17 00:00:00 2001 From: "Josh Schairbaum (via Claude agent)" Date: Mon, 15 Jun 2026 10:36:12 -0500 Subject: [PATCH] test(kernel): regression test for circular-print mark restoration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #20 added the fe_write() cycle-detection fix (borrow GCMARKBIT, clear it in unmarkpairs) but shipped no test for it. This closes that gap. The mark bit lives in the low byte of a pair's car field, so a leaked mark corrupts the car pointer — which makes walking the structure after repr a direct, deterministic check of mark restoration (no GC needed). repr also runs fe_write twice internally (measure + fill), so a stale mark would skew the two passes. Verified the test has teeth: neutering unmarkpairs to a no-op makes the walkability checks segfault (exit 139), which ctest scores as a failure. 20/20 ctest, 23 checks in the kernel file. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 3 +++ tests/kernel/kernel.lsp | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2db255a..fb46aed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,9 @@ with `setcar`/`setcdr`; the old code followed the cycle forever, causing a stack overflow or hang. The fix borrows `GCMARKBIT` during traversal to detect cycles and prints `...` in their place, then immediately clears the marks. + Covered by the `kernel/circular-print` test, which also pins mark restoration + (a leaked mark bit corrupts the pair's car pointer, so post-print walkability + is a direct check). - **Comments terminated by `\r` now parse correctly** (upstream rxi/fe PR #25 partial). Source files with Windows-style `\r\n` line endings had their `\r` swallowed into the next token after a `;` comment, corrupting the parse. The diff --git a/tests/kernel/kernel.lsp b/tests/kernel/kernel.lsp index 4967d1a..e1b0d45 100644 --- a/tests/kernel/kernel.lsp +++ b/tests/kernel/kernel.lsp @@ -35,3 +35,25 @@ (deftest "kernel/string-escape-eof" ; backslash at EOF in a string must raise "unclosed string", not overflow (check-err (read-string "\"\\"))) + +;; fe_write detects cycles by borrowing GCMARKBIT, then clears it in +;; unmarkpairs. The mark bit lives in the low byte of a pair's car field, so a +;; *leaked* mark corrupts the car pointer — making post-print walkability a +;; direct test of mark restoration (no GC needed). `repr` runs fe_write twice +;; internally (measure + fill), so a stale mark would also skew the two passes. +(deftest "kernel/circular-print" + ; tail-cycle: x's last cdr points back at x. Prints finite, ends in "...". + (set x (list 1 2 3)) + (setcdr (cdr (cdr x)) x) + (check (is (repr x) "(1 2 3 ...)")) + ; head-cycle: car of the head is the head itself. + (set y (list 1 2 3)) + (setcar y y) + (check (is (repr y) "(... 2 3)")) + ; after printing, the cycle is intact and pointers are uncorrupted — + ; proves unmarkpairs cleared every borrowed mark bit. + (check (is (cdr (cdr (cdr x))) x)) ; identity: the cycle still closes on x + (check (is (car (cdr (cdr (cdr x)))) 1)) ; walk through the back-edge → x[0] + (check (is (car x) 1)) + ; reprʼing twice yields the same string — marks don't accumulate. + (check (is (repr x) (repr x))))