Avatar of Ric0chet

Ric0chet's solution

to Forth in the Kotlin Track

Published at Jul 01 2019 · 0 comments
Instructions
Test suite
Solution
import java.util.Stack

// 07-01-19
class ForthEvaluator {
    var stack = Stack<Int>()
    var defs = hashMapOf<String, String>()

    // 55ms
    fun evaluateProgram(input: List<String>): List<Int> = input.forEach { eval(it) }.let { stack }

    private fun eval(commands: String) {
        val cmds = commands.toLowerCase().trim()
        if (cmds.isEmpty()) return

        // Custom definition?
        val semi = cmds.indexOf(';')
        if ((cmds.first() == ':') && (semi >= 2)) {
            addCommand(cmds.substring(2 until semi))  // trim delims & whitespace
            //addCommand2(cmds)                       // raw command(s)

            // Recurse remainder, if any (not tested in Kotlin track)
            eval(cmds.substringAfter(';'))

        } else {
            // Known commands & numbers
            cmds.split("\\s".toRegex()).forEach {
                val num = it.toIntOrNull()
                when {
                    // Stack numbers
                    num != null -> stack.push(num)

                    // Recurse custom command(s)
                    defs.containsKey(it) -> eval(defs.get(it)!!)

                    // Basic commands
                    else -> when (it) {
                        // Arithmetic
                        "+" -> add()
                        "-" -> sub()
                        "*" -> mul()
                        "/" -> div()

                        // Stack manip
                        "dup" -> dup()
                        "drop" -> drop()
                        "swap" -> swap()
                        "over" -> over()

                        // No Op
                        "" -> return

                        // Unpaired or nested? (not tested in Kotlin track)
                        in (":;") -> throw IllegalArgumentException("Malformed command: unexpected delimiter")

                        else -> throw IllegalArgumentException("No definition available for operator \"$it\"")
                    }
                }
            }
        }
    }

    // Fun with MATH!
    private fun add() = stack.requires("Addition", 2).push(stack.pop() + stack.pop())

    private fun sub() = stack.requires("Subtraction", 2).push(-stack.pop() + stack.pop())

    private fun mul() = stack.requires("Multiplication", 2).push(stack.pop() * stack.pop())

    private fun div() = require(stack.lastOrNull() != 0) { "Division by 0 is not allowed" }.let {
        stack.requires("Division", 2).push((1 / stack.pop().toDouble() * stack.pop()).toInt())
    }

    // Stack tricks
    private fun dup() = stack.requires("Duplicating", 1).push(stack.peek())

    private fun drop() = stack.requires("Dropping", 1).pop()

    private fun over() = stack.requires("Overing", 2).push(stack[stack.size - 2])

    private fun swap() = Pair(stack.requires("Swapping", 2).pop(), stack.pop()).let { (x, y) ->
        stack.push(x)
        stack.push(y)
    }

    // Refactored requirements
    private fun Stack<Int>.requires(action: String, size: Int) = require(this.size >= size) {
        "$action requires that the stack contain at least $size value${if (size == 1) "" else "s"}"
    }.let { this }

    // Just enough to pass the tests...
    private fun addCommand(cust: String) {
        val sep = cust.indexOfFirst { it.isWhitespace() }
        val name = cust.substring(0 until sep)
        require(!name.all { it.isDigit() }) { "Cannot redefine numbers." }

        defs[name] = cust.substring(sep).trim()
    }

// ------------------------------

    // This is more robust
    private fun addCommand2(cmd: String) {
        var name = ""
        var defn = ""
        cmd.substring(2).split("\\s".toRegex()).forEach {
            when (it) {
                ":" -> throw IllegalArgumentException("Malformed command: multiple colons before terminator.")
                ";" -> {
                    // Store name & definition(s)
                    if (name.isEmpty() || defn.isEmpty()) throw IllegalArgumentException("Custom commands must contain at least one definition.")
                    else defs.put(name, defn)
                    return
                }
                else -> {
                    // Collect name & definition(s)
                    if (name.isEmpty()) {
                        if (it.toIntOrNull() != null) throw IllegalArgumentException("Cannot redefine numbers.")
                        name = it
                    } else {
                        defn += "$it "
                    }
                }
            }
        }
    }

}

Community comments

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

Ric0chet's Reflection

Some elegant code, some ugly. Includes a more robust addCommands() function, which falls squarely into the "ugly" camp. [NOTE: this exercise is currently (01-Jul-19) missing from the GitHub folders & won't download. I copied the directory structure from an earlier exercise (editing the .exercism/metadata.json file to enable correct uploading). Instructions & unit tests can be found on any submitted solution's page.]