@@ -33,12 +33,12 @@ Path := [
3333 ## This function does not traverse symbolic links; symbolic links (including
3434 ## broken ones) return `Bool.False`.
3535 ## Sockets, FIFOs, devices, and other special filesystem objects also return `Bool.False`.
36- is_file! : Path => Try (Bool , [PathErr (IOErr ), ..])
36+ is_file! : Path => Try (Bool , [PathErr (IOErr , Path ), ..])
3737 is_file! = |path|
3838 match type!(path) {
3939 Ok (IsFile ) => Ok (Bool . True )
4040 Ok (_ ) => Ok (Bool . False )
41- Err (PathErr (NotFound )) => Ok (Bool . False )
41+ Err (PathErr (NotFound , _ )) => Ok (Bool . False )
4242 Err (err) => Err (err)
4343 }
4444
@@ -47,12 +47,12 @@ Path := [
4747 ## This function does not traverse symbolic links; symbolic links (including
4848 ## broken ones) return `Bool.False`.
4949 ## Sockets, FIFOs, devices, and other special filesystem objects also return `Bool.False`.
50- is_dir! : Path => Try (Bool , [PathErr (IOErr ), ..])
50+ is_dir! : Path => Try (Bool , [PathErr (IOErr , Path ), ..])
5151 is_dir! = |path|
5252 match type!(path) {
5353 Ok (IsDir ) => Ok (Bool . True )
5454 Ok (_ ) => Ok (Bool . False )
55- Err (PathErr (NotFound )) => Ok (Bool . False )
55+ Err (PathErr (NotFound , _ )) => Ok (Bool . False )
5656 Err (err) => Err (err)
5757 }
5858
@@ -61,21 +61,21 @@ Path := [
6161 ## This function will not traverse symbolic links - it checks whether the path
6262 ## itself is a symlink.
6363 ## Sockets, FIFOs, devices, and other special filesystem objects return `Bool.False`.
64- is_sym_link! : Path => Try (Bool , [PathErr (IOErr ), ..])
64+ is_sym_link! : Path => Try (Bool , [PathErr (IOErr , Path ), ..])
6565 is_sym_link! = |path|
6666 match type!(path) {
6767 Ok (IsSymLink ) => Ok (Bool . True )
6868 Ok (_ ) => Ok (Bool . False )
69- Err (PathErr (NotFound )) => Ok (Bool . False )
69+ Err (PathErr (NotFound , _ )) => Ok (Bool . False )
7070 Err (err) => Err (err)
7171 }
7272
7373 ## Returns `True` if the path exists on disk.
74- exists! : Path => Try (Bool , [PathErr (IOErr ), ..])
74+ exists! : Path => Try (Bool , [PathErr (IOErr , Path ), ..])
7575 exists! = |path|
7676 match type!(path) {
7777 Ok (_ ) => Ok (Bool . True )
78- Err (PathErr (NotFound )) => Ok (Bool . False )
78+ Err (PathErr (NotFound , _ )) => Ok (Bool . False )
7979 Err (err) => Err (err)
8080 }
8181
@@ -84,105 +84,111 @@ Path := [
8484 ## `IsOther` represents sockets, FIFOs, block and character devices, and any
8585 ## platform-specific object that is not a regular file, directory, or symbolic
8686 ## link. On Windows this includes unrecognized reparse-point types.
87- type! : Path => Try ([IsFile , IsDir , IsSymLink , IsOther ], [PathErr (IOErr ), ..])
87+ type! : Path => Try ([IsFile , IsDir , IsSymLink , IsOther ], [PathErr (IOErr , Path ), ..])
8888 type! = |path| {
8989 Host . path_type !(to_raw (path ))
90- . map_err (|err| PathErr (err))
90+ . map_err (|err| PathErr (err, path ))
9191 . map_ok (path_type_from_host)
9292 }
9393
9494 ## Read all bytes from a file at this path.
95- read_bytes! : Path => Try (List (U8 ), [PathErr (IOErr ), ..])
96- read_bytes! = |path| map_file_result(Host . file_read_bytes !(to_raw (path )))
95+ read_bytes! : Path => Try (List (U8 ), [PathErr (IOErr , Path ), ..])
96+ read_bytes! = |path| map_file_result(Host . file_read_bytes !(to_raw (path )), path )
9797
9898 ## Write bytes to a file at this path, replacing any existing contents.
99- write_bytes! : Path , List (U8 ) => Try ({}, [PathErr (IOErr ), ..])
100- write_bytes! = |path, bytes| map_file_result(Host . file_write_bytes !(to_raw (path ), bytes))
99+ write_bytes! : Path , List (U8 ) => Try ({}, [PathErr (IOErr , Path ), ..])
100+ write_bytes! = |path, bytes| map_file_result(Host . file_write_bytes !(to_raw (path ), bytes), path )
101101
102102 ## Read a UTF-8 file at this path.
103- read_utf8! : Path => Try (Str , [PathErr (IOErr ), ..])
104- read_utf8! = |path| map_file_result(Host . file_read_utf8 !(to_raw (path )))
103+ read_utf8! : Path => Try (Str , [PathErr (IOErr , Path ), ..])
104+ read_utf8! = |path| map_file_result(Host . file_read_utf8 !(to_raw (path )), path )
105105
106106 ## Write a UTF-8 file at this path, replacing any existing contents.
107- write_utf8! : Path , Str => Try ({}, [PathErr (IOErr ), ..])
108- write_utf8! = |path, content| map_file_result(Host . file_write_utf8 !(to_raw (path ), content))
107+ write_utf8! : Path , Str => Try ({}, [PathErr (IOErr , Path ), ..])
108+ write_utf8! = |path, content| map_file_result(Host . file_write_utf8 !(to_raw (path ), content), path )
109109
110110 ## Replace every occurrence of `pattern` with `replacement` in the UTF-8 file
111111 ## at this path.
112112 ##
113113 ## This reads the whole file, substitutes, and writes it back, so it is not
114114 ## atomic: a failure mid-write can leave the file partially written, exactly
115115 ## as a bare [write_utf8!] would.
116- replace_utf8! : Path , Str , Str => Try ({}, [PathErr (IOErr ), ..])
116+ replace_utf8! : Path , Str , Str => Try ({}, [PathErr (IOErr , Path ), ..])
117117 replace_utf8! = |path, pattern, replacement| {
118118 content = read_utf8!(path)?
119119 write_utf8!(path, Str . replace_each (content , pattern, replacement))
120120 }
121121
122122 ## Delete a file at this path.
123- delete! : Path => Try ({}, [PathErr (IOErr ), ..])
124- delete! = |path| map_file_result(Host . file_delete !(to_raw (path )))
123+ delete! : Path => Try ({}, [PathErr (IOErr , Path ), ..])
124+ delete! = |path| map_file_result(Host . file_delete !(to_raw (path )), path )
125125
126126 ## Return the size of the file at this path in bytes.
127- size_in_bytes! : Path => Try (U64 , [PathErr (IOErr ), ..])
128- size_in_bytes! = |path| map_file_result(Host . file_size_in_bytes !(to_raw (path )))
127+ size_in_bytes! : Path => Try (U64 , [PathErr (IOErr , Path ), ..])
128+ size_in_bytes! = |path| map_file_result(Host . file_size_in_bytes !(to_raw (path )), path )
129129
130130 ## Check whether the file at this path has any executable bit set.
131- is_executable! : Path => Try (Bool , [PathErr (IOErr ), ..])
132- is_executable! = |path| map_file_result(Host . file_is_executable !(to_raw (path )))
131+ is_executable! : Path => Try (Bool , [PathErr (IOErr , Path ), ..])
132+ is_executable! = |path| map_file_result(Host . file_is_executable !(to_raw (path )), path )
133133
134134 ## Check whether the file at this path has a readable owner permission bit set.
135- is_readable! : Path => Try (Bool , [PathErr (IOErr ), ..])
136- is_readable! = |path| map_file_result(Host . file_is_readable !(to_raw (path )))
135+ is_readable! : Path => Try (Bool , [PathErr (IOErr , Path ), ..])
136+ is_readable! = |path| map_file_result(Host . file_is_readable !(to_raw (path )), path )
137137
138138 ## Check whether the file at this path has a writable owner permission bit set.
139- is_writable! : Path => Try (Bool , [PathErr (IOErr ), ..])
140- is_writable! = |path| map_file_result(Host . file_is_writable !(to_raw (path )))
139+ is_writable! : Path => Try (Bool , [PathErr (IOErr , Path ), ..])
140+ is_writable! = |path| map_file_result(Host . file_is_writable !(to_raw (path )), path )
141141
142142 ## Return the last accessed time as nanoseconds since the Unix epoch.
143- time_accessed! : Path => Try (U128 , [PathErr (IOErr ), ..])
144- time_accessed! = |path| map_file_result(Host . file_time_accessed !(to_raw (path )))
143+ time_accessed! : Path => Try (U128 , [PathErr (IOErr , Path ), ..])
144+ time_accessed! = |path| map_file_result(Host . file_time_accessed !(to_raw (path )), path )
145145
146146 ## Return the last modified time as nanoseconds since the Unix epoch.
147- time_modified! : Path => Try (U128 , [PathErr (IOErr ), ..])
148- time_modified! = |path| map_file_result(Host . file_time_modified !(to_raw (path )))
147+ time_modified! : Path => Try (U128 , [PathErr (IOErr , Path ), ..])
148+ time_modified! = |path| map_file_result(Host . file_time_modified !(to_raw (path )), path )
149149
150150 ## Return the creation time as nanoseconds since the Unix epoch.
151- time_created! : Path => Try (U128 , [PathErr (IOErr ), ..])
152- time_created! = |path| map_file_result(Host . file_time_created !(to_raw (path )))
151+ time_created! : Path => Try (U128 , [PathErr (IOErr , Path ), ..])
152+ time_created! = |path| map_file_result(Host . file_time_created !(to_raw (path )), path )
153153
154154 ## Create a hard link at `link` pointing to `original`.
155- hard_link! : Path , Path => Try ({}, [PathErr (IOErr ), ..])
155+ ##
156+ ## Errors report `original`, which is what a missing-file failure refers to.
157+ ## Some failures concern `link` instead, such as `AlreadyExists`.
158+ hard_link! : Path , Path => Try ({}, [PathErr (IOErr , Path ), ..])
156159 hard_link! = |original, link|
157- map_file_result(Host . file_hard_link !(to_raw (original ), to_raw(link)))
160+ map_file_result(Host . file_hard_link !(to_raw (original ), to_raw(link)), original )
158161
159162 ## Rename a file from `from` to `to`.
160- rename! : Path , Path => Try ({}, [PathErr (IOErr ), ..])
163+ ##
164+ ## Errors report `from`; a failure may still concern `to`, such as its
165+ ## parent directory not existing.
166+ rename! : Path , Path => Try ({}, [PathErr (IOErr , Path ), ..])
161167 rename! = |from, to|
162- map_file_result(Host . file_rename !(to_raw (from ), to_raw(to)))
168+ map_file_result(Host . file_rename !(to_raw (from ), to_raw(to)), from )
163169
164170 ## Create a directory at this path.
165- create_dir! : Path => Try ({}, [PathErr (IOErr ), ..])
166- create_dir! = |path| map_dir_result(Host . dir_create !(to_raw (path )))
171+ create_dir! : Path => Try ({}, [PathErr (IOErr , Path ), ..])
172+ create_dir! = |path| map_dir_result(Host . dir_create !(to_raw (path )), path )
167173
168174 ## Create a directory and any missing parent directories at this path.
169- create_all! : Path => Try ({}, [PathErr (IOErr ), ..])
170- create_all! = |path| map_dir_result(Host . dir_create_all !(to_raw (path )))
175+ create_all! : Path => Try ({}, [PathErr (IOErr , Path ), ..])
176+ create_all! = |path| map_dir_result(Host . dir_create_all !(to_raw (path )), path )
171177
172178 ## Delete an empty directory at this path.
173- delete_empty! : Path => Try ({}, [PathErr (IOErr ), ..])
174- delete_empty! = |path| map_dir_result(Host . dir_delete_empty !(to_raw (path )))
179+ delete_empty! : Path => Try ({}, [PathErr (IOErr , Path ), ..])
180+ delete_empty! = |path| map_dir_result(Host . dir_delete_empty !(to_raw (path )), path )
175181
176182 ## Delete a directory and all contents at this path.
177- delete_all! : Path => Try ({}, [PathErr (IOErr ), ..])
178- delete_all! = |path| map_dir_result(Host . dir_delete_all !(to_raw (path )))
183+ delete_all! : Path => Try ({}, [PathErr (IOErr , Path ), ..])
184+ delete_all! = |path| map_dir_result(Host . dir_delete_all !(to_raw (path )), path )
179185
180186 ## List the entries in the directory at this path.
181- list! : Path => Try (List (Path ), [PathErr (IOErr ), ..])
187+ list! : Path => Try (List (Path ), [PathErr (IOErr , Path ), ..])
182188 list! = |path|
183189 match Host . dir_list !(to_raw (path )) {
184190 Ok (paths) => Ok (paths. map (from_raw))
185- Err (DirErr (err)) => Err (PathErr (err))
191+ Err (DirErr (err)) => Err (PathErr (err, path ))
186192 }
187193
188194 ## Create a UTF-8 text path.
@@ -356,18 +362,18 @@ Path := [
356362 }
357363}
358364
359- map_file_result : Try (a, [FileErr (IOErr )]) -> Try (a, [PathErr (IOErr ), ..])
360- map_file_result = |result|
365+ map_file_result : Try (a, [FileErr (IOErr )]), Path -> Try (a, [PathErr (IOErr , Path ), ..])
366+ map_file_result = |result, path |
361367 match result {
362368 Ok (value) => Ok (value)
363- Err (FileErr (err)) => Err (PathErr (err))
369+ Err (FileErr (err)) => Err (PathErr (err, path ))
364370 }
365371
366- map_dir_result : Try (a, [DirErr (IOErr )]) -> Try (a, [PathErr (IOErr ), ..])
367- map_dir_result = |result|
372+ map_dir_result : Try (a, [DirErr (IOErr )]), Path -> Try (a, [PathErr (IOErr , Path ), ..])
373+ map_dir_result = |result, path |
368374 match result {
369375 Ok (value) => Ok (value)
370- Err (DirErr (err)) => Err (PathErr (err))
376+ Err (DirErr (err)) => Err (PathErr (err, path ))
371377 }
372378
373379str_from_valid_utf8 : List (U8 ) -> Str
@@ -696,3 +702,32 @@ expect path_type_from_host(Other) == IsOther
696702expect Path . join (Path . unix ("foo "), "bar ") == Path . unix ("foo /bar ")
697703expect Path . join (Path . windows ("foo "), "bar ") == Path . windows ("foo \\bar ")
698704expect Path . join (Path . utf8 ("foo "), "bar ") == Path . utf8 ("foo /bar ")
705+
706+ ## Errors carry the path they were about, and successes pass through untouched.
707+ expect {
708+ result : Try ({}, [PathErr (IOErr , Path )])
709+ result = map_file_result(Err (FileErr (NotFound )), Path . utf8 ("a .txt "))
710+ match result {
711+ Err (PathErr (NotFound , path)) => path == Path . utf8 ("a .txt ")
712+ _ => Bool . False
713+ }
714+ }
715+
716+ expect {
717+ result : Try ({}, [PathErr (IOErr , Path )])
718+ result = map_dir_result(Err (DirErr (NotADirectory )), Path . unix_bytes ([0x2F, 0xFF]))
719+ match result {
720+ # Raw non-UTF-8 bytes survive the trip through the error.
721+ Err (PathErr (NotADirectory , path)) => path == Path . unix_bytes ([0x2F, 0xFF])
722+ _ => Bool . False
723+ }
724+ }
725+
726+ expect {
727+ result : Try (U64 , [PathErr (IOErr , Path )])
728+ result = map_file_result(Ok (42 ), Path . utf8 ("a .txt "))
729+ match result {
730+ Ok (value) => value == 42
731+ Err (_ ) => Bool . False
732+ }
733+ }
0 commit comments