Как стать автором
Обновить

Комментарии 45

Про синтаксис.
А есть синтаксис где вместо MOV используется LD? =)
(В своё время так привык на Z80, что на целую букву больше писать было странно...)
или просто делаешь #DEFINE LD MOV?
Байт-код — это прекрасно. Осталось выяснить, при чем тут форт. Вы как-то планируете добавить интерпретатор юзерского ввода и компиляцию его в шитый код?
Да, об этом я написал в заключительной части статьи. Хочу сделать интерпретатор и компилятор форта. Только не в шитый код, а в байт-код этой машины.

Кстати, хороший вопрос что будет быстрее работать — один из вариантов шитого кода или ваш байткод.

Самому интересно. В теории, шитый год должен быть быстрее, так как там только одно чтение из памяти на команду (из кода). В байт-коде два — из кода, и из таблицы адресов. Потом можно будет сравнить с другими форт-системами, но, там другие детали реализации, другая разрядность…

Есть такой учебный проект Форта (Forthress) преподователя Igor Zhirkov из ITMO
"Low-Level Programming: C, Assembly, and Program Execution on Intel x86-64 Architecture".
сделанного на NASM.


P.S. И тоже вопрос, почему не FASM для реализации?
На FASM сделано ядро операционной системы KolibriOS (и Форт к ней тоже уже к ней портировали c базиса российской Форт-системы SPF4)
В теги к статье можно добавить "Forth" (Форт) "Ненормальное программирование" :)

Маленькое замечание: раз уж у вас и Linux, и AS, и LD, расширение исходников лучше указывать .s, а не .asm.

Форт это прекрасно. Ждём новых статей.

Здесь есть перевод материалов проекта Jonesforth с Github
rigidus.ru

Ассемблер, это прекрасно! Ждем новых статей.


Хотя, выбор AS несколько смущает. Хоть в intel синтакс и на том спасибо. Но для полностью ассемблерных проектов все-таки FASM или NASM лучше будут. AS, скорее всего бакенд компиляторов. Для людей не очень удобный.

Если уж писать на ассемблере, то вершину стека нужно в регистре хранить.
Не совсем понял идею. Сейчас указатель на вершину стека данных хранится в RSP, стека возвратов в RBP.
Вершину стека держать в регистре, допустим, в EAX. RSP указывает на настоящий стек. Полный стек состоит из двух частей.

Упрощает и стековые, и арифметические операции, и добавляет скорости.

NEGATE neg RAX
SWAP xchg EAX, [ESP]
DROP mov [ESP], EAX; add ESP,4

и т.д.
Понятно. Держать одно верхнее значение в регистре, остальное в памяти. Можно попробовать, протестировать производительность.
Вообще, я пытался придумать, как держать несколько значений с вершины в регистрах… но несколько точно не стоит, слишком много движений при стековых операциях получается. Одно — может быть…
Может несколько уменьшиться количество обращений к памяти, а вот команд процессора — наверняка возрастет. Надо тестировать.
Количество команд, как правило, сокращается. На те же действия исчезает одна команда загрузки из стека и одна команда помещения в стек.
Сам делал нечто подобное. Хотя мой интерпретатор/транслятор написан на C, для кроссплатформенности, и может быть как 32 битным, так и 64. Словарь формируется скриптом на python. Основной же особенностью является то, что форт представляет собой модуль ядра линукс. Разрабатывался для того, чтобы можно было изменять регистры и работать с железом напрямую. Есть возможность вызова функций ядра, однако, callback'и пока не реализованы.
github.com/y-salnikov/kforth
Словарь формируется скриптом на python.

Ну это какое святотатство. Форт должен быть написан только на Форт.

Так он и написан на форте, просто этот форт интерпретируется на python. Это делается только на этапе сборки модуля, после запуска пополнять словарь можно только исключительно средствами Форта.
Что бы можно было положить любое число, нужно 64 бита. Но, каждый раз команда будет занимать 5 байт, что бы положить одно число?
это будет байт-команда, за которой лежит параметр — адрес перехода 64 бита. Опять пять байт
Девять же, а не пять?
mov ecx, offset msg_bye # указатель на выводимую строку
Повезло, что старшие 32 бита нули, но лучше ECX заменить на RCX (это копипаста 32-битного кода?)
Точно, девять! Все мыслю 32-мя битами :) Исправил.
И с ECX косяк, исправлю в исходнике.
Спасибо!
В данной реализации не решается задача плаформонезависимости.
Доступ к реальному железу, тоже не ограничивается.
Т.ч. смысл не понятен.
В составе моего мультиадатера www.forth.org.ru/news/stm32f105MultiAdapter#0
есть полноценная виртуальная Форт машина, используемая для целевой компиляции.
В чем здесь платформозависимость байт-кода?
Как можно получить доступ к железу из байт-кода?
Для достижения платформонезависимости применения байт-кода недостаточно.
Требуется еще виртуальное адресное пространство.
К примеру, адрес msg_hello устанавливается линковщиком и может быть каким угодно.
Доступ к реальному адресному пространству и есть доступ к железу.
Цель — сделать максимально быструю байт-машину, протестировать ее производительность. Поэкспериментировать с различными вариантами. В текущем варианте доступ к памяти прямой, адреса, используемые байт-командами — это адреса, используемые в командах процессора. При желании, можно модифицировать байт-команды, добавить виртуальное адреса, различные проверки. Это снизит производительность. Сам байт-код от платформы не зависит.
Если байт-код использует адреса, используемые в командах процессора, он уже зависим от платформы. Не за что, ваш кэп.
Что прямая ссылка на msg_hello ломает платформонезависимость, полностью согласен. На самом деле, это не совсем корректный пример. Это пример, что бы просто протестировать байт-машину. Строковый литерал было делать лень, и, видимо, зря. В байт-коде не используются адреса в явном виде. В следующей статье сделаю и опишу строковый литерал и байт-команду var.
Сам интерпретатор байт-кода это 3 команды. Все зависит от насыщения.
Насыщения на ассемблере. Борьба с тормозами виртуализации не может
быть целью этой виртуализации. Проблема совместимости на уровне
исходников Форта связана не с тем, что это трудно обеспечить.
А с тем, что нет стимула об этом заботится.
Нифига не круто! Вот я в 88-м писал форт для Spectrum (по книжке Баранова и Ноздрунова), так у меня даже дисковода не было. Магнитофон юзал. А тут блин жесткий диск с немерянными гигабайтами, и это называется по-индейски… Вот у меня понятное дело, да, действительно только лес, деревья и ассемблер zeus!!! :))))
Шучу, сами понимаете. Респект, отличная работа (дочитал всё статьи до конца)! Особенно учитывая что делалось с абсолютного нуля, с hello world. Плюсую. Сейчас сам потихоньку дозреваю до написания своей форт-системы со строгой статической типизацией. А значит с контролем ошибок, отличной поддержкой IDE, мультидиспатчингом слов (как в julia) и прочими вкусностями. При сложности чуть-чуть бОльшей, чем обыкновенный форт.

Эх… Плюсануть не удалось… Период голосования истёк. Ладно, плюсую мысленно! :)))
дозреваю до написания своей форт-системы со строгой статической типизацией
Есть идеи, как в compile-time запретить вызов слова, если на стеке нет значения соответствующего типа?
Если очень грубо, то примерно вот так www.stephan-becher.de/strongforth/index.htm
Пишется стековая аннотация (тут она ОБЯЗАТЕЛЬНА, а не в качестве правила хорошего тона), и компилятор знает, что при вызове лежит на стеке. А дальше компилируя просто отслеживает стек времени исполнения. Точно так же будет работать поддержка автокомплита в IDE. Но там ещё будет учитываться порядок поиска в словарях. Самый серьёзный недостаток — система будет несовместима ни с чем существующим. Совершенно другая структура словаря. Так что в виде библиотеки это сделать не получится. На всякий случай предупреждаю, это пока очень и очень сырые мысли. Я ещё не написал ни строчки кода.

Сорри, немного нечётко. Короче, ВСЕ слова в системе снабжены стековыми аннотациями. Что на входе, и что на выходе. Тут это не комментарий, а неотъемлемый элемент языка. Например вот так:
: 2swap ( 2cell 2cell — 2cell 2cell ) overwrite swap

;
Тут во-первых вводится стековая аннотация. Откуда видно, что при вызове в стеке две двойных ячейки и на выходе тоже две двойных ячейки. Во-вторых говорится, что это слово замещает слово swap, если при вызове система определяет, что на стеке две двойных ячейки. Как 2swap оно тоже ищется. И поскольку ВСЕ слова снабжены такими аннотациями, во время компиляции состояние стека известно на каждом шаге.
Точно так же работает автокомплит. Начинаем набирать определение слова через двоеточие. Как только набрали стековую аннотацию, языковому серверу сразу ясно, что лежит на стеке в начале определения. И он ищет в подключенных словарях слова, с такой же сигнатурой параметров, предлагая их в списке автокомплита. На каждом шаге набора состояние стека отслеживается. И всегда для ввода предлагается что-то адекватное. Вот примерно так.
Я вижу сложности с анализом потока выполнения.
Например, если в программе есть цикл от 1 до N, внутри цикла кладём на стек int, то для
этого куска кода нужно написать вручную инварианты на входе и выходе, либо система сама сможет их вывести? Если второе, то очень похоже на решение проблемы останова и вообще анализа алгоритмов в общем виде.
Я их тоже вижу. Но тут такое дело, пока не начнёшь реально писать — не поймёшь. Хотя разумеется предвидишь такие вещи заранее. С проблемой останова это никак не связано, потому что компилятору достаточно пройтись по ветке один раз. Его же интересуют типы, а не значения. А они не меняются, иначе типизация не будет статической. Так что грабли я тут конечно вижу, но не фатальные. Писать надо, тогда станет ясно. Блин, в julia с её множественной диспечеризацией в конце концов такая же трудность. И ничего, разруливают.
А они не меняются, иначе типизация не будет статической
Меняются, если в ветке THEN кладём на стек один тип, а в ветке ELSE — другой тип.
Замаскированный вариант той же неопределённости — кладём на стек одно значение, а затем N значений другого типа, где N — не вычислимо не этапе компиляции.
Нельзя. Слово должно возвращать результаты. Их количество всегда должно совпадать для всех веток. А типы либо совпадать, либо в слове объявляться возвращаемым их супертип.

Ой, сорьки. Я не на тот вопрос ответил. Пройти по всем вариантам исполнения кода можно. И довольно легко, слова обычно не длинные. Так что какие типы возвращаются — мы знаем. Если на стек кладётся значение одного типа, а затем N другого, слово всегда берёт со стека столько, сколько ему надо. И это записывается в стековой нотации. Компилятор это тоже проверяет. И ругается, если скажем при верхнем элементе стека равным 0 снимается 5 элементов, а при 1 снимается 10. Да, возможно это некое ограничение свободы. Но моё глубокое IMHO что за слова, берущие (возвращающие) разное число значений в зависимости от входа, программистам надо отрывать руки, причём по самые гланды. Вот как-то так… :)))
Ну ещё придётся запретить слова типа PICK, ROLL и ?DUP.
Зачем же? Они возвращают самый общий тип, для которого известен только размер (целое, плавающее, указатель и т.п.). Если нужен более конкретный тип, то cast, исключительно под Вашу ответственность. Вот примерно так…

P.S. Я понимаю, что всё это накладные расходы. Но как бы это в режиме работы форт-системы. При целевой компиляции всё это уйдёт вместе со словарями и заголовками слов.
Понятно. Но тогда лучше PICK заменить на специализированные варианты, например PICK-INT, PICK-NUM и т.д. Для пользовательских типов PICK запретить (если только не придумать аналоги generic-ов, чтобы слово могло параметризироваться пользовательским типом).

Так у нас программа может упасть в рантайме, если на нужной глубине стека не окажется значения указанного типа, но хотя бы это избавит от неопределённости типа на стеке после успешного выполнения слова.

С ROLL непонятно, что делать.

?DUP запретить, потому что оно меняет глубину стека в зависимости от входа.
?DUP согласен. Что-то надо думать. Хотя мне такой подход никогда не нравился, но слово стандартное. С pick и т.п. — зачем ??? Стек у нас состоит не из ячеек, а из объектов — кусков памяти, размер которых может меняться, но для каждого конкретного всегда известен. Какая разница что мы положили на стек, и что с чем обменяли. Границы всегда размечены. Так что это будет работать и для целых и для длинных и для плавающих. Хоть для структурных объектов. Частные случаи, когда скажем обмениваются целые, имеют более простой и производительный код. Компилятор просто выбирает его, если возможно. А имя слова одно и то же. Именно это сделано в julia.
Так N PICK обозначает взять N-е слово со стека, а, значит, его тип зависит от значения N и не может быть вычислен в compile-time.
Почему ??? Слово pick снимает со стека целое N (тип cell), затем лезет в стек на N позиций, смотрит что там лежит, и кладёт его на вершину стека. Что там лежит конкретно, мы не знаем. Но знаем, что что-то обязательно лежит (если нет ошибки исчерпания). Значит тип возвращаемого значения не none (ничего не возвращает). Тогда это object — хреновина, для которой определено только свойство size и больше ничего. Реальной пользы от него мало, он нужен только чтобы поддерживать правильную разметку стека. И его обязательно нужно преобразовывать в какой-то полезный тип, что уже целиком на совести программиста. Я его честно вытащил, с меня взятки гладки. Ну а если программист сам не помнит что там лежало, это уже вопросы не ко мне :))))
Если ввести тип object, который совместим в обе стороны с любым типом, то да, это как void* в C. Использование будет небезопасным, а компилятор не сможет выполнять оптимизации (хотя, какие оптимизации в Форте). Лучше думать в сторону generic-ов и определения всех типов в compile-time.
Как бы сейчас честно говоря не особо хочется об этом думать. Как говорил дедушка Ленин — «Главное ввязаться в драку, а там посмотрим» :)))
Вчера вечером всё-таки начал. В качестве морской свинки взял этого таварисча www.softsynth.com/pforth/index.php. Тут в принципе без разницы с чего начинать, конечный продукт всё равно будет написан на самом себе, как SPF гражданина Черезова. Но для опытов лучше взять что-то готовое, написанное на С. Перенёс его в eclipse, нормально собрал и полночи фиксировал на бумаге (точнее в файле) тезисы. Вобщем огромное спасибо за обсуждение, наконец-то это сподвигло меня к действиям :))))
Когда будет что показывать, хоть минимально, обязательно на любимом хабре отпишусь :))
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации