9 Owned values and the region
Builds on: Chapter 5 (the region, mark/release), chapter 7 (rows of pools), chapter 8 (failures).
Questions this chapter answers
- How does the processor make sure that something like an open file gets closed?
- How does it know a file is closed on the paths of failures too?
- How does a loop that makes much text spare the region?
- How can the closing code be written once instead of on every way out?
9.1 Values that must be handed over
The signature of file.open_read is (path str gives owned file or fail). owned means the value is an owned value. Whoever receives it shall hand it over exactly once on every path: give it to a word that takes it owned (file.close), store it in a global, or return it as an owned result. It can be lent any number of times to words that take it without owned (file.write, file.read_line).
Side by side, the two uses compare like this.
| Lending | Giving | |
|---|---|---|
| The receiver’s signature | (f file …): no owned | (f owned file …), or an owned result |
| How often | any number of times | exactly once on every path |
| Afterwards | still usable | no longer usable |
| Examples | f file.write, f file.read_line | f file.close, storing into a global, returning it |
09-copy-file.pnx
use io
use file
rem Copy a file line by line. Both files are owned: each must be closed on every path.
word copy_lines (source str, target str gives n int or fail)
source file.open_read or_fail to src
target file.open_write or_else (e) src file.close e fail end to dst
0 to n
loop
src file.read_line or_else (e) break end to line
cat (line, "\n") dst file.write or_else (e) src file.close dst file.close e fail end
n 1 add to n
end
src file.close
dst file.close
n
end
word setup (or fail)
"in.txt" file.open_write or_fail to f
"one\ntwo\nthree\n" f file.write or_else (e) f file.close e fail end
f file.close
end
setup or_else (e) end
word main () cat ("copied ", "in.txt" "out.txt" copy_lines or_else (e) -1 end, " lines") io.println end
main
Output
$ pnx 09-copy-file.pnx
copied 3 lines
copy_lines has two files and four ways out. When the second file does not open, it closes the first and fails. When writing fails, it closes both and fails. The processor follows all these paths at load, and refuses if any one of them does not close.
09-forgot-close.pnx
use file
word first_line (path str gives str or fail)
path file.open_read or_fail to f
f file.read_line or_fail
end
Refused
$ pnx 09-forgot-close.pnx
09-forgot-close.pnx:4:20: an owned value in a local is not given on every path (close, free or store it)
This word leaves through or_fail when read_line fails, and on that path it does not close f; it does not close it on the path of success either. The processor points at where f was made.
09-close-twice.pnx
use file
word f (path str or fail)
path file.open_read or_fail to h
h file.close
h file.close
end
Refused
$ pnx 09-close-twice.pnx
09-close-twice.pnx:5:3: this local gave its owned value away; it holds nothing to read
Q&A
Q. What happens to open files on a fault?
A. The handles a run that ends with a fault opened are closed by the processor. So the owned check looks only at paths that are not faults.
Common mistake: Keeping a dup of it is handy
owned value cannot be doubled with dup or dropped with drop. Doubled, it would be closed twice; dropped, never. Put it into a local and lend it by name.09-drop-owned.pnx
use file
word glance (path str or fail)
path file.open_read or_fail rem an owned file on the stack
drop rem thrown away: it would never be closed
end
Refused
$ pnx 09-drop-owned.pnx
09-drop-owned.pnx:4:3: an owned value is kept in a local or given on at once; this word would lose it
9.2 Sparing the region
cat writes new text into the region. The region has a fixed size, so a loop that keeps making text gives the region back in every turn with mark and release. It gives it back on the way out through a failure too — what mark gives is an owned value as well.
09-region-loop.pnx
use io
use str
rem Make a long report in a small region: each turn gives its bytes back.
buffer report 4096
word build (or fail)
1 200 range do (i int)
mark to m
cat ("row ", i, ": ", i i mul, "\n") report append or_else (e) m release e fail end
m release
end each
end
build or_else (e) cat ("full: ", e err_text) io.println end
cat ("report: ", report len, " bytes") io.println
Output
$ pnx 09-region-loop.pnx
report: 2735 bytes
One turn, followed. The mark that mark gives is an owned value too, so the way out on failure hands it to release as well.
| Token | Stack after it | What happened |
|---|---|---|
mark | m | Puts the region’s current place on as a mark: an owned value. |
to m | — | Names it. |
cat (…) | "row 1: 1\n" | New text goes into the region. |
report append | — | Copies it into the buffer. Fails when the buffer is full. |
or_else (e) m release e fail end | — | The way of failure: gives the region back and fails for the same reason. On success, skipped. |
m release | — | The region goes back to the mark; the next turn reuses the bytes cat wrote. |
Bytes appended to a buffer are copied into it, so they survive giving the region back.
9.3 Deferring: defer
copy_lines repeated, at each place that can fail, code that closes everything opened so far. defer CODE end lets that code be written once. It runs on every way out of its block (a word’s body, one turn of a loop, a do block): at the end, through or_fail, through return or break. Several run in the reverse of the order they are written.
09-copy-defer.pnx
use io
use file
rem The same copy with defer: each file is closed once, on every way out.
word copy_lines (source str, target str gives n int or fail)
source file.open_read or_fail to src
defer src file.close end
target file.open_write or_fail to dst
defer dst file.close end
0 to n
loop
src file.read_line or_else (e) break end to line
cat (line, "\n") dst file.write or_fail
n 1 add to n
end
n
end
word setup (or fail)
"in.txt" file.open_write or_fail to f
defer f file.close end
"one\ntwo\nthree\n" f file.write or_fail
end
setup or_else (e) end
word main () cat ("copied ", "in.txt" "out.txt" copy_lines or_else (e) -1 end, " lines") io.println end
main
Output
$ pnx 09-copy-defer.pnx
copied 3 lines
After defer src file.close end, the defer takes src over. The body only lends it, as in src file.read_line, and the defer closes it on the way out. The closing code in or_else goes away and or_fail can be used as it is. A mark of the region works the same way.
09-region-defer.pnx
use io
use str
buffer report 4096
word build (or fail)
1 200 range do (i int)
mark to m
defer m release end
cat ("row ", i, ": ", i i mul, "\n") report append or_fail
end each
end
build or_else (e) cat ("full: ", e err_text) io.println end
cat ("report: ", report len, " bytes") io.println
Output
$ pnx 09-region-defer.pnx
report: 2735 bytes
Common mistake: Closing at the end as well, after writing defer
09-defer-close.pnx
use file
word first_line (path str gives str or fail)
path file.open_read or_fail to f
defer f file.close end
f file.read_line or_fail
f file.close
end
Refused
$ pnx 09-defer-close.pnx
09-defer-close.pnx:6:5: a defer gives this owned value when the block is left; here it can only be lent
defer is written right in a word’s body, a loop’s body or a do block; not inside a branch of if. Deferred code leaves the stack as it found it, and a word that can fail inside it is taken with or_else (standard 10.7).
Q&A
Q. Who closes an owned value I put into a global?
A. Nobody. Storing into a global is a handing over too, so the check ends there and the rest is the program’s business. Write a word that takes it out of the global and closes it, and call it before the run ends. Only the handles of a run that ended with a fault are closed by the processor.
Summary
- An
ownedvalue is handed over exactly once on every path; lending is allowed any number of times (standard 12.4). - It is handed over on the paths of failures (
or_failor_return) and atreturntoo (11.2). defer CODE endruns on every way out of a block; the value it takes over is only lent in the body (10.7).ownedvalues cannot beduped ordropped (12.4).- On a fault the processor closes the handles (11.3).
- Loops give the region back with
mark/release(12.2).