Skip to content

Fix pathname print/read round-trip and logical pathname stream tracking#746

Open
blakemcbride wants to merge 1 commit into
armedbear:masterfrom
blakemcbride:pathname-printing
Open

Fix pathname print/read round-trip and logical pathname stream tracking#746
blakemcbride wants to merge 1 commit into
armedbear:masterfrom
blakemcbride:pathname-printing

Conversation

@blakemcbride
Copy link
Copy Markdown

Fix pathname print/read round-trip and logical pathname stream tracking

Summary

Repairs three ANSI pathname failures rooted in two separate issues:
the readable printer emitted a namestring for pathnames whose
components couldn't be recovered by the reader, and OPEN discarded
the original logical pathname when it translated it for the
underlying file I/O. Three previously-failing ANSI conformance
tests now pass, and no previously-passing test regresses.

Test Area
PATHNAMES-PRINT-AND-READ-PROPERLY readable pathname printing
PRINT.PATHNAME.1 readable pathname printing
LOGICAL-PATHNAME.3 (logical-pathname stream) on open file

Root causes

A. #P".txt" printed for name=NIL type="txt" but reads as name=".txt" type=NIL

Pathname.printObject falls back to getNamestring() whenever a
namestring is available, with a short list of exceptions that force
the non-namestring (i.e. #<PATHNAME …>) form. The existing
exceptions were:

  1. host non-NIL (and not a URL)
  2. version non-NIL
  3. name equal to . or ..
  4. name containing the platform File.separatorChar

They did not cover the common case where name is NIL and
type is non-NIL. For (make-pathname :type "txt") the namestring
is ".txt", which the reader parses back as
name=".txt", type=NIL, version=NIL — not round-trippable. The
same problem applies to (make-pathname :type :wild) whose
namestring is ".*".

PATHNAMES-PRINT-AND-READ-PROPERLY walks the ANSI *pathnames*
list with write-to-string … :readably t followed by
read-from-string and demands EQUAL round-trip, catching
print-not-readable as an acceptable outcome. The affected entries
were (make-pathname :name nil :type "txt") and
(make-pathname :type :wild).

PRINT.PATHNAME.1 uses randomly-check-readability, which also
accepts a print-not-readable signal, so the same fix satisfies
both tests.

B. (logical-pathname stream) returned a physical pathname

OPEN translates a logical pathname to its physical target before
calling make-file-stream:

(let* ((p (merge-pathnames filename))
       (pathname (if (typep p 'logical-pathname)
                     (translate-logical-pathname p)
                     p)))
  …
  (make-file-stream pathname element-type direction if-exists …))

FileStream then stored the translated pathname as its pathname
field, so (pathname stream) and (logical-pathname stream) both
returned the physical pathname. LOGICAL-PATHNAME.3 opens a file
via a logical pathname and calls (logical-pathname stream)
inside the with-open-file body:

(let ((name "CLTEST:TEMP.DAT.NEWEST"))
  (with-open-file
    (s (logical-pathname name)
       :direction :output …)
    (or (equalt (logical-pathname s) (logical-pathname name)) …)))

Because the stream forgot the logical pathname used to open it,
(logical-pathname s) went through the Lisp wrapper:

(stream
 (let ((result (pathname pathspec)))
   (if (typep result 'logical-pathname)
       result
       (error 'simple-type-error
              :datum result
              :expected-type 'logical-pathname))))

and raised a type error, since result was a regular Pathname.

CLHS explicitly permits (pathname stream) to return the pathname
used to open the file, including a logical pathname. The fix is to
carry the original (possibly logical) pathname into the stream and
translate internally when opening the underlying java.io.File.

Changes

1. Pathname.java: detect the name=NIL, type≠NIL case

printObject gains one more exception to the useNamestring test:

} else if (getName() == NIL && getType() != NIL) {
    // Namestring would be ".type" which reads back with
    // name="type" rather than name=NIL, type=type.
    useNamestring = false;
}

With useNamestring false, the function falls through to the
non-namestring branch, which calls unreadableString(…). That in
turn signals print-not-readable whenever *print-readably* is
true — satisfying both PATHNAMES-PRINT-AND-READ-PROPERLY and
PRINT.PATHNAME.1, which treat that condition as acceptable.

2. FileStream.java: store the original pathname, translate only for I/O

The constructor now translates a LogicalPathname into its
physical target for the getFile() call, but assigns the caller's
pathname — unchanged — to this.pathname:

super(Symbol.FILE_STREAM);
final Pathname physicalPathname
    = (pathname instanceof LogicalPathname)
      ? (Pathname) LogicalPathname.translateLogicalPathname((LogicalPathname) pathname)
      : pathname;
final File file = physicalPathname.getFile();

The make-file-stream primitive needs to dispatch on the
translated form (so a logical pathname that resolves to a jar or
URL still routes to JarStream / URLStream), but for the plain
FileStream case it now forwards the original:

final Pathname dispatchPathname
    = (pathname instanceof LogicalPathname)
      ? (Pathname) LogicalPathname.translateLogicalPathname((LogicalPathname) pathname)
      : pathname;

if (dispatchPathname.isJar())      …  // uses dispatchPathname
else if (dispatchPathname instanceof URLPathname && …) …  // uses dispatchPathname
else                                  new FileStream(pathname, …);  // original

3. open.lisp: pass the pre-translation pathname to make-file-stream

Three call sites changed from (make-file-stream pathname …) to
(make-file-stream p …), where p is the result of
(merge-pathnames filename) before the logical→physical
translation. All other uses of pathname in OPEN (probe-file,
create-new-file, wild-pathname-p, etc.) continue to use the
translated form — only the stream construction is affected.

Files changed

  • src/org/armedbear/lisp/Pathname.java
  • src/org/armedbear/lisp/FileStream.java
  • src/org/armedbear/lisp/open.lisp

Test plan

  • ant abcl builds cleanly.
  • Full ANSI suite (ant test.ansi.compiled): all three target
    tests (PATHNAMES-PRINT-AND-READ-PROPERLY,
    LOGICAL-PATHNAME.3, PRINT.PATHNAME.1) disappear from the
    unexpected-failure list. Remaining failures match the
    pre-existing baseline from other unmerged branches (PPRINT
    dispatch, numeric FORMAT, MAKE-LOAD-FORM, CLOS corrections,
    etc.).
  • Round-trip probe over the ANSI *pathnames* list: every
    pathname that does not throw print-not-readable now
    satisfies (equal p (read-from-string (write-to-string p :readably t))). The name=NIL, type="txt" and
    name=NIL, type=:WILD cases signal print-not-readable
    (acceptable per the test's handler-case).
  • Manual spot checks:
    - (with-open-file (s (logical-pathname "CLTEST:TEMP.DAT.NEWEST") …)
    (logical-pathname s))#P"CLTEST:TEMP.DAT.NEWEST"
    of type LOGICAL-PATHNAME, equal to
    (logical-pathname "CLTEST:TEMP.DAT.NEWEST").
    - (with-open-file (s "/tmp/plain.txt" …) (pathname s))
    → plain Pathname (regression guard for non-logical opens).
    - A logical pathname whose translation resolves to a jar or
    URL still dispatches to JarStream / URLStream.

Compatibility

No public API change. Two behavioural shifts, both in the
direction CLHS requires:

  • (pathname stream) on a stream opened via a logical pathname
    now returns a logical pathname (previously: physical). CLHS
    22.1.3 describes the pathname stored by a stream as "the file
    name used to open the file" and permits either form. Callers
    that expected the physical form should wrap the result in
    translate-logical-pathname.
  • Pathnames with name=NIL and type non-NIL now signal
    print-not-readable under *print-readably* t, rather than
    printing a non-round-trippable namestring. Unreadable (non-:readably)
    output is unchanged.

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