Avatar of Drako

Drako's solution

to Forth in the Kotlin Track

Published at Aug 23 2019 · 0 comments
Instructions
Test suite
Solution
import java.util.Stack

class ForthEvaluator {
  companion object {
    private val SEPARATOR = Regex("""\s+""")

    private fun tokenize(source: String): List<ForthToken> {
      return source.split(SEPARATOR).map { element ->
        element.toIntOrNull()?.let(ForthToken::Number) ?: run {
          when (element) {
            ":" -> ForthToken.DefineWord
            ";" -> ForthToken.EndDefineWord
            else -> ForthToken.Word(element.toLowerCase())
          }
        }
      }
    }
  }

  enum class State {
    Default,
    RecordName,
    RecordBody
  }

  sealed class ForthToken {
    data class Number(val value: Int) : ForthToken()
    data class Word(val value: String) : ForthToken()
    object DefineWord : ForthToken()
    object EndDefineWord : ForthToken()
  }

  private val stack = Stack<Int>()

  private var state = State.Default

  private val words = mutableMapOf<String, () -> Unit>(
      "+" to this::add,
      "-" to this::subtract,
      "*" to this::multiply,
      "/" to this::divide,
      "dup" to this::duplicate,
      "drop" to this::drop,
      "swap" to this::swap,
      "over" to this::over
  )

  private fun add() {
    if (stack.size < 2) {
      throw IllegalArgumentException("Addition requires that the stack contain at least 2 values")
    }
    stack.push(stack.pop() + stack.pop())
  }

  private fun subtract() {
    if (stack.size < 2) {
      throw IllegalArgumentException("Subtraction requires that the stack contain at least 2 values")
    }
    val subtrahend = stack.pop()
    val minuend = stack.pop()
    stack.push(minuend - subtrahend)
  }

  private fun multiply() {
    if (stack.size < 2) {
      throw IllegalArgumentException("Multiplication requires that the stack contain at least 2 values")
    }
    stack.push(stack.pop() * stack.pop())
  }

  private fun divide() {
    if (stack.size < 2) {
      throw IllegalArgumentException("Division requires that the stack contain at least 2 values")
    }
    val divisor = stack.pop()
    val dividend = stack.pop()
    if (divisor == 0) {
      throw java.lang.IllegalArgumentException("Division by 0 is not allowed")
    }
    stack.push(dividend / divisor)
  }

  private fun duplicate() {
    val value = stack.lastOrNull()
        ?: throw IllegalArgumentException("Duplicating requires that the stack contain at least 1 value")
    stack.push(value)
  }

  private fun drop() {
    if (stack.isEmpty()) {
      throw IllegalArgumentException("Dropping requires that the stack contain at least 1 value")
    }
    stack.pop()
  }

  private fun swap() {
    if (stack.size < 2) {
      throw IllegalArgumentException("Swapping requires that the stack contain at least 2 values")
    }
    val a = stack.pop()
    val b = stack.pop()
    stack.push(a)
    stack.push(b)
  }

  private fun over() {
    if (stack.size < 2) {
      throw IllegalArgumentException("Overing requires that the stack contain at least 2 values")
    }
    val a = stack.pop()
    val b = stack.last()
    stack.push(a)
    stack.push(b)
  }

  inner class CustomWord(val name: String) : () -> Unit {
    private val tokens = mutableListOf<ForthToken>()

    fun addToken(token: ForthToken.Number) {
      tokens.add(token)
    }

    fun addToken(token: ForthToken.Word) {
      tokens.add(token)
    }

    override fun invoke() {
      tokens.forEach { token ->
        when (token) {
          is ForthToken.Number -> stack.push(token.value)
          is ForthToken.Word -> {
            val word = words[token.value]
                ?: throw IllegalArgumentException("No definition available for operator \"${token.value}\"")
            word()
          }
        }
      }
    }
  }

  private lateinit var newWord: CustomWord

  fun evaluateProgram(lines: List<String>): List<Int> {
    return evaluateProgram(lines.joinToString(separator = " "))
  }

  fun evaluateProgram(program: String): List<Int> {
    val tokens = tokenize(program)
    tokens.forEach { token ->
      when (state) {
        State.Default -> {
          when (token) {
            is ForthToken.Number -> stack.push(token.value)
            is ForthToken.Word -> {
              val word = words[token.value]
                  ?: throw IllegalArgumentException("No definition available for operator \"${token.value}\"")
              word()
            }
            is ForthToken.DefineWord -> state = State.RecordName
          }
        }
        State.RecordName -> {
          when (token) {
            is ForthToken.Word -> {
              newWord = CustomWord(name = token.value)
              state = State.RecordBody
            }
            is ForthToken.Number -> {
              throw IllegalArgumentException("Cannot redefine numbers")
            }
            else -> TODO("not specified what to do in this case")
          }
        }
        State.RecordBody -> {
          when (token) {
            is ForthToken.Number -> newWord.addToken(token)
            is ForthToken.Word -> newWord.addToken(token)
            is ForthToken.EndDefineWord -> {
              words[newWord.name] = newWord
              state = State.Default
            }
            else -> TODO("not specified what to do in this case")
          }
        }
      }
    }
    return stack
  }
}

Community comments

Find this solution interesting? Ask the author a question to learn more.

What can you learn from this solution?

A huge amount can be learned from reading other people’s code. This is why we wanted to give exercism users the option of making their solutions public.

Here are some questions to help you reflect on this solution and learn the most from it.

  • What compromises have been made?
  • Are there new concepts here that you could read more about to improve your understanding?