Skip to content

Commit a655912

Browse files
committed
feat: configure EOF behavior
Brainfuck specification doesn't define EOF and it's considered an implementation detail
1 parent c3bb351 commit a655912

File tree

3 files changed

+34
-19
lines changed

3 files changed

+34
-19
lines changed

src/main/interpreter/InputOutput.scala

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,23 @@ import cats.{Applicative, Id}
66
import cats.syntax.all.*
77

88
trait InputOutput[F[_]]:
9-
def read: F[Either[InputOutputError, Byte]]
9+
def read: F[Either[InputOutputError, Option[Byte]]]
1010
def write(b: Byte): F[Either[InputOutputError, Unit]]
1111

1212
object InputOutput:
1313
transparent inline def apply[F[_]](using InputOutput[F]): InputOutput[F] = summon
1414

15-
given id: InputOutput[Id] = make
15+
given id(using eof: EOF = EOF.NoChange): InputOutput[Id] = make
1616

17-
def make[F[_]: Applicative]: InputOutput[F] = new InputOutput[F]:
18-
def read = stdin.pure[F]
19-
def write(b: Byte) = stdout(b).pure[F]
17+
def make[F[_]: Applicative](using eof: EOF): InputOutput[F] = new InputOutput[F]:
18+
def read = stdin.map(b => if b == -1 then eof.byte else Some(b.toByte)).pure[F]
19+
def write(b: Byte) = stdout(b.toChar).pure[F]
2020

21-
private def stdin: Either[InputOutputError, Byte] =
22-
Try((0 max System.in.read).toByte).toEither.leftMap(InputOutputError.read)
21+
private def stdin: Either[InputOutputError, Int] =
22+
Try(System.in.read).toEither.leftMap(InputOutputError.read)
2323

24-
private def stdout(b: Byte): Either[InputOutputError, Unit] =
25-
Try(System.out.print(b.toChar)).toEither.leftMap(InputOutputError.write)
24+
private def stdout(ch: Char): Either[InputOutputError, Unit] =
25+
Try(System.out.print(ch)).toEither.leftMap(InputOutputError.write)
2626

2727
enum InputOutputError(msg: String) extends Throwable(msg):
2828
case Read(msg: String) extends InputOutputError(s"Read: $msg")
@@ -31,3 +31,16 @@ enum InputOutputError(msg: String) extends Throwable(msg):
3131
object InputOutputError:
3232
inline def read(e: Throwable): InputOutputError = InputOutputError.Read(e.getMessage)
3333
inline def write(e: Throwable): InputOutputError = InputOutputError.Write(e.getMessage)
34+
35+
enum EOF:
36+
/** Return 0 on EOF. */
37+
case Zero
38+
/** Return -1 (255 as byte) on EOF. */
39+
case MinusOne
40+
/** Leave unchanged on EOF. */
41+
case NoChange
42+
43+
def byte: Option[Byte] = this match
44+
case Zero => Some(0)
45+
case MinusOne => Some(-1)
46+
case NoChange => None

src/main/interpreter/Interpreter.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ object Interpreter:
3333
case Command.IncByte => EitherT.rightT(tape.inc)
3434
case Command.DecByte => EitherT.rightT(tape.dec)
3535
case Command.OutputByte => EitherT(io.write(tape.get)).leftMap(InterpretError.IO(_)).as(tape)
36-
case Command.InputByte => EitherT(io.read).leftMap(InterpretError.IO(_)).map(tape.set)
36+
case Command.InputByte => EitherT(io.read).leftMap(InterpretError.IO(_)).map(_.fold(tape)(tape.set))
3737
case Command.Loop(cmds) => tape.iterateWhileM(eval(_, cmds))(_.get != 0)
3838

3939
enum InterpretError(msg: String) extends Throwable(msg):

src/test/interpreter/Interpreter.test.scala

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,19 @@ import dev.meetree.brainfuck.core.*
1111
class InterpreterSpec extends AnyFlatSpec with Matchers with EitherValues:
1212
import Interpreter.eval
1313

14-
case class Data(inputs: List[Byte], outputs: List[Byte], faulty: Boolean = false):
15-
def read: (Data, Either[InputOutputError, Byte]) =
14+
type F[A] = StateT[Id, Data, A]
15+
16+
given eof: EOF = EOF.NoChange
17+
given InputOutput[F] with
18+
def read: F[Either[InputOutputError, Option[Byte]]] = StateT.apply(s => s.read)
19+
def write(byte: Byte): F[Either[InputOutputError, Unit]] = StateT.apply(s => s.write(byte))
20+
21+
case class Data(inputs: List[Byte], outputs: List[Byte], faulty: Boolean = false)(using eof: EOF):
22+
def read: (Data, Either[InputOutputError, Option[Byte]]) =
1623
inputs match
1724
case _ if faulty => (this, Left(InputOutputError.Read("read")))
18-
case head :: tail => (Data(tail, outputs, faulty), Right(head))
19-
case Nil => (this, Right(0))
25+
case head :: tail => (Data(tail, outputs, faulty), Right(Some(head)))
26+
case Nil => (this, Right(eof.byte))
2027

2128
def write(byte: Byte): (Data, Either[InputOutputError, Unit]) =
2229
if faulty then (this, Left(InputOutputError.Write("write")))
@@ -26,11 +33,6 @@ class InterpreterSpec extends AnyFlatSpec with Matchers with EitherValues:
2633
final val empty: Data = Data(Nil, Nil)
2734
final val faulty: Data = Data.empty.copy(faulty = true)
2835

29-
type F[A] = StateT[Id, Data, A]
30-
given InputOutput[F] with
31-
def read: F[Either[InputOutputError, Byte]] = StateT.apply(s => s.read)
32-
def write(byte: Byte): F[Either[InputOutputError, Unit]] = StateT.apply(s => s.write(byte))
33-
3436
val interpreter = Interpreter.make[F]
3537
import interpreter.interpret
3638

0 commit comments

Comments
 (0)