Skip to content

Tidy the NIO output-option mapper and finish translating open errnos - #899

Merged
yogthos merged 3 commits into
mainfrom
chore/nio-output-options-followups
Sep 8, 2026
Merged

Tidy the NIO output-option mapper and finish translating open errnos#899
yogthos merged 3 commits into
mainfrom
chore/nio-output-options-followups

Conversation

@yogthos

@yogthos yogthos commented Sep 8, 2026

Copy link
Copy Markdown
Member

Follow-ups to #897, in two parts.

1. Finish the errno translation (the actual fix)

#897 gave nio-open-output-port a guard with arms for EEXIST and ENOENT and an
else that re-raised. Everything else escaped as the raw Chez condition, which
jolt rendered as a bare java.io.IOException:

(Files/newOutputStream <a directory>)
=> java.io.IOException
   "open-file-output-port: failed for /tmp/d: is a directory"

The JVM answers java.nio.file.FileSystemException reading /tmp/d: Is a directory. That is what #897's class-hierarchy entry for FileSystemException
was for — nothing had reached it yet.

The contract is UnixException.translateToIOException: ENOENT, EEXIST and EACCES
each get a class and carry only the path as their message, and every remaining
errno (EISDIR, ELOOP, ENOTDIR, ENOSPC) is a plain FileSystemException whose
message is <path>: <reason>. Chez hands us the strerror text as the second
irritant of the &i/o-filename condition, so the rendering is byte-identical on
Linux and macOS — both spell EISDIR Is a directory and ENOTDIR Not a directory. The reason is picked by scanning the irritants for a string that is
not the filename rather than by position, so an unexpected irritant list degrades
to the bare path instead of mislabeling the error.

This is strictly a refinement for callers: FileSystemException is an
IOException in the hierarchy, so a catch java.io.IOException that caught the
old escape still catches these.

The new row covers EISDIR and ENOTDIR through both newOutputStream and write,
asserting the class, the IOException relationship, and the message's shape
<path>: <non-empty reason>. It stops short of pinning the reason text: that is
libc's strerror, not jolt's to promise, and the unit gate runs only on Linux (the
macOS job is the flake workflow's packaging smoke and never invokes make unit),
so an exact-wording row could only rot unseen on macOS.

It also leaves the EACCES arm uncovered: provoking it needs a mode-500 directory,
which does not fail for a suite run as root, and a gate row whose outcome depends
on the runner's uid is worse than an uncovered one-line mapping.

2. Cleanups and a test gap

  • nio-write! is dead. Its only caller was Files/write, which fix(nio): honor output stream open options #897 moved
    onto the shared option mapper. nio-write-bv! (still used by copy) and
    nio-output-data->bv cover what it did.
  • The Files/write lambda shadowed Chez's file-options macro with a let*
    variable of the same name. It reads correctly today because nothing below the
    binding wants the macro — but a later (file-options no-fail) added inside that
    body would silently become an application of a fixnum. Renamed to fopts.
  • APPEND + TRUNCATE_EXISTING now reports the JDK's own wording,
    APPEND + TRUNCATE_EXISTING not allowed.
  • CREATE_NEW's refusal was half-covered. It was asserted for plain
    CREATE_NEW and for a symlink, but the other legal combinations —
    CREATE_NEW+APPEND, CREATE_NEW+TRUNCATE_EXISTING, CREATE_NEW+CREATE — were
    exercised only against a missing path, i.e. the create side, so the
    direction the fix is about went unchecked for them. Those take a different
    file-options branch than plain CREATE_NEW does ((no-truncate append) vs
    (file-options)), and what makes them refuse is that neither branch carries
    no-fail, so the open is O_EXCL. The new row pins that each must throw
    FileAlreadyExistsException and leave the existing bytes intact.

Validation

  • make unit: 1688/1688 (1686 before this branch, plus two new rows)
  • make smoke: 200 passed, 0 failed

Yogthos added 2 commits September 8, 2026 17:23
Three follow-ups to #897, none of them behavior the gate was missing:

- `nio-write!` became dead when Files/write moved onto the shared option
  mapper — its only caller. `nio-write-bv!` (still used by copy) and
  `nio-output-data->bv` cover what it did.

- The Files/write lambda bound a `let*` variable named `file-options`,
  which shadows the Chez macro of that name for the rest of the scope. It
  reads fine today because nothing below it wants the macro; a later
  `(file-options no-fail)` in that body would silently become an
  application of a bytevector-sized fixnum instead. Renamed to `fopts`.

- APPEND + TRUNCATE_EXISTING now reports the JDK's own wording,
  "APPEND + TRUNCATE_EXISTING not allowed".

The new row closes the one gap in the suite: CREATE_NEW's atomic refusal
was asserted for plain CREATE_NEW and for a symlink, but its legal
combinations — CREATE_NEW+APPEND, CREATE_NEW+TRUNCATE_EXISTING,
CREATE_NEW+CREATE — were only exercised against a *missing* path, so the
direction the fix is about went unchecked for them. Each combination now
has to throw FileAlreadyExistsException and leave the existing bytes
alone. Chez's exclusive-create is what enforces it: the `append` and
default file-options both omit `no-fail`, so the open carries O_EXCL.

unit gate: 1687/1687.
#897 gave nio-open-output-port a guard with arms for EEXIST and ENOENT
and an `else` that re-raised. Everything else therefore escaped as the
raw Chez condition, and jolt rendered it as a bare java.io.IOException
whose message named open-file-output-port:

  (Files/newOutputStream <a directory>)
  => java.io.IOException
     "open-file-output-port: failed for /tmp/d: is a directory"

The JVM answers a java.nio.file.FileSystemException reading
"/tmp/d: Is a directory". This is what #897's class-hierarchy entry for
FileSystemException was for; nothing had reached it yet.

The contract is UnixException.translateToIOException: ENOENT, EEXIST and
EACCES each get a class and carry only the path as their message, and
every remaining errno is a plain FileSystemException whose message is
"<path>: <reason>". Chez hands us the strerror text as the second
irritant of the &i/o-filename condition, so the rendering comes out
byte-identical on Linux and macOS -- both spell EISDIR "Is a directory"
and ENOTDIR "Not a directory". The reason is picked by scanning the
irritants for a string that is not the filename rather than by position,
so an unexpected irritant list degrades to the bare path instead of
mislabeling the error.

Strictly a refinement for callers: FileSystemException is an IOException
in the hierarchy, so a `catch java.io.IOException` that caught the old
escape still catches these.

The new row covers EISDIR and ENOTDIR through both newOutputStream and
write, asserting the class, the IOException relationship, and the exact
message. It deliberately does not cover the EACCES arm: provoking it
needs a mode-500 directory, which does not fail for a suite run as root,
and a gate row whose outcome depends on the runner's uid is worse than
an uncovered one-line mapping.

unit gate: 1688/1688. cli smoke: 200 passed, 0 failed.
@yogthos yogthos changed the title Tidy the NIO output-option mapper and cover CREATE_NEW's refusal side Tidy the NIO output-option mapper and finish translating open errnos Sep 8, 2026
The row I just added pinned the reason text ("Is a directory", "Not a
directory") exactly. That text is libc's, not jolt's: jolt's contract is
that it forwards strerror into the JDK's position in the message, and
"<path>: <reason>" is the part worth pinning.

It also could not be checked where it would break. The unit gate runs on
Linux in the `test` job; the macOS job is the flake workflow's packaging
smoke and never invokes `make unit`. So an exact-wording row would rot
unseen on macOS and surface as a local failure for a contributor there.

Now asserts the path prefix and a non-empty reason instead.

unit gate: 1688/1688.
@yogthos
yogthos merged commit e033db1 into main Sep 8, 2026
8 checks passed
@yogthos
yogthos deleted the chore/nio-output-options-followups branch September 8, 2026 22:32
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