Merge branch '23-add-forth-task' into 'master'

Resolve "add forth task"

Closes #23

See merge request slon/shad-go-private!36
This commit is contained in:
verytable 2021-02-18 20:13:55 +00:00
commit 9e4f34caeb
4 changed files with 440 additions and 0 deletions

104
forth/README.md Normal file
View file

@ -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/

18
forth/eval.go Normal file
View file

@ -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
}

274
forth/eval_test.go Normal file
View file

@ -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
}

44
forth/main.go Normal file
View file

@ -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, ", "))
}