Basic pro Tesla Ondra SPO-186: (2) Převod na Ondru

Jako základ jsem vzal Wang Basic (Palo Alto Tiny Basic).

Jeho autorem je Li-Chen Wang, který je, mimo jiné, i autorem minimalistického jazyka WFSN pro programování robotů.
V upravené verzi (s želví grafikou místo ovládání robota) se tento jazyk dostal i na osmibitové Atari – pod jménem Extended WSFN.
Jeho 127bajtový program Kaleidoscope pro barevný grafický interface od Cromemco zastavil dopravu na 5. Avenue v New Yorku!

Wang Basic (Palo Alto Tiny Basic) je extrémní kousek z roku 1976.
Originál má 1,7 kB!
Do toho nacpal Wang kompletní interpret Basicu i s editorem.

Vlastně se mi tento Basic nabídl sám, protože implementací Tiny Basicu v mnemonice Z80 moc dostupných není.
Máme sice výpis MikroBasicu ze SAPI-1, ale to je mnemonika 8080 a vzhledem k tomu, že jsem předpokládal přidávání částí určených pro Ondru, chtěl mít možnost používat rozšířených schopností Z80, kterou Ondra má (je škoda, že pro MIKOS na Ondrovi byl k dispozici Tool pracující „jen“ s instrukcemi 8080 v mnemonice Intelu).
Sháněl jsem sice archiv „Dr. Dobb’s journal of Tiny BASIC Calisthenics & Orthodontia„, ale je to bohužel jen automatické OCR, a tak jsou nejen v textu a zdrojácích chyby (např TINT BASIC místo TINY BASIC), ale zdrojáky, které jsou vytištěny „otočené na bok“, nejsou čitelné vůbec. Takže jiný zde uveřejněný Tiny Basic taky nebylo možné zpracovat.

Wang Basic je opravdu extrémní kousek kódu, každá pitomá instrukce, každá neobvyklá konstrukce nebo jen nezvyklý zápis mají význam, je to neskutečně navzájem provázané a promyšlené. Že si připadám až svatokrádežně, že to kurvím.
Obtížněji se do toho dělají zásahy (never touch a running system!), ale je to krása.

Měl v základu to podstatné – integer Basic (-32767..32767), proměnné, pole (je jen jedno, jmenuje se @(), zato zabírá celou volnou paměť), a je tu velmi dobře zpracovaná základní matematika.
Základní příkazy, PRINT, INPUT, vstupy, výstupy, nějaké funkce.

Napsaný byl celý kompletně přímo v assembleru.
V té době bývalo zvykem, aby se opravdu šetřila paměť, vytvořit jakousi virtuální mašinu (jakási proto-Java), z jejíž interpretovaných základních instrukcí (typu „vytiskni čílo“, „porovnej řetězec se seznamem příkazů a při shodě vykonej příkaz, při neshodě skoč na zadanou adresu“) je interpret Basicu vytvořen – na kompletní interpret prý stačí 120 takovýchto instrukcí.
Basic psaný ve virtuálním stroji je pak opravdu krátký, ale penalizací za použií interpretovaných pseudoinstrukcí je pak pomalejší provádění – no jako v Javě.
Wang tedy touto cestou nešel a jeho zdroják je opravdu zdrojákem v assembleru, který je pro assemblerového programátora čitelný a modifikovatelný.

Originální manuál, kde si můžete nastudovat schopnosti původní Wangovy implementace Basicu, najdete ZDE.

Bylo to jako vrátit se do roku 1976, kdy Tiny Basic dával tak velký smysl, že mu byl věnován celý časopis, Apple ][ ještě nebylo ani na rýsovacím prkně a Apple I teprve mělo vzninout jako jedna z mnoha kutilských konstrukcí.
A pro všechny ty desky ve skříních z ořechového dřeva nebo plechu (či jak všelijak byly mikroprocesorové napodobeniny minipočítačů vytvořeny) si každý sám napsal nebo převedl či upravil svůj Basic, vzhledem k velikosti tehdy dostupných ROM a RAM čím méně toho uměl, tím lépe.
Jako třeba Minol, „Tiny Basic podporující řetězce“, který měl osmibitovou aritmetiku, takže 16bitovou adresu bylo třeba zadat jako dvě čísla – LET (24,15)=201 uložilo hodnotu 201 (kód instrukce RET) na adresu 6159. A „řetězce“ byly ještě ubožejší, než se povedlo mně.
Ale ten návrat o roky zpátky zafungoval. Pocítít ten pocit, který měli tehdejší autoři implementací Basicu na tehdy dostupném hardware, a přitom se nemuset omezovat jako oni a využít zkušenosti, které jejich generace nashromáždila, to vše stálo za to.

Wangův Basic netokenizuje.
Nevýhoda – delší zdroják Basicu, pomalejší provádění (plné dekódování slov při běhu).
Výhoda – zdroják je (kromě čísel řádku) čitelný prostý text.
A není potřeba řešit, jak kombinovat tokeny s češtinou (v Basicu EXP řešeno řídícím znakem „následující token není token“ a předznamenáváním každého českého znaku tímto řídícím znakem, který musel samořejmě vkládat chudák uživatel).

V orignále má Basic jen omezený repertoár chybových hlášení:
WHAT? – je v podstatě něco jako SYNTAX ERROR, špatně zapsaný povel.
HOW? je vlastně BAD PARAMETER – interpret neumí něco zpracovat, Spadala sem i přetečení a patřila sem i dělení nulou a chyby při LOAD a SAVE, ale ty jsem oddělil (pokud jste na Ondrovi, který má k dispozici nějakých 52 kB RAM volných, a nahrajete do něj program délky 1,7 kB, opravdu se nemusíte bát jej prodlužovat).
SORRY? – to je v podstatě OUT OF MEMORY.
No a jak jsem se neomezoval, stejně počet chybových hlášení nestoupl na víc jak 7.
A délka kódu Basicu zatím, i se zabudovanou hudební rutinou a Kecalem (pro příkaz SAY) dosahuje celých dlouhých 6 kB!

Basic využíval toho, že je v paměti sám, a obsazoval vstupní body RST svými často používanými subrutinami.
Volání takové rutiny pomocí RST pak zabere jen jeden bajt místo tří (CALL XXYY) a pomáhá to zmenšit velikost výsledného kódu.
To jsem ale na Ondrovi nemohl potřebovat, RST využívá jeho vlastní systém pro své účely a já se do toho nechtěl míchat (zjišťovat, co je obsazeno a co není, implementovat RST pro Basic jen částečně, a podobně).
Z důvodu bezpečnosti a proto, aby se celý Basic umístil do jednoho bloku kódu a netoulal se po kouscích po paměti, přesunul jsem provádění RST rutin normálně dovnitř programu a všechna RST přepsal na CALL.

Ještě ani nebyl upraven pro hardware Ondry (výstup znaku, čtení klávesnice) a už přestal být proveditelný.
Nejětší problém bylo volání RST1.
Podproram nazvaný TSTC (test character) za svým voláním očekává dva bajty. Znak a hodnotu relativního skoku.
RST 1 ; TSTC
DEFB "*" ; is it an asterisk?
DEFB 3 ; if no, jump 3 bytes ahead
JP ASTERISK ; it s asterisk
... ; it is not

No a teď si představte, že přeskakujete nějaký kus kódu (třeba 182 bajtů dopředu – a v Basicu byly snad i ještě delší skoky), a ten kód obsahuje další RST volání, která jsem nahradil CALLem a tím z jednoho bajtu prodloužil na 3… Čiže skok nyní směřuje někam jinam, než původně měl.

Takže bylo potřeba všechny hodnoty relativních skoků (které 8080 normálně neumí) prověřit (zjistit, kam mají správně směřovat, tedy kam skáčou před úpravou RST skoků), kam směřují teď (kolik bajtů bylo přidáno nebo ubráno) a hodnotu přepočítat na nový cíl.
Vzhledem k tomu, že všechny relativní skoky byly zapsány hodnotou (většinou v osmičkové soustavě), a ne návěštími, byl to úkol opravdu jen pro borce.
Ale naštěstí 8080 nepoužívá instrukce s prefixem, takže délka instrukcí se dala odpočítat přímo podle mnemoniky a použitých parametrů, tož jsme se s tím popasoval a jsem borec.

Samozřejmě další úpravy zdrojáku pak cíli skoků opět hýbou, takže se musí znovu přepočítávat, nebo převádět na návěští, která je potřeba nově přidat.
Ale jo, jak to tak po sobě čtu, jsem borec.

Pak už to byla sranda, přizpůsobit hardwarově závislé části.
Výpis znaku a čtení z klávesnice, to se rozumí samo sebou.

Protože mne štval pomalý scroll obrazu, doplnil jsem provádění tisku konce řádku (znak CR, kód 13) o vypínání zobrazování, posun obsahu obrazovky je pak mnohem rychlejší.
Bohužel, funguje to opravdu jen při tisku CR. Pokud se přechází na nový řádek proto, že ten předchozí prostě přetekl, stará se o přechod na další řádek sám systém Ondry a scroll je pak pomalejší, protože je bez probliknutí obrazu, které provedení scrollu urychlí.

Systém zhasínání obrazu je samozřejěm trochu komplexnější, protože bylo potřeba doplnit užitečné povely SLOW a FAST (jako na ZX81), přičemž některé procesy musí probíhat se zapnutým zobrazováním (třeba editace z klávesnice), i když je aktivní režim FAST, jiné zas potřebují zobrazování vypnuté (hudba, mluvení, …) i když je aktivní režim SLOW.
Protože občas je potřeba zvýšit rychlost provádění (tedy vypnout obraz), ale přeci jen je potřeba u toho i něco málo zobrazovat, přidal jsem později povel LINES, který zapne zobrazování jen zadaného počtu mikrořádek (takže je možné zobrazovat jen jeden řádek s nápisem PLEASE WAIT nebo s ukazatelem, že je hotovo 59%), takže je to ještě komplexnější.
Hlavně proto, že LINES 0 není to samé, co FAST (ale to je dáno tím, jak je to implementováno v systému Ondry).

Ondra má vlastní zabudovaný celoobrazovkový editor, takže původní zpracování vkládání řádku a vkládání dat při INPUT jsme vyhodil a nahradil vlastním voláním zabudovaného editoru Ondry.

Divte se nebo ne, ale po těchto úpravách by měl být Basic teoreticky funkční.
No, trochu to fungovalo, nějaké drobné chyby (které jsem do původního bezchybného kódu zavlekl samozřejmě já) jsem postupně odstranil a nakonec byl skoro funkční interpret Basicu.

Cokoli jsem se snažil po Wangovi opravit, dopadlo špatně.
Vezměte si třeba tento kód:
LD HL,ST2+1 ; LITERAL 0 POINTER
LD (CURRNT),HL ; CURRNT->LINE # = 0
ST2: LD HL,0 ; HERE POINTS LITERAL 0: DO NOT CHANGE
LD (LOPVAR),HL
LD (STKGOS),HL

Takže CURRNT (číslo aktuálně prováděného řádku) se nastaví na nulu, pak se uloží nula do dalších (dvojbajtových) proměnných Basicu…
Jenže moje úprava, kde se do (CURRNT) uložila nula, nefungovala. Já si nevšiml, že ST2+1 není v závorkách!
(CURRNT) je jen pointer, který ukazuje na hodnotu, takže v něm nikdy není uloženo číslo řádky, která se provádí, ale adresa začátku řádky v paměti, kde je její číslo uloženo.
Na tuto skutečnost jsme narazil ještě jednou při úpravě povelu INPUT, bude o tom řeč.

Dodělal jsem LOAD a SAVE programu, ale záhada – při návratu po dokončení programu to házelo chybu What?
Což bylo divné, protože LOAD a SAVE můžou házet chybu How?, LOAD může hodit i Sorry? (v tomto okamžiku již přejmenovanou na Out of memory!) a moji novou chybu I/O error !, která zobrazuje číslo chyby, které vrátil systém Ondry při chybě během nahrávání (a o jejichž kódech se dostupná dokumentace moc nezmiňuje, nebo jsem je moc nehledal).
Ale What?

I řekl jsem si, že jsem asi nechal nějaký bordel na zásobníku, že to tak divně skáče, a nechal to na později.
Teprve později jsem zjistil, že to žádný zásobník nebyl (ale, jak se později často opakovalo, byl akorát programátor debil. Takové chybové hlášení ale Basic implementováno nemá).

Bylo načase začít upravovat samotný Basic.

Ten potřeboval jméno. Už to nebyl Wangův Tiny Basic.
Začal jsem ho nafukovat a svou původní velikost 1.7 kB překročil násobně.
Zase to ale nebyl plnotučný Basic, pořád to byl jen ten jednoduchý základ, jen doplněný o grafické a další příkazy.
Zatím mu říkám Piko-Basic.
Nic lepšího mne nenapadlo, a názvy, jako Ondra Basic, nechávám do budoucna někomu, kdo bude chtít implementovat něco opravdovějšího. Třeba převést Basic EXP.
Protože tohle je, přese všechno, jen ukázka toho, že to jde, a že ten malý mikropočítač na svůj Basic teprve čeká.

Původně jsem potenciálně nebezpečné (protože Ondra s okolím prostě komunikuje jinak, než standardně, a instrukci IN jeho systém používá pro nastavování zobrazení a nahrazuje ji čtením memory-mapped a navíc stránkovaných portů) povely … už nevím, jak se jmenovaly, já je hned přejmenoval na INPORT, OUTPORT a WAITPORT … chtěl ponechat, ale nakonec jsem je vyhodil, aby si nepoučený uživatel nerozhodil jejich použitím počítač.

Čekaly mne úpravy matematické části.
Tak, jak to bylo, na tom nešel spustit ani Logoutův retrobenchmark.
Tož jsem doplnil modulo.

Aby se daly doplňovat složitější podmínky, doplnil jsem i operátory AND, OR, XOR, které slouží nejen jako operátory logické, ale zároveň i aritmetické.

A pak už jsem dodal jen nějaké funkce. Jako třeba sinus a cosinus.

Nelíbila se mi funkce SIZE, vracející velikost volného místa.
Tedy ona byla hezká, ale protože volné místo bylo větší, než 32767, zobrazovala jako výsledek nějaký záporný sranec.
Štvalo mne, že musím to záporné číslo pořád nějak přepočítávat.
Přišla tedy možnost výpisu čísla jako normální unsigned integer.

Chtělo by to ještě výpis desetinných čísel, jednak jsou užitečná a druhak už tam stejně mám SIN/COS, tak by se to aspoň pořádně využilo, ale to ještě musím promyslet a je to věc budoucnosti.
Napsal jsem k tomu tuto poznámku:

„Sinus, cosinus a rudimentární podporu desetinné čárky mám vymyšlenou (ale rozsah bude omezený, od -127,996 do 128,996, na goniometrické funkce, číslo pí a možná i Mandelbrotovy fraktály to ale stačí).
Nepůjde porovnávat mezi sebou INT a FLOAT, ale INT mezi sebou a FLOAT mezi sebou ano.
Dělat oddělené proměnné celočíselné a floatové, kdy s každými by se prováděly jiné operace, mi přišlo divné. Jaká proměnná nese hodnotu v INT a která FLOAT si ale bude muset pamatovat programátor sám.

Funkce SIN bude samozřejmě dávat výsledek v desetinné čárce a pro vypsání bude potřeba použít speciální příkaz.
Nutno mu vymyslet název, PRINTFLOAT je moc dlouhé a nedá se dobře zkracovat (PR. by se považovalo spíš za obyčejný PRINT), PFLOAT nevím, jestli je dost intuitivní, FPPRINT je moc divné a FPRINT může znamenat cokoliv, PRTFLOAT a PRTF jsou taky dlouhé nebo divné.

Název FLOAT bych vyhradil pro samostatnou funkci, která by sloužila pro vstup desetinného čísla, respektive by číslo sestavila z části před a za desetinnou čárkou (dvouparametrová funkce). Např. číslo pí LET P=FLOAT (3,14159)
PRINT P ovšem vypíše INTeger 804 (tak je číslo uloženo) a pro vypsání původní formy má sloužit právě nějaký PFLOAT, který vypíše 3,140 nebo 3,141 – podle zaokrouhlení, je to jen jednoduchá úprava celočíselného Basicu s malou přesností, ne plnohodnotný floating point Basic.

Už jsem testoval, že sčítání, odčítání a dělení čísel je v pohodě (opět jen INT mezi sebou a FLOAT mezi sebou, ne navzájem).
Násobení FLOAT*FLOAT je problém, tam by se musela použít upravená funkce násobení.
Což je ale dost častá operace, například 2*pí, musel bych na to udělat buď funkci, což by bylo pro uživatele krkolomné – například známý vzorec o=2*PI*r – D=FPMULT(2,PI):o=FPMULT(D,r).
To je lepší udělat operátor „násobení s desetinnou čárkou“ třeba ^ a zapisovat o=2^PI^r
Pak bude ale potřeba hlídat, abych náhodou nenapsal třeba B=2*SIN(90) a pak se nedivil, že lezou hausnumera, místo B=2^SIN(90).

MODULO u floatu nedává moc smysl, tak bych ani neřešil, co z něj leze za čísla.“

Problém s násobením upřesňuje pozdější poznámka:

„Desetinná čísla. Musím ještě promyslet – nechtělo se mi řešit je jako tříbajtové a nechal jsem jen dvoubajtovou přesnost, je pak ale potřeba dávat pozor, aby uživatel nemíchal INT a FP čísla mezi sebou (2+2 bude fungovat, 2,0+2,0 bude fungovat, ale 2+2,0 fungovat nebude, protože 2,0 není 2).“

K poznámce bych dodal, že žádný PRINTFLOAT nevznikne, používat by se měl normální PRINT, s nastavením formátu výpisu čísla „F“ (stejně, jako se už teď přepíná mezi „S“ signed a „U“ unsigned integer).
No a protože vkládání a výpis desetinných čísel stále zatím nejsou zpracované, je zbytek poznámky zatím opravdu jen poznámka.

Pokračování příště…