Basic pro Tesla Ondra SPO-186: (4) Řetězce

Správný Basic by měl umět nějak pracovat s textovými řetězci, aby bylo možné napsat si alespoň nějakou textovku.

Na SAPI-1 byly místo polí řetězcové funkce I$ a O$, sloužící pro vstup a výstup řetězce – jako parametr se zadávala adresa, na které je řetězec uložen nebo kam se má uložit, ale uživatel si musel hlídat rozložení paměti, délku řetězců.

Budiž, mějme tedy řetězce imlementované jen jako pointery (koneckonců, třeba v C se s řetězci ani jinak nepracuje, a to je nějaký jazyk).

Poznámka k řetězcům praví:
“Trochu nevýhodou bude to, že concatenate řetězců bude muset dělat PRINT (tedy ne D$=A$+B$, ale PRINT A$,B$), protože pointery nelze sčítat.
Všechny řetězce A$-Z$ (možná bude potřeba pojmenovat je $A-$Z) tak kromě samotného programu zaberou navíc jen “strašlivých” 54 bajtů (všechny dohromady).
Napadla mne ještě jedna možnost, jak řetězce řešit, a sice vkládat je do obyčejných číselných proměnných (žádná paměť zabraná navíc) se zápisem jako funkce, LET A=$”KOKO” : PRINT $(A)
Pak by v tom ale byl asi moc guláš, a nešlo by použít jedno písmeno pro dvě proměnné (A a A$).
Budu to muset ještě implementačně promýšlet.
Jen si nejsem jist, zda se vůbec budu chtít psát porovnávání řetězců pro příkaz IF.”

A o něco později:
“Nakonec to myslím vyšlo líp – pontery lze svázat s proměnnými (nebo prvky pole), které je pointerují (a mohou ukazovat na vlastní definici v Basicovém zdrojáku, řetězec pak nemusí existovat duplicitně jak ve zdrojáku, tak v paměti proměnných).
Když už mám v programu text LET A=”KOKO”, proč zakládat v paměti řetězcovou proměnnou a do ní kopírovat text “KOKO” a mít tak v paměti vlastně 2x “KOKO”?
To by potom 1024znakový řetězec v paměti nezabíral 1 kB, ale 2 kB (protože způvodního umístění ve zdrojovém textu se nemaže). Místo toho chci pointerovat na původní umístění.

Samořejmě řetězec přímo ve zdrojáku je jen taková věc “navíc”, protože při INPUT$ (ve videu se povel jmenoval GET, přejmenoval jsem to) hrozí, že si delším (něco přepíše) nebo kratším (vznikne syntax error) zadaným řetězcem uživatel program rozbourá.
(Což je výzva k tomu, doplnit INPUT řetězce o parametr požadované délky řetězce.)
Řetězce se samozřejmě dají ukládat do “volného místa”, tedy do prostoru pole @(), začíná tam, kde končí zdrojový text v Basicu (adresa vracená funkcí FREE), velikost je vracená funkcí SIZE. Kolize s nižšími prvky pole nehrozí, protože pole se ukládá od konce volného prostoru dolů, zatímco řetězce naopak od nižších adres k vyšším. Znamená to ovšem, že obsah řetězce je dostupný nejen PEEK a POKE, ale i prostřednictvím čtení/zápisu příslušného (dvoubajtového) prvku pole, se kterým se kryje.”

Opět o něco později jsem se vrátil zpět k příkazu pro zadávání řetězce textu:
“Štvaly mne ty dva příkazy pro INPUT (INPUT a INPUT$), tak jsem z nich udělal jeden, kde jde kombinovat vstup textu i čísla:
INPUT "Vaše jméno"$free+1234,"Váš věk"a
.”

Bylo ale nutné promyslet řetězce celkově trochu víc do hloubky:
“Porovnání řetězců. Mohlo by se hodit na psaní textovek (IF CMP$(a,”NAHORU”) THEN …)
Uvidíme, jak se podařilo implementovat myšlenku z téhle poznámky:
“Řetězce kromě funkce pro porovnání (nutné pro tvorbu textovek) budou chtít i funkci pro kopírování (céčkové STRCPY, STRCMP) , asi s názvy COPY$ a CMP$.

Protože uložit řetězec za jiný řetězec (třeba od FREE) jde jednoduše
INPUT $FREE
INPUT $FREE+LEN(FREE)+1

a další ještě jednodušeji
INPUT $FREE+LEN(FREE)+1+LEN(FREE+LEN(FREE)+1)+1
a další úplně nejjednodušeji
INPUT $FREE+LEN(FREE)+1+LEN(FREE+LEN(FREE)+1)+1+LEN(FREE+LEN(FREE)+1+LEN(FREE+LEN(FREE)+1)+1)+1

měl bych vymyslet funkci “řetěžec N-tý od zadané adresy”
(NTH$(adresa, pořadí) nebo SUCC(adresa, pořadí))

I když by to šlo dělat inteligentněji, třeba
@(1)=FREE
INPUT $@(1)
@(2)=@(1)+LEN(@1)+1
INPUT$@(2)
@(3)=@(2)+LEN(@2)+1
INPUT $@(3)

… ale to bych po uživateli asi chtěl moc, ne?”

Na implementaci STRCPY a STRCMP došlo brzy poté, co jsem poznámku napsal:

“Poněkud složitější parametry má příkaz COPY:
COPY odkud TO kam,délka
To je jasné, je to vlastně jako LDIR.
“Řetězcová proměnná” je pointer, znak $ se používá jen tehdy, když se má pracovat ne s adresou, ale s celkem nebo písmeny, např. při PRINT, takže v COPY se znak $ u názvu nepoužívá:
10 A="KOKOT":B="HUBERT"
20 COPY B TO A,2

vytvoří řetězec HUKOT.
Samozřejmě délka se nemusí zadávat jako číslo, lze zadat znak, kterým je řetězec ukončen. Jak tisknutelný (“K”) tak netisknutelný (&13).
COPY B TO A,"U"
provede to samé, co předchozí příklad (koncový znak se přenese, po přeneseném písmenu U se přenos zarazí).
Protože řetězce jsou ukončeny ne znakem &0, &10 nebo &13, ale uvozovkou, a musel bych nutit uživatele, aby zadávali ukončovací znak &34, lze použít znak $ jako známku toho, že se pracuje s řetězci:
COPY A TO FREE,$
zkopíruje celý řetězec z adresy A do pole (“volného místa” na adresu vrácenou funkcí FREE), včetně ukončující uvozovky.
FOR A=FREE TO FREE+6:.&PEEK(A),:NE.A
vypíše
KOKOT”
(tečka je PRINT a NE. je zkrácený NEXT.)
Úplně ideálně by ten úvodní příklad, aby se nehrabalo do zdrojáku programu, vypadal takto:
10 A="KOKOT":"B="HUBERT"
20 COPY A TO FREE,$,B TO FREE,2

(vytvoří řetězec HUKOT od adresy FREE)

Podobně je na tom funkce CMP, která vrací, zda dvě oblasti paměti (nebo řetězce) se sobě rovnají nebo nerovnají.
PRINT CMP(A TO B,12)
porovná 12 znaků (bajtů).
Opět je možné zadat ukončení znakem (tisknutelným i netisknutelným) nebo $, pokud jsou to uvozovkou ukončené řetězce.
V případě ukončovacího znaku se porovnání ukončí v okamžiku, kdy se ukončovací znak najde v jednom z řetězců (je jedno, který je kratší).
Takže platí, že při tomto porovnávání PRINT CMP(A TO B,$) platí “KOKO”=”KOKOT”.
Ale při .CMP(A TO B,LEN(B)) to tak není, protože uvozovka není T.

Hezká funkce STR(n,adresa) vrátí adresu entého řetězce od zadané adresy.
Takže mít v paměti od adresy 12345 uloženo KOKOKO”HAHAHA”BLEBLEBLE”CHICHI”
funkce STR(0,12345) vrátí adresu 12345 (počáteční adresu řetězce KOKOKO), STR(1,12345) adresu řetězce HAHAHA, STR(3,12345) adresu řetězce CHICHICHI.”

Nebylo to ovšem bez problémů:
“Bohužel přitom řeším tajemnou chybu, kvůli které nefunguje to, kvůli čemu tahle konstrukce vlastně vznikala:
10 A="KOKOKO"
20 COPY A TO FREE,$
30 LET D=STR(1,FREE)
40 COPY A TO D,$

od adresy FREE vznikne KOKOKO”KOKOKO”
Ale:
10 A="KOKOKO"
20 COPY A TO FREE,$
30 COPY A TO STR(1,FREE),$

nepřenese na řádku 30 nic a na adrese FREE je jen KOKOKO” (přitom STR(1,FREE) při všech pokusech vrací správnou adresu).”

I takové záhadné problémy ale mají celkem přirozená vysvětlení (a sice, že programátor je debil).

Jak osvětluje poněkud delší poznámka:
“Tak tajemná chyba nakonec nebyla tak tajemná.
Vlastně to nebyla chyba, jen programátor byl debil.

Ne že by Z80 měla málo registrů, ale v Basicu je potřeba v nich držet spoustu údajů a v případě potřeby je někam odkládat.
Wang to řeší mohutným využíváním zásobníku, což značně snižuje přehled o tom, co se se zásobníkem zrovna děje. V programu je JP někam, kde následuje POP… co se děje? Občas se POPne něco, co se nepoužije, to se asi jen čistí ze zásobníku něco, co se mohlo použít, ale nepoužilo…
Napopujete si parametry, skočíte na subrutinu, ale ouha, chyba, je potřeba ji ošetřit.. jenže skočit ze subrutiny na zpracování chyby nemůžete, protože ukazatel na zásobníku, který si chybová rutina POPne, je pod těmi parametry, a ty jsou pod návratovou adresou… a zvlášť u ošetření chyb pak nevíte, z jaké hloubky kaskádovitě se volajících subrutin jste vylezli, neustále je potřeba likvidovat bordel ze zásobníku, aby postupně nenarůstal a nepřemazával paměť…

Tam, kde to opravdu bylo potřeba uložit všechny registry, protože se taky mohly všechny změnit (volání služeb pro SAVE a LOAD), jsem uložil prostě registry do paměti.
A pak si je zase vyzvedl.
Na zásobníku jako v pokojíčku, žádný problém s mícháním uložených parametrů s návratovmi adresami, v paměti uložená data se mohou nebo nemusí vyzvednout, nebo i napřeskáčku, naprosto beztrestně.
A když už tam to místo pro uložení HL, BC a DE je, využil jsem ho i v jiných příkazech.
Konec sraní se se zásobníkem!

Jenže ten mrcha Wang věděl, co dělá.
(Narážím na to pořád. Jakmile mu opravím nějakou chybu nebo něco zoptimalizuju, ukáže se, že byla hovadina zkoušet ho poučovat a musím honem znovu obnovit jeho řešení a zapátrat po tom, proč to takhle divně vlastně udělal. Krásný příklad je, že v rutině pro INPUT před voláním vkládání řádku z klávesnice si označí, že provádí INPUT, aby to věděla chybová rutina a při chybě nekončila s hlášením a vypadnutím do Basicu, ale chtěla vstup zopakovat. To označení se provede tím, že se jako číslo právě prováděného řádku uloží adresa začátku INPUT rutiny.
Když jsem dodělával INPUT řetězce, řeší to jiná rutina a potřeboval jsem, aby při chybě neskočil na input čísla, ale zpět na vstup textu.
Začal jsem tedy hledat, jakou adresu mu podstrčit. Ale kterou vzít, když původní předloha skákala hned na samotný začátek celé INPUT rutiny? To znamená analýzu případného promptu, … v mém případě i rozhodování, kterou rutinu pro vstup zvolit, zda text nebo původní Wangovu rutinu pro vstup čísla…
Ne, při chybě se neskáče na adresu uloženou v proměnné pro číslo aktuálně prováděného řádku. Ona ta proměnná totiž neobsahuje číslo řádku, ale adresu, na které je to číslo řádku uloženo – tedy začíná tam řádek, a ten je uvozen dvoubajtovým vyjádřením čísla řádku – je to jediná věc, která v programu není uložena jako prostý text, ale jako binární sranec.
Tedy chybová rutina si přečte to, co je na této adrese, tedy zde na začátku rutiny pro INPUT, a podle toho se rozhoduje, zda je v režimu provádění programu, v přímém příkazovém režimu nebo je aktivní INPUT.
A co si přečte? PUSH DE: CALL… Tedy v H je 201, to je číslo větší než 127, má nastavený 7. bit, celek je tedy větší než 32767, tedy je to záporné číslo…
Kdybych změnil začátek INPUT rutiny a za jednobajtovou instrukcí nenásledoval ten CALL
(nebo jiná instrukce s kódem větším jak 127), tak jsme v prdeli i s ambulancí a návrat z ošetření chyby v INPUT by nefungoval.
To je jen taková odbočka.)

V příkazech je to skoro jedno, jeden příkaz nemůže být (v zásadě) volán z jiného před jeho dokončením, ale je tu jedna věc, a to jsou funkce.
A parametrem funkce může být jiná funkce, nebo i ta samá funkce (není to tak dávno, co jsem vymyslel konstrukci INPUT $FREE+LEN(FREE)+1+LEN(FREE+LEN(FREE)+1)+1+LEN(FREE+LEN(FREE)+1+LEN(FREE+LEN(FREE)+1)+1)+1 ).
Jenže když si v příkazu schovám do své paměťové schovávačky obsah registrů, vím, že s jiným příkazem nebude kolidovat. Než příkaz opustím, už uložená data nepotřebuju a ať si s nimi nový příkaz dělá co chce.

Jenže funkci můžu zavolat opakovaně, a její nové volání by přepsalo data té staré.
A ta stará by přepsala uschovaná data toho příkazu, co ji volal.

Z toho plyne poučení: mají-li být funkce rekurzivní, tento způsob uschovávání registrů nelze použít a je potřeba ucpávat jimi ten zásobník, tak, jak to Wang dělal už od začátku.
Takže Chuck Moore měl s Forthem opět jednou pravdu.

Takže tajemná chyba nedělala nic jiného, než že příkaz COPY, který měl uložené registry v paměti RAM, při zavolání funkce STR měl najednou jen nějaká hausnumera, která si tam předtím uložila funkce STR.
A celek spolu dohromady nefungoval, i když jak příkaz COPY, tak funkce STR samostatně fungovaly bezchybně.”

V současné době zná Piko-Basic 14 operátorů, 5 modifikátorů dat pro tisk, 23 funkcí, 6 příkazů vyhrazených jen pro přímý režim provádění a 32 ostatních příkazů.
To se ale samozřejmě ještě bude měnit.

V poznámkách jsem si krátce rekapituloval, co asi ještě zbývá dodělat:
“LOAD a SAVE proměnných, pole a binárek. No a poslední povel:
COMPILE
Ten si nechávám na konec, až bude všechno hotovo a nějak odladěno. Měl by uložit samostatný spustitelný kód. Aby, když už v tom někdo něco vytvoří, aby to nebylo vázané na Basic (protože nahrávat do paměti nejdřív Basic a pak program v něm napsaný nemá prostě tu eleganci).
Ten ale nemůžu na 100% slíbit, protože s ním bude kupa práce.”

Tenhle příběh ale ještě nemá konec.

Končím článek, ale klidně by mohl ještě jednou pokračovat.
Protože jinak Piko-Basic pro Ondru skončí stejně, jako spousta jiných projektů:
slavnostně oznámený světu a pak nerealizovaný, nedodělaný, nedotažený.

Tomu jsem se chtěl vyhnout a proto jsem o něm ze začátku pomlčel, dokud se ve vývoji nedostanu aspoň k něčemu, co bude možné předvést jako fungující fragment.

Dokumentace k Piko-Basicu je anglicky – průběžně updatuju původní anglickou dokumentaci k Wang Basicu.
Samořejmě, protože Ondra je český počítač, bych chtěl napsat českou dokumentaci ve stylu původních příruček, aby se dala vysázet podobně, jako příručka ke Karlovi.
Kvůli dokumentaci jsem uvažoval i o nějakém crowdfundingu, kdy by přispěvatelé, přispěvavší na provedení tisku, dostali “originálku” kazetu s vytištěnou dokumentací (nejen k Basicu, mohla by se takhle vytisknout celá dokumentace k SSM programům, podobně jako se dělaly repliky příruček od Tesly), vymyslel jsem i “personalizované kopie”, kde by povel OWNER vypsal jméno svého majitele s poděkováním, ale Solaris104 si myslí, že to takhle udělat nepůjde.
Takže těch asi tak 10 lidí, kteří to vůbec někdy do nějakého Ondry nahrají, se bude muset popasovat asi normálně se stahováním někde z webu a s dokumentací v nějakém PDF.

K O N E C
(zatím…)