diff --git a/config.json b/config.json index ff096cf..12e1570 100644 --- a/config.json +++ b/config.json @@ -53,6 +53,14 @@ "practices": [], "prerequisites": [], "difficulty": 2 + }, + { + "slug": "forth", + "name": "Forth", + "uuid": "dafa6994-946a-462e-9107-25becff892ce", + "practices": [], + "prerequisites": [], + "difficulty": 8 } ] }, diff --git a/exercises/practice/forth/.docs/instructions.md b/exercises/practice/forth/.docs/instructions.md new file mode 100644 index 0000000..91ad26e --- /dev/null +++ b/exercises/practice/forth/.docs/instructions.md @@ -0,0 +1,23 @@ +# Instructions + +Implement an evaluator for a very simple subset of Forth. + +[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. + +[forth]: https://en.wikipedia.org/wiki/Forth_%28programming_language%29 diff --git a/exercises/practice/forth/.meta/config.json b/exercises/practice/forth/.meta/config.json new file mode 100644 index 0000000..2f3eb3d --- /dev/null +++ b/exercises/practice/forth/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [ + "quintuple-mallard" + ], + "files": { + "solution": [ + "forth.nu" + ], + "test": [ + "tests.nu" + ], + "example": [ + ".meta/example.nu" + ] + }, + "blurb": "Implement an evaluator for a very simple subset of Forth." +} diff --git a/exercises/practice/forth/.meta/example.nu b/exercises/practice/forth/.meta/example.nu new file mode 100644 index 0000000..dbac307 --- /dev/null +++ b/exercises/practice/forth/.meta/example.nu @@ -0,0 +1,171 @@ +def macro? [item] { + let trimmed = ($item | str trim) + ($trimmed | find --regex "^:\\s+[^\\s]+\\s+.+\\s+;$") == $trimmed +} + +def parse_definition [def: string] { + let parsed = ($def | parse ": {name} {body} ;") + let name = ($parsed.name | get 0 | str trim | str downcase) + let tokens = ($parsed.body | get 0 | str trim | split row " ") + { + name: $name, + tokens: $tokens + } +} + +def replace_tokens [tokens: list, defname: string, definition: list] { + mut result = [] + for token in $tokens { + let token_lower = ($token | str downcase | str trim) + let name_lower = ($defname | str downcase | str trim) + if $token_lower == $name_lower { + for deftoken in $definition { + $result = ($result | append $deftoken) + } + } else { + $result = ($result | append $token) + } + } + $result +} + +def expand_macros [instructions: list] { + mut defs = [] + mut program = [] + + for instr in $instructions { + if (macro? $instr) { + let def = parse_definition $instr + if ($def.name | find --regex "^-?\\d+$") == $def.name { + error make {msg: "illegal operation"} + } + mut tokens = $def.tokens + for prev_def in $defs { + $tokens = (replace_tokens $tokens $prev_def.name $prev_def.tokens) + } + let matches = ($defs | where name == $def.name) + if ($matches | length) > 0 { + $defs = ($defs | where name != $def.name | append { + name: $def.name, + tokens: $tokens + }) + } else { + $defs = ($defs | append { + name: $def.name, + tokens: $tokens + }) + } + } else { + $program = ($program | append $instr) + } + } + + mut result = [] + for line in $program { + mut tokens = ($line | split row " ") + mut changed = true + + while $changed { + $changed = false + mut next = [] + + for token in $tokens { + let token_lower = ($token | str downcase) + let matches = ($defs | where name == $token_lower) + if ($matches | length) > 0 { + let match = ($matches | first) + for deftoken in $match.tokens { + $next = ($next | append $deftoken) + } + $changed = true + } else { + $next = ($next | append $token) + } + } + $tokens = $next + } + + $result = ($result | append $tokens) + } + + $result | flatten | str join " " +} + +def perform [stack: list, action: string] { + if ($action | find --regex "-?\\d") == $action { # Literals + $stack | append ($action | into int) + } else if $action in ["+", "-", "*", "/", "over", "swap"] { # Binary actions + match ($stack | length) { + 0 => (error make {msg: "empty stack"}) + 1 => (error make {msg: "only one value on the stack"}) + } + match $action { + "+" => ( + ( + ( + ($stack | get (($stack | length) - 2)) + ) + ($stack | get (($stack | length) - 1)) + ) | prepend ($stack | slice ..-3) + ) + "-" => ( + ( + ( + ($stack | get (($stack | length) - 2)) + ) - ($stack | get (($stack | length) - 1)) + ) | prepend ($stack | slice ..-3) + ) + "*" => ( + ( + ( + ($stack | get (($stack | length) - 2)) + ) * ($stack | get (($stack | length) - 1)) + ) | prepend ($stack | slice ..-3) + ) + "/" => ( + try { + ( + ( + (($stack | get (($stack | length) - 2))) // ($stack | get (($stack | length) - 1)) + ) | prepend ($stack | slice ..-3) + ) + } catch { + error make {msg: "divide by zero"} + } + ) + "over" => ([ + ($stack | get (($stack | length) - 2)), + ($stack | get (($stack | length) - 1)), + ($stack | get (($stack | length) - 2)) + ] | prepend ($stack | slice ..-3)) + "swap" => ([ + ($stack | get (($stack | length) - 1)), + ($stack | get (($stack | length) - 2)) + ] | prepend ($stack | slice ..-3)) + } + } else if $action in ["dup", "drop"] { + if ($stack | length) == 0 { + error make {msg: "empty stack"} + } + match $action { + "dup" => ([ + ($stack | get (($stack | length) - 1)), + ($stack | get (($stack | length) - 1)) + ] | prepend ($stack | slice ..-2)) + "drop" => ($stack | slice ..-2) + } + } else { + error make {msg: "undefined operation"} + } +} + +export def evaluate [instructions: list] { + let program_str = expand_macros $instructions + let tokens = ($program_str | split row " " | where {|x| $x != ""}) + + mut stack: list = [] + for token in $tokens { + let norm = ($token | str downcase | str trim) + $stack = perform $stack $norm + } + $stack +} \ No newline at end of file diff --git a/exercises/practice/forth/.meta/tests.toml b/exercises/practice/forth/.meta/tests.toml new file mode 100644 index 0000000..d1e146a --- /dev/null +++ b/exercises/practice/forth/.meta/tests.toml @@ -0,0 +1,175 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[9962203f-f00a-4a85-b404-8a8ecbcec09d] +description = "parsing and numbers -> numbers just get pushed onto the stack" + +[fd7a8da2-6818-4203-a866-fed0714e7aa0] +description = "parsing and numbers -> pushes negative numbers onto the stack" + +[9e69588e-a3d8-41a3-a371-ea02206c1e6e] +description = "addition -> can add two numbers" + +[52336dd3-30da-4e5c-8523-bdf9a3427657] +description = "addition -> errors if there is nothing on the stack" + +[06efb9a4-817a-435e-b509-06166993c1b8] +description = "addition -> errors if there is only one value on the stack" + +[1e07a098-c5fa-4c66-97b2-3c81205dbc2f] +description = "addition -> more than two values on the stack" + +[09687c99-7bbc-44af-8526-e402f997ccbf] +description = "subtraction -> can subtract two numbers" + +[5d63eee2-1f7d-4538-b475-e27682ab8032] +description = "subtraction -> errors if there is nothing on the stack" + +[b3cee1b2-9159-418a-b00d-a1bb3765c23b] +description = "subtraction -> errors if there is only one value on the stack" + +[2c8cc5ed-da97-4cb1-8b98-fa7b526644f4] +description = "subtraction -> more than two values on the stack" + +[5df0ceb5-922e-401f-974d-8287427dbf21] +description = "multiplication -> can multiply two numbers" + +[9e004339-15ac-4063-8ec1-5720f4e75046] +description = "multiplication -> errors if there is nothing on the stack" + +[8ba4b432-9f94-41e0-8fae-3b3712bd51b3] +description = "multiplication -> errors if there is only one value on the stack" + +[5cd085b5-deb1-43cc-9c17-6b1c38bc9970] +description = "multiplication -> more than two values on the stack" + +[e74c2204-b057-4cff-9aa9-31c7c97a93f5] +description = "division -> can divide two numbers" + +[54f6711c-4b14-4bb0-98ad-d974a22c4620] +description = "division -> performs integer division" + +[a5df3219-29b4-4d2f-b427-81f82f42a3f1] +description = "division -> errors if dividing by zero" + +[1d5bb6b3-6749-4e02-8a79-b5d4d334cb8a] +description = "division -> errors if there is nothing on the stack" + +[d5547f43-c2ff-4d5c-9cb0-2a4f6684c20d] +description = "division -> errors if there is only one value on the stack" + +[f224f3e0-b6b6-4864-81de-9769ecefa03f] +description = "division -> more than two values on the stack" + +[ee28d729-6692-4a30-b9be-0d830c52a68c] +description = "combined arithmetic -> addition and subtraction" + +[40b197da-fa4b-4aca-a50b-f000d19422c1] +description = "combined arithmetic -> multiplication and division" + +[f749b540-53aa-458e-87ec-a70797eddbcb] +description = "combined arithmetic -> multiplication and addition" + +[c8e5a4c2-f9bf-4805-9a35-3c3314e4989a] +description = "combined arithmetic -> addition and multiplication" + +[c5758235-6eef-4bf6-ab62-c878e50b9957] +description = "dup -> copies a value on the stack" + +[f6889006-5a40-41e7-beb3-43b09e5a22f4] +description = "dup -> copies the top value on the stack" + +[40b7569c-8401-4bd4-a30d-9adf70d11bc4] +description = "dup -> errors if there is nothing on the stack" + +[1971da68-1df2-4569-927a-72bf5bb7263c] +description = "drop -> removes the top value on the stack if it is the only one" + +[8929d9f2-4a78-4e0f-90ad-be1a0f313fd9] +description = "drop -> removes the top value on the stack if it is not the only one" + +[6dd31873-6dd7-4cb8-9e90-7daa33ba045c] +description = "drop -> errors if there is nothing on the stack" + +[3ee68e62-f98a-4cce-9e6c-8aae6c65a4e3] +description = "swap -> swaps the top two values on the stack if they are the only ones" + +[8ce869d5-a503-44e4-ab55-1da36816ff1c] +description = "swap -> swaps the top two values on the stack if they are not the only ones" + +[74ba5b2a-b028-4759-9176-c5c0e7b2b154] +description = "swap -> errors if there is nothing on the stack" + +[dd52e154-5d0d-4a5c-9e5d-73eb36052bc8] +description = "swap -> errors if there is only one value on the stack" + +[a2654074-ba68-4f93-b014-6b12693a8b50] +description = "over -> copies the second element if there are only two" + +[c5b51097-741a-4da7-8736-5c93fa856339] +description = "over -> copies the second element if there are more than two" + +[6e1703a6-5963-4a03-abba-02e77e3181fd] +description = "over -> errors if there is nothing on the stack" + +[ee574dc4-ef71-46f6-8c6a-b4af3a10c45f] +description = "over -> errors if there is only one value on the stack" + +[ed45cbbf-4dbf-4901-825b-54b20dbee53b] +description = "user-defined words -> can consist of built-in words" + +[2726ea44-73e4-436b-bc2b-5ff0c6aa014b] +description = "user-defined words -> execute in the right order" + +[9e53c2d0-b8ef-4ad8-b2c9-a559b421eb33] +description = "user-defined words -> can override other user-defined words" + +[669db3f3-5bd6-4be0-83d1-618cd6e4984b] +description = "user-defined words -> can override built-in words" + +[588de2f0-c56e-4c68-be0b-0bb1e603c500] +description = "user-defined words -> can override built-in operators" + +[ac12aaaf-26c6-4a10-8b3c-1c958fa2914c] +description = "user-defined words -> can use different words with the same name" + +[53f82ef0-2750-4ccb-ac04-5d8c1aefabb1] +description = "user-defined words -> can define word that uses word with the same name" + +[35958cee-a976-4a0f-9378-f678518fa322] +description = "user-defined words -> cannot redefine non-negative numbers" + +[df5b2815-3843-4f55-b16c-c3ed507292a7] +description = "user-defined words -> cannot redefine negative numbers" + +[5180f261-89dd-491e-b230-62737e09806f] +description = "user-defined words -> errors if executing a non-existent word" + +[3c8bfef3-edbb-49c1-9993-21d4030043cb] +description = "user-defined words -> only defines locally" + +[7b83bb2e-b0e8-461f-ad3b-96ee2e111ed6] +description = "case-insensitivity -> DUP is case-insensitive" + +[339ed30b-f5b4-47ff-ab1c-67591a9cd336] +description = "case-insensitivity -> DROP is case-insensitive" + +[ee1af31e-1355-4b1b-bb95-f9d0b2961b87] +description = "case-insensitivity -> SWAP is case-insensitive" + +[acdc3a49-14c8-4cc2-945d-11edee6408fa] +description = "case-insensitivity -> OVER is case-insensitive" + +[5934454f-a24f-4efc-9fdd-5794e5f0c23c] +description = "case-insensitivity -> user-defined words are case-insensitive" + +[037d4299-195f-4be7-a46d-f07ca6280a06] +description = "case-insensitivity -> definitions are case-insensitive" diff --git a/exercises/practice/forth/forth.nu b/exercises/practice/forth/forth.nu new file mode 100644 index 0000000..60e8671 --- /dev/null +++ b/exercises/practice/forth/forth.nu @@ -0,0 +1,3 @@ +export def evaluate [items] { + error make {"msg": "Remove this line and implement evaluate"} +} diff --git a/exercises/practice/forth/tests.nu b/exercises/practice/forth/tests.nu new file mode 100644 index 0000000..9e057c9 --- /dev/null +++ b/exercises/practice/forth/tests.nu @@ -0,0 +1,101 @@ +use forth.nu evaluate +use std/assert +assert equal (evaluate ["1 2 3 4 5"]) [1, 2, 3, 4, 5] +assert equal (evaluate ["-1 -2 -3 -4 -5"]) [-1, -2, -3, -4, -5] + +assert equal (evaluate ["1 2 +"]) [3] +assert equal (try {evaluate ["+"]} catch {|e| $e.msg}) "empty stack" +assert equal (try {evaluate ["1 +"]} catch {|e| $e.msg}) "only one value on the stack" +assert equal (evaluate ["1 2 3 +"]) [1, 5] + +assert equal (evaluate ["3 4 -"]) [-1] +assert equal (try {evaluate ["-"]} catch {|e| $e.msg}) "empty stack" +assert equal (try {evaluate ["1 -"]} catch {|e| $e.msg}) "only one value on the stack" +assert equal (evaluate ["1 12 3 -"]) [1, 9] + + +assert equal (evaluate ["2 4 *"]) [8] +assert equal (try {evaluate ["*"]} catch {|e| $e.msg}) "empty stack" +assert equal (try {evaluate ["1 *"]} catch {|e| $e.msg}) "only one value on the stack" +assert equal (evaluate ["1 2 3 *"]) [1, 6] + +assert equal (evaluate ["12 3 /"]) [4] +assert equal (evaluate ["8 3 /"]) [2] +assert equal (try {evaluate ["4 0 /"]} catch {|e| $e.msg}) "divide by zero" +assert equal (try {evaluate ["/"]} catch {|e| $e.msg}) "empty stack" +assert equal (try {evaluate ["1 /"]} catch {|e| $e.msg}) "only one value on the stack" +assert equal (evaluate ["1 12 3 /"]) [1, 4] + +assert equal (evaluate ["1 2 + 4 -"]) [-1] +assert equal (evaluate ["2 4 * 3 /"]) [2] +assert equal (evaluate ["1 3 4 * +"]) [13] +assert equal (evaluate ["1 3 4 + *"]) [7] + +assert equal (evaluate ["1 dup"]) [1, 1] +assert equal (evaluate ["1 2 dup"]) [1, 2, 2] +assert equal (try {evaluate ["dup"]} catch {|e| $e.msg}) "empty stack" + +assert equal (evaluate ["1 drop"]) [] +assert equal (evaluate ["1 2 drop"]) [1] +assert equal (try {evaluate ["drop"]} catch {|e| $e.msg}) "empty stack" + +assert equal (evaluate ["1 2 swap"]) [2, 1] +assert equal (evaluate ["1 2 3 swap"]) [1, 3, 2] +assert equal (try {evaluate ["swap"]} catch {|e| $e.msg}) "empty stack" +assert equal (try {evaluate ["1 swap"]} catch {|e| $e.msg}) "only one value on the stack" + + +assert equal (evaluate ["1 2 over"]) [1, 2, 1] +assert equal (evaluate ["1 2 3 over"]) [1, 2, 3, 2] +assert equal (try {evaluate ["over"]} catch {|e| $e.msg}) "empty stack" + +assert equal (try {evaluate ["1 over"]} catch {|e| $e.msg}) "only one value on the stack" + +assert equal (evaluate [ + ": dup-twice dup dup ;", + "1 dup-twice" + ]) [1, 1, 1] +assert equal (evaluate [ + ": countup 1 2 3 ;", + "countup" + ]) [1, 2, 3] +assert equal (evaluate [ + ": foo dup ;", + ": foo dup dup ;", + "1 foo" + ]) [1, 1, 1] +assert equal (evaluate [ + ": swap dup ;", + "1 swap" + ]) [1, 1] +assert equal (evaluate [ + ": + * ;", + "3 4 +" + ]) [12] +assert equal (evaluate [ + ": foo 5 ;", + ": bar foo ;", + ": foo 6 ;", + "bar foo" + ]) [5, 6] +assert equal (evaluate [ + ": foo 10 ;", + ": foo foo 1 + ;", + "foo" + ]) [11] +assert equal (try {evaluate [": 1 2 ;"]} catch {|e| $e.msg}) "illegal operation" +assert equal (try {evaluate [": -1 2 ;"]} catch {|e| $e.msg}) "illegal operation" +assert equal (try {evaluate ["foo"]} catch {|e| $e.msg}) "undefined operation" + +assert equal (evaluate ["1 DUP Dup dup"]) [1, 1, 1, 1] +assert equal (evaluate ["1 2 3 4 DROP Drop drop"]) [1] +assert equal (evaluate ["1 2 SWAP 3 Swap 4 swap"]) [2, 3, 4, 1] +assert equal (evaluate ["1 2 OVER Over over"]) [1, 2, 1, 2, 1] +assert equal (evaluate [ + ": foo dup ;", + "1 FOO Foo foo" + ]) [1, 1, 1, 1] +assert equal (evaluate [ + ": SWAP DUP Dup dup ;", + "1 swap" + ]) [1, 1, 1, 1] \ No newline at end of file