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

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.

LendingGiving
The receiver’s signature(f file …): no owned(f owned file …), or an owned result
How oftenany number of timesexactly once on every path
Afterwardsstill usableno longer usable
Examplesf file.write, f file.read_linef 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

An 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.

TokenStack after itWhat happened
markmPuts 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

Giving away a value that a defer took over is refused: it would be closed twice on the way out. Leave the closing to the 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