Skip to content

Fixes from the pre-release review - #1368

Merged
yogthos merged 13 commits into
mainfrom
fix/release-review
Oct 15, 2026
Merged

yogthos merged 13 commits into
mainfrom
fix/release-review

Conversation

@yogthos

@yogthos yogthos commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Three bugs from the pre-release review of the changes since v0.8.21.

  • The char writer behind io/writer (since Take Clojure's options in clojure.java.io's openers #1320), FileWriter and OutputStreamWriter wrote a char[] as #object[...] and ignored append's start/end. It now uses writer-piece-range / append-text like the other writers. (.write w buf 0 n) is the reader-buffer copy pattern, so this broke real code.
  • Files/delete / deleteIfExists discarded delete-file's #f: a file in a read-only directory stayed on disk and the call returned normally (deleteIfExists answered true). The failure is raised with the JDK's class for the errno (AccessDenied, DirectoryNotEmpty, else FileSystemException with strerror); Windows, which sets no errno, reads as AccessDenied. move's replace path relied on the silent failure and re-checked; it now lets nio-delete1 raise.
  • PrintWriter.checkError always answered false; it asks the wrapped stream, as PrintStream's checkError does since Drop writes to a failed process stream instead of raising #1337.

All three checked against Clojure 1.12 on the JVM. Unit rows for the first two were red before the fix; the closed-stdout smoke has a PrintWriter case.

Second commit, smaller findings from the same review:

Third commit: make test on macOS failed #1366's corpus row "an ISO-2022-JP reader keeps its shift state across reads" (CI runs the corpus on Linux only). Apple's iconv decodes a double-byte character cut off at the end of the input as ASCII and loses the shift state; glibc stops before it with EINVAL. The ISO-2022 decoders now track the shift state themselves and hand iconv only whole characters. New corpus row reads ISO-2022-JP and KR in 3-byte chunks; KR read a가3*b on macOS before.

…kError

The char writer behind io/writer, FileWriter and OutputStreamWriter rendered
a char[] argument as #object[...] and ignored append's start/end; it now goes
through the same writer-piece-range/append-text as the other writers.

Files/delete and deleteIfExists threw away delete-file's #f, so a file in a
read-only directory stayed put while the call succeeded. The failure is now
raised with the JDK's class for the errno.

PrintWriter.checkError answered false unconditionally; it now asks the stream
it wraps, as PrintStream's does.
…nm versioned names

bigint/biginteger of a double went through the double's binary value; the JVM
uses BigDecimal.valueOf, the shortest decimal string, so (bigint 1e30) is 10^30.

A socket write region outside its array raised ex-info from ffi/write-array;
it checks first and raises IndexOutOfBoundsException with the JDK's message.

Chez on Windows rounds a timed wait to the nearest millisecond, so the exact
sub-millisecond deadlines spun through their last half millisecond. Waits
there round up to whole milliseconds.

GNU nm prints a versioned import as name@VERSION, which the :static {:lib}
symbol check never matched against the C name the app binds.
@yogthos yogthos changed the title Fix char-writer write/append, Files/delete failures, PrintWriter.checkError Fixes from the pre-release review Oct 15, 2026
Apple's iconv decodes a double-byte character cut off at the end of the input
as ASCII and drops the shift state, where glibc stops before it with EINVAL.
A stream decode on ISO-2022-JP/KR/CN now tracks the shift state itself and
passes iconv only up to the last whole character, so the macOS corpus row for
the ISO-2022-JP reader passes and a KR stream read in 3-byte chunks no longer
reads a가3*b.
…side the object

evaluate read the whole stack on every call to have the caller's frames ready
for an error, 2.6x the cost of a successful eval. It is read in the handler
now, where the stack below is still evaluate's and its caller's.

The flonum payload probe counted down from 8, but the payload sits 6 past the
pointer on 64-bit Chez, so the first two probes read past the object's end.

The bigint fix passes one more clojure-test-suite assertion; baseline updated.
Source mode installs no collector policy, so the stack limit's default fell
back to 80% of physical RAM, per thread: a runaway recursion could take most
of the machine before StackOverflowError. It now uses the ceiling a built
program would get (JOLT_MAX_HEAP, else 25% of RAM), and RAM only when the
ceiling is explicitly off.
It kept only the host string and resolved on first use inside =/hash, printed
host:port, answered an InetAddress for a host that did not resolve, and had no
createUnresolved. It now holds the InetAddress (or none), prints and compares
the JDK's way, and connect/bind refuse an unresolved one. A literal-built
InetAddress carries no name, so getByName "127.0.0.1" prints /127.0.0.1 and
getHostName does the reverse lookup. Drops the two known-divergence entries
this covered.
…the member

PrintWriter let an IOException from the writer under it escape every write,
flush and close. As the JDK's, it now records it for checkError (an
InterruptedIOException re-asserts the interrupt) and lets anything else
through. Whether the target is a Writer is decided once, at construction, so
the guard costs nothing on a StringWriter or process stream.

A static that a provided class lacks reported "No dependency provides" the
class: (InetSocketAddress/createUnresolved ...) said so with jolt.socket loaded.
A class with a registered constructor answers "No matching field or method".
The keyword, get and collection-call arms of emit-invoke bypass emit-call, so
#1336's restore never reached them: a deftype valAt or a sorted map comparator
that tail-called a jolt fn left that returned fn as the innermost frame of the
next host throw. A non-tail lookup now restores the slot as other calls do;
the keyword-lookup loop measures the same within noise. Reminted.
make -j builds release and debug at once, and both write build-boot.inputs and
build-boot.so. On Windows a rename onto a name the other build just wrote
raises, since MoveFile does not replace, which failed the build at the inputs
rename. Both builds hash the same sources, so the second keeps the first's file
and drops its own temp.
A callback that ended in a tail call and returned to untraced code (a core HOF
runs the callbacks) left its site pair in the slot, and the next callback's
throw from host code reported that returned fn as its innermost frame. A
non-tail call now moves the slot's pair to an outer slot (vreg 12) and clears
the slot while it runs, so a throw inside it has no innermost pair. The outer
pair, the nearest erased caller further out, is captured with the throw and
used only as a gap bridge or landed below the live frame its tail call went to;
with no live frame confirming it, it is dropped. The apply-erased caller the
stack-trace gate pins still comes back. ~1.5% on a non-tail-call-bound loop.
Reminted.
A (lambda) literal list in its syntax-rules read to the checker as a lambda
form with no body, and the file came back unreadable.
@yogthos yogthos mentioned this pull request Oct 15, 2026
babashka.process adds the hook after .start returns, so a SIGTERM landing in
between ended jolt with no hook, and :shutdown destroy-tree left the child
running; process-test's fd-level INHERIT SIGTERM row caught it on a loaded CI
runner. jolt.process wraps process*, the spawn every entry point uses, to
register the hook first with a promise of the process, delivered when the
spawn returns; a hook run inside the window waits for it and kills the tree.
Upstream's drop-and-run on exit and drop on a failed spawn are kept.
@yogthos
yogthos merged commit 9c26613 into main Oct 15, 2026
13 checks passed
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.

1 participant