Avatar of DennisHartrampf

DennisHartrampf's solution

to Forth in the Kotlin Track

Published at Apr 16 2019 · 0 comments
Instructions
Test suite
Solution

Note:

This exercise has changed since this solution was written.

Implement an evaluator for a very simple subset of Forth.

Forth is a stack-based programming language. Implement a very basic evaluator for a small subset of Forth.

Your evaluator has to support the following words:

  • +, -, *, / (integer arithmetic)
  • DUP, DROP, SWAP, OVER (stack manipulation)

Your evaluator also has to support defining new words using the customary syntax: : word-name definition ;.

To keep things simple the only data type you need to support is signed integers of at least 16 bits size.

You should use the following rules for the syntax: a number is a sequence of one or more (ASCII) digits, a word is a sequence of one or more letters, digits, symbols or punctuation that is not a number. (Forth probably uses slightly different rules, but this is close enough.)

Words are case-insensitive.

Submitting Incomplete Solutions

It's possible to submit an incomplete solution so you can see how others have completed the exercise.

ForthEvaluatorTest.kt

import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import kotlin.test.assertEquals

class ForthEvaluatorTest {

    @Rule
    @JvmField
    var expectedException: ExpectedException = ExpectedException.none()

    private lateinit var forthEvaluator: ForthEvaluator

    @Before
    fun setUp() {
        forthEvaluator = ForthEvaluator()
    }

    @Test
    fun testNumbersAreJustPushedOntoTheStack() {
        assertEquals(
                listOf(1, 2, 3, 4, 5),
                forthEvaluator.evaluateProgram(listOf("1 2 3 4 5")))
    }

    @Ignore
    @Test
    fun testTwoNumbersCanBeAdded() {
        assertEquals(
                listOf(3),
                forthEvaluator.evaluateProgram(listOf("1 2 +")))
    }

    @Ignore
    @Test
    fun testErrorIfAdditionAttemptedWithNothingOnTheStack() {
        expectedException.expect(IllegalArgumentException::class.java)
        expectedException.expectMessage("Addition requires that the stack contain at least 2 values")

        forthEvaluator.evaluateProgram(listOf("+"))
    }

    @Ignore
    @Test
    fun testErrorIfAdditionAttemptedWithOneNumberOnTheStack() {
        expectedException.expect(IllegalArgumentException::class.java)
        expectedException.expectMessage("Addition requires that the stack contain at least 2 values")

        forthEvaluator.evaluateProgram(listOf("1 +"))
    }

    @Ignore
    @Test
    fun testTwoNumbersCanBeSubtracted() {
        assertEquals(
                listOf(-1),
                forthEvaluator.evaluateProgram(listOf("3 4 -")))
    }

    @Ignore
    @Test
    fun testErrorIfSubtractionAttemptedWithNothingOnTheStack() {
        expectedException.expect(IllegalArgumentException::class.java)
        expectedException.expectMessage("Subtraction requires that the stack contain at least 2 values")

        forthEvaluator.evaluateProgram(listOf("-"))
    }

    @Ignore
    @Test
    fun testErrorIfSubtractionAttemptedWithOneNumberOnTheStack() {
        expectedException.expect(IllegalArgumentException::class.java)
        expectedException.expectMessage("Subtraction requires that the stack contain at least 2 values")

        forthEvaluator.evaluateProgram(listOf("1 -"))
    }

    @Ignore
    @Test
    fun testTwoNumbersCanBeMultiplied() {
        assertEquals(
                listOf(8),
                forthEvaluator.evaluateProgram(listOf("2 4 *")))
    }

    @Ignore
    @Test
    fun testErrorIfMultiplicationAttemptedWithNothingOnTheStack() {
        expectedException.expect(IllegalArgumentException::class.java)
        expectedException.expectMessage("Multiplication requires that the stack contain at least 2 values")

        forthEvaluator.evaluateProgram(listOf("*"))
    }

    @Ignore
    @Test
    fun testErrorIfMultiplicationAttemptedWithOneNumberOnTheStack() {
        expectedException.expect(IllegalArgumentException::class.java)
        expectedException.expectMessage("Multiplication requires that the stack contain at least 2 values")

        forthEvaluator.evaluateProgram(listOf("1 *"))
    }

    @Ignore
    @Test
    fun testTwoNumbersCanBeDivided() {
        assertEquals(
                listOf(4),
                forthEvaluator.evaluateProgram(listOf("12 3 /")))
    }

    @Ignore
    @Test
    fun testThatIntegerDivisionIsUsed() {
        assertEquals(
                listOf(2),
                forthEvaluator.evaluateProgram(listOf("8 3 /")))
    }

    @Ignore
    @Test
    fun testErrorIfDividingByZero() {
        expectedException.expect(IllegalArgumentException::class.java)
        expectedException.expectMessage("Division by 0 is not allowed")

        forthEvaluator.evaluateProgram(listOf("4 0 /"))
    }

    @Ignore
    @Test
    fun testErrorIfDivisionAttemptedWithNothingOnTheStack() {
        expectedException.expect(IllegalArgumentException::class.java)
        expectedException.expectMessage("Division requires that the stack contain at least 2 values")

        forthEvaluator.evaluateProgram(listOf("/"))
    }

    @Ignore
    @Test
    fun testErrorIfDivisionAttemptedWithOneNumberOnTheStack() {
        expectedException.expect(IllegalArgumentException::class.java)
        expectedException.expectMessage("Division requires that the stack contain at least 2 values")

        forthEvaluator.evaluateProgram(listOf("1 /"))
    }

    @Ignore
    @Test
    fun testCombinedAdditionAndSubtraction() {
        assertEquals(
                listOf(-1),
                forthEvaluator.evaluateProgram(listOf("1 2 + 4 -")))
    }

    @Ignore
    @Test
    fun testCombinedMultiplicationAndDivision() {
        assertEquals(
                listOf(2),
                forthEvaluator.evaluateProgram(listOf("2 4 * 3 /")))
    }

    @Ignore
    @Test
    fun testDupCopiesTheTopValueOnTheStack() {
        assertEquals(
                listOf(1, 1),
                forthEvaluator.evaluateProgram(listOf("1 dup")))
    }

    @Ignore
    @Test
    fun testDupParsingIsCaseInsensitive() {
        assertEquals(
                listOf(1, 2, 2),
                forthEvaluator.evaluateProgram(listOf("1 2 dup")))
    }

    @Ignore
    @Test
    fun testErrorIfDuplicatingAttemptedWithNothingOnTheStack() {
        expectedException.expect(IllegalArgumentException::class.java)
        expectedException.expectMessage("Duplicating requires that the stack contain at least 1 value")

        forthEvaluator.evaluateProgram(listOf("dup"))
    }

    @Ignore
    @Test
    fun testDropRemovesTheTopValueOnTheStackIfItIsTheOnlyOne() {
        assertEquals(
                emptyList(),
                forthEvaluator.evaluateProgram(listOf("1 drop")))
    }

    @Ignore
    @Test
    fun testDropRemovesTheTopValueOnTheStackIfItIsNotTheOnlyOne() {
        assertEquals(
                listOf(1),
                forthEvaluator.evaluateProgram(listOf("1 2 drop")))
    }

    @Ignore
    @Test
    fun testErrorIfDroppingAttemptedWithNothingOnTheStack() {
        expectedException.expect(IllegalArgumentException::class.java)
        expectedException.expectMessage("Dropping requires that the stack contain at least 1 value")

        forthEvaluator.evaluateProgram(listOf("drop"))
    }

    @Ignore
    @Test
    fun testSwapSwapsTheTopTwosValueOnTheStackIfTheyAreTheOnlyOnes() {
        assertEquals(
                listOf(2, 1),
                forthEvaluator.evaluateProgram(listOf("1 2 swap")))
    }

    @Ignore
    @Test
    fun testSwapSwapsTheTopTwosValueOnTheStackIfTheyAreNotTheOnlyOnes() {
        assertEquals(
                listOf(1, 3, 2),
                forthEvaluator.evaluateProgram(listOf("1 2 3 swap")))
    }

    @Ignore
    @Test
    fun testErrorIfSwappingAttemptedWithNothingOnTheStack() {
        expectedException.expect(IllegalArgumentException::class.java)
        expectedException.expectMessage("Swapping requires that the stack contain at least 2 values")

        forthEvaluator.evaluateProgram(listOf("swap"))
    }

    @Ignore
    @Test
    fun testErrorIfSwappingAttemptedWithOneNumberOnTheStack() {
        expectedException.expect(IllegalArgumentException::class.java)
        expectedException.expectMessage("Swapping requires that the stack contain at least 2 values")

        forthEvaluator.evaluateProgram(listOf("1 swap"))
    }

    @Ignore
    @Test
    fun testOverCopiesTheSecondElementIfThereAreOnlyTwo() {
        assertEquals(
                listOf(1, 2, 1),
                forthEvaluator.evaluateProgram(listOf("1 2 over")))
    }

    @Ignore
    @Test
    fun testOverCopiesTheSecondElementIfThereAreMoreThanTwo() {
        assertEquals(
                listOf(1, 2, 3, 2),
                forthEvaluator.evaluateProgram(listOf("1 2 3 over")))
    }

    @Ignore
    @Test
    fun testErrorIfOveringAttemptedWithNothingOnTheStack() {
        expectedException.expect(IllegalArgumentException::class.java)
        expectedException.expectMessage("Overing requires that the stack contain at least 2 values")

        forthEvaluator.evaluateProgram(listOf("over"))
    }

    @Ignore
    @Test
    fun testErrorIfOveringAttemptedWithOneNumberOnTheStack() {
        expectedException.expect(IllegalArgumentException::class.java)
        expectedException.expectMessage("Overing requires that the stack contain at least 2 values")

        forthEvaluator.evaluateProgram(listOf("1 over"))
    }

    @Ignore
    @Test
    fun testUserDefinedOperatorsCanConsistOfBuiltInOperators() {
        assertEquals(
                listOf(1, 1, 1),
                forthEvaluator.evaluateProgram(listOf(": dup-twice dup dup ;", "1 dup-twice")))
    }

    @Ignore
    @Test
    fun testUserDefinedOperatorsAreEvaluatedInTheCorrectOrder() {
        assertEquals(
                listOf(1, 2, 3),
                forthEvaluator.evaluateProgram(listOf(": countup 1 2 3 ;", "countup")))
    }

    @Ignore
    @Test
    fun testCanRedefineAUserDefinedOperator() {
        assertEquals(
                listOf(1, 1, 1),
                forthEvaluator.evaluateProgram(listOf(": foo dup ;", ": foo dup dup ;", "1 foo")))
    }

    @Ignore
    @Test
    fun testCanOverrideBuiltInWordOperators() {
        assertEquals(
                listOf(1, 1),
                forthEvaluator.evaluateProgram(listOf(": swap dup ;", "1 swap")))
    }

    @Ignore
    @Test
    fun testCanOverrideBuiltInArithmeticOperators() {
        assertEquals(
                listOf(12),
                forthEvaluator.evaluateProgram(listOf(": + * ;", "3 4 +")))

    }

    @Ignore
    @Test
    fun testCanUseDifferentWordsWithSameName(){
        assertEquals(
                listOf(5,6),
                forthEvaluator.evaluateProgram(listOf(": foo 5;", ": bar foo ;", ": foo 6 ;", "bar foo")))
    }

    @Ignore
    @Test
    fun testCanDefineWordThatUsesWordWithTheSameName(){
        assertEquals(
                listOf(11),
                forthEvaluator.evaluateProgram(listOf(": foo 10;", ": foo foo 1 + ;", "foo")))
    }

    @Ignore
    @Test
    fun testCannotRedefineNumbers() {
        expectedException.expect(IllegalArgumentException::class.java)
        expectedException.expectMessage("Cannot redefine numbers")

        forthEvaluator.evaluateProgram(listOf(": 1 2 ;"))
    }

    @Ignore
    @Test
    fun testErrorIfEvaluatingAnUndefinedOperator() {
        expectedException.expect(IllegalArgumentException::class.java)
        expectedException.expectMessage("No definition available for operator \"foo\"")

        forthEvaluator.evaluateProgram(listOf("foo"))
    }

    @Ignore
    @Test
    fun testCaseInsensitivity(){
        assertEquals(
                listOf(1,1,1,1),
                forthEvaluator.evaluateProgram(listOf("1 DUP Dup dup")))
    }

    @Ignore
    @Test
    fun testDropIsCaseInsensitive(){
        assertEquals(
                listOf(1),
                forthEvaluator.evaluateProgram(listOf("1 2 3 4 DROP Drop drop")))
    }

    @Ignore
    @Test
    fun testSwapIsCaseInsensitive(){
        assertEquals(
                listOf(2,3,4,1),
                forthEvaluator.evaluateProgram(listOf("1 2 SWAP 3 Swap 4 swap")))
    }

    @Ignore
    @Test
    fun testOverIsCaseInsensitive(){
        assertEquals(
                listOf(1,2,1,2,1),
                forthEvaluator.evaluateProgram(listOf("1 2 OVER Over over")))
    }

    @Ignore
    @Test
    fun testUserDefinedWordsAreCaseInsensitive(){
        assertEquals(
                listOf(1,1,1,1),
                forthEvaluator.evaluateProgram(listOf(": foo dup;", "1 FOO Foo foo")))
    }

    @Ignore
    @Test
    fun testDefinitionsAreCaseInsensitive(){
        assertEquals(
                listOf(1,1,1,1),
                forthEvaluator.evaluateProgram(listOf(": SWAP DUP Dup dup ;", "1 swap")))
    }
}

src/main/kotlin/ForthEvaluator.kt

import java.util.*

class ForthEvaluator {
    private val stack = LinkedList<Int>()
    private val definitionsHolder = DefinitionsHolder()

    fun evaluateProgram(instructions: List<String>): List<Int> {
        instructions.forEach { line ->
            line.split(" ").forEach {
                evaluateToken(it)
            }
        }
        return stack.reversed()
    }

    private fun evaluateToken(input: Any) {
        val token = toLowerCase(input)
        val definition = definitionsHolder.definitionFor(token)
        val operation = Operations.of(token)
        when {
            token == DEFINITION_END -> definitionsHolder.endDefining()
            token is String && token.endsWith(DEFINITION_END) -> {
                evaluateToken(token.substring(0, token.length - 1))
                evaluateToken(DEFINITION_END)
            }
            token == DEFINITION_START -> definitionsHolder.startDefining()
            definitionsHolder.needsName() -> definitionsHolder.definitionName = token as String
            definition != null -> definition.forEach { evaluateToken(it) }
            definitionsHolder.defining -> definitionsHolder.addOperation(token)
            token is List<*> -> token.forEach { evaluateToken(it!!) }
            operation != null -> operation.performOperation(stack)
            token is String && NUMBER.matches(token) -> stack.push(token.toInt())
            else -> throw IllegalArgumentException("No definition available for operator \"$token\"")
        }
    }

    private fun toLowerCase(input: Any) = if (input is String) {
        input.toLowerCase()
    } else {
        input
    }

    companion object {
        val NUMBER = Regex("\\d+")
        const val DEFINITION_START = ":"
        const val DEFINITION_END = ";"
    }

}

src/main/kotlin/Operations.kt

import java.util.*

enum class Operations(val token: kotlin.String, private val spokenName: kotlin.String, private val requiredOperands: Int, private val operation: (LinkedList<Int>) -> Unit) {
    PLUS("+", "Addition", 2, { it performBinaryOperation { o1, o2 -> o1 + o2 } }),
    MINUS("-", "Subtraction", 2, { it performBinaryOperation { o1, o2 -> o1 - o2 } }),
    MULTIPLICATION("*", "Multiplication", 2, { it performBinaryOperation { o1, o2 -> o1 * o2 } }),
    DIVISION("/", "Division", 2, {
        it performBinaryOperation { o1, o2 ->
            if (o2 == 0) {
                throw IllegalArgumentException("Division by 0 is not allowed")
            } else {
                o1 / o2
            }
        }
    }),

    DUPLICATE("dup", "Duplicating", 1, { it.push(it.peek()) }),
    DROP("drop", "Dropping", 1, { it.pop() }),
    SWAP("swap", "Swapping", 2, {
        val first = it.pop()
        val second = it.pop()
        it.push(first)
        it.push(second)
    }),
    OVER("over", "Overing", 2, {
        it.push(it[1])
    }),
    ;

    fun performOperation(stack: LinkedList<Int>) {
        if (stack.size < requiredOperands) {
            throw IllegalArgumentException("$spokenName requires that the stack contain at least " +
                    "$requiredOperands ${if (requiredOperands == 1) "value" else "values"}")
        }
        operation.invoke(stack)
    }

    companion object {
        private val map = values().map { Pair(it.token.toLowerCase(), it) }.toMap()

        fun of(word: Any): Operations? {
            return map[word]
        }

        private infix fun LinkedList<Int>.performBinaryOperation(binaryOperation: (Int, Int) -> Int) {
            val secondOperand = this.pop()
            val firstOperand = this.pop()
            this.push(binaryOperation.invoke(firstOperand, secondOperand))
        }
    }
}

src/main/kotlin/DefinitionsHolder.kt

internal class DefinitionsHolder {
    private val definitions = mutableMapOf<String, List<Any>>()
    var defining = false
        private set
    var definitionName: String? = null
        set(value) = if (value != null && ForthEvaluator.NUMBER.matches(value)) {
            throw IllegalArgumentException("Cannot redefine numbers")
        } else {
            field = value
        }

    private var definitionOperations = mutableListOf<Any>()

    fun definitionFor(input: Any) = definitions[input]?.toList()

    fun startDefining() {
        defining = true
    }

    fun endDefining() {
        defining = false
        definitions[definitionName!!] = definitionOperations
        definitionOperations = mutableListOf()
        definitionName = null
    }

    fun addOperation(token: Any) {
        definitionOperations.add(token)
    }

    fun needsName() = defining && definitionName == null
}

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?