Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
43 changes: 43 additions & 0 deletions exercises/forth/README.md
Original file line number Diff line number Diff line change
@@ -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/<exerciseName>` directory.

For example, if you're submitting `bob.py` for the Bob exercise, the submit command would be something like `exercism submit <path_to_exercism_dir>/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.
58 changes: 58 additions & 0 deletions exercises/forth/example.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions exercises/forth/forth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def evaluate(input_data):
pass
254 changes: 254 additions & 0 deletions exercises/forth/forth_test.py
Original file line number Diff line number Diff line change
@@ -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()