diff --git a/config.json b/config.json index d0d26be0e30..5b6df0d86a3 100644 --- a/config.json +++ b/config.json @@ -844,6 +844,17 @@ "conditionals" ] }, + { + "uuid": "e348a307-078c-5280-65af-a159283d4e79438b755", + "slug": "forth", + "core": false, + "unlocked_by": null, + "difficulty": 5, + "topics": [ + "parsing", + "stacks" + ] + }, { "uuid": "e7351e8e-d3ff-4621-b818-cd55cf05bffd", "slug": "accumulate", diff --git a/exercises/forth/README.md b/exercises/forth/README.md new file mode 100644 index 00000000000..e987a4ac257 --- /dev/null +++ b/exercises/forth/README.md @@ -0,0 +1,43 @@ +# Forth + +Implement an evaluator for a very simple subset of Forth. + +[Forth](https://en.wikipedia.org/wiki/Forth_%28programming_language%29) +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. + +## Hints +- To parse the text, you could try to use the [Sprache](https://github.com/sprache/Sprache/blob/develop/README.md) library. You can also find a good tutorial [here](https://www.thomaslevesque.com/2017/02/23/easy-text-parsing-in-c-with-sprache/). + + +### Submitting Exercises + +Note that, when trying to submit an exercise, make sure the solution is in the `exercism/python/` directory. + +For example, if you're submitting `bob.py` for the Bob exercise, the submit command would be something like `exercism submit /python/bob/bob.py`. + + +For more detailed information about running tests, code style and linting, +please see the [help page](http://exercism.io/languages/python). + +## Submitting Incomplete Solutions +It's possible to submit an incomplete solution so you can see how others have completed the exercise. diff --git a/exercises/forth/example.py b/exercises/forth/example.py new file mode 100644 index 00000000000..8d653f4c843 --- /dev/null +++ b/exercises/forth/example.py @@ -0,0 +1,58 @@ +class StackUnderflowError(Exception): + pass + + +def is_integer(string): + try: + int(string) + return True + except ValueError: + return False + + +def evaluate(input_data): + defines = {} + while input_data[0][0] == ':': + values = input_data.pop(0).split() + values.pop() + values.pop(0) + key = values.pop(0) + if is_integer(key): + raise ValueError() + defines[key] = values + stack = [] + input_data = input_data[-1].split() + while any(input_data): + word = input_data.pop(0).lower() + try: + if is_integer(word): + stack.append(int(word)) + elif word in defines: + input_data = defines[word] + input_data + elif word == '+': + stack.append(stack.pop() + stack.pop()) + elif word == '-': + stack.append(-stack.pop() + stack.pop()) + elif word == '*': + stack.append(stack.pop() * stack.pop()) + elif word == '/': + divider = stack.pop() + if divider == 0: + raise ZeroDivisionError() + stack.append(int(stack.pop() / divider)) + elif word == 'dup': + stack.append(stack[-1]) + elif word == 'drop': + stack.pop() + elif word == 'swap': + stack.append(stack[-2]) + del stack[-3] + elif word == 'over': + stack.append(stack[-2]) + else: + raise ValueError() + except ZeroDivisionError: + raise + except IndexError: + raise StackUnderflowError() + return stack diff --git a/exercises/forth/forth.py b/exercises/forth/forth.py new file mode 100644 index 00000000000..f8362ec7f47 --- /dev/null +++ b/exercises/forth/forth.py @@ -0,0 +1,2 @@ +def evaluate(input_data): + pass diff --git a/exercises/forth/forth_test.py b/exercises/forth/forth_test.py new file mode 100644 index 00000000000..66a4ec819a7 --- /dev/null +++ b/exercises/forth/forth_test.py @@ -0,0 +1,254 @@ +import unittest + +from example import evaluate, StackUnderflowError + + +# test cases adapted from `x-common//canonical-data.json` @ version: 1.2.0 + + +class ForthAdditionTest(unittest.TestCase): + def test_can_add_two_numbers(self): + input_data = ["1 2 +"] + expected = [3] + self.assertEqual(expected, evaluate(input_data)) + + def test_errors_if_there_is_nothing_on_the_stack(self): + input_data = ["+"] + with self.assertRaises(StackUnderflowError): + evaluate(input_data) + + def test_errors_if_there_is_only_one_value_on_the_stack(self): + input_data = ["1 +"] + with self.assertRaises(StackUnderflowError): + evaluate(input_data) + + +class ForthSubtractionTest(unittest.TestCase): + def test_can_subtract_two_numbers(self): + input_data = ["3 4 -"] + expected = [-1] + self.assertEqual(expected, evaluate(input_data)) + + def test_errors_if_there_is_nothing_on_the_stack(self): + input_data = ["-"] + with self.assertRaises(StackUnderflowError): + evaluate(input_data) + + def test_errors_if_there_is_only_one_value_on_the_stack(self): + input_data = ["1 -"] + with self.assertRaises(StackUnderflowError): + evaluate(input_data) + + +class ForthMultiplicationTest(unittest.TestCase): + def test_can_multiply_two_numbers(self): + input_data = ["2 4 *"] + expected = [8] + self.assertEqual(expected, evaluate(input_data)) + + def test_errors_if_there_is_nothing_on_the_stack(self): + input_data = ["*"] + with self.assertRaises(StackUnderflowError): + evaluate(input_data) + + def test_errors_if_there_is_only_one_value_on_the_stack(self): + input_data = ["1 *"] + with self.assertRaises(StackUnderflowError): + evaluate(input_data) + + +class ForthDivisionTest(unittest.TestCase): + def test_can_divide_two_numbers(self): + input_data = ["3 4 -"] + expected = [-1] + self.assertEqual(expected, evaluate(input_data)) + + def test_performs_integer_division(self): + input_data = ["8 3 /"] + expected = [2] + self.assertEqual(expected, evaluate(input_data)) + + def test_errors_if_dividing_by_zero(self): + input_data = ["4 0 /"] + with self.assertRaises(ZeroDivisionError): + evaluate(input_data) + + def test_errors_if_there_is_nothing_on_the_stack(self): + input_data = ["/"] + with self.assertRaises(StackUnderflowError): + evaluate(input_data) + + def test_errors_if_there_is_only_one_value_on_the_stack(self): + input_data = ["1 /"] + with self.assertRaises(StackUnderflowError): + evaluate(input_data) + + +class ForthCombinedArithmeticTest(unittest.TestCase): + def test_addition_and_subtraction(self): + input_data = ["1 2 + 4 -"] + expected = [-1] + self.assertEqual(expected, evaluate(input_data)) + + def test_multiplication_and_division(self): + input_data = ["2 4 * 3 /"] + expected = [2] + self.assertEqual(expected, evaluate(input_data)) + + +class ForthDupTest(unittest.TestCase): + def test_copies_the_top_value_on_the_stack(self): + input_data = ["1 DUP"] + expected = [1, 1] + self.assertEqual(expected, evaluate(input_data)) + + def test_is_case_insensitive(self): + input_data = ["1 2 Dup"] + expected = [1, 2, 2] + self.assertEqual(expected, evaluate(input_data)) + + def test_errors_if_there_is_nothing_on_the_stack(self): + input_data = ["dup"] + with self.assertRaises(StackUnderflowError): + evaluate(input_data) + + +class ForthDropTest(unittest.TestCase): + def test_removes_the_top_value_on_the_stack_if_it_is_the_only_one(self): + input_data = ["1 DROP"] + expected = [] + self.assertEqual(expected, evaluate(input_data)) + + def test_removes_the_top_value_on_the_stack_if_it_not_the_only_one(self): + input_data = ["3 4 DROP"] + expected = [3] + self.assertEqual(expected, evaluate(input_data)) + + def test_is_case_insensitive(self): + input_data = ["1 2 Drop"] + expected = [1] + self.assertEqual(expected, evaluate(input_data)) + + def test_errors_if_there_is_nothing_on_the_stack(self): + input_data = ["drop"] + with self.assertRaises(StackUnderflowError): + evaluate(input_data) + + +class ForthSwapTest(unittest.TestCase): + def test_swaps_only_two_values_on_stack(self): + input_data = ["1 2 SWAP"] + expected = [2, 1] + self.assertEqual(expected, evaluate(input_data)) + + def test_swaps_two_two_values_on_stack(self): + input_data = ["1 2 3 SWAP"] + expected = [1, 3, 2] + self.assertEqual(expected, evaluate(input_data)) + + def test_is_case_insensitive(self): + input_data = ["3 4 Swap"] + expected = [4, 3] + self.assertEqual(expected, evaluate(input_data)) + + def test_errors_if_there_is_nothing_on_the_stack(self): + input_data = ["swap"] + with self.assertRaises(StackUnderflowError): + evaluate(input_data) + + def test_errors_if_there_is_only_one_value_on_the_stack(self): + input_data = ["1 swap"] + with self.assertRaises(StackUnderflowError): + evaluate(input_data) + + +class ForthOverTest(unittest.TestCase): + def test_copies_the_second_element_if_there_are_only_two(self): + input_data = ["1 2 OVER"] + expected = [1, 2, 1] + self.assertEqual(expected, evaluate(input_data)) + + def test_copies_the_second_element_if_there_are_more_than_two(self): + input_data = ["1 2 3 OVER"] + expected = [1, 2, 3, 2] + self.assertEqual(expected, evaluate(input_data)) + + def test_is_case_insensitive(self): + input_data = ["3 4 Over"] + expected = [3, 4, 3] + self.assertEqual(expected, evaluate(input_data)) + + def test_errors_if_there_is_nothing_on_the_stack(self): + input_data = ["over"] + with self.assertRaises(StackUnderflowError): + evaluate(input_data) + + def test_errors_if_there_is_only_one_value_on_the_stack(self): + input_data = ["1 over"] + with self.assertRaises(StackUnderflowError): + evaluate(input_data) + + +class ForthUserDefinedWordsTest(unittest.TestCase): + def test_can_consist_of_built_in_words(self): + input_data = [ + ": dup-twice dup dup ;", + "1 dup-twice" + ] + expected = [1, 1, 1] + self.assertEqual(expected, evaluate(input_data)) + + def test_execute_in_the_right_order(self): + input_data = [ + ": countup 1 2 3 ;", + "countup" + ] + expected = [1, 2, 3] + self.assertEqual(expected, evaluate(input_data)) + + def test_can_override_other_user_defined_words(self): + input_data = [ + ": foo dup ;", + ": foo dup dup ;", + "1 foo" + ] + expected = [1, 1, 1] + self.assertEqual(expected, evaluate(input_data)) + + def test_can_override_built_in_words(self): + input_data = [ + ": swap dup ;", + "1 swap" + ] + expected = [1, 1] + self.assertEqual(expected, evaluate(input_data)) + + def test_can_override_built_in_operators(self): + input_data = [ + ": + * ;", + "3 4 +" + ] + expected = [12] + self.assertEqual(expected, evaluate(input_data)) + + def test_is_case_insensitive(self): + input_data = [ + ": foo dup ;", + "1 FOO" + ] + expected = [1, 1] + self.assertEqual(expected, evaluate(input_data)) + + def test_cannot_redefine_numbers(self): + input_data = [": 1 2 ;"] + with self.assertRaises(ValueError): + evaluate(input_data) + + def test_errors_if_executing_a_non_existent_word(self): + input_data = ["foo"] + with self.assertRaises(ValueError): + evaluate(input_data) + + +if __name__ == '__main__': + unittest.main()