Минимальный Forth с нуля

Table of Contents

Интро

Forth - один из тех инопланетных языков программирования, который большинство программистов относят к категории странных, вроде Haskell, Lisp и.т.д. Настолько странных, что они предпочли бы не думать об этом и продолжать писать код, за который им платят. Но это, конечно неверно, и если вы настоящий программист, вы должны хотя бы понимать этот язык, даже если вы никогда не будете его использовать.

Если вам не чуждо чувство прекрасного, и при достаточном везении, возможно вы сможете даже полюбить его и тогда он откроет вам могущественное понимание аспектов программирования, которые вы вряд-ли сможете найти в других языках.

Lisp является вершиной языков высокого уровня, и возможности из Lisp вот уже десятки лет добавляются в более распространенные языки. Forth в этом же смысле является вершиной низкоуровневых языков. Он настолько низкоуровневый что "из коробки" у него нет возможностей вроде динамического управления памятью и даже строк. Фактически, в его примитивном варианте отсутствуют даже такие базовые концепции как IF-выражения и циклы.

Почему же тогда вы могли бы захотеть изучить Forth? На это есть несколько очень веских причин:

  • Прежде всего, Forth минимален. Вы действительно можете написать Forth целиком в, скажем, 2000 строк кода. Я имею в виду не только программу Forth, я имею в виду полную "операционную систему" Forth, среду, и язык. Вы можете загружать такой Forth на голом ПК, без операционной системы, и он выдаст подсказку, командную строку, где вы можете начать делать какую-то полезную работу. Forth, который описан ниже, не минимален и использует Linux-процесс как его "базовый ПК" исключительно для обучающих целей. Это позволяет полностью понять Forth-систему. Кто может сказать, что он полностью понимает, как работает Linux, или gcc?
  • Во-вторых, у Forth есть своеобразное свойство bootstarp-инга.. Под этим я подразумеваю, что после написания небольшого ассемблерного кода, для общения с оборудованием и реализации нескольких примитивов, весь остальной язык и компилятор написан в самом Forth. Помните, я уже говорил, что Forth не хватает IF-выражений и циклов? Конечно, на самом деле это не так, потому что такой язык был бы бесполезен, но я имел в виду, что IF-выражения и циклы написаны в самом Forth. В других языках мы называем это "библиотеками". Например, в Си printf представляет собой библиотечную функцию, написанную на Си. Но в Forth это выходит за рамки просто библиотек. Можете ли вы представить, как написать на Cи if?
  • И это подводит нас к третьей причине: если вы можете написать if в Forth, то зачем ограничивать себя обычными конструкциями if, while, for, switch? Вам нужен итератор по списку чисел? Вы можете добавить её в язык. Как насчет оператора, который извлекает переменные непосредственно из файла конфигурации и делает их доступными как переменные Forth? Или как насчет добавления языка зависимостей вроде Makefile к языку? В Forth в этом нет проблем. Как насчет модификации компилятора Forth, чтобы позволить сложные стратегии инлайнинга? - да легко! Эта концепция не распространена в языках программирования, но имеет имя (на самом деле два имени): макросы (под которыми я имею в виду макросы в стиле Lisp, а не хромой препроцессор Cи) и языки предметной области (Domain Specific Language - DSL). Иногда также встречается термин Программирование, ориентированное на языки (Language Oriented Programming - LOP).

Эта статья не посвящена изучению Forth как языка, здесь только рассказывается о том, как написать Forth. Однако, пока вы не поймете, как написан Forth, у вас будет только очень поверхностное понимание того, как его использовать.

Вы можете пропустить следующую часть, в которой описывается, как скомпилировать все то, что мы изучим дальше и перейти сразу к разделу Внутреннее устройство Forth-машины

Сборка

Системные требования

Если вы хотите запустить этот Forth, а не просто прочитать его, вам понадобится Linux на процессоре не ниже i386.

Я мог бы обойтись без операционной системы, используя кросс-ассемблер для компиляции в машинный код целевой архитектуры, например, какого-нибудь микроконтроллера. Но для учебных целей это не годится - у вас просто может не быть соответствующего микроконтроллера, эмулятора, кросс-ассемблера, программатора, и.т.д. Но у вас наверняка есть Linux, поэтому мы предположим, что процесс Linux и будет нашим "железом". Как вы увидите дальше, мы используем только несколько базовых системных вызовов (чтение, запись, выход, etc).

i386 необходим, потому что мне пришлось написать немного кода на ассемблере, а i386 на сегодняшний день является наиболее распространенным. Конечно, когда я говорю «i386», любой 32-разрядный или 64-разрядный процессор x86 подойдет. Я компилирую все это на 64-битном процессоре. Технически вы можете заменить весь ассемблер на какой-нибудь другой host-язык. Например, мы могли бы написать часть, взаимодействующую с host-машиной на Си, Java или даже Lisp.

Итак, чтобы собрать все это, вам понадобится gcc и GAS (GNU-ассемблер). Обычно это все присутствует на почти любой Linux системе.

Для красивых графических расширений я буду использовать библиотеку libSDL2. Если вам не нужны красивости - вы можете обойтись без нее.

Компиляция на i386

Чтобы собрать Forth-систему на 32-разрядной системе, достаточно запустить

make

Я уже написал небольшой Makefile. К сожалению, при экспорте кода из литературного источника теряются символы табуляции (потому что я настроил свой Emacs заменять все табуляции пробелами при закрытии файла, и не хочу это менять). Поэтому я не экспортирую Makefile, а просто держу его рядом с литературным исходником. Тем не менее копия для справочных целей остается тут:

SHELL = /bin/sh

CC = gcc
CFLAGS  += -m32 -g
LDFLAGS += -m32 -g

TARGET = forth

INC = inc

CSRC = main.c sdlwrap.c
ASMO = jonesforth.o
TOASMS = $(CSRC:.c=.s)
OBJS = $(CSRC:.c=.o) $(ASMS:.s=.o)


.SUFFIXES:
.SUFFIXES: .c .o .s

.PHONY: all toasm clean

VPATH = src:inc

CFLAGS  += $(shell pkg-config --cflags  sdl2)
LDFLAGS += $(shell pkg-config --libs    sdl2)


all: sdlwrap.o jonesforth.o main.o
    $(CC) $^ $(LDFLAGS) -I$(INC)  -o $(TARGET)

sdlwrap.o: sdlwrap.c
    $(CC) -c $(CFLAGS) -I$(INC) $^ -o $@

main.o: main.c
    $(CC) -c $(CFLAGS) -I$(INC) $^ -o $@

jonesforth.o: jonesforth.s
    $(CC) -c $(CFLAGS) $^ -o $@

toasm: $(CSRC)
    $(CC) -S $^ $(CFLAGS) `pkg-config --cflags --libs sdl2` -I$(INC)

clean:
    rm -Rf $(TARGET) $(OBJS) $(ASMO) $(TOASMS)

Кросс-компиляция из x64

Сборка на 64-разрядной системе немного сложнее.

Чтобы собрать 32-разрядный исполняемый ELF-файл внутри 64-разрядного окружения я использую chroot, в котором запущена 32-разрядная операционная система. Проведите следующую последовательность действий, чтобы установить себе ее:

sudo su
apt install debootstrap
mkdir /mnt
cd /mnt
mkdir /mnt/xen
debootstrap --arch i386 xenial /mnt/xen

Когда установка будет завершена, войдите в ваше chroot-окружение для настройки:

sudo su
chroot /mnt/xen

Следует добавить репозитории в /etc/apt/sources.list

deb http://ru.archive.ubuntu.com/ubuntu/ xenial main restricted
deb http://ru.archive.ubuntu.com/ubuntu/ xenial-updates main restricted
deb http://ru.archive.ubuntu.com/ubuntu/ xenial main universe
deb http://ru.archive.ubuntu.com/ubuntu/ xenial-updates main universe
deb http://ru.archive.ubuntu.com/ubuntu/ xenial universe
deb http://ru.archive.ubuntu.com/ubuntu/ xenial-updates universe
deb http://ru.archive.ubuntu.com/ubuntu/ xenial multiverse
deb http://ru.archive.ubuntu.com/ubuntu/ xenial-updates multiverse

и сделать

apt-get update

После этого, если вы хотите получить поддержку графики, установите библиотеку libSDL2

apt-get install libsdl2-dev

И создайте папку, где будет проводиться компиляция. На этом настройка chroot-окружения завершена:

mkdir /j
exit

Для tangling-га из литературного исходника (в папку ./src) я использую Emacs, и я хотел бы производить ее на host-машине, а компиляцию - на целевой. Поэтому я написал bash-скрипт go.sh, который копирует Makefile и собранные из литературного исходника файлы на целевую машину. Он же запускает там компиляцию. Вам, придется изменить в нем пути для вашего окружения.

sudo rm -Rf /mnt/xen/j/src
sudo cp -R  /path/to/source/on/host/system/* /mnt/xen/j/
sudo chroot /mnt/xen /j/inchroot.sh
if [ $? -eq 0 ]; then
    cp /mnt/xen/j/forth ./
    ./forth
fi

Другой скрипт inchroot.sh занимается компиляцией в chroot окружении:

cd /j
make
if [ $? -eq 0 ]; then
    echo "===============OK==============="
    exit 0
fi
echo "===============ERR==============="
exit 1

После того, как минимальная ассемблерная часть Forth-системы успешно скомпилирована, добавляем остальную часть, уже написанную на Forth

Добавление Forth-части

cat jonesforth.f - | ./jonesforth

Если вы хотите запустить свои собственные программы Forth, вы можете:

cat jonesforth.f myprog.f | ./jonesforth

Если вы хотите загрузить свой собственный код Forth, а затем продолжить чтение пользовательских команд, вы можете сделать следующее:

cat jonesforth.f myfunctions.f - | ./jonesforth
Яндекс.Метрика
Home