Fuzion Logo
fuzion-lang.dev — The Fuzion Language Portal
JavaScript seems to be disabled. Functionality is limited.

io/buffered/reader.fz


# This file is part of the Fuzion language implementation.
#
# The Fuzion language implementation is free software: you can redistribute it
# and/or modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation, version 3 of the License.
#
# The Fuzion language implementation is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
# License for more details.
#
# You should have received a copy of the GNU General Public License along with The
# Fuzion language implementation.  If not, see <https://www.gnu.org/licenses/>.


# -----------------------------------------------------------------------
#
#  Tokiwa Software GmbH, Germany
#
#  Source code of Fuzion standard library feature io.buffered.reader
#
# -----------------------------------------------------------------------


# buffered.reader effect allows buffered reading
# by using the given Read_Handler
#
# note: anything in the buffer when effect is uninstalled will be discarded.
#
public reader(rh Read_Handler, buf_size i32) : effect
pre debug: buf_size > 0
is


  # circular buffer backing this reader
  #
  buffer := (mutate.circular_buffer u8).new LM buf_size.as_i64 0


  # read returns the current buffer or end of file.
  # in case the buffer is empty it fills the buffer
  # before returning it.
  #
  public read switch (array u8) (outcome io.end_of_file) => read buf_size


  # read returns the current buffer or end of file.
  # in case the buffer is empty it fills the buffer
  # with up to n bytes before returning it.
  #
  public read(n i32) switch (array u8) (outcome io.end_of_file)
    post
      debug: (result ? outcome => true | a array => !a.is_empty)
  =>
    if buffer.buffered = 0
      match fill_buffer
        unit =>
          # NYI: remove double buffering implementation outside of the reader,
          # just flush the buffer here.
          # m := min n.as_i64 buffer.buffered
          # buffer.flush m
          m := min n buffer.buffered.as_i32
          (buffer.as_array.slice 0 m).as_array
        o outcome io.end_of_file => o
    else
      # NYI: remove double buffering implementation outside of the reader,
      # just flush the buffer here.
      # m := min n.as_i64 buffer.buffered
      # buffer.flush m
      m := min n buffer.buffered.as_i32
      (buffer.as_array.slice 0 m).as_array



  # fill the currently empty buffer with up to buf_size bytes
  #
  fill_buffer switch unit (outcome io.end_of_file)
    pre
      debug: buffer.buffered = 0
  =>
    match rh.read buffer.available.as_i32
      a array u8 =>
        match buffer.enqueue a
          unit => unit
          e error => e
      io.end_of_file =>
        replace
        io.end_of_file
      e error => e


  # discard n items from buffer
  #
  public discard(n i32) unit
    pre
      debug: n >= 0
  =>
    m := min n.as_i64 buffer.buffered
    _ := buffer.flush m


  # discard complete buffer
  #
  public discard unit
  =>
    discard buf_size


# short hand for getting the currently installed `buffered.reader`
#
# NYI: CLEANUP: remove?
#
reader =>
  reader.env


# read n bytes using the currently installed byte reader effect
# if the returned sequence is empty or count is less than n, end of file has been reached.
#
public read_bytes(n i32) Sequence u8 ! reader =>

  res := (mutate.array u8).new LM

  _ :=
    for n_read := 0, n_read + r
    while n_read < n
      r := match reader.read   # NYI: we should limit the number of byted read by reader.read, we can exceed n!
          outcome => -1
          a (array u8) =>
            reader.discard a.length
            for b in a do
              res.add b
            a.length
    until r < 0
  res.as_array


# read string, up to n codepoints or until end of file
# requires `buffered.reader` effect to be installed.
#
public read_string(n i32) outcome String ! reader
pre debug: n >= 0
=>

  take_valid_codepoints(a Sequence u8, max i32) =>
    v := String.from_bytes a
      .codepoints_and_errors
      .take_while x->
        match x
          codepoint => true
          error => false
      .take max
      .map String x->
        match x
          c codepoint => c
          error => exit 1
      .as_array

    bytes_used := (v.map c->c.as_string.byte_length).fold i32.sum
    reader.discard bytes_used
    v

  for
    is_eof                 := reader.read  ? outcome => true | array => false
    next_bytes Sequence u8 := (reader.read ? outcome => []   | a array => a), rest ++ (reader.read ? outcome => [] | a array => a)
    next_codepoints        := take_valid_codepoints next_bytes n, take_valid_codepoints next_bytes n-codepoint_count
    # if we did not use any bytes and `next_bytes` contains not enough bytes for a codepoint potentially,
    # we trigger a `discard` and remember what we read so far via `rest`.
    # this is necesarry e.g. for stdin where we read one byte at a time.
    rest Sequence u8       := if n>0 && next_codepoints.is_empty && next_bytes.count < 4 then reader.discard; next_bytes else []
    codepoint_count        := next_codepoints.count, codepoint_count+next_codepoints.count
    res Sequence String    := next_codepoints, res ++ next_codepoints
  while !is_eof && codepoint_count < n
  else
    tmp outcome String :=
      if is_eof && res.is_empty
        error "-- end of file --"
      else
        String.join res
    tmp


# use the currently installed byte reader effect
# to read until the specified delimiter byte occurs
# if specified, strips carriage return bytes before
# the delimiter before returning the read data
#
public read_delimiter (delim u8, strip_cr bool) switch String io.end_of_file ! reader =>

  if reader.read ? outcome => true | * => false
    io.end_of_file
  else
    res := (mutate.array u8).new LM

    while
      match reader.read
        outcome =>
          false
        a array =>

          # trailing carriage returns are dropped
          add_to_res(a0 Sequence u8) unit =>
            if !a0.is_empty
              a1 := if strip_cr && a0.last.as_equatable = encodings.ascii.cr
                        (a0.slice 0 a0.count-1)
                    else
                       a0
              for b in a1 do
                res.add b

          match a.index_of delim
            idx i32 =>
              add_to_res (a.slice 0 idx)
              reader.discard idx+1
              false
            nil =>
              add_to_res a
              reader.discard
              true

    ref : String is
      public redef utf8 Sequence u8 := res.as_array


# use the currently installed byte reader effect
# to read until a line feed occurs.
# returns the line
#
public read_line switch String io.end_of_file ! reader =>
  read_delimiter encodings.ascii.lf true


# Read input fully into an array of bytes until end_of_file is reached
#
public read_fully array u8 ! reader =>
  for
    r Sequence u8 := [], r++n
    n := read_bytes 8192
  while n.count > 0 else
    r.as_array


# Read input fully and split it at the given delimiter. If specified, delete
# any trailing carriage returns (ASCII 13) from the resulting strings.
#
public read_delimiter_full(delim String, strip_cr bool) array String ! reader =>
  str := String.from_bytes read_fully
  if str.byte_length > 0
    (str.ends_with "\n" ? str.substring 0 str.byte_length-1 : str)
      .split delim
      .map (s -> if strip_cr && s.ends_with "\r" then s.substring 0 s.byte_length-1 else s)
      .as_array
  else
    []

# Read input fully and split it at linefeed (ASCII 10) characters. Delete
# any trailing carriage returns (ASCII 13) from the resulting strings.
#
public read_lines array String ! reader =>
  read_delimiter_full "\n" true


# Read input line by line calling `f` for each line until `f` returns false
# or end_of_file is reached.
#
public read_line_while(f String -> bool) String ! reader =>
  for s := "",  s + "\n" + match rl
                             s1 String => s1
                             io.end_of_file => panic "unreachable code path"
      rl := read_line
  while match rl
          str String => f str
          io.end_of_file => false
  else
    s

last changed: 2025-07-10