?

Log in

Mon, Jun. 3rd, 2013, 12:54 pm
MK4TH: Первые слова

MK4TH: Первые слова

Я исхожу из того, что мне не нужно рассказывать базовые вещи ни про ПМК, ни про форт :)

Поэтому перехожу непосредственно к особенностям mk4th-а.

Сама система - http://arbinada.com/pmk/system/files/mk4th.zip

Для начала немного терминологии и соглашений:
Стек - это стек gforth-а на этапе компиляции.
МК-стек - это стек калькулятора.
Почти все слова для работы с памятью имеют префикс, обозначающий тип памяти: p - программ (ПП), d - данных (ПД) (как правило десятичных (ПДД), но и выше), b - байтовая память, t - текстовая.
Файлы с программой для mk4th-а имеют расширения .mkf

Программа для ПМК пишется в отдельном текстовом файле, который будет потом "скормлен" gforth-у.
Первой строкой программы, как правило идёт команда gforth-у на подключение нужного нам модуля:
include mk4th.fs
(Возможно подключение любых других библиотек и модулей).

Для переключения между словарями gforth-а и mk4th-а служат слова FORTH и MK.
При этом многие слова mk4th-а вынесены в forth-словарь. В словаре MK в основном только совпадающие по написанию со стандартными форт-словами.

Слово ORG ( a -- ) обнуляет область памяти, отведённую под память МК и указатели, снимает со стека адрес и компилирует по нему следующую "стартовую" обвязку кода:

org+0 : PGSB
org+1 : 0
org+2 : 0
org+3 : R/S
org+4 : (P)GOTO
org+5 : org
(org+6): (org)

В переменную ppage заносится номер страницы.
А по адресу org+1(2) в дальнейшем будет подставляться адрес перехода на компилируемый код ПМК. (Адрес хранится в переменной enterpoint).

ВНИМАНИЕ! Предполагается, что слово ORG должно встречаться только один раз перед компиляцией программы. Файл mk4th.fs уже содержит 0 org , поэтому в тексте своей программы это дублировать не обязательно, но можно. (пока так).

Слово : <имя> создаёт новое слово <имя> в пространстве gforth-а, но указывающее на ПП. Также, по адресу org+1(2) будет записан текущий адрес ПП. Таким образом стартовым получится последнее определённое через : слово.
При вызове слова <имя> , (определённого через : ) будет скомпилирована ссылка (p)GSB на МК-код этого слова.

т.е. в программе:

: имя1 .... ;
: имя2 ... имя1 ... ;

внутри слова имя2 будет скомпилирован вызов подпрограммы имя1.

Чтобы узнать адрес в явном виде: ' <имя> >pa ( в этом месте на стеке остаётся адрес памяти программ, с которого начинается описание слова <имя> ). Адрес, например, можно скомпилировать в МК-код и использовать для косвенной адресации.

Слово ; компилирует код МК-команды RET и выполняет оптимизацию адресов перехода с точки последней оптимизации.

По сути дела весть код от : <имя> до ; является кодом подпрограммы с меткой <имя>.

ПРИМЕЧАТЕЛЬНО, что одна такая "подпрограмма" может иметь несколько точек входа.
С несколькими ; сложнее - там оптимизация может накосячить, поэтому для досрочного выхода (случись такой, но лучше - не надо :) ) нужно использовать команду RET непосредственно.

Слово == <имя> аналогично слову constant (или .EQU из МК-ассемблера), просто писать короче и удобнее :)
Константы создаются в пространстве gforth-а и их садержимое потом кладётся на его же стек.
Предполагается, что все регистры с 0 по 14 и с 9000 по 9999 будут заданы программистом явным образом как константы, поскольку они обладают спец-свойствами и им нужно особое внимание :)

Слова dvar <имя> ( bvar , tvar ) создаёт переменную <имя>, начиная с адреса 15 (1000, 5096 - соответственно) и далее автоматически.
Т.е. слово <имя> оставляет на стеке номер регистра (адрес памяти переменной).

Резервировать диапазоны регистров можно словом dallot.
Так, например, после последовательности слов: dvar aa 10 dallot dvar bb
aa выдаст 15, а bb - 26

Собственно это и есть тот самый механизм с громким названием "менеджер номеров регистров" :) На самом деле такой подход позволяет избежать пересечения областей памяти данных при использовании библиотек и т.п. Но регистры 0-14 всё равно остаются проблемой.

Слова ! и @ снимают со стека число и, рассматривая его как номер регистра (адрес памяти данных), компилируют команду M, PM, PPM или RM, PRM, PPRM соответственно.

Так программа:

include mk4th.fs
MK
0 ORG
5 == IN
dvar OUT
: from-in-to-out
in @ out !
;

будет скомпилирована в следующий код:

0000 F3h P GSB 0006 ;
0001 00h ;
0002 06h ;
0003 50h R/S ;
0004 51h GOTO 00 ;
0005 00h ;
0006 65h RM 5 ;
0007 4Fh P M 15 ;
0008 15h ;
0009 52h RTN ;

Кроме слов @ и ! существуют слова k@ , k! , +k@ , +k! , -k@ , -k! , компилирующие команды (P)KRM и (P)KM. При этом слова с префиксом "-" проверяют, чтобы им "подсунули" регистры с 0 по 3, а с префиксом "+" - с 4 по 6. А без префикса - с 7 по 99.

Пока вроде почти как в обычном форте.
Но пора бы сказать про числа. И тут есть ВАЖНЫЙ нюанс.
Если в программе написать число, то оно будет скомпилировано на стек gfortha и калькулятор никогда о нём не узнает.
Для переноса числа со стека gfortha на МК-стек служит слово #, (диез-запятая).
Это слово компилирует число со стека в набор клавишных команд-цифр.
123 #, скомпилируется в МК-код 01 02 03

-34 == тридцатьчетыре
тридцатьчетыре #, 43 k!
скомпилируется в:
3
4
/-/
PKM 43

Да, числа могут быть отрицательными, но не вещественными - таков стек форта.
Если нужно скомпилировать десятичное вещественное число (или несколько) есть слово #[
Это слово считывает строку, ограниченную ] и пытается скомпилировать её как одно вещественное число или как несколько вещественных чисел, разделённых запятой. При этом непонятные символы просто игнорируются. Т.е. контроль за корректностью лежит на программисте.
#[ -123.456e-78, -1a2s3 ] будет скомпилировано в:
1 2 3 /./ 4 5 6 /-/ ВП 7 8 /-/ В^ 1 2 3 /-/

Другая возможность: слова с #0 по #9 компилируют цифры напрямую.
Т.е. строки:
#[ -123 ]
-123 #,
#1 #2 #3 /-/
скомпилируют одинаковый код.

Слова, позволяющие делать вычисления с вещественными числами на этапе трансляции с последующей компиляцией или заполнением (и сохранением в файл) регистров пока не реализованы.

Собственно из линейного программирования - почти всё.
Все остальные команды mk4th за редким исключением совпадают с принятым написание команд ПМК или с написанием на клавиатуре ПМК, только, как правило пишутся без префиксных F и K (если это не косвенное обращение).

$0A /./ $0B /-/ $0C EE $0D Cx
$0E ENT $0F Bx $10 + $11 -
$12 * $13 / $14 <-> $15 10^x
$16 EXP $17 LG $18 LN $19 ASIN
$1A ACOS $1B ATG $1C SIN $1D COS
$1E TG $20 PI $21 SQRT $22 x^2
$23 1/x $24 x^y $25 Ro $26 M->D
$27 K- $28 PM@ $29 K/ $2A MS->D
$30 D->MS $31 |x| $32 SGN $33 D->M
$34 [x] $35 {x} $36 MAX $37 AND
$38 OR $39 XOR $3A NOT $3B RND
$50 R/S $52 RET $54 NOP $55 SCR
$56 GRAPH $F2 IRTN

$4x M $6x RM $4F PM $6F PRM
$BF PKM $DF PKRM $F4 PPM $F6 PPRM

(Некоторые команды имеют "дубликаты". Так вместо ENT можно писать привычное DUP, а вместо <-> SWAP. Можно пойти дальше и команду Ro обозвать DROP ( ' Ro alias drop ). Ввести команды : OVER swap swap Bx ; и : ROT Ro Ro <-> Ro ; и : NIP swap drop ; и .т.д. Впрочем я увлёкся :) )

Обращаю внимание, что команды обращения к памяти снимают номер регистра со стека (причём делают это на этапе компиляции).
Но вместо них лучше пользоваться @ и ! и их вариантами.

mk4th понимает и команды перехода (снимая со стека адрес), но их лучше в явном виде не использовать.
(Забегая вперёд и вглубь: для организации переходов есть слова >pmark presolve IF <код истины> THEN <какой-то там код>
или
<условие> IF <код истины> ELSE <код лжи> THEN <какой-то там код>

Отличий от классического форта тут ровно 2:
1) обязательно наличие условия перед IF ;
2) Вершина МК-стека (х) после проверки условия может содержать "мусор" (результат преобразования величин для сравнения с 0).

dvar minus dvar plus

: -or+ ( n -- n1|n2 )
0< if minus @
else plus @
then
;

На выходе из подпрограммы на МК-стеке содержимое соответствующей переменной.
( В скобках - как и у "обычного" форта - комментарий ).

Условием может быть:
0= 0<> 0< 0>= - компилируются в аналогичные МК-команды ( Х не меняется );
0> 0<= - добавляется команда смены знака ( Х = -X );
= <> < >= - предварительно скомпилирован "-" ( X = Y - X );
> <= - предварительно идут и вычитание и смена знака ( X = X - Y ).

: error<0 ... ;

: error>9 ... ;

dvar 1digit

: 0-9
0< ( значение сохраняется )
if error<0
else dup #9 > ( значение модифицируется, поэтому предварительный dup, а потом будет swap )
if error>9
else swap 1digit !
then
then
;

Косвенное ветвление не реализовано. Нужно будет - теоретически можно будет добавить.

Циклы.

Все начинаются с BEGIN:

BEGIN <тело> AGAIN
BEGIN <тело> <условие> UNTIL
BEGIN <условие> WHILE <тело> REPEAT
<установка счётчика> ... BEGIN <тело> <счётчик> NEXT

<тело> - любой код.
<условие> - см. условие для IF-ELSE-THEN. В отличие от "классического" форта - оно обязательно.
<счётчик> - номер регистра (0-3), который используется как счётчик циклов L0 - L3.

BEGIN-AGAIN - бесконечный цикл;
BEGIN-UNTIL - выполняется, пока условие ложно;
BEGIN-WHILE-REPEAT - выполняется, пока условие истинно;
BEGIN-NEXT - выполняется заданное число раз (аналог МК-цикла L0-L3. Собственно это он и есть :) ).

Подробнее про цикл BEGIN-NEXT на примере:

mk 0 org

0 == x
1 == y
9011 == pixel

: fillscreen
64 #, y ! begin
128 #, x ! begin
y @ #1 - x @ #1 - pixel !
x next
y next
graph
;

Получаем код, эквивалентный вот такой ПМК-программе:

A0: ; с адреса 4
P GSB A6
R/S
GOTO A0

A6: ; с адреса 0
6
4
M 1

A9: ; с адреса 24
1
2
8
M 0

A13: ; с адреса 22
RM 1
1
-
RM 0
1
-
PP M 9011 ; Вывод точки (Графический экран)
F L0 A13
F L1 A9
K GRPH
RTN
.END

Вообще в "голой" mk4th - системе уже определены константы I J K L как счётчики 0 1 2 3.

Теперь про данные.

Слова p, 2p, pd, pt, позволяют компилировать байт, 2 байта, байт с признаком данных и байт с признаком текста в память программ.

Пример аналогии:
AAA: .DB 1, 2, 3, 4, ; для МК-ассемблера
pcreate AAA 1 pa, 2 pa, 3 pa, \ для mk4th
( ; и \ - знаки комментария в соответствующих языках, если что, а не часть конструкции :) ).

Понятно, что в отличие от МК-ассемблера в форте можно придумать и описать любые слова для любых структур данных. Как на этапе компиляции, так и для работы непосредственно на МК.
Например просятся слова для графических образов, в которых изображение "рисовалось" бы.
Или вот как уже рабочий пример одной из таких структур:

Слово p" читает строку до " и компилирует это как текст, ограниченный 0 и оставляет на стеке адрес начала строки. Служебные коды не понимает. (Пока так. Но еще думаю, как правильнее).
Внимание! Внутри определений через двоеточие не применяется.

p" Text" == my_text \ В дальнейшем, при вызове my_text на стеке останется МК-адрес начала строки
: print ... my_text #, 9026 ! ... ; \ вывод текста my_text в строку комментариев (явно заданный регистр 9026)

Русский текст может быть в одной из 3-х кодировок: 866, 1251, utf8
Кодировки переключаются словами cp866, cp1251, utf8 (по умолчанию cp866).
Сам "перекодировщик" сделан довольно топорно и неполноценно, поэтому возможны косяки. И вообще он мне не нравится - может быть переделаю :)

Аналогии:
.CHARSET 1251
TTT: .TEXT "Это текст\0" ; МК-ассемблер

cp1251
p" Это текст" == TTT ( mk4th )

Собственно это всё, что можно вкратце сказать про основные слова mk4th-а.

Ну а как расширять набор компилирующих слов - это уже другая, долгая и глубокая история.
Собственно по исходникам можно попробовать понять (а заодно их причесать, оптимизировать и половить багов :) )

Есть еще вспомогательное слово mkdump (без параметров), которое на этапе компиляции выдаёт дамп памяти программ, начиная значением org и заканчивая текущим указателем phere.

А! :)
И самое главное слово: MKP: <имя-файла> :)
Это слово сохраняет откомпилированный код в файл <имя-файла> в формате .mkp

Ну и наверное последнее слово должно быть BYE, чтобы выйти из gforth-а обратно в систему после компиляции и сохранения.
(Если на этапе компиляции возникнут ошибки - оно и так само в систему выпадет :) )

И вот тут уже пришла пора написать классику:

\ ------------------------------------

include mk4th.fs

mk

0 org

9026 == .(pstr)

p" Hello world!" == hwstr

: hw hwstr #, .(pstr) ! ;

mkp: hw.mkp

bye

\ ----------------------------------

Теперь осталось набрать в терминале (или запустить свой батник/эсашник): gforth <ИмяФайлаСПрограммой.mkf> и получить hw.mkp, залить его на ПМК (пока еще) штатными средствами и ... В/О С/П :)

Теперь чуть глубже.

Система позволяет не только писать программы на МК-форте, но и расширять сам этот МК-форт (и вспомогательные структуры) средствами gfortha.

Вот, например, на ПМК можно сделать такйю конструкцию (реальный пример подпрограммы из программы simon):
...

TOXY: 1 GSB TOX ;
2 ;
TOX: RM0 KAND RMA * ;
RTN ;

Т.е. При вызове подпрограммы toxy будет вызов программы tox с аргументом 1, а затем её же с аргументом 2, а потом выход иобратно. И всё это на одном RTN.

Ближайшая конструкция на mk4th будет такой:

: tox 0 @ and 10 @ * ;
: toxy #1 tox #2 tox ;

Она таёт код:

TOX: RM0 KAND RMA * ;
RTN ;
TOXY: 1 GSB TOX RET ;
2 GSB TOX RET ;

Явно перегружен вызовами и возвратами (а если он еще и на разрыв страницы попадёт - вызовы станут "длинными", что еще его увеличит.

Но форт на то и форт :)
Пишем слова-телепорт :)

forth
: -->: (GSUB) >pmark ;
: :--> pmark резервирует в памяти программ 2 байта для адреса перехода по команде GSB (оптимизатор потом может сократить 1 байт. Но это уже "по факту"). И оставляет на стеке адрес, куда потом слово pmrk.

Но эта конструкция не будет скомпилирована до тех пор, пока не будет вызваны слова -->: и :-->

Теперь переипешем нашу подпрограмму так:

: toxy ( -- y x ) #1 -->: #2 :--> count @ and YY @ * ;

Вместо -->: будет скомпилирован код GSB < на место, помеченное :--> >,
В итоге компилируемый код ничем не будет отличаться от исходного кода на ПМК.
Сначала на МК-стек будет положена 1, затем произойдёт вызов "хвоста" слова ( команда #2 останется незамеченой), затем произойдёт возврат к команде #2 и "хвост" слова будет выполнен повторно.

Чуть подробнее о нюансах компиляции.

Допустим нам нужно слово over
: over ( y x -- y x y ) <-> <-> Bx ;

Если мы его определим в контексте MK, то его код будет присутствовать в памяти МК только в одном месте, а при его вызове будет компилироваться ссылка на это место.
mk : over ... ;

А если его описать в контексте форта, то при его вызове в память МК будет всякий раз компилироваться весь код этого слова.
forth : over ... ; mk

Т.е.

вот такой текст:

mk
: over <-> <-> Bx ;
: aaa over over over ;

даст такой код:

0000 F3h P GSB 0010 ;
0001 00h ;
0002 10h ;
0003 50h R/S ;
0004 51h GOTO 00 ;
0005 00h ;
0006 14h <-> ;
0007 14h <-> ;
0008 0Fh F ANS ;
0009 52h RTN ;
0010 53h GSB 06 ;
0011 06h ;
0012 53h GSB 06 ;
0013 06h ;
0014 53h GSB 06 ;
0015 06h ;
0016 52h RTN ;

А вот такой ткст:

forth
: over <-> <-> Bx ;
mk
: aaa over over over ;

вот такой код:

0000 F3h P GSB 0006 ;
0001 00h ;
0002 06h ;
0003 50h R/S ;
0004 51h GOTO 00 ;
0005 00h ;
0006 14h <-> ;
0007 14h <-> ;
0008 0Fh F ANS ;
0009 14h <-> ;
0010 14h <-> ;
0011 0Fh F ANS ;
0012 14h <-> ;
0013 14h <-> ;
0014 0Fh F ANS ;
0015 52h RTN ;

Аналогично можно описывать и новые структуры

Например можно ввести такие слова:

forth
: LIST: ( -- name ) create here 0 , 0 does> @ ;
: ;LIST ( -- a ) phere >r begin dup while 2p, repeat drop r> swap ! ;

Используются вне определений через :, в контексте:

list: <имя>
p" строкаN"
p" строкаN-1"
...
p" строка1"
p" строка0"
;list

Компилирует в ПП таблицу ссылок на текстовые строки и сами строки. Слово <имя> кладёт на стек адрес начала этой теблицы.

Ну и т.д.




Копия на арбинаде: http://arbinada.com/pmk/node/977