From 69288fe91ea526dfdafc5a459050b48a2ffb40df Mon Sep 17 00:00:00 2001 From: verytable Date: Thu, 18 Feb 2021 20:13:55 +0000 Subject: [PATCH] Resolve "add forth task" --- forth/README.md | 104 +++++++++++++++++ forth/eval.go | 18 +++ forth/eval_test.go | 274 +++++++++++++++++++++++++++++++++++++++++++++ forth/main.go | 44 ++++++++ 4 files changed, 440 insertions(+) create mode 100644 forth/README.md create mode 100644 forth/eval.go create mode 100644 forth/eval_test.go create mode 100644 forth/main.go diff --git a/forth/README.md b/forth/README.md new file mode 100644 index 0000000..f8842d0 --- /dev/null +++ b/forth/README.md @@ -0,0 +1,104 @@ +## forth + +В этой задаче нужно написать интерпретатор [языка программирования Forth](https://en.wikipedia.org/wiki/Forth_(programming_language)). + +Forth - это стек-ориентированный, конкатенативный (конкатенация двух фрагментов кода выражает их композицию) язык программирования. + +В нашем примитивном случае язык состоит из стека, хранящего целые числа, и словаря известных операций. +Каждая операция вынимает некоторое (возможно нулевое) количество чисел со стека и кладёт некоторое (возможно нулевое) количество чисел на стек. + +Полноценный Forth, с ячейками памяти, циклами, условными переходами и множеством стандартных операций +позволяет писать довольно сложные программы и используется в микропроцессорах. + +#### Примеры программ + +В каждом примере на последней строке приведено результирующее значение стека. + +Положить 3 числа на стек: +``` +1 2 3 +Stack: 1, 2, 3 +``` + +Положить 2 числа на стек, исполнить операцию `+` (достать 2 числа со стека и положить на стек их сумму) +``` +1 2 + +Stack: 3 +``` + +Положить число на стек и выполнить операцию `drop` (достать верхнее значение из стека) +``` +1 drop +Stack: +``` + +Определить операцию ("слово" в терминологии Forth) `drop2` для удаления двух верхних значений со стека, +положить 2 числа на стек и воспользоваться `drop2`: +``` +: drop2 drop drop ; +1 2 drop2 +Stack: +``` + +### Грамматика + +У языка нет официальной грамматики. +Она определяется простым алгоритмом. +Интерпретатор считывает строку ввода, +разбивает на части, используя пробелы в качестве разделителя, +анализирует каждую часть на принадлежность к лексемам языка ("словам"). + +Все известные языку слова хранятся в словаре. +Если алгоритм натыкается на знакомое слово, он исполняет связанный с ним исходный код. +Незнакомые слова интерпретируются как числа и добавляются в стек. + +В случае успеха интерпретатор продолжает разбор входного потока. +Если и поиск, и преобразование чисел завершаются неудачно, +интерпретатор печатает слово, за которым следует сообщение об ошибке, указывающее, что слово не распознано, +сбрасывает входной поток и ждет нового пользовательского ввода. + +### Что нужно сделать? + +Нужно написать реализацию функций `NewEvaluator` и `Evaluator.Process`. +Первая создаёт объект исполнителя Forth кода, а вторая парсит и исполняет. + +Требуется поддержать следующие слова: +* `+`, `-`, `*`, `/` удаляют со стека два верхних значения и кладут результат соответствующей целочисленной арифметической операции +* `dup` кладёт на стек верхнее значение +* `over` кладёт на стек второе сверху значение +* `drop` удаляет верхнее значение со стека +* `swap` меняет два верхних значения стека местами + +При нехватке аргументов `Process` должна возвращать ошибки. + +Также нужно поддержать определение новых слов. +``` +: word definition ; +``` +где `word` -- это case-insensitive имя новой операции, +а `defenition` состоит из известных слов и чисел, разделённых пробелами. +Слова можно переопределять. + +### Проверка решения + +Для запуска тестов нужно выполнить следующую команду: +``` +go test -v ./forth/... +``` + +#### Интерактивная среда + +В [main.go](./main.go) написана небольшая обёртка вашей реализации, +позволяющая интерактивно взаимодействовать с интерпретатором. +``` +go build . && ./forth +Welcome to Forth evaluator! To exit type "bye". +>1 2 + +Stack: 3 +> +``` + +### Ссылки + +* https://en.wikipedia.org/wiki/Forth_(programming_language) +* https://golang.org/pkg/strings/ diff --git a/forth/eval.go b/forth/eval.go new file mode 100644 index 0000000..59738e6 --- /dev/null +++ b/forth/eval.go @@ -0,0 +1,18 @@ +// +build !solution + +package main + +type Evaluator struct { +} + +// NewEvaluator creates evaluator. +func NewEvaluator() *Evaluator { + return &Evaluator{} +} + +// Process evaluates sequence of words or definition. +// +// Returns resulting stack state and an error. +func (e *Evaluator) Process(row string) ([]int, error) { + return nil, nil +} diff --git a/forth/eval_test.go b/forth/eval_test.go new file mode 100644 index 0000000..eca76d3 --- /dev/null +++ b/forth/eval_test.go @@ -0,0 +1,274 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +type testCase struct { + description string + input []string + expected []int + error bool +} + +var testCases = []testCase{ + { + description: "push numbers", + input: []string{"1 2 3 4 5"}, + expected: []int{1, 2, 3, 4, 5}, + }, + { + description: "add", + input: []string{"1 2 +"}, + expected: []int{3}, + }, + { + description: "nothing to add", + input: []string{"+"}, + error: true, + }, + { + description: "add arity", + input: []string{"1 +"}, + error: true, + }, + { + description: "sub", + input: []string{"3 4 -"}, + expected: []int{-1}, + }, + { + description: "nothing to sub", + input: []string{"-"}, + error: true, + }, + { + description: "sub arity", + input: []string{"1 -"}, + error: true, + }, + { + description: "mul", + input: []string{"2 4 *"}, + expected: []int{8}, + }, + { + description: "nothing to mul", + input: []string{"*"}, + error: true, + }, + { + description: "mul arity", + input: []string{"1 *"}, + error: true, + }, + { + description: "div", + input: []string{"12 3 /"}, + expected: []int{4}, + }, + { + description: "integer division", + input: []string{"8 3 /"}, + expected: []int{2}, + }, + { + description: "division by zero", + input: []string{"4 0 /"}, + error: true, + }, + { + description: "nothing to div", + input: []string{"/"}, + error: true, + }, + { + description: "div arity", + input: []string{"1 /"}, + error: true, + }, + { + description: "add sub", + input: []string{"1 2 + 4 -"}, + expected: []int{-1}, + }, + { + description: "mul div", + input: []string{"2 4 * 3 /"}, + expected: []int{2}, + }, + { + description: "dup", + input: []string{"1 dup"}, + expected: []int{1, 1}, + }, + { + description: "dup top", + input: []string{"1 2 dup"}, + expected: []int{1, 2, 2}, + }, + { + description: "nothing to dup", + input: []string{"dup"}, + error: true, + }, + { + description: "drop", + input: []string{"1 drop"}, + expected: []int{}, + }, + { + description: "drop top", + input: []string{"1 2 drop"}, + expected: []int{1}, + }, + { + description: "nothing to drop", + input: []string{"drop"}, + error: true, + }, + { + description: "swap", + input: []string{"1 2 swap"}, + expected: []int{2, 1}, + }, + { + description: "swap top", + input: []string{"1 2 3 swap"}, + expected: []int{1, 3, 2}, + }, + { + description: "nothing to swap", + input: []string{"swap"}, + error: true, + }, + { + description: "swap arity", + input: []string{"1 swap"}, + error: true, + }, + { + description: "over", + input: []string{"1 2 over"}, + expected: []int{1, 2, 1}, + }, + { + description: "over2", + input: []string{"1 2 3 over"}, + expected: []int{1, 2, 3, 2}, + }, + { + description: "nothing to over", + input: []string{"over"}, + error: true, + }, + { + description: "over arity", + input: []string{"1 over"}, + error: true, + }, + { + description: "user-defined", + input: []string{": dup-twice dup dup ;", "1 dup-twice"}, + expected: []int{1, 1, 1}, + }, + { + description: "user-defined order", + input: []string{": countup 1 2 3 ;", "countup"}, + expected: []int{1, 2, 3}, + }, + { + description: "user-defined override", + input: []string{": foo dup ;", ": foo dup dup ;", "1 foo"}, + expected: []int{1, 1, 1}, + }, + { + description: "built-in override", + input: []string{": swap dup ;", "1 swap"}, + expected: []int{1, 1}, + }, + { + description: "built-in operator override", + input: []string{": + * ;", "3 4 +"}, + expected: []int{12}, + }, + { + description: "no redefinition", + input: []string{": foo 5 ;", ": bar foo ;", ": foo 6 ;", "bar foo"}, + expected: []int{5, 6}, + }, + { + description: "reuse in definition", + input: []string{": foo 10 ;", ": foo foo 1 + ;", "foo"}, + expected: []int{11}, + }, + { + description: "redefine numbers", + input: []string{": 1 2 ;"}, + error: true, + }, + { + description: "non-existent word", + input: []string{"foo"}, + error: true, + }, + { + description: "DUP case insensitivity", + input: []string{"1 DUP Dup dup"}, + expected: []int{1, 1, 1, 1}, + }, + { + description: "DROP case insensitivity", + input: []string{"1 2 3 4 DROP Drop drop"}, + expected: []int{1}, + }, + { + description: "SWAP case insensitivity", + input: []string{"1 2 SWAP 3 Swap 4 swap"}, + expected: []int{2, 3, 4, 1}, + }, + { + description: "OVER case insensitivity", + input: []string{"1 2 OVER Over over"}, + expected: []int{1, 2, 1, 2, 1}, + }, + { + description: "user-defined case insensitivity", + input: []string{": foo dup ;", "1 FOO Foo foo"}, + expected: []int{1, 1, 1, 1}, + }, + { + description: "definition case insensitivity", + input: []string{": SWAP DUP Dup dup ;", "1 swap"}, + expected: []int{1, 1, 1, 1}, + }, +} + +func TestEval(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + stack, err := eval(tc.input) + if tc.error { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tc.expected, stack) + } + }) + } +} + +func eval(input []string) ([]int, error) { + e := NewEvaluator() + var stack []int + for _, row := range input { + var err error + stack, err = e.Process(row) + if err != nil { + return nil, err + } + } + return stack, nil +} diff --git a/forth/main.go b/forth/main.go new file mode 100644 index 0000000..22ee925 --- /dev/null +++ b/forth/main.go @@ -0,0 +1,44 @@ +// +build !change + +package main + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" +) + +const exitCommand = "bye" + +func main() { + e := NewEvaluator() + + fmt.Printf("Welcome to Forth evaluator! To exit type %q.\n", exitCommand) + scanner := bufio.NewScanner(os.Stdin) + for { + fmt.Print(">") + scanner.Scan() + + text := scanner.Text() + if text == exitCommand { + break + } + + stack, err := e.Process(text) + if err != nil { + fmt.Printf("Evaluation error: %s\n", err) + } + + printStack(stack) + } +} + +func printStack(stack []int) { + s := make([]string, 0, len(stack)) + for _, n := range stack { + s = append(s, strconv.Itoa(n)) + } + fmt.Printf("Stack: %s\n", strings.Join(s, ", ")) +}