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_Provider
#
# note: anything in the buffer when effect is uninstalled will be discarded.
#
public reader(private rp Read_Provider, buf_size i32, buffer array u8 | io.end_of_file) : effect effect_mode.plain is


  # install this effect and execute 'f'. Wrap the result of 'f' into an
  # 'outcome' if 'f' returns normally, otherwise if 'f' is aborted early
  # via a call to 'raise' wrap the 'error' passed to 'raise' into the
  # resulting 'outcome'.
  #
  public with(R type, f ()->R) outcome R =>
    try reader R (() -> run f (() -> exit 1))


  # terminate immediately with the given error wrapped in 'option'.
  #
  raise(e error) =>
    (try reader).env.raise e


  # read returns the current buffer or end of file.
  # in case the buffer is empty it fills the buffer
  # before returning it.
  #
  public read => 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 max_n bytes before returning it.
  #
  public read(max_n i32) array u8 | io.end_of_file
  post result ? io.end_of_file => true | a array => !a.is_empty
  =>
    match buffer
      b array =>
        if b.is_empty
          fill_buffer max_n
        buffer
      io.end_of_file => io.end_of_file



  # fill the currently empty buffer with up to max_n bytes
  #
  private fill_buffer(max_n i32)
  pre match buffer
        a array => a.is_empty
        io.end_of_file => false
  is
    match rp.read max_n
      a array =>
        set buffer := a
        replace
      eof io.end_of_file =>
        set buffer := eof
        replace
      e error => raise e



  # discard n items from buffer
  #
  public discard(n i32)
  pre n >= 0
  is
    match buffer
      b array =>
        set buffer := (b.slice n b.length).as_array
        replace
      io.end_of_file =>



  # discard complete buffer
  #
  # NYI naming this feature discard leads to error: Duplicate feature declaration
  public discard_all
  is
    match buffer
      b array =>
        set buffer := []
        replace
      io.end_of_file =>



# short hand for getting the currently installed `buffered.reader`
#
reader =>
  io.buffered.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 =>

  mi : mutate is

  mi.go ()->

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

    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!
          io.end_of_file => -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) String ! reader
pre 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 ? io.end_of_file => true | a array => false
    next_bytes Sequence u8 := (reader.read ? io.end_of_file => []   | a array => a), rest ++ (reader.read ? io.end_of_file => [] | 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_all` 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_all; 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
    if is_eof && res.is_empty
      reader.raise (error "-- end of file --"); ""
    else
      String.join res


# use the currently installed byte reader effect
# to read until a line feed occurs.
# returns the line
#
public read_line String|io.end_of_file ! reader =>

  if reader.read ? io.end_of_file => true | * => false
    io.end_of_file
  else
    mi : mutate is

    mi.go String ()->
      res := (mutate.array u8).new mi

      while
        match reader.read
          io.end_of_file =>
            false
          a array =>

            # trailing carriage returns are dropped
            add_to_res(a Sequence u8) is
              if !a.is_empty
                a1 := if a.last = character_encodings.ascii.cr
                          (a.slice 0 a.count-1)
                        else
                          a
                for b in a1 do
                  res.add b

            match (container.searchable_sequence a).index_of character_encodings.ascii.lf
              idx i32 =>
                add_to_res (a.slice 0 idx)
                reader.discard idx+1
                false
              nil =>
                add_to_res a
                reader.discard_all
                true

      ref : String
        utf8 Sequence u8 := res.as_array


# 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 := (io.buffered.read_bytes 8192)
  while n.count > 0 else
    r.as_array


# 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 =>
  (String.from_bytes read_fully).split "\n"
                                     .map s->
                                       s.ends_with "\r" ? TRUE  => s.substring 0 s.byte_length-1
                                                        | FALSE => s
                                     .as_array