100 KiB
A "based" programnyelvről
Ha megcsinálom a "strukturált formában tárolt programszöveges" ötletem, akkor egy saját nyelvet is implementálnák vele, ami "nem memory safe, hanem nehezebbé teszi a memória hibákat" - tehát nem lenne bonyolult borrow checker hozzá.
Megj.: Május elsején kéne az első igazi változatát megcsinálni? Ugyanis a BASIC akkor futtatott először kódot:
https://prog.hu/hirek/6677/basic-60-eves-programozasi-nyelv-szuletesnap-evfordulo-darthmouth-kemeny
Elnevezések
- *.basd
- ./based build
- "Bézd"
- Based Advanced Simple Effective Devlang
Based - szleng: olyan önálló vélemény vagy tett, ami ki mer állni magáért és nem feltétlenül a trendeket követi, és ezt nem fél kinyilatkozni. Azaz van egy alapja/bázisa (base) annak amit vallasz, ettől leszel bázisolt (based). Nem feltétlenül elfogult vélemény, de mivel a tömeggel nem mindig ért egyet, ezért azt sokan vitathatják.
Magyarul: "Megalapozott" vagy esetleg "Alapos" / "Alapozott" - Magyar szlengben (másra) még használt: "Alap" (alap, hogy ismerem!)
Based - Slang: true to one’s self or secure in one’s beliefs regardless of what others think, especially when those beliefs fall outside of the mainstream.
For ex.:
Her presentation was so based—did you see the teacher's expression? Priceless!
Operátor precedencia
https://www.youtube.com/watch?v=fIPO4G42wYE
^^Ez baromi hasznos és szerintem nem csak rekurzív leszállásnál lehet szerintem ez! https://www.youtube.com/watch?v=AikEFhCaZwo
- Zárójel, adattag-hozzáférés, függvényhívás, konstruktor, alaptípus-konverzió, postfix ++ és --, typeof, sizeof
- Pozitív és negatív operátorok, logikai és bináris tagadás, prefix ++ és --, $ a típusra és kifejezésre
- Szorzás, maradékos és maradék nélküli osztás, modulus
- Összeadás, kivonás
- Bit-eltoló operátorok
- Kisebb, nagyobb (vagy egyenlő), array(ptr, n), asif, is (típus equals check generic-re), islike(???)
- Egyenlő és nem egyenlő operátorok
- Logikai/Bináris ÉS
- Logikai/Bináris XOR
- Logikai/Bináris VAGY
- Feltételes ÉS
- Feltételes VAGY
- Feltételes operátor (?:)
- Értékadás
Ez is esetleg király lehet (Shunting Yard Algorithm):
https://www.youtube.com/watch?v=unh6aK8WMwM
Meg ez is (basic block-os módszer):
https://dengking.github.io/compiler-principle/Optimization/Basic-block/
OOP helyett: handler
handler Vector<T> {
...
}
^^Ezek "kezelik" a memóriákat és resource-okat RAII-módon, konstruktorral és destruktorral. Szerintem a struct-hoz és class-hoz képest is jobb szó.
Lesz továbbá tagged union is - lásd lejjebb - és interfészek.
Szerintem itt a 'T' helyett talán érdemes csak egy interfész-nevet megengedni??? Esetleg nem is ilyen <..> stílusban?
Placement new
Igazából szükség van valamire, amivel emplace_back
jellegű dolgokat írhatunk le, tehát a konstruktor kódját egy adott
másik memória címen futtatjuk, ahol elég hely van az adatnak. A placement new C++ alakja nekem nem túl szimpi:
// placement new in buf1-2
int *pInt = new (buf1) int(3);
DataSource *pds = new (buf2) DataSource("localhost:1212");
A C++20-as megoldás valamivel jobb, de az sem teljesen kényelmes szerintem...
https://en.cppreference.com/w/cpp/memory/construct_at
Főleg, hogy a C++ esetén ugye ahhoz, hogy adott memóriacímen lévő dolgot destruáljak, külön kell mókolni kézi destruktor hívással. De annál a const adattagok értékét a fordító optimalizálásból trükkösen értheti, majd persze emiatt ilyen hack-elések jelentek meg, mint az std::launder ami ezt megakadályozza...
Szóval erre szerintem jobb syntax kell - esetleg úgy is dönthetünk, hogy egyáltalán ne legyen ilyen (optimizer megoldja?)
Kérdés: Ha csinálok ilyet, akkor kell-e "placement delete" is?
Rekord VS handler: copy és move konstruktorok / assignment
Mindenképp jobbnak látom a C++ féle move szemantikát a rust-hoz képest: tehát ha én mondom meg mi történjen! Egy érdekes (és megfontolandó) elképzelés viszont az ún. "destruktív move szemantika" ötlete, ugyanis az nem feltétlen optimális, ahogy a C++ például csak destruálható állapotba kell tegye azt, ahonnan move-oltunk. A destruktív move esetén a dolgot amiből move-oltunk, már nem használhatjuk. Lásd pdf itt a könyvtárban erről. Ezt ott kifejtik miként lehetett volna C++ esetén is csinálni - de úgy, hogy továbbra is mi írjuk le mi történik!
Egy érdekes kérdés a programnyelvre nézvest, hogy vajon jobb-e, ha nem generálódik automatikusan le a copy assignment operátor, mert ugye ha pointereket tárolsz, akkor ez az automatikusan generált dolog őőő... Szóval hibás, hisz amikor ownershiped is van akkor elég necces, mert lemásolva akkor osztott owner lett hirtelen...
Viszont sima "hagyományos" adaton ez nem probléma, tehát:
handle Vec2f {
public:
float x;
float y;
};
^^erre kényelmes, hogy generálódik!
Lehetséges variációk - amitől a programnyelv kényelmes és "viszonylag biztonságos" is egyben:
- Csak move generálódik, a default move az swap-olást jelent a másikkal (miint c++ esetén)
- Generálódik copy assign meg konstruktor, de csak akkor, ha nincs pointer adattagod - ez ugye garantálja, hogy nem történnek "meglepetések"
- Lesz struct és lesz "handle" a handle-nek van konstruktor-destruktorja, de a struct-nak nincs. A struct-nak is lehet "függvénye" és persze ott is minden public default-ban. A struct továbbá automatikusan másol. Ha a struct-ban akarok hívni malloc-ot, vagy free-t meg hasonló @resource-os dolgokat, akkor @unsafe-elnem kell - tehát ha pointer van benne, akkor az ownership így érzésem szerint általában nem nálam van egyébként sem és ugye mivel destruktor sincs, ezért a "meglepetés" egyébként sem történhet meg..... esetleg ezt struct helyett "rekord"-nake kéne nevezni, hogy semmiképp se kezdjék megpróbálni az emberek "mindenre is ezt használni" ha C/C++ felől jönnek....
Jelenleg a harmadik variáció tűnik nyerőnek a "rekord" adattal. Fontos! A tagged és egyéb union-ok SEM tudnak konstru-destruktort!
Ezzel ilyet tehát leírhatunk (a rekordnak a public a default, a handle-nak valamely privátabb dolog):
record Vec2f {
float x;
float y;
};
Megj.: Azért nem a "struct" nevet választom erre, hogy ne akarják a C/C++ felől jövők mindenre is ezt használni / erőltetni!
Memória és resource kezelés
A lényeg ez (kb. "kötelező" RAII + extra szabályok dangling ref / pointer ellen):
- referenciákat lehet paraméterként megadni elvárt típusnak ÉS visszatérési értékként is metódusoknál
- De referenciát eltárolni handle mezőben nem lehet - sima változóban félreteheted magadnak a stack-en ha nem akarod mindig kiírni az expression-t.
- Pointerekre meg van egy ownership modell, hogy a handle-khez tartoznak a pointereik. Csak ők írhatják őket (más nem) - típus szerint ők írhatják, tehát más azonos típusú handle-nek is írhatom!
- Ugye pointereket vissza tudnak adni és el tudnak fogadni paraméterként @resource-nak és @release-nek jelölt függvények (mint pl. a malloc meg a free). Ez egyszerűen kell az alap működéshez.
- Az ilyen (így jelölt) függvényeket, csak konstruktorból és destruktorból tudod meghívni!!!
- De persze a konstruktor beállíthat a pointerednek valami értéket - nem kell malloc-ból inicializálja meg! és ugye nem kell free-szerű dolgokat hívnia rá...
- Meg persze a @resource nem kötelező, hogy pointer-t adjon vissza - mert adhat valami handle-t is mondjuk.
- Nyilván ezek mind privátok: de ha valaki referenciát ad paraméternek a saját típusodból egy metódusodban, az abban lévőket eléred mert van rá ugye visibility-d úgy. Meg esetleg ugye csinálhatsz friend jellegű dolgot, vagy "owner" class-t aki eléri a pointereid. Esetleg ha egymásba ágyazott class-okat csinálhatsz, akkor ez default bekerül friend-ként, hogy a class törzsében definiált másik class alapból a kintit tekinti ownernek... Ez az "owner class" jó ötletnek tűnik - el is neveztem protectornak - és így lehetnek protected pointerek - na azokat éri el az a speciális class / handler!
- A pointereknek adhatsz értéket, növelheted, csökkentheted, szorozhatod meg a faszomsetudja még mit akarsz vele. De csak a saját pointereidre van normális esetben láthatóságod... Tehát más pointerei amit kapsz azokat nem tudod változtatni se értékadás jobb oldalában szerepeltetve "lementeni" magadnak.
- Alapból a pointer lehet null - alapból a referencia nem lehet null! Külön kell jelezni, ha lehet null egy visszaadott referencia, vagy paraméterben leírt ref - tehát ez a típus része. Mondjuk egy &ref? jellegű syntax, vagy valami ilyesmi.
^^ez szerintem majdnem mindent kezel, de nyilvánvalóan nem mindent és pont az a célom, hogy a rust-al ellentétben NE akarjak mindent safe-en tartani, csak minél többet...
asszem egyedül ezt nem kezelem így:
T& genyoka() { vector tmp; tmp.pushback(T()); return &tmp[0]; } // obviously dangling reference
^^de mivel a referenciákat kvázi "nem tárolhatod el" ezért az ilyen problémák baromira lokálisak maradnak szerintem - nem szétszóródnak a kódban.
Természetesen ha @unsafe-nek jelölöd a függvényeid, akkor eltekinthetsz ezektől - olyankor mondjuk hozzáférsz mások pointer adattagjaihoz, használhatsz és adhatsz vissza pointer-eket és hasonlók. De ha a hívó maga nem unsafe, akkor nem tudja eltárolni a pointered, mert ugye nem tud rá változót csinálni (ha van valami var/auto jellegű típuskikövetkeztetés, akkor ott le kell állítani, ha nincs akkor eleve nem probléma, mert le se tudja írni az illető). Ugyebár természetesen az ilyen módon jelölt függvények továbbá mindenféle módon jelölt függvényt simán meg tudnak hírni - tehát pl. malloc-ot, vagy egy adatbázis kapcsolat létrehozást / elengedést is.
A @unsafe-t lehet hogy handle pointer blokkjára is engedhetek rátenni, ami azt jelentené, hogy az ő pointereit más is látja, esetleg egész dologra, amelybe lévő dolgok így öröklik lefele?
Mondanom se kell: GC nyilván nincs így!
Ez meg szerintem fasság - nem jó érvek:
https://dept-info.labri.fr/~strandh/Teaching/MTP/Common/Strandh-Tutorial/need-for-garbage-collection.html
Jobb pointerek 1: safety
A pointerek ahogy fenn írtuk, alapvetően csak @resource és @release függvényekben menthetők és használhatók korlátlanul, és persze a handle-k adattagjai is lehetnek pointerek amiket akkor kezelünk, mert saját pointer állhat értékadás bal oldalán!
VISZONT! Kiadhatunk pointereket a user kód / caller felé, de számukra nem "menthetők" le ezek. Tehát alapból egy pointer nem lesz "menthető", de @resource-os és @release-es függvények esetén, @unsafe kódban és a saját ownership alatt valamennyire:
- Menthető saját owningolt pointerbe cím közvetlenül a
int a = 42; ... int *b = &a;
módon (adattag / stack-en ptr) - Menthető bele @resource-ból visszaadott érték, továbbá @resource-nak átadható ** módon (írásra) is.
Kapott pointer-t lokális változóba elmenthetjük, de adattagba, vagy globálisba nem menthető el semmi - csak "saját pointer".
Még egy fontos kivétel talán:
for (char* c = str; *c; ++c) {
...
}
while (char* c = file.nextline()) {
...
}
^^Ennél szerintem engedni kéne, hogy a for/while használhassa talán? De ez egy nagyon jó kérdés, mert speciális eset! Szerintem valahogy ez jó volna, mert praktikus baj nincs vele és C-interop miatt kellhet (főleg az utóbbi pl.)
Megj.: Zsolti hívta fel a figyelmem, hogy ezekkel a szabályokkal a BASED-ben talán többször tudunk jobbérték referenciát auto-konvertálni (például ha érték szerinti amit a return visszaad és az nem static / threadlocal, szerintem mehet már) és ezáltal könnyebben kerülhetek automatikus move-olódás helyzetébe.
Jobb pointerek 2: speed
Valahogy jó lenne elérni, hogy a default a "restrict" kulcsszóhoz legyen közelebb. Egy lehetőség, hogy tömbre (tehát ptr+méret-re)a default a restrict, de pointerre ki kell írni.
- Talán a tömbök külön típussá emelése ebben már eleve segít - igen... legyenek továbbá mérettel is ellátva stb. lásd ott
- Mivel az ownerek kezelnek csak ptr-eket, ők adhatnk ki referenciákat és array-eket, de azok ugye nem alias-olnak be könnyen
- A restrict kulcsszó támogatása a minimum - de talán kéne gondolkozni mit lehetne még kihozni, hogy jobb legyen!
- Ugye mivel van külön "tömb" típusunk, ezért arra is lehetővé kéne tenni a restrict-et pl. paramétereknél, különben bakis lesz.
Egy (talán elvetendő) ötlet: A pointer típusa lehessen csoportosítható:
handle A {
int n;
};
A@aliasinggroup1 variable = { 1 };
A@aligrp2 array[5] = { 1, 2, 3, 4, 5 };
inline int magic(A *ptr1, A *ptr2) {
ptr2->n = 42;
return ptr1->n; // return 1 when inlined
}
magic(&variable, &array[3]); // see that it cannot alias
Esetleg szükséges legyen ilyenkor a használati helyen is kiírni a típust?
inline int magic(A@aliasinggroup1 *ptr1, A@aligrp2 *ptr2);
Az a bajom ezzel... hogy "gyalázatosan ocsmány" sajnos ez mind...
Alternatíva:
- Tömbökről feltételezzük, hogy sosem alias-olnak be (hacsak nem "unrestrict" kulcsszavas).
- Referenciák sem alias-olódnak
Alternatíva (all-in):
- Mindent restrict-nek veszünk by default és az unrestrict-et be kell írni...
- Kicsit unsafe-nek hangzik és sok benne a meglepetés, de a perf jó.
Lásd FORTRAN: https://flang.llvm.org/docs/Aliasing.html
Alternatíva (typedef-szerű):
restrictptr PooledPtr int*;
restrictptr RawPtr int*;
inline int magic(PooledPtr ptr1, RawPtr ptr2) {
*ptr2 = 42;
return *ptr1; // Should optimize as: return 2
}
int i1 = 1;
int i2 = 2;
magic(&i1, &i2); // can optimize
Jelenleg ez az alternatíva tetszik a legjobban + persze a restrict kulcsszó támogatása emellett még pluszban!
+ Talán lehetne restrictptr PooledArr int[]; jellegű típus definíció is (tömbök alias kerülésére)
Szemantika:
- Ha csak különböző ilyeneket látok egy paraméterlistán, akkor restrict-et kódgenerálunk mindre.
- Ha csak különböző ilyeneket ÉS más típusú dolgokat... akkor is...
- Egy ugyan ilyen típusú és egy másik ugyan ilyen típusú restricptr között viszont kell aliasing!
- Egy (fenti példával élve) PooledPtr-es int* és egy sima int* között viszont nem kell!
- Van továbbra is restrict kulcsszó (C-s szemantikával)
Ezekkel a szabályokkal egész jó dolgot csináltunk szerintem, mert ha valaki kiad egy pointert, akkor kiadhat hozzá ilyen plusz infókat, ami jelzi a fordítónak, hogy mik alias-olhatnak és mik nem. Egy jó példa, ha egy játék mondjuk ilyen memory arénákban tárol dolgokat és az "engine" tudhatja, hogy melyik arénából adjuk ki éppen a mutatót. Gyakorlatilag ez majdnem olyan, mintha valamit restrictptr-el deklarálunk, akkor annak az összes előfordulásánál beíródik a generált pointer elé a "restrict" - ez csak annyiban téves, hogy ha több ilyen is van egy függvényben / scope-ban azonos típusnéven, akkor azok egymással nem restrict-elnek! Ez utóbbit első körben implementálhatjuk úgy, hogy ilyenkor lekerül a restrict, de úgy is, hogy kódgenerálással megoldjuk csak a kettő közötti restrict-álást és fenn hagyjuk a kulcsszót. Ez egy kicsit bonyolítja a dolgokat...
Az ilyen pointer cast-olható az alaptípusára - de ezzel elveszítjük a speed boost lehetőségét olyankor! Megj.: Szerintem ezzel generálható C-kód -fno-strict-aliasing mellett is akár, ha típus alapút nem akarok!
Jobb referenciák
- A referenciának lehetne "értéket adni" - nem mint C++ban...
- De ahogy fentebb írtuk másolni csak saját ownolt mezőbe és lokális változóba lehet a stackre
- A referencia "lényegében egy pointer, de nincs rajta aritmetika"... szóval nem tömböt jelképez.
- Kérdőjel hozzáadásával jelölhető, hogy null lehet-e (default: nem).
Jobb tömbök
Néhány dologra van szerintem szükségünk:
- (Pointer + méret / range) jellegű tömb történet, ahol a méret / range lekérhető a típusból ADA-szerűen (.len) és az elemtípus is!
- Range subtype (ada szerűen) NEM kell külön a típusrendszerben, mert hajlok arra, hogy 0..méret lehessen csak (praktikus: dyn)
- Dinamikus esetben, hívhasd az ilyen függvény paramétert sort(array(ptr, 42)) történettel (ahol ptr egy pointer, 42 méret).
A fordító két függvényt is generál ilyen függvényekből. A típusrendszer szerint azt a függvényt hívjuk meg, amelyik gyorsabb! Igazából ha fordítási időben ismert méretű tömbbel hívjuk meg, akkor a "sima" függvény hívódik, kivéve, ha ez valami linkelt lib, mert abban az esetben természetesen a "valódi" függvény fog hívódni - ezt C-re fordításnál még nem látom át hogyan kéne, de majd meg kellene oldani...
A lényeg, hogy a "tömb" típus az pointer + hossz minden esetben, de ahol tudja, a fordító kioptimalizálja a történetet! Ha valami mást akar az ember, például performancia okokból, akkor simán adjon át egy pointert (és mondjuk null termináns).
Ha az "array" második paramétere nem fordítási idejű konstans szám, akkor ott is a dinamikus függvényt hívjuk majd meg.
A range, mint altípus megadás lehetővé tenné, hogy dolgok "osztozzanak" a méreten - pl. ECS egy játékmotorban, stb. De mivel nem tól-ig, vagyis ADA-szerű range-jeink vannak, simán csak méret, ezért ha ilyet akarsz konstans számként tárolod.
Emellett természetesen a standard library-ban kell "vector" típus és az tudjon array-t is adni neked.
Szerintem az így kapott kód "már kellően biztonságos" általában. A vektorra debug build esetén legyen range check kötelezően!
A tömböknél a restrict lehet ESETLEG a default - de ennek fényében kellene unrestrict kulcsszó rájuk!
Lásd még:
https://youtu.be/MUISz2qA640?t=1980
ui.: Ha nem lesz operátor overload, akkor szerintem legyen több dimenziós tömb (nem a tömbök tömbje, hanem: matrix[15, 10, 42] = 5;
restrict-ált tömbök
A tömbökre is kell a pointerekhez hasonló restrict.
Típusos indexek
Egy pointer a modern gépen 64 bit, sokszor tömörebben tudsz tárolni "referencia-szerűséget" ha indexet tárolsz rá!
struct A { uint32_t a, b, c; }; // 3*4=12 bytes with 4 byte alignment
struct B { child *a, *b, *c; }; // 3*8=24 bytes with 8 byte alignment
struct C { A aa; uint32_t x; }; // 4*4=16 bytes with 4 byte alignment
struct D { B bb; uint32_t x; }; // 4*8=32 bytes with 8 byte alignment (4 wasted bytes)
Viszont! A pointerek - mint itt is látszik - típusosak, de az indexek NEM! Viszont miért ne lehetnének?
child@uint32_t index; // Például így
A syntax-on lehetne még variálni...
De az a helyzet, hogy erre már van megoldás jelenleg is (sőt C-ben is, struct-al):
record ChildIndex {
uint32_t index;
};
esetleg még jobb, ha "like-olást" is használunk közben:
record child_index {
like uint32_t index;
};
NonOwningPointer - smartpointer
Ezt szerintem érdemes C++ template-el előre megcsinálni! A lényeg az, hogy kreálni lehet a dolgot - tehát van egy factory-szerű,
class Owner {
private:
std::vector<int> vec = {1, 2, 3};
// Has a vector of pointers to children
// So when this go out of scope we can set child pointers backpointers to nulls (indicating non-living)
OwnPtrFactory<int> fac;
public:
// or access(..) etc.
// NownPtr has a pointer to the factory's own elem and on its move assign / move constructor calls updates facs child vec's
// pointer. Caller can ask the pointer if owner is already deleted or not. Maybe should be threadsafe?
NownPtr<int> getMid() {
return fac.create(&vec[1]);
}
};
Ezt követően a használati ponton olyasmi történik, hogy:
class User {
void f(..) {
...
// Safe használat
NownPtr mid = owner.getMid();
mid.if_exists([](int &i) {
... i is accessible ...
});
// Unsafe használat (esetleg nálam @unsafe és konstruktor/destruktor esetben?)
int j = *mid + 40;
// Unsage-nél is lehessen kérdezni
if(mid) {
int k = *mid;
}
...
}
}
Ez tehát úgy látom C++ nyelven is lehetséges, egyedül mozgatáskor tűnik kicsit is "lassabbnak" meg ugye ez egy "fat" pointer, ami 2x pointernyi méretet használ fel... az egyik maga a mutatott terület címe, a másik a "factory"-ban lévő vektor megfelelő elemére mutat rá - itt vigyázni kell: a vektor resize esetén invalidálódik ez! Kéne valami "stabil-vektor" ami blokkos és a resize-nál NEM invalidálódik, de nem is egyesével, hanem blokkosával láncolt lista!
Ezt esetleg nem is nyelvi elemként tennénk be, hanem library-ként a BASED-be is, az talán jobb?
Custom allokátorok / arénák
A go-hoz újabban ajánlott "arénás" modell-t is lehet hogy valahogy alkalmazhatnánk - tehát hogy azzal oldjuk meg a custom allokátor kérdést... De szerintem egyszerűen úgy kell kialakítani a standard libet / kódokat, hogy template paraméter szerűen átvegye milyen @resource és @release függvényeket használ - tehát nem konkrétan malloc-ot, meg free-t mondjuk hanem csak azoknak megfelelő deklarációs függvényeket... Ezt C-ben is szoktam csinálni (pl. a kismap-ban van ilyen), de ugye ott függvény pointerekkel, amiket vagy eltárolok (de akkor runtime költsége van), vagy ha a perf fontos, akkor pl. a kismap esetén azt szoktam, hogy ezeket nem csak konstruálásnál, hanem minden hívásnál át kell adni ami használja - azért mert látom, hogy a compiler be tudja inline-olni, ha egy static inline force-inline stb. függvényt adok át neki function pointernek... De ez ugye "trükközgetés" feleslegesen és jó lenne, ha nyelvi elemként tudnánk ezt. Esetleg érdemes a hagyományos generik / template témától ezt különszedni? Nem biztos, lehet hogy azzal kezelendő. Cpp esetén template-ekkel szoktam ezt csinálni...
A C++ esetén ugye probléma, hogy van "new", de mi eleve nem használunk ilyen dolgot.... A "new"-t amúgy is át kéne úgy nevezni, hogy "allocate-on-heap-while-being-slow-and-randomly-place-in-memory"... Mert nem kéne, hogy össze legyen kötve az allokálás fogalma az "új dolog létrehozásával" ugyebár... Ez csak az OOP miatti agymenés...
Megj.: A "generational arenas" esetleg legyen támogatott? Ez egy olyan dolog, hogy az elemekhez amit allokálsz, odakerül egy "int generation" mező prefixként. Az indexek ezt követően "fatpointer" szerűek (mondjuk 2x32bit, vagy akár 2x16) és a "generációt" is tárolják "optimistic locking" szerűen. Ha törlésre kerül egy elem, akkor a generációja (-1*) szorozva lesz: ebből látni, hogy nem ugyan az, mint amire még az indexem valid lenne, tehát már dead object. Ellenben ez a memória cella / terület újra kiadható (és nem kell hosszú free-list, mert elég párat tárolni és utána szkennelni (akár külön szálon szkennelni amikor kiadok a listáról - ez a szál nyilván thread pool-os jelleggel már létezik, csak pihen) és ilyenkor a területre beadható a ((-1*gen) + 1) új érték, ami miatt a generáció ugye megint csak nem az lesz, mint amit hivatkoznak! Ez valszeg nem kéne nyelvi elem legyen, de attól függ mennyire arénázunk.
allokálgatás manuálisan
Szerintem talán nem is szükséges ez nyelvi elemként - elég, ha a standard library "jó filozófiával" lesz csinálva (lásd kismap)
Context struct
Lásd:
https://www.youtube.com/watch?v=XoiFOK2m0pc
Temporary storage
Érdekes ez a JonBlow-féle temp storage koncepció magában is - de szerintem az alapeseteink jól kezelik ezt (C++hoz képest is), csak talán tanulni lehetne abból, hogy a Jai erre milyen kényelmi featúrákat ad...
https://jai.community/t/temporary-storage/133
https://www.youtube.com/watch?v=MeF4a75kxk0
Tehát ad vissza string-eket, meg hasonlókat mind temporary storage-en a videójában...
Így lehet referencia / ptr csak minden (érték-szemantika helyett), az owner meg scope / context függő thread_local
lényegében.
Tehát mondjuk egy játékban minden frame-nél hívódik meg a "release-all" függvény, ami a temporary storage-t elengedi.
Az előny is, hogy egyben kerülnek elengedésre, meg azért tegyük hozzá hátrány is, a RAII/defer még mindig jobban tetszik...
Defer
Jó kérdés, hogy legyen-e a RAII-s megoldáson kívül zig-szerű defer is. Szerintem nem kéne, csak összezavarja az embereket. Annyiból lehet érdekes, hogy ugye a RAII-s handle behozza a copy / move szemantikát, amit a defer magában nem hoz még be, de ígyis-úgyis érteni kell mi történik. Inkább a copy/move legyen kicsit logikusabb, mint C++ban szerintem (nem error-prone).
"Stackvector"
Furcsa, hogy senkinek nem jutott eszébe, hogy a stack-en is lehessen vektor-t csinálni. Ezt C++ nyelven meg is lehet csinálni egy kis kézi asm-el, meg hasonlókkal, de elég trükkösnek hangzik (és ott ez error prone, mert nem nyelvi elem). Kérdés kéne-e ilyen?
Párhuzamosság / Multithreading
Az "async"-ot szerintem felejtsük el... Én undorítónak tartom - a corutin támogatás is jobb annál...
Esetleg lehetne egy direkt párhuzamosságra csinált "al-programnyelv" / DSL?
Mint ez:
https://github.com/HigherOrderCO/Bend
-
Egy rust-os példa "könnyen elrontható aszinkronitásra:
(Van egy nem-async függvény, ami hibázhat (errors as values) és result object-et ad, de tudod, hogy nem hibázik... ... ekkor ha async-á írod át mi lesz, ha előtte '_'-nek adtad értékül a result-ját? Mostantól a future-t fogod... ... és a compiler nem szól, hogy elfelejtetted az await-et! Szóval kb. elcsesz neked mindent!!! Ilyesmi is baj... ... nem csak az "elméleti" biztonság fontos egy nyelvben, hanem a pszichológiai is!!!)
Esetleg a go-ból átemelni dolgokat? Meg Java JCIP-ből? - De ez lehet talán library szinten is / makró szinten is...
De ha ezen nem is, akkor is jó lehetne TALÁN, pár experimental dolog:
- Azonos logikai (és fizikai) CPU-ra confine-olt thread-ek, amiknél így nem kell lock-olás (greenthread szerű, esetleg a go arénákhoz hasonlóan scope-olt allokáció hogy hova spawnoljanak).
- A go féle dolgok jók - kérdés, hogy java-szerűen ezek lib elemek legyenek, vagy mint a go-nál inkább nyelvi elemek.
- Lock-free programozáshoz legyen memory modell... Lehetőleg a C/Cpp-hez hasonló??
- Pub-sub primitívnél library szinten lehetne az ötletem amit balásznak meséltem a volatile (igen a C-s volatile!) változóról, azonos fizikai, de külön hyperthread processzoron lévő szálnál gyorsabb kommunikálásra??? Nem hiszem, hogy nyelvi elem kéne legyen, de esetleg standard lib.
De a minimálisabb követelmény / alapabb szett
Szerintem kell nyelvi szintű memory modell, az atomic jellegű dolgok és memory barrier-ek átvétele a C++ból kb. jó lesz és még talán a volatile is maradhat a C-s jelentésében (vagyis nem java-szerűen). Emellett egy sima és egy read-write lock még jó cucc.
Jó kérdés, hogy akarunk-e valami SIMD történetet. Első körben legyen elérhető a C-re fordítás, majd ott bele gányolják ha kell.
Ilyen go-jellegű, gorutinos történet talán jó volna, esetleg pipeline és tisztán funkcionális esetre ilyen automatikus PP.
Low-prio... Őszinte leszek - igazából a memory model csak az "igazán" fontos, a többire lehet hogy elég egy pthread...
Új ötlet
Nyelvi elem a metaprogramhoz hasonló pipeline-os és assoc-os és funkcionális tételekre + Java-féle library + memory modell + gorut
^^Ez szerintem egy jó kombó, igazából sokkal kényelmesebb, mint a rust async mágia, de elég powerful oszt csőváz.
Standard library
- Az "igazán" standard libnek szerintem nem kell "prefix" meg "namespace".
- Legyen string típus
- stl-szerű dolgok, de pl. lehetne numberizer-alapú a sort algók interfésze (Magyarsort, meg kicsit átgondoltabb, stb. stb.)
- Felmerül, hogy akarunk-e Jai-szerű opcionális desktop standard libet: ablakozás, alap grafika, stb.
- Felemrül, hogy akarunk-e az előző ponthoz hasonlóan opcionális web standard libet: pl. rest server, uWebsocket-el, clientside bindingok a dom-on mászkálásra, stb... Talán első körben nem kéne
Memória a standard lib esetén
Legyen a kismaphoz hasonló - tehát (template) paraméterként kapnak a handle-k malloc és free függvényeket? Vagy van erre nyelvi cucc
Filozófia
- Suckless - amennyire csak lehet legyünk minimalisták és egyszerűek!
- Lehetőleg ne millióféleképpen lehessen valamit megoldani...
- Ne akarjunk "tökéletességet", inkább praktikusságot. Pl. A borrow checker talán tökéletesebb, de bonyolultabb érteni.
- Minimális függőségek (pl. lehetőleg ne kelljen LLVM ehhez!)
Összességében "worse-is-better" jellegű filozófia atekintetben, hogy inkább legyen az implementáció egyszerű, mint mindenre kiterjedő - és a konzisztenciához képest is fontosabb ez, mellesleg az implementációs egyszerűség fontosabb az interfész egyszerűségénél is.
Egyéb dolgok
Szintaxis
Jobb szeretném, ha a C-hez közelebb állna a syntax, mint pl. a zig, vagy rust csinálja. Sőt az optimális az lenne, ha bármely C kód fordulna is - amennyiben egy @unsafe-t ráteszünk az adott függvényre. Ez persze nem strict cél, hanem hogy "általában így legyen" mondjuk az már jó lenne. Tehát nem feltétlen lenne jó mindent is implementálni a C-ből, de pár alap dologban legyünk hasonlatosak szerintem.
Bitfield-ek
Lásd C/C++ esetén:
struct s {
unsigned int a : 10;
unsigned long b : 60;
}; // 10 bitet használ az 'a', majd 60 bitet a 'b' - tehát 2*8 = 16 byte-on kell tárolni a struct-ot!
Lásd:
https://en.cppreference.com/w/cpp/language/bit_field
Ez szerintem low-level helyekre jó lehet... Ha jól definiálom mi történik, akkor padding / align helyett (mellett) is talán.
Holy-C féle kiegészítések a switch statement-hez
Erről még nem vagyok meggyőzve, de néha hasznosnak tűnnek:
- begin és end klózok, melyek több switch-et körbefognak és mindegyikük előtt és után végrehajtódnak
- checked switch - ahol sose generálódik "mi történik, ha nincs olyan case" code path / jmp, de compile time check: mindent kezelsz?
Az előbbi egy kis usability feature és ugye a jump táblás megoldáson annyit változtat, hogy inline-ol oda közös részeket, mintha azok egy függvénybe forceinline lennének kitéve: tehát igazából C kódot tudunk generálni, ami ezt tudja.
A második egyébként lehet hogy pont checked switch
kéne legyen. Miért? Hiszen ha compile time ellenőrzés van - hasonlóan a rust
match statement-jéhez erre, hogy ne hagyjál ki valami esetet, akkor az pont a checked nem? Első körben ebből is generálhatunk
C kódot is a fordítónkkal, de akkor nem lesz "gyorsabb" a generált kód - saját fordítóval elvileg lehet gyorsabb is majd, mert
ugyebár így tényleg teljesen jump tábla fog keletkezni.
Több-dimenziós switch:
// checked meand all cases must be handled, unchecked means not all!
unchecked switch(shiftState, button) {
case (true, 'a'):
run_left(); break;
case (true, 'w'):
run_up(); break;
case (true, 's'):
run_down(); break;
case (true, 'd'):
run_right(); break;
case (false, 'a'):
walk_left(); break;
case (false, 'w'):
walk_up(); break;
case (false, 's'):
walk_down(); break;
case (false, 'd'):
walk_right(); break;
}
^^Viszont szerintem ezt talán feláldozhatjuk a "nem kell break-t kiírni, de lehet a case mögött több esetet sorolni".
NEM! Ne áldozzuk fel, mert akár lehet úgy is, hogy a ':' helyett más jelezze az összevont eseteket:
case 'A'>
case 'B':
...
case 'C':
++c;
fallthrough;
case 'D':
...
TL;DR: nem lesz break; statement, de lessz fallthrough; statment! Megj.: Ha nem lesz break; - akkor viszont kell szerintem "nop;" is - tehát kell parancs a semmittevésre, hogy üres case lehessen!
Ciklus break és continue helyett + goto kérdéskör
Legyenek ezek: enditer; és nextiter;
Esetleg: Simán válasszuk azt, hogy csak goto lesz és csőváz? Mindenesetre a GOTO-ból legyen számított goto is (ez gcc / clang extension).
Miért?
- Hát a continue-t sose volt könnnyű érteni (a szó alapján)
- A break;-t nem célszerű használni, mert a switch-ből kikerül és így aki ott "reflexből" beírná, az break-elne a kinti ciklusából!
Visszatérési értékek, több visszatérési érték
Legyen-e ilyen még a sima errors as values mellett? Mindenképp mellett, mert az error kezelés kikényszerítő plusz hatású, de amúgy más tekintetben simán ilyenre fordulhatna a compileren belül, ha ez IS van!
(int, int) swap(int a, int b) {
return b, a;
}
Szerintem hasznos lehet mondjuk állapotgépeknél, a tagged enum-okkal együtt, ahol az állapottól függően a tagged enum a következő állapotot ÉS mondjuk lexer esetén a nemterminális token-t is visszaadhatjuk (ha lett). Ezzel megspórolható egy out param!
Ha lesz korutin, ott a "hagyományos" értelemben kéne a yield;-et használni szerintem.
Pattern matching
Ez jó kérdés, hogy legyen-e. A rust-os match statement-nek azért vannak előnyei. Megfontolandó.
^^Szerintem a "checked switch" jellegű dolgok első körben elegek lesznek? Sőt a checked legyen szerintem a default!
Öröklődés, polimorfizmus, like-olás generic
- Nem lesz altípusos polimorfizmus.
- Lesz generic / template jellegű fordítási idejű polimorfizmus
- Lesz "tagged union" jellegű polimorfizmus (lásd rust enum, vagy pl. std::variant)
- A kompozíció könnyítésére lesznek "protector"-ok, vagyis védnök szerepek a handle-k között a protected mezők elérésére.
- El kéne gondolkozni a kompozíció típusrendszerbeli megjelenítésén: Lehessen-e "int-like" vagy "MyType-like" stb?
Lennének ugye a hagyományos típusok, olyankor ezek "konkrétumot" jelentenek. Tehát egy int az egy integer és nem más, sem pedig nem valami dolog ami az "int-ből öröklődik". De ez igaz a konkrétság a nyelvben ugye így öröklődés hiányában igaz lenne a saját handle típusokra is (a struct-ok okosabb neve itt a handle).
Like-olásos generic polimorfizmus
Ellenben megadhatjuk egy típusnak, hogy egy-egy mezője (annak típusa alapján) "legyen a mi típusunk más típusú 'nézete' ott"!
Például:
handle A {
like int key;
string name;
};
// Megj.: Elfogad A a[42]; jellegű tömböt!
void bubblesort(int-like arr[]) {
int n = arr.len;
int i, j;
bool swapped;
for (i = 0; i < n - 1; i++) {
swapped = false;
for (j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// Swaps full handles (or when simple ints, just ints)
$arr.etyp tmp = $arr[j]; // Can access 'real' type
$arr[j] = $arr[j + 1]; // can operate on 'real' type and this works also for
$arr[j + 1] = tmp; // move, copy, functions (ad-hoc polymorphism), etc.
swapped = true;
}
}
// If no two elements were swapped by inner loop,
// then break
if (swapped == false)
break;
}
}
- Vegyük észre, hogy ez csakis fordítási idejű polimorfizmus ebben a formában! Ki kell tudnom számítani az offsetet!
- Azt is vegyük észre, hogy kell valami "operátor": Ugyanis akarok egy elemet írni - vagy csak a nézetét (default: nézete).
- Továbbá szerintem jó, ha le tudjuk kérdezni a "valódi típust" - mondjuk hogy deklarálhassam lokális tmp-nek a bubble sortban...
- Az eredmény: sokkal tisztább, kisebb algoritmusok "mintha csak számokon dolgoznék és nem adaton".
Fontos: Nincs auto-konverzió! Ha nem azt írom, hogy "int-like" dolgot várok, akkor csak int-et kaphatok! Ez igaz akkor is, ha egy
RovarAdat dolgot várok és egy BogarAdatban lenne egy like RovarAdat r;
akkor RovarAdat paraméterré nem válik a BogarAdat, csak
akkor, ha direkt kiírom, hogy "BogarAdat-like" az a bizonyos paraméter! Továbbá ha nem referencia / nem pointer, akkor másolódik
az egész handler természetesen azon a ponton!
Megj.: Több ilyenje is lehet egy handle-nek! Nyugodtan lehet like string name;
is itt!
Megj.: Szerintem beleférhet még: like readable int get() {..}
és/vagy esetleg `*like int get() {.. return pointer ..} ?
** FONTOS **
Még fontos azt is látni, hogy ilyen "int-like" vagy "MyHandleType-like" típusok csak deklarációkban lehetnek, a sima like kulcsszó pedig csak definícióban / implementációnál! Tehát NEM lehet csinálni int-like arr[]; tömböt - csak mondjuk A arr[]; tömböt, de ez utóbbit viszont át lehet adni paraméterként a sort(..) függvénynek így!
Odin-szerű "using"-ok
Lásd:
https://odin-lang.org/docs/overview/#using-statement-with-structs
Ez szerintem hasonló a like-oláshoz - érdemes elgondolkozni, hogy legyen-e(odin kód alább):
Vector3 :: struct{x, y, z: f32}
Entity :: struct {
position: Vector3,
orientation: quaternion128,
}
foo :: proc(using entity: ^Entity) {
fmt.println(position.x, position.y, position.z)
}
foo :: proc(entity: ^Entity) {
using entity.position
fmt.println(x, y, z)
}
A like-olás másik formájával analóg dolog meg ez a rész(odin kód alább):
Entity :: struct {
using position: Vector3,
orientation: quaternion128,
}
foo :: proc(entity: ^Entity) {
fmt.println(entity.x, entity.y, entity.z)
}
Igazából szerintem lehet mindkettő, nagyon jó poszt-OOP tool mindkettő.
Tagged union
tagged union AB {
int szam:
void triple() { this->szam *= 3; }
int integral() { return this->szam; }
void add(int other) {...}
float lebego:
void triple() { this->lebego *= 3; }
void add(float other) {...}
double legego2:
void triple() { this->lebego2 *= 3; } // kotelezo azonos szignaturaval ismetelni, amire nincs default!
string nevptr:
void triple() { this->nevptr = this->nevptr + this->nevptr + this->nevptr; }
default:
// Statics only work as defaults
static niniple(AB &a) { a.triple(); a.triple(); }
// Can add default implementation switch-cased with either checked or unchecked switch-es
int integral() {
//
unchecked switch(this->tag) {
case lebego:
...
case lebego2:
...
}
}
}
AB ab;
ab.szam = 42;
assert(ab.tag is int);
ab.nevptr = "kula";
assert(ab.tag is string);
Másik lehetőség (esetleg a kettő párhuzamosan is lehet?):
tagged union DynamicNumber {
int i;
float f;
string to_string(string s) {
switch(this->tag) {
case int:
...
case float:
...
}
}
static DynamicNumber from(string s) {
// ...
}
}
Talán jobb volna, ha csak ez a "második" forma létezne? Lásd még tagged enum esete?
Harmadik - legjobb? - lehetőség (ha tagged union-ban vagyunk, nem kell a switch-et kiírni):
tagged union DynamicNumber {
int i;
float f;
string to_string(string s) {
int:
...
float:
...
}
void add(DynamicNumber &other) {
int:
...
float:
...
}
static DynamicNumber from(string s) {
// ...
}
}
Negyedik lehetőség - második + interfészes ötlet, ami talán még jobb:
interface IGameObject { spawn(float x, float y); update(float gametime); }
handle Player implements IGameObject { spawn(float x, float y) { ... } update(float gametime) { ... } } // ... Zombie...Boss...
// The "implements" clause is optional here tagged union GameObject implements IGameObject{ public: // All of them need to implement the interface and calls get delegated to the proper one! Player p; Zombie z; Boss b;
// One can add methods as-is, when implements not added to union, only these count
string to_string(string s) {
switch(this->tag) {
case Player:
return "A jatekos";
default:
return "Egy ellenseg";
}
}
protected: static void testAll() { ... } }
^^Ez a harmadik változat abban is jó, hogy könnyebben játszik össze a public-private kulcsszókkal is! ^^Mellesleg így kikerülhető, hogy default-olhassák (mert itt szerintem nem szabadna hagyni).
Ahogy látható, ez a sima "union" kulcsszó egy "változata", amikor is a méret az a méretek maximuma + tagméret. A tag alapján lehet szépen switch-elni rajta - de a dolog támogat egyfajta "polimorfizmust" is: Lehetnek az adott mezők után így kettőspontos módszerrel (és beljebb identálni illik ilyenkor) az ahhoz köthető függvények - de ez nem kötelező!
Ilyenkor egy "triple()"-t hívni rá, az helyben "legenerálja" a switch-case-t és a megfelelő hívódik meg!
FONTOS: Talán érdemes volna kötelezővé tenni, hogy minden "esetnek" legyen meg minden implementációja, vagy hogy kettőspont nélkül leírt függvény esetén neked kelljen magadnak switch-case-elned a történetet (és az fusson le). Ezzel egy adott függvénynek vagy minden esetre kell legyen definíciója, vagy az únió eseteitől függetlenül álljon ott - de runtime error-t nem szeretnék!
Interfészek
Zsolt vetette fel, hogy neki azért öröm töltené el a szívét, ha lehetne egy "kicsit OO style" azért annyiban, hogy egy tömbben tud keverni különböző típusú dolgokat, amiknek közös az interfésze és kapni ilyeneket függvény paraméterként...
Megoldás
Lásd tagged union-oknál a "negyedik változat". Szerintem ez a legszebb!!! A tagged union tud implementálni interfészt és így sokkal egzaktabban történik ugyanez "háttérben titkos tagged union generálás nélkül" - na így lett letisztult!
- a hívási ponton az egy lightweight generic támogatás / ad-hoc polimorfizmus is! Emiatt elvileg generic se kéne!
Az interfészekre lehet többszörös öröklődés (egymás közt is, meg itt is).
FONTOS: A shared object-ek között NEM ajánlódnak ki ilyen jellegű típusok! Csak rekordok!
Megj.: A második változat (tagged union-os megoldás) kell szerintem + lehessen handle interfészelés! Tehát egy handle is "implementálhat" interfészt és olyankor generikus kód keletkezik majd. Ennél azt is szeretném, ha "tömbre" is működne, tehát IGameObject stuff[] paraméterrel a függvény a hívási pontok alapján többféle módon is generatívan generálódik. Szerintem talán ki kell írni ott, hogy IGameObject-like és interfész "like-olása" mindig ezt jelentené? FONTOS: Adattagnak így nem lehet "interfész" típusa, csak tagged union-os esetben ugyebár...
Láthatóság
TODO: Egyszeűsítendő?
- public
- readable
- resource
- protected (*)
- readable protected (*)
- protected resource (*)
- readable protected resource (*)
- private
Ezekből több blokk is lehet (C++ szerűen) és többször is szerepelhetnek, ezáltal lehetővé tesszük a layout megadást!
A public gondolom elsőre is érthető s a default. A readable az lényegében egy segítő ficsőr, hogy ne legyen annyi "getter": Ugyanis ez azt adja, hogy mások kívülről olvashatják, de én belül még írhatom.
A resource csak a konstruktorban és destruktorban írható. Tehát az "életciklus" alatt readonly ("ezt kezeli a handler").
A protected itt érdekes, mert nincs öröklődés, ellenben a handle-k megadhatják kiket protektálnak (kiknek védnökei):
handle A {
protected:
int a = 10;
};
handle B {
protected:
int b = 42;
};
handle C protects A, B {
static work(A &a, B &b) {
print(a.a + b.b); // possible
}
}
Nem inheritál, csak simán ad láthatóságot - főleg akkor hasznos, ha van B adattagod, vagy egy vektorod rá, vagy dolgozol vele. Ez segít kicsit több kontrollt elérni kompozíciónál. Esetleg ez a típus része kéne legyen?
handle C {
static work(protectee A &a, protectee B &b) {
print(a.a + b.b); // possible
}
}
Így egy "readable protected" kombó is járja akkor már ami kifele olvasható, de csak protectoroknak írható. Hasonlóan jön még a "protected resource" és a "readable protected resource" is - de ezek igazából kombinációk / modifier-ek ilyen módon.
Ha valaki erre azt mondja - ez mekkora hülyeség! A C++ legalább a friend-nél úgy csinálja, hogy a class meg kell mondja a barátait, nem csak úgy "jelentkeznek" a barátai a semmiből..... annak azt mondom: az örökléshez meg nem kell semmi ilyesmi, szóval eddig pontosan ez volt örökléssel, hogy aki csak szeretett volna ilyet, odaírta, hogy örököl tőled és ugye hozzáfért a protectedhez....
Metaprogramozás
- Az implementációs közegből kifolyóan, a compiler scriptelésével - esetleg itt-ott erre valami syntax sugar, hogy ne legyen bonyi.
- A zig comptime-ra érdemes még ránézni és valami hasonlót esetleg csinálni? Azt sokan szeretik és produktívabb, mint a rust makrók.
- Valamilyen szintű generic: lehetőleg nem ducktype-os, hanem a template-ekhez közelebbi módon okosabb, de nem turing teljes
- Igazából a fent leírt "trükközés" az "AKA" kulcsszóval és "*AKA" kulcsszóval!
- FONTOS: Legyen már lehetőség függvényt adni template paramnak (int-et is, meg típust is, de függvényt is please).
- Az if constexpr (...) { .. } az tetszett a modern C++ból!
Érdekes írás:
https://willcrichton.net/notes/the-coming-age-of-the-polyglot-programmer/
Tagged enum / smartenum
Ez hasonlít a taged union-hoz, de a union esetén a típus határozza meg a TAG-et. Ebben az esetben a típus mindig azonos és valamelyik integer alaptípus (pl. int).
Példa:
tagged enum(int) FileError {
NOT_FOUND = 0:
const char *msg() { return "File not found!"; }
PERMISSION_ERROR = 1:
const char *msg() { return "Not suitable permissions!"; }
}
Másik lehetséges alak:
tagged enum(int) FileError {
NOT_FOUND = 0;
PERMISSION_ERROR = 1;
const char *msg() {
switch(this->tag) {
case NOT_FOUND:
return "Not suitable permissions!";
case PERMISSION_ERROR:
return "File not found!";
}
}
}
Lényegében a java smartenum-jához hasonló dolog, vagy ahhoz a smartenumhoz amit én csináltam az impexlib C++ kódbázisban!
Megj.: Itt is talán a második formát kéne csak engdni? Lásd "tagged union"-nál lévő két alak közül a második?
Megj.: Esetleg megfontolni miként segíti ez az állapotgépek használatát / parzer építést? Esetleg a 2D switch-el?
Kivétel és hibakezelés
- Exception-ök (kivételek) nincsenek!
- Go és rust-hoz hasonló hibakezelés az szerintem jó ötlet, de egy-az-egyben egyik se tetszik ebből a kettőből. Jó ha van valami syntax sugar és az is, ha kötelezővé tudod tenni az error-t is visszaadó függvény hívásánál a hibakezelést, vagy tovább propagálást.
- Stack unwinding, meg ilyesmi sincs így: egyszerűen syntax sugar van, hogy fordításkor kikényszerítheted a hiba lekezelését ha valaki hív téged. Az egyetlen kérdés, hogy mi történik a tagged union-os esetben ha polimorfizmus van és más-más esetben lehet, vagy nem lehet hiba... hát a válasz az, hogy a függvény deklarációban benne kell legyen a hiba szóval a hiba maga itt nem polimorf (vagyis csak ugyan ennyire, de nem lehet olyan, hogy valamely tagged union variáns olyan hibát dob és úgy, ahogy az nincs interfészben jelölve).
A legjobb egy Zig-szerű megoldás lenne - jelenleg ezt mondanám a választásomnak:
// TODO: Just example, not real code
tagged enum(int) FileError {
NOT_FOUND = 0:
const char *msg() { return "File not found!"; }
PERMISSION_ERROR = 1:
const char *msg() { return "Not suitable permissions!"; }
}
FileHandle open_file(const char *path) onfail FileError {
...
fail FileError.NOT_FOUND; // error handlers can also fail again: both their own errors or "fail err;" to re-fail.
...
}
void testcode() {
// onfail clause is mandatory if function declaration has an onfail - unlike with exceptions
// Its "errors as values" philosophy and implemented via record returns / double return values.
FileHandle h = open_file("help.txt") onfail(DatabaseError err) {
// Must handle all of the cases (or have a "default"?)
NOT_FOUND:
// Can chain, and have visibility for variables in the main expression (here: h)
h = open_file("secondary_help.txt") onfail(DatabaseConnection err2) {
default:
printf("help.txt not found and secondary_help.txt errs with: %s\n", err2.msg());
}
PERMISSION_ERROR:
puts(err.msg());
}
}
A konstruktor hívási ponton való hibakezelés ugyanígy működhet - ezért is megint csak jó, hogy az onfail egy nyelvi elem, mert normális esetben a konstruktornak nincs visszatérési értéke (logikailag a konstruált dolog az értéke). Ezzel mégis működik.
FONTOS (destruktor error):
Gondoljunk bele, hogy destruktor / release is dobhat hibát (például a resource egy adatbázis kapcsolat és nem lezárható).
Emiatt FONTOS, hogy a destruktor is csinálhasson "onfail"-t és ilyenkor a scope végére is KELL tennünk onfail-t.
Ezt a fordító ellenőrízni tudja! Hiszen látja, hol záródik a scope. Ha move-olunk (nem-destruktívan), akkor ez jól alakul!!!
Kód:
handle DatabaseConnection {
private:
socket s;
public:
DatabaseConnection(string target) { ... }
// Maybe DatabaseError.CannotClose ??? That is we might say a subset?
~DatabaseConnection() onfail DatabaseError {
...
fail DatabaseError.CannotClose;
...
}
}
/**
* Does the stuff...
*
* @param sc A socket connection to send ackknowledgements from the primary database
* @returns true when socket message was sent and db1 got data (db2 might not)
*/
bool dostuff(SocketConnection &sc) {
DatabaseConnection dc;
dc.insert(...);
dc.commit();
sc.send(dc.ack); // (*)
DatabaseConnection dc2;
dc2.insert(...);
dc2.commit();
return dc.ack.ok();
} onfail(DatabaseError err) {
CannotClose:
// The objects are still in scope here
// but their destructors might have
// been already called (or error flags present)
if(dc.failed_flag) {
log(LogLevel.ERROR, "Primary database dostuff error!");
return false;
} else if(dc2.failed_flag) {
log(LogLevel.ERROR, "Secondary database dostuff error!");
return true;
}
// You can have code that is generic here
...
} onfail(SocketError err) {
// You can add multiple clauses and
// handle non-destructor errors too.
// If we are here, code after (*) didn't run!
CannotSend:
log(LogLevel.ERROR, "Could not send ack through network!");
// return value is needed here, because we might get here
// for regular failures - instead of destructor errors too!
return false;
}
Megjegyzés: A scope végére helyezett onfail talán kiválthatja a lokálisat / függvényhívás pontján lévőt, de mindkettő lehet!
Ezért javasolt a scope-osnál szükség lehet - mint az látható - több error kezelőt írni (típusonként egyet).
Megjegyzés: thread_local-ba tehet mondjuk error esetén void* plusz adatot az error kiváltó kód (ha kell). (szabvány v egyedi?)
FONTOS:
A fentieknek működnie kell konstruktor esetén is (destruktornál viszont semmiképp). Gondolj bele: az kell a RAII-hoz!
VISZONT! Talán jó lenne nem RAII-nak nevezni a handle-s dolgokat, mert exception jellegű távolhatás nem lesz!
Lásd még, ahol a Muratori emiatt nem szereti a RAII-t:
https://www.youtube.com/watch?v=OP_w_oEU1Wk
https://www.youtube.com/watch?v=xt1KNDmOYqA
Alternatíva:
- Volt az a gondolatom az error-stream hibakezelésről, ahol error stream-eket lehet definiálni
- Ez kicsit egybemossa / együtt kezeli a logolást és hibakezelést. Konkrétan az error-ok és malfunction-ök ilyen stream-be kerülnek, ahonnal popp-olhatjuk őket - vagy ignorálhatjuk is. Ha hibát ignorálunk, akkor ebből alapesetben leállás lesz, ha malfunction-t, akkor viszont logolódik. De ignorálás helyett tehetünk is valamit - ami lehet az is, hogy kézzel leignoráljuk pl., vagy akár a sorban hagyjuk. Ez egyfajta globális hibakezelés, csak a getlasterror-jellegű dolgok helyett nem felejtődnek el a hibák - hacsak úgy nem konfiguráljuk a dolgokat. Ha "úgy" konfiguráljuk, akkor thread-local szemantikával globálisak csak - tehát nem úgy, mint a C-s esetben a nagyon-nagyon globális hibák.
Ezt elég barokkosan kidolgoztam, de az volt a bajom vele, hogy túl összetett és talán nehezen érthető. Az, hogy a hibák simán értékek, mint go-ban, vagy rust-ban az sokkal egyszerűbb - és ennek a nyelvnek az egyik előnye az egyszerűség lenne ugyebár pont!
Szerintem hanyagoljuk az alternatív irányt. A fenti nagyon szép és egyszerű ahhoz képest, sokkal áttekinthetőbb...
Visszafelé kompatibilitás
Igazából nem cél a visszafelé kompatibilitás. Nem is nagyon lehet megmondani, hogy mi lenne a nyelv "őse" ami felé kompatibilis kéne legyen nyelvi és szintatktikai szemmel nézve...
Ami viszont jó lenne, ha egyből tudnánk fordítani C/C++ kódot a zig-hez hasonlóan. Ez a zig-nek azért brutális előnye! Viszont vannak ezzel kapcsolatban azért komoly nehézségek, ezért lehet hogy sokkal járhatóbb út, ha ezt a lépésenkénti átállást inkább úgy támogatjuk, hogy az egyik referencia implementációja a fordítónak C/C++ kódot fordít! Azért így írom, hogy C "slash" C++, mert szép dolog C-re fordulni (és onnantól minden mikrovezérlőn is elfutni, meg mindenhol, ahol van C), de a C++-ra fordulásnak akkor van előnye, ha C++ interop-ot szeretnénk és például hívni akarunk onnan osztályokhoz tartozó metódusokat, vagy hasonlókat.
Az itt a probléma, hogy ha megengedjük, hogy C++ kódokat hívhassunk (azáltal, hogy C++ra fordulni is tudunk), akkor vajon a nem C++-ra forduló fordító backend ezt hogyan fogja kivitelezni? Az extern "C" jellegű dolognál nincs annyira ez a probléma, de praktikusan ezt megoldani mégis elég hasznosnak tűnik, mert akkor legacy kódot tudnak erre a nyelvre emelni a zig-hez hasonlóan.
Talán a megoldás valami olyasmi, hogy ne teljesen seamless integrációt csináljunk, hanem "csak majdnem".
^^Azért a nyelv saját magával legyen már visszafelé kompatibilis majd... szóval nem akarok python-szerűt csinálni majd...
Típusrendszer
- Alapjában véve erősebben lenne típusos, mint a C/C++. Tehát pl. nem engedünk int-ből double-be csak úgy adatot tenni meg ilyenek, kevesebb lenne az ilyen mágia is...
- Jó kérdés, hogy legyen-e típus-kikövetkeztetés, meg ilyen modern cuccok amit a "nyelvek típusrendszere" kurzuson tanultam még egyetemen és sok modern nyelvben van.
- A referenciánál az, hogy lehet-e null az a típus része!
- Öröklődés alapú polimorfizmus: nincs és nem is akarok!
- Szerintem NEM duck-typing-al kéne generic-et csinálni, vagy template-et...
- runtime type info: szerintem nem kéne ilyen... érdekes, de a JonBlow-nak kell a Jai-ba és Gamedev-re is sokan sírnak érte?
- reflection: fú szerintem nem nagyon kéne - maximum compile time kiszámítható pár dolog...
Típuskonverziók
- Szerintem ne bonyolítsuk és legyen c-style cast-olás, csak sokkal erősebben típusosan és ne legyenek implicit konverziók.
- Mellette legyen egy "asif" kulcsszó, amit reinterpret_cast műveletként használhatunk.
Lásd precedenciánál.
Szerintem ez elég tömör, de mindent leír:
int a = 0x80000000;
float f = a asif float;
int b = int(f);
Azért nem simán "as" a kulcsszó a reinterpretálásra, mert szerintem ez jobban kifejezi, de még mindig rövidebb, mint a "reinterpret", de nem gondol az ember mást róla, mint ami!
Az alaptípusokra pedig az, hogy "konstruktor-szerűen" működnek az szerintem egyébként is konzisztensebb, mintha elé írom C-szerűen zárójelben a cast-olást...
Mivel a nyelvben az (alap)típus mögött nem jön zárójel (konstruktor hívást jelent), ezért ez minden további nélkül megoldható a fordítóban és mentálisan tényleg konzisztensebb...
Annotációk
Ez egy jó kérdés - lehet hogy kéne, lehet hogy nem... A @Resource miatt lehet hogy ez egy jó dolog volna.
Ez is érdekes, hogy ne csak feltétlen string legyen:
https://www.youtube.com/watch?v=QzDVKN2kcek
Itt beszél a név ütközésekről és egyéb hátrányokról és Jai-ban például struct literált akar ott pl. talán...
AOP
- Szerintem pár dolog azért hasznos lenne az aspectj-ből átvéve. Úgy, hogy nem lesz reflection, azért elég sok mindent pótolna ez!
- Igazából az erős metaprogramozás miatt ez szerintem megvalósítható ilyen @Before és @After dolgokkal meg @MyAOP jelölőkkel.
- Megj.: Talán egyszerűen a @-os szavak koncenció szerint dolgozhatnak a mögöttük lévő cuccoson / talán ez nem is külön effort, de úgy érzem valahogy jobb lenne ezt külön nyelvi szintre emelni? Tényleg nem biztos, mert elvileg ez megírható anélkül..
Plugin / modul architektúra
- Lehet hogy jó volna nyelvi szinten rendesen támogatni? Vagy akár csak library szinten? Lásd videót ahol TsodingDaily hot code replace-t csinál C-ben. A rákövetkező videó is jó, ahol X-makrókkal csinálja meg hogy olvashatóbb legyen (amikor fordítunk dolgokat akkor is jó volna ezt is ismerni - addigra valszeg elfelejtem és nézhetem újra).
- Fordítási idejű plugin/modulozás viszont tök hasznos volna! Az talán nyelvi szinten is!
- importálás talán java szerűen, csak annál is egyszerűbben. Majd megnézem a go hogyan csinálja pl. A lényeg, hogy include helyett jó lenne ha a fordító tudná hogy vannak modulok és azok mit exportálnak, onnan mit importálunk. A C/C++ modulokat is meg kéne nézzem hogyan működnek - főleg ha kéne az interop (vagy akár az erre fordító fordító backkend..)
- Fordítási idejű dependency injection is lehetne.
Amit nem kéne: Nem kéne, hogy az egész világ annyira dinamikus és szálkezelős legyen, mint az OSGi-al java-ban. De pl. onnan a product-modul-komponens felosztást jó lenne megcsinálni. A modul-t és namespace-t összekötném (mint ahogy java-ban OSGi modulokkal össze is kötöttem jellemzően?) - esetleg a runtime esetet úgy kezelve, hogy a namespace megjelölhető modulnak, de így ugye lehetnek sub-namespace-jei.
Azt, hogy hol keresse a kódot jó kérdés hogy java-szerűen akarom-e, vagy C/C++ szerűen... tehát hogy a namespace szabályok írják-e le, vagy sem. Mindkettőnek van előnye ugyebár... és hátránya is...
Operator (és egyéb) overloading
- Magam is megosztott vagyok, hogy legyen-e. Valamennyire azért kívánatos pl. egy vektor típusnál hogy simán tömb-szerűen használható.
- Azoknak is igaza van, hogy nehezebb követni mi történik: mekkora a költsége egy műveletnek...
- Igazából a múltkor a "dinamikus programozás és algebrai csigamátrix optimalizálás" videómban nagyon kényelmes volt! De talán külön kéne "rendes" 2D tömb nyelvi (vagy meta?) szinten és csőváz? Vagy esetleg ha már overload-al van csinálva, akkor megoldhatnánk, hogy matrix[x][y] legyen? Egyébként szerintem ez C++ esetén is megoldható... vissza kellet volna adnom egy objektumot az offset értékkel és az eredeti tömb pointerrel - és ezen objektumnak lett volna szintén operátor overload-ja, ami a végső számítást végzi...
Fordítás
Mint említettük korábban a "strukturált formában tárolt programszöveges" ötlettel lenne ehhez referencia implementáció - sőt ha metaprogramozásnak azt tartjuk meg, akkor valószínű, hogy más implementációk is azt kell használják - ez egy érdekes kérdést vet fel, mert lehet hogy gyorsabb kód fordítás lenne elérhető, ha nem azzal metaprogramoznánk, hanem csak arra átírnánk a kódot - tehát ha kikötjük, hogy nem lehet ilyen metaprogramozás (forth-szerű szó definiálás), hanem mi magunk saját metaprogramozást alakítunk ki, amit arra fordítunk az első lépésben le... Ez lehetővé tenné, hogy mások, más módon is implementálhassák a nyelvet - ami vagy jó, vagy nem jó...
Én arra gondoltam, hogy hasznos lenne olyan, ami (emberileg olvasható) C vagy C++ kódra fordít bináris helyett - de emellé egy "rendes" fordítót is érdemes volna írni. Először is, ezzel nagyon jó optimalizációkat kapunk "ingyen", másodszor az interop-nak nagyon segít.
Viszont ugye a saját megoldás is nagyon fontos, de egyben ott a strukturált formás ötlet, ami erre lehetőséget is ad. Milyen target-eket kéne viszont supportálni? Szerintem a legjobb az volna, ha direkt webassembly (wasm) targetünk lenne! Emellé esetleg még egy x86-os target mondjuk linux-ra. Onnan a community kihozhatja a többit ha nagyon kell. A wasm lehetne optimalizálni is képes compiler, ami viszont gyorsan is fordít (jai-szerűen gyorsan?), de ha nagyon-nagyon release-t akarunk, akkor ott lenne a C/C++ kimenet.
A C/C++ kimenettel az a nehéz, hogy egyeztetni kell a szemantikáinkat - de mivel nem valami extra bonyolult programnyelvet csinálunk, ez talán ugye nem is olyan nehéz ;-)
- Természetesen érdemes elgondolkozni, hogy eleve adjunk-e build-system-et, vagy ne. Ez egy érdekes kérdés. Ha nem adunk, akkor a modul lookup stb. is bonyolódhat pl. de könnyebb beilleszteni a BASED programokat meglévő build rendszerbe amikor interop van (pl. makefile) és ugye kevesebb effort is megcsinálni. De inkább arra hajlok, hogy mi magunk, a fordítóval build-eljünk. Ez mondjuk még a "strukturált..." megvalósításától is függ, hogy miként is lenne jobb... Szóval valamennyire nyitott kérdés is...
LSP
- Sajnos egyetértek JonBlow-val és fasságnak tartom... De ha valaki akar, próbáljon írni egyet oké...
Csomagkezelés
Nem igazán támogatom, bár ha véletlen sikeres nyelv volna, nehéz lenne megoldani, hogy mások ne csináljanak hozzá. De a legjobb az volna, ha first-class támogatás lenne a manuális csomag / modul kezelésnek, a sima bemásolgatósnak...
OS support
Linux-first, onnan aki szeretné implementálja át portolással más OS-re szerintem...
A fordítóprogram szerkezete
Linkek:
Infix -> lengyelforma
https://brilliant.org/wiki/shunting-yard-algorithm/
Leírás
Nem a tipikus rekurzív leszállós, vagy LALR jellegű fordítást csináljuk, hanem a korábbi ötletem mentén történne! Ez egy FORTH-szerű nyelv (stack alapú), ami viszont kiegészül hierarchikussággal és zárójelezéssel!
Lényegében itt is "szavak" vannak csupán, melyek whitespace-el elválasztottak, de adott a hierarchia-kezelés, sőt a python-féle őrültek miatt "kiolvasható" a tabuláltság mértéke (tab/space-szám) is, ami a hibajelzést is segíti.
Lényegében így a lexer nagyon-nagyon egyszerű, a parzer pedig már "library" jellegű. Az összes ilyen metaszót konvenció szerint # jellel kezdjük: tehát például #include, vagy #if, esetleg #dup.
- Ha a fordításhoz akarunk új szavakat hozzáadni, azt konvenció szerint *.mag fájlokkal tesszük meg!
- Az ilyen fájlok lényegében DSL-t írnak így le nekünk alapvetően és #include-al hozandók be!
- Alapvetően #passes(...) {...} leíróval írható le, hogy milyen meneteket futtatunk, mely *.mag (.slc?) fájlokkal!
Compiler implementációs tippek-trükkök
Érdekes gondolatok (zig fordító implementáció):
https://www.youtube.com/watch?v=IroPQ150F6c
De szerintem mi a köztes adatot meghagyjuk string formában (ez sajnos valszeg több ramot eszik, mint kellene)
Megj.:
Igazából a bináris formát is támogatjuk, ha az SLC tud primitíveket bináris írás/olvasásra (és kell tudni legalább írásra!)
SLC: Strukturált formában tárolt programszöveg (tm) fordítás
Az SLC jelentése kettős: SuckLessCode és SuckLessCompiler. Egy általános fordító / interpreter / transpiler / metaprogramozás tool. Alapjában véve egy forth-szerű, interpretált általános célú nyelv önmaga is, de speciális nyelvi elemekkel compiler íráshoz!
Az címet nehezebb érteni, ha nem ismerjük a kontextust: Van ez az ötletem, ami forth-szerű, de faszerkezetben strukturált "compiler", ahol a forth szavaknak lehetnek "gyerekei" és ilyen blokkjai. A (...) blokk is csak egy block, mint a [..], vagy akár a {...}. Ugyanúgy forth szavakat definiálunk és meg tudjuk mondani, hogy a compiler az adott szóhoz érve mit csináljon. Tehát konkrétan scriptelni tudjuk a fordítót. Most nem mennék bele ebbe, de pár kiegészítéssel (papíron ez részletesen is megvan) kb. az összes "értelmes" nyelvet lehet így "parzolni". A fordításban csak egymásba pipe-olva állítjuk elő a következő és következő változatokat több pass-ban - és az utolsó pass ugye mondjuk fizikai fájlba ír, vagy interpreter esetén mondjuk végrehajt. Ezzel debuggolni is tök könnyű a "compiler/interpreter"-t vagy nézni hol mit optimalizál.
Viszont ha ezt megcsinálom, akkor ezt az itt leírt nyelvet, ezzel implementálva alapból van metaprogramozásunk - méghozzá mindennél erősebb. De ugye nem feltétlenül kényelmes is... Pl. a jai, vagy a zig comptime lehet hogy sokkal kényelemsebb.
Megj.: A "néhány kiegészítés / részlet" azok olyasmik mint hogy nyilvántartjuk az indentálást (mind a hibajelzések, mind az ilyen python-szerű fos nyelvek miatt), kicsit trükközni kell a <..> jellegű dolgoknál (pl. generics, template), vagy épp hogy a "fordító scriptelése" azt is jelenti, hogy tudunk előre-olvasni vagy akár kézzel karakterenként olvasni és úgy mozogni az "input szöveg nyelén" ahogy Csörnyei mondaná Magyarosan a "handle"-t... Tehát ezzel tényleg nagyon testre szabható mi történik. Alapból stack-ed is van, mint a forth-nak, meg mellé vannak a fordító adatszerkezetei is (bár esetleg azokat csak akkor éred el, ha speciális jellegű szót definiálsz). De a gyakori state-machine használat miatt változói és adattagjai is vannak a szavaknak, továbbá zárójelezésük!
Fordítási időben / interpretálási időben futó szavak és példák
#: int
process_varname
declare_int_var
#
int alma;
Macerásabb:
struct MyAlma {
};
var MyAlma alma; // ??? var ??? több menetessel megoldható, hogy ne kelljen var, mert a típusok
Mivel a szóvégek nem mindenhol "jó" helyen vannak, ezek átírandók:
set x=1+ a(1, b(3, 4));
Ilyenre:
set x = 1 + a(1 b(3 4));
Ha egy programnyelvnek saját "zárójelezése" van, ami esetleg még kontextusfüggő is, akkor ilyesmit lehet:
#: PROCESS_LOOPS
#read
#if(TOP=="L") {
// .. O O P S...
}
#
#: LOOP
WRITE('loops{')
PROCESS_LOOPS // -> {... + count
PROCESS_ENDLOOPS // -> }... + count
#read_until('E');
#
LOOP
...
ENDLOOP
Deklaráció fordítási időben
Ezt a turbobuf-szerű dolgot lehet aztán úgy is használni, ahogy a Seed7 programnyelv deklarál típustól függően például egy min/max függvényt! Erről videót itt lehet látni:
https://youtu.be/9m8gdgbAIrE?t=580
Náluk ez így néz ki:
const proc: DECLARE_MIN_MAX(in type: T) is func
begin
const func T: min (in T: x, in T: y) is return x < y ? x : y;
const func T: max (in T: x, in T: y) is return x >= y ? x : y;
end func;
...
DECLARE_MIN_MAX(integer);
DECLARE_MIN_MAX(MyType);
Tehát náluk a "deklaráció" és definíció ugyanúgy kód, mint a függvény egyéb más törzsei, ám ha top-levelként, azaz fordításkor futtatják ezt a procedúrát (nálunk "szó" lenne), akkor ugye deklarálódnak a dolgok.
No mármost, nálunk ez ehhez hasonlatosan néz ki:
#: DECLARE_MIN_MAX
( #readword ) // runs first
// code that uses compiler primitives to add / build the word
#
Azonban ez a forma nem éppen a legkényelmesebb. Ugyanis alapvetően a #: segítségével definiálunk egy "szót" a forth nyelvhez hasonlatosan, ám nálunk a szavaknak lehet hierarchiája és így amikor a "fordító" meglátja az adott szót, nem csak a definícióját (függvény-szerűen, threaded-code szerűen) végrehajtja, hanem az alatta adott szavak úgy futnak le, hogy lehetnek (...) és [...] és {...} részei is (egészen a #-ig). Az adott szakaszban csak a belső hierarchiát látó kódot írhatjuk, tehát a fenti példában a #readword egy egész szót olvas (mint magunk is), de azt a (...) követő részéből a szónak - ha nincs ilyen, akkor semmi sem történik. A zárójeleken kívüli résznél lévő kód pedig a külső, a szó megjelenésének első betűjén álló olvasó (és író) fejjel fut.
Rendelkezésünkre áll egy(esetleg kettő?) forth-szerű stack, erre a #push stb. szavakkal lehet hivatkoznunk. Itt a push a nyelv struktúrája miatt használható "42" helyett "#push(42)" - ez azért kell, hogy a valódi prognyelv immediate-jei normálisan használhatók maradjanak, ugyanis nem tűnik jónak, ha alapesetben minden szám itt a stack tetejére csak felmenne, ha egy szó helyett egy számot látunk épp...
Egy minimál forth(-szerű) szószett áll rendelkezésre: #swap, #drop, #iadd, #imul, fadd(?), fmul(?), stb. De nem ilyen 32 / 10 szavas forth-ból érdemes kiindulni (bár lehetne), hanem valami praktikusból!
Lásd például innen jó sok dolog:
http://www.murphywong.net/hello/simple.htm
Emellett olyan szavak is rendelkezésre állnak, amelyek a "fordítót scriptelik". Ilyen például a #readword, ami a stackre olvassa a "talált szót" és mozgatja az input nyelét. A stack-en alapvetően uint32 méretű szám lehet! Természetesen rendelkezésre áll még a #readchar, #readline, #readuntil is, továbbá a #write(...) is!
Régi forth-os esetekre lásd hasonlóért:
https://www.forth.com/starting-forth/11-forth-compiler-defining-words/
A #readword használata a következő:
- A szó string belseje olvasható karakterenként: 42 #wordchar, #wordchar[42], 1 #wordchar(41)
- Ehhez természetesen a stack-re kerül a szó TÖVE (zárójelek nélkül) és hossza is (ebben a sorrendben)!
- Szerintem nem árt egy #strcmp('for') jellegű dolog, amivel a word szó része olvasható... 0-1-et tesz a stackre!
- Jó lehet még: #strprefix('starts-with-text'), esetleg #strsuffix(...) is ami 0 vagy N ad (N db megfelelő char!)
- Ez a kialakítás pazarlónak tűnhet a 32 bites stack miatt, de így nem lesz ram szivárgás a rendszerben!
- Egyébként a reprezentációt nem kötöttem ki ezzel - szóval lehet 4 karaktert tárolni egy szóban itt ám!
- Ennek megfelelően kell #popword utasítás, ami leszedi az egész szót a stack-ről ha nem érdekel már!
- Szükség lehet például még #dupword utasításra is - ezek végül is "string kezelő" rutinok...
- Az olvasó fej a szó (nem-zárójeles része) mögé mozog.
A #readword(raw) a zárójelezéssel együtt az egészet olvassa be, míg #readword(full) ugyanez, de wspace*->space! Szükséges még a #skipword és #skipword(full) szavak, melyek spórolósabbak, mint olvasni és drop-olni...
Vegyük észre, hogy a zárójeleket ezzel alapesetben nem kezeltük:
- #has(), #has[], #has{} - ezek kiadják, a szót követőe nyélen állva, ahhoz tartozik-e adott szekció (0 vagy 1)
- Nem tartozhat több (..) egy adott szóhoz (se a másik eseteknél)! Tehát mindegyik blokkféleségből max egy van!
- De a fordító scriptelős feldolgozásnál ott viszont állhat többször és a sorrendiséget fejezi ki csupán!
- #enter(), #enter[], #enter{} - ezekkel bemegyünk olyan módba, hol az adott blokk vége eof-ként értődik
- #exit - ez lép eggyel vissza - de az adott enterrel megkezdett dolog VÉGE mögé állunk (feldolgoztuk)
- FONTOS: Az enter-exit lényegében automatikus, amikor a definícióban a megfelelő helyre írjuk a kódunkat, tehát a zárójelek belsejébe írtuk azt a részt, amikor a zárójelen belüli dolgokat dolgozzuk fel...
Viszont azt is vegyük észre, hogy a szó tartalmazhat whitespace-t, ha teljességében olvassuk be
Insert-álás (az input stream-en)
Factor programnyelven láttam olyat, hogy a bejövő szó-folyam aktuális pontjánra "írhatok" is akár... Ezt valahogy talán jó lenne támogatni, de lehet hogy felesleges. Ezzel nem csak a #write outputra írása lenne, hanem jelenleg ahol tart a végrehajtás, azt a programszöveget tudnánk módosítani. A fordításhoz szerintem ez nem kifejezetten ad plusz funkcionalitást, de az interpreterként működéshez viszont igen...
FONTOS: Itt (state-machine) a body-ban nem lehet #insert-esen deklarált szó, mert
Ennek megfelelően valami hasonló lehetne:
#inserts: DECLARE_MIN_MAX_INT
#: MIN_INT // HIBÁS: Ezt nem lehet, insertálni deklarációt nem fog menni...
#IF(#LT) {
#DROP
} [
#SWAP
#DROP
]
#
#: MAX_INT // HIBÁS: Ezt nem lehet, insertálni deklarációt nem fog menni...
#IF(#GT) {
#DROP
} [
#SWAP
#DROP
]
#
#
Megj.:
Ahogy fentebb írom is, ez a példa már NEM működik, mert az insert-stack temporális jellege miatt deklarációra
való mutatókat nem tudunk ezen a ponton kezelni! Tehát az insert-bufferbe ilyet nem írhatunk (error).
Viszont! Fontos lenne tudni írni a szótárba valami módon tehát ezt kihelyettesíthetővé tenni szerintem...
Vegyük észre, hogy itt a #insert mögötti blokkban szereplő dolgok mind az olvasó fej jelen pozíciója MÖGÉ kerülő
nyers programszöveg - itt az olvasó fej már ugyebár beolvasta a DECLARE_MIN_MAX_INT
szót, tehát az mögött állunk
és olyan eredményt kapunk, mintha az adott dolgokat titkon oda gépelte volna valaki, szóval amint kész ezen szó
futtatása, a rendszer a #: deklarációs szót fogja az inputján látni. Ezzel az input kiegészítésre került!
A #inserts-hez jó lenne, ha tartozhatnának paraméterek:
42 21 #inserts (N K) { #push($N) #writechar #push($K) #push(2) #imul #writechar }
Természetesen ez sokkal érdekesebb, ha nem így magában áll, hanem szó deklarációban, de a lényeg,
hogy a zárójelek között felsorolhatok dolgokat, ami a stack-ről értéket kap. A $a
escape-elése csupán...
Talán itt kellhet típust adni:
'some_text_word' 21 #inserts (word(W) float(F)) { .... }
Az insert-hez szükséges:
- Egy plusz verem / adatszerkezet, ahova az kerül amivel kiegészítettem a jelen pozíciót, ha beljebb vagyok, nyélmozog+másol!
- A többit igazából meg kell tudjuk írni a nyelv meglévő elemeivel, de perf okból valszeg jó ha beépítetten jön?
Szimbólumtábla
Rendelkezésre áll továbbá a "szimbólumtábla" is, ami egy map adatszerkezet és szónévből egy definícióra mutat illetve annak tulajdonságainak elérésére, illetve új bejegyzések beírására hasznos szavaink is lesznek...
Szükség lehet ezen a ponton fullword(W) jellegű paraméterre is, ami a zárójelekkel együtt értett teljes szót jelképezi, de szerintem alapjában a word, mint típus, lényegében "string" típus ugye a hossz + tárolás mód..
A szimbólumtábla egy szimbóluma megnyitható "fájlszerűen", ezért a definícióját write paranccsal megírhatjuk!
Megj.: Talán ezzel kiváltható a #inserts kulcsszó használata! De egy plusz stack-el is...
A #write
működése?
Ezzel a szimbólumtáblával manuálisan már sok mindent megoldhatunk és a #insert-el együtt ez powerful már, de a fordítóprogram-jellegű használathoz jó, ha szerepel itt "#write0 {...}" szó is. Ez az inserts-hez hasonló módon, de külső fájlba (vagy standard kimenetre, error-ra) tud írkálni.
Természetesen ennek megfelelően kell még #fopen0 jellegű dolog is, ami a nullás fájlt nyitja (mondjuk 4-8 ilyen lehessen) és #fclose[0] szó is. Kell még stdout[0] és stderr[1] jellegű dolog is...
Igazából a "fordító" úgy fordít, hogy sok menetben, folyamatosan át-transzformálja a programszöveget egy másik programszöveggé, melynek elején van egy #include, ami behozza az adott új "makrókat" és azokkal az új menet mit fog pontosan is csinálni már... Ennek megfelelően például az ifeket hamar ugrásokra tudjuk fordítani stb.
A nyelvnek a köztes reprezentációja lényegében szintén emberileg olvasható szöveges kód és meg is állítható egy adott menet közepén a fordítás. Természetesen ez akkor van, ha az alaprendszert fordítónak használjuk és nem egy interpreternek éppen.
Az #include
működése
#include(wordset.tsc)
Fontos, hogy az include-oláskor C-szerűen mennek a dolgok. Valószínűleg kell majd #ifdef-es guard, meg #define is. Az include-oláshoz az úgynevezett session-storage-t használjuk és amikor ide jutunk, akkor a parzer / engine szépen beolvassa a fájlt (hibajelzés, hogy ha nincs olyan) és ezt a kis szöveget kihelyettesíti úgy, hogy a zárójel között vagy a pointer van - vagy arra szükség sincs és csak kerül be a session-be a cucc egyszerűen... plusz feldolgozzuk...
Kommentek
A kommentek kezelését az engine végzi. Ha valaki programnyelvet akar vele definiálni, akkor meg kell adni a listát a sorvégi komment karaktersorozatról, meg a többsoros kommentekéről (parancssori paraméter). Default-ként a C-s dolgok...
Fontos: ezeket a parzoláskor egyszerűen skippeljük!
Template makró és egyéb példák
Fordítandó (pl. JASS transpilerhez):
if a > 1 then
set b = 2
elseif a < -1 then
set b = 3
else
set b = 0
endif
Jó lenne (bnf-szerű):
$if = if $expr then $code (elseif $expr then $code)* (else $code)? endif
De imperatívan így írható ez le (expression gyárilag jöhet(?) és stack segítségével lengyelformára hozás, shunting yard algó):
#: expr
#expression(+ - / *) [0 1 2 3]
#writeexpr
#
Azért code reuse lehetséges:
#: if
#progress_until("then") {
#expr
}
#
Az első menet után "standardabb" formába írva vannak a dolgok és deklaratív leírás adható már:
// PASS #2
if (expr(3)[a 1 >])
{
set b = 2
}
[
if (expr(3)[3 a -1 <])
{
set b = 3
}
[
set b = 0
]
]
// Alapvetően #: esetén a () utáni részbe írt az imperatívan fut le
// Itt viszont paraméter a template-ezéshez - mindegy a sorrend?
// Ez már nem kell C-s szó legyen, csak egy #insert-re forduljon!
##: if($con){$then}[$else]
condition($con) // ebx és eax között hasonlításra felépít - stack-re vissza adja milyen jump
asm(cmp eax,ebx)
jumptype(else) // jnz / jg / jl stack alapján....
loclabel(then:)
$then
loclabel(else:)
$else
#
Bonyolultabb példa (bogus példa - "bement-e az if ágba az alapján az if true-false ad vissza mint expression pl") de valamiért külön match-elni akarom
##: if(if($con) {$value1} [$value2]) {$then} [$else]
^^lehet hogy ezt azért nem kéne, mert fadiffelést okoz...
másik példa (Jass nyelvről újra):
set a = 1
LOOP
set a = (a + 1)
EXITWHEN a == 42
ENDLOOP
Ebből legyen - útána meg már elvileg nem olyan nehéz:
set a = 1
LOOP {
set(a) {(a + 1)}
EXITWHEN(a == 42)
}
VÁLTOZÓK: A makró nyelven a FORTH-szerű szavaknak változói / adattagjai is lehetnek (nem csak kódja)!
- a szóhoz tartozó olvasása a stack-re tehát csak a neve, illetve a kukacos neve
- írása meg a zárójelbe tett dolgot írja bele - kivéve hogy ha ott "pont" áll akkor a stack tetejét írja bele és csőváz
- csak 4 byte-os int van és csőváz... Ha valaki mást akar megoldja "handle"-kkel majd meg float-ra lesz gyakorlatilag reinterpret cast-olgatás vagyis a művelet határozza meg, hogy float-ként dolgozol-e a négy byte-al, de ez talán nem baj, ha extension, mert a fordítóba nem nagyon kellenek float-ok szerintem.....
Egy adattagokat(változókat) is tartalmazó szó és példa a "lokális" változó-elérésre...
#: MY_WORD @a; @b; @c;
@a
#inc
@a(.)
#
Máshonnan elérni így lehet, namespace-elés szerűen:
MYWORD@a(41)
MYWORD
MYWORD@a
#intprint
Ha nem szerepel több szó a definíciós első sorban az enter előtt / zárójelek előtt, akkor csak kódot tartalmazó "funkcionális" szó:
#: just_code
#dup
#inc
#swap
#
#push(12)
just_code
#intprint // 12
#intprint // 13
Vagy(zárójelen belüli futásra):
#: just_code (
#dup
#inc
#swap
)
#
Globális változót ezt követően úgy definiálhatunk, hogy namespace-eléssel valami értelmes "namespace"-be tesszük egy szó alá, tehát alapból van egy rendszerezettség (például egy adott state-machine globális változói stb.)
Ha nagyon ki akarjuk hagyni a namespace-elést, akkor a változó név helyett írjunk "típust" (ez csak név!) és üres szót neki:
#: MY_VAR @int;
#
Esetleg:
#: MY_VAR @int; #
Használat:
#push(41)
MYVAR@int(.)
MYVAR@int
#inc
#print // 42
Ja nem mondtam, de az érték megadása a zárójelek közt működik, a kiolvasás meg anélkül. Ha pont szerepel, a stackről kap értéket!
Megj.:
A zárójelek közti rész NEM expression... csak simán egy szám (esetleg ugye 'a' 'abcd' módon 0..4 karakter!)
Megj.:
Igazából kicsit fura, hogy a forth-ban nem találták ezt ki a változókra... jó ötletnek néz ki interpretálásnál...
Megj.:
Az adattagok arra is "jók", hogy a menetek egymásnak kommunikáljanak vele és így kvázi "blokkokra ható" dolgokat írhatunk.
Ennek akkor van jeletősége, ha a "blokkot" előtte lévő, korábban azt megelőző dolgok "változtatják" jelentésében, mert akkor
létrehozhatunk egy számozott blokk_42 szót a kimeneten (definícióval), míg a blokk helyén "hívhatjuk" és ott elérhető a
korábban beállított változó anélkül, hogy nagy káosz keletkezne (de ez csak egy példa, nem kell így csinálni!)
Típusok a meta-nyelven
Alapból a változók és a stack 32bites (előjeles) int típusú - ez kb. mindent jól leír és a legtöbb architektúrán gyors is.
Nem igazán vannak, de a forth-szavakba elkódolható a típus (assembly-hez hasonlóan).
Például string-kezelő dolgok kellenek inputon! Az input nyelét olvasva pl. karakter kerülhet a stack-re (int ascii), de hasonlóan karakterláncot is "építhetünk" (vagy olvashatunk) a stack-en (-re), olyankor minden elem max 4 karakter és van a stack tetején egy hossz jelző! Ahhoz, hogy könnyen építhessük a stack mélyebb része a szó eleje és a tetejéhez közel a vége! Ez fontos, mert így minimális stack műveletet eredményez ha "stringbuilderezgetünk"! Ami egy transpiler tech-nek elég fontos...
Szerintem kellenek ugyan ilyen szavak arra is, hogy bináris adat stream-et olvassunk és írjunk. Ugyanez oldja meg ezt is!
Naming convention ('#'-jelek itt mindenfele...)
Ha az slc-t csak magában használjuk (interpreterként - default működés):
Akkor nincsenek a gyári szavaknak prefixei, ellenben meg kell adni a "záró" szót (ami pontosvessző defaultként).
Ha programnyelvet csinálunk:
Alapből minden, nem a célnyelvhez tartozó szó '#'-al kezdődjön. A "gyári" forth szavak így jönnek tehát, de amiket csinálsz is
jó ha ilyenek - kivéve amikor a nyelvet, amit transpile-olsz szeretnéd már definiálni, vagy ha eleve nem nyelvet csinálsz
hanem a meta-nyelven magán "nyersen" programozol... tehát ha interpretált programnyelvnek használod a meta-nyelvet magát...
Lehetővé tesszük, hogy a meta-nyelv indításakor (pl. parancssori paraméterrel) a gyári szavak prefixje megváltoztatható legyen... Tehát ne #dup hanem mondjuk @dup legyen és a tieiddel ezt lekövetheted...
slc -prefix='#' my.slc
Ha nem akarunk prefix-et, akkor meg kellhet mondani, hogy mi a "záró jelző" karakter
-
ez alapból a prefix maga, de csak ahogy önmagában áll, tehát a fenti példában '#' (lásd a példáinkat)
-
üres prefix esetén alapból a pontosvessző karakter (forth-os szokás)
slc -ender='%' hello.lang
Szükséges lehet még a 'kukacolás' átírása is valami másra, például így:
slc -at='~~' hello.lang2
Default: Üres prefix és pontosvessző ender, kukac -at mellett. Ezzel forth-szerű interpretert kapunk!
Az slc-nek továbbá szükséges lehet egy "prefix kód". Ez gyakorlatilag "auto-include"-olás:
slc -prefixcode='based.slc' hello.basd
Megj.:
Trükkösködést jelenthet, ha adott fordító implementációban a "gyári" szavak egy részét SLC kódként implementáljuk,
például helytakarékossági okokból egy embedded környezetben. Ilyenkor ugyanis az így implementált "gyári" szavak
prefixje is megfelelően kell változzon. Ennek az egyik egyszerű módja, ha "gyári szó deklaráláshoz" külön szó van,
de igazából ezt a "külön szót" valószínűleg mi magunk is megint csak implementálhatjuk valamilyen insertálással!
Megoldás erre:
#builtin: prefixelendo
#swap
#dup
Ezt követően hívható a szó prefixelt változata
#push(5)
#push(4)
#prefixelendo
#intprint // 5
#intprint // 5
#intprint // 4
Megj.: A #builtin esetben mindig van elég byte hely majd a parzernek helyben átírni a szót a megfelelőre (whitespace-es törléssel) Megj.: Erre nem kell külön runtime type - ezt lehet simán a deklarációkor helyben átfordítani!
Névtelen szó - mégsem lesz!!!
Hasznos leíírni, hogy szó nélkül, csak zárójel esetén mi történjen...
Ez lehet hogy baromira elbonyolít mindent mondjuk... Talán inkább csak API-t kéne adni a zárojeles parzolás segítésére...
Hibakezelés: MISSING szó
#MISSING:(...)[...]{...}...#
Ha a runtime olyan szót talál, amit elvileg FUTÁSKOR odaérve nem definiáltak le, akkor ez a missing meghívódik.
Igazából ezen a ponton hibakezelést írhatunk le!
Hibakezelés: Zárójelezés
Az engine amikor interpretál, akkor megkapja az "indentáltság mértéke" karbantartott értékét! Ez nagyon fontos, mert ezzel értelmes humán hibaüzeneteket tudunk kapni a zárójelezés elmaradásakor!
Tehát alapvetően az engine maga ellenőríz zárójelezést, nem kell a programnyelv tervezőjének ezt magát megcsinálni!
Pipeline és párhuzamosság
Megj.: Bár van fájl kezelés, de szerintem alapvetően inkább a pass leíró kezelje, hogy mikből mely inputok jönnek létre! Azért is, mert ott akkor össze lehetne huzalozni - erre kell egy módszer valahogy! (imp-exp)
Talán valami ilyesmi:
#passes(pass1 #both(#both(pass2a pass2b) pass3) pass4) {
// Default legyen stdout->stdin pipe? Az egymás utáni pass-ok közt szerintem - amúgy manuálisan írható így le:
#pipe(pass1[stdout] pass2a[stdin])
#pipe(pass1[stderr] pass2b[stdin])
#pipe(pass2a[stdout] pass3[stdin])
#pipe(pass3[stdout] pass4[stdin])
#pipe(pass2b[stdout] pass4[stdin])
} // stdin, stdout és stderr mellett ott szerepelhet "név" is! Ebben lehet "kiterjesztés" is, tehát fájlra / pipe-ra fordul!
// A pipe sorrend majdnem mindegy, de "törekedjünk" a "jó" sorrendre, mert nem hash, hanem tömb keresős ábrázolás lesz?
Ezzel a megadott lépéseket futtatjuk az input fájlon úgy, hogy az adott standard output/error (és egyéb max 8) kimeneteiket a pipe-ok segítségével gráfosan kötjük össze. Természetesen csak DAG lehet az a gráf, különben sose fog lefutni a történet majd - erre gondolom nem árt majd valami check is.
Alternatíva(jobban tetszik):
#passes(
lexer
stop_if_error(stdout){}
parse_blocks{imports.tmp types.tmp vars.tmp funcs.tmp components.tmp}
handle_imports(imports.tmp){names.tmp}
handle_types(types.tmp)
handle_vars(vars.tmp){names.tmp}
#parallel(
handle_names(names.tmp){names_bytecodes.tmp}
handle_funcs(funcs.tmp){funcs_bytecodes.tmp}
handle_components(components.tmp){component_bytecodes.tmp}
)
write_asm(names_bytecodes.tmp funcs_bytecodes.tmp component_bytecodes.tmp){out.asm}
assemble()
)
Mit jelent ez?
- A pass inputja (..) közötti, az outputja {..}. A neve egy "szó" (ami már "fentebb" definiált kell legyen, vagy include-olt)
- Ez a szó független stack-ekkel fut: Vagy van nekik sajátjuk (valódi párhuzamosság), vagy szekvenciális és törlődik a stack...
Ez igazából "meta-nyelvi elem" is lett így, hiszen a pass-ok és a both esetén is a paraméterek másolatokat fognak csak kapni a stack-jeikből és azok "párhuzamosan" futnak, amennyiben ez lehetséges. Ha nem lehet, akkor természetesen sorban futnak le, balról-jobbra kiértékeléssel. Mindazt amit művelnek a kimeneteiken, egymásba kötve a pipe kötéssel tudják kommunikálni - ezzel low powered device-on egyszerűen továbbra is csak egy stack kell, de szükség van "temporális fájlokra" amikbe az implementáció gyűjti az eredményeket a későbbi menetnek, de párhuzamosítható lépések írhatók le fordítóprogramhoz, illetve magát az alap "forth-szerű" nyelvet is így kiegészítettünk potencionálisan egy jó kis aszinkronitással.
Ennek a megfelelője talán meglehetne a BASED programnyelven is egyébként! Esetleg ott nem stdout/err meg ilyen szöveges interfésszel, hanem simán csatornákkal és egy DAG-ot leírva, azért sok minden kifejezhető.
Reprezentáció
struct word {
char colon; uint8_t flags; // ':' + whitespace, but the whitespace becomes flags!
char name[]; // inline! Tehát nem egy pointer! Zero terminált a whitespace / zárójel felülírásával.
char vars[]; // inline! Maga a tárhely is itt lesz! Elfér 4 byte ' @i,' vagy ' @j<EOL>' miatt!
char data[]; // inline! Előbb a változók, aztán a kód - így írható primitív forth-szavakkal is kódszó (pl. outputra).
char ender[]; // inline!
};
A szavakat az "ender" string terminálja, a hosszt itt alapból nem tároljuk. Amikor a parzer látja a szó defet, akkor a változókat (pl. C stack-en) megjegyzi és a szimbólumtáblába ezt a címet írja! A változó helyén a programszövegben pont elfér 4 byte, tehát magában ott lesz! A szimbólumtábla rámutat, a forth szó belében pedig ha látjuk a @-os kezelését, akkor szimbólumtábla nélküli írásra fordítható át, tehát a lokális használat sokkal hatékonyabb tud lenni, mert közvetlen memória címzés történik!
Ehhez arra is szükség van, hogy egy szónak maximum 0..255 db változója lehessen - ugyanis legalább egy karakter a név és a @-al kezdődő szó, vagyis a -at='^^' esetén pl. azzal... Szóval úgy két byte a kódban a hely, de az első parzerbe szükséges szóval csak a maradék byte-ot (a névből számmá alakítással) tudjuk az interpreterben fordítani! Ilyen névtér nélküli kukacozás csak szódefinícióban lehetséges - ezért az engine tudhatja, hogy mindig át kell ezt írni és ha a szót a definíciója szerint "lefuttatunk" interpretálva, akkor pedig azt, hogy mindig már az átírt alak áll ott (nem a név)!
Ha már THREADED formába került a szó, akkor lényegében egyfajta FORTH-os jit-elésen már átesett, de ez nem akkor történik, amikor a szót a parzer meglátja és a szimbólumtáblába bekerül, hanem amikor ELŐSZÖR MEGHÍVÓDIK. Ugyanis amikor először látja az engine a szót, lehet hogy nem fordítható, még pl. későbbi szóra hivatkozik! Viszont ekkor nem is szükséges még "csinálni" vele semmit a szimbólumtáblába a kezdőcímének beírásán túl, meg hasonló dolgokon túl! Amikor az első hívás történik, akkor megpróbáljuk, hogy mekkora lenne a méret az átírással - ehhez ideiglenes tárhely sem szükséges, csak egy plusz számítási menet, amikor még NEM csináljuk meg, csak kiszámoljuk mibe kerülne és mennyi ram kell hozzá - kifér-e majd a jelen dolog helyére! Ha igen, akkor átírjuk + használjuk a stack-et.
Flag-ek (flags):
0. offset_top
1. offset_top
2. offset_top
3. offset_top
4. offset_top
5. offset_top
6. WORD_TYP0: a típus első bitje
7. WORD_TYP1: a típus második bitje
enum SLC_WORDTYP {
SLC_WORDTYP_TEXT = 0,
SLC_WORDTYP_NATIVE = 1,
SLC_WORDTYP_THREADED_SESSION = 2,
SLC_WORDTYP_THREADED_INLINE = 3,
};
Mi az a "threaded kód"?
A "threaded kód" lényegében karaktersorozat helyett egy uint32_t-sorozat, ahol szavak helyett indexek vannak
az adott szavak definíciós pontjaira - DE! Viszont a zárójelek is el vannak kódolva (spéci uint32_t-ként)
Ettől jellemzően tömörebb és hatékonyabb lesz az interpretálás hiszen nem kell szöveget parzolni,
nincs szótár lookup, viszont megeshet, hogy "nincs elég hely" tárolni az adatokat a forrás szöveg helyén
és vagy nem optimalizálható olyankor, vagy amit mi csinálunk: a session storage-be tesszük az adatot...
Érdekes elképzelés lehet egy kifejezetten UTF8-szerű encoding is, hogy lehetőleg 8-16-32 bitet használjunk csak / szó!
FONTOS: A threaded kódot megpróbáljuk a lehető leghamarabb "process-álással" generálni a session-be, de ha nem tudjuk azonnal a függőségek miatt, akkor ugyebár valamit tennünk kell, addig marad ahogy van...
Mint az egyértelmű, a szó deklaráció a kód szövegben legalább 4 byte-ot igénybe vesz és minden változó-deklarálás is!
- ':'
- Egy whitespace - ebből lesz a "flag" is!
- Minimum egy karakter a névből (kötelező)
- lezáró karakter majd a végén
- Bár a szó tartalma lehet üres - de jellemzően van ott valami, csak erre nem számíthatunk!
A szó deklarációnál a "FLAG" mezőben az "adat elérési offsetje" felső bitjei jelennek meg. Ez fontos, mert így tudjuk tárolni a változók egészét, kihagyni ott a zéró termináltatást! Mivel a szimbólumtábla miatt a "név" már számunkra nem szükséges, azt a minimum egy byte nevet is használhatjuk tárolásra és az offset alsó byte-ja van benn ott! Ez jó így, mert ezáltal 8+6 bit eltolást tudunk, ami elég sok változót és névhosszakat enged meg (~16kb)
A másik esetben, a változóknál a @ + legalább egybetűs név + pontosvessző + whitespace/sorvége miatt, ahogy fent említettük szintén hasonló betárolást végez az interpreter engine!
Megjegyzés:
* Azért ilyennek választottam, mert a ": " miatt a forrás szöveget beolvasva pont marad ott 16 bit mindenképp!
* Lásd még a ZIG compilerről szóló youtube videó valahol fentebb az SLC-s blokk előttről miért jó ez cache-ileg így!
* Ugyanis: a számított mezők jobbak, mint nagyobb / másik memória terület szükséges volna csak a node-ok tárolgatására.
Minden esetben threaded kód optimalizálás
Most arra jutottam, hogy minden esetben csináljuk meg a threaded kódot. Annyi, hogy ha nem fér el a régi helyén, akkor úgy hát tegyük be a session storage-ba alternatívaként és mivel ez mindig akkor történik, amikor első lefutás van, ezért ott tudjuk, hogy milyen nevet / sőt target indexet tart jelenleg ez a szó fenn - ilyenkor az összes előfordulást megkeressük és át fogjuk majd írni az újra és kész. Ez egy kicsit hosszadalmas is lehet, ha nagy a kód, de csak 1x történik és hát jó ritkán történik majd csak meg...
Az engine-hez szükséges
Szerintem ezeket inline-olható, function pointerré kéne "elkódolnom", akár úgy, hogy feles paraméterek vannak + szétifelés!
- Engine kódja
- Callstack - előre adott méret (pl. parancssori paraméter, vagy embeddednél beállított)
- Adatstack, a műveleteivel - előre adott méret (pl. parancssori paraméter, vagy embeddednél beállított)
- Insert-stack - az input "nyelének" kiegészítésére egy char elemekből álló stack
- Session storage - egy charakter vektorhoz hasonló, de csak állandóan növő storage + session reset rá.
- Szimbólumtáblára MAP adatstruktúra (pl. kismap - név alapú lookupra + változó tároláshoz is [key alapján tudható melyik!])
- IO accessor (fájlműveletek, pipe-ok stb.)
Megj.:
- A különdféle "stack"-ek igazából használhatják ugyan azt az API-t.
- A session storage viszont nem lehet stack, sőt "vektor" sem, mert nem pointer invalidálódhat ki! Pl. egy realloc-os tömb se jó!
^^Ezzel a memória kialakítással a memory layout:
| enginecode | callstack | datastack | symbolmap -> ... <-malloc_store|insert_stack-> ... <- session_storage | src |
Igazából embedded környezetben, ilyen kismap-szerű (vagy még egyszerűbb) szimbólumtáblával tehát a legtöbb dolog vagy konstans, vagy csak két ponton van balról-jobbra és jobbról-balra folyton akár végtelenig (memhatár) növő rész!
A malloc_store
egy embedded megoldásban "mozoghat", tehát mivel csak indexálással érjük el és NEM pointeresen, ezért
ott memcpy-vel mozgatható, ha kezd elfogyni vagy a szimbólumtábla, vagy a session storage.
A jobbra-balra mozgó indexes elérésű malloc-ozgatón túl még egy ilyen memóriánk van, tehát egy cövek két oldalát is használva! Így került oda a "insert-stack", ami interpretereknek nagyon hasznos és a callstack-beli ID-tól függően (mélység szám) bizonyos szavak esetén az src-t kiegészíthetjük vele! Tehát "írhatunk az inputra" úgy, hogy épp egy feldolgozott szó mögé! Ez az insert-álás!
Ennél bonyolultabb memory management nem szükséges - tehát nem kell "malloc-implementáció" hozzá embeddedben!
Megj.:
A session storage alapvetően arra kell, hogy stringeket építsünk. Tudom... Erre a stack is alkalmas, de kellhet az olvasott
inputról (src) valahogy a complex szó átírásnál, meg az outputra írásokhoz előkészítésnél valami ilyen tárhely, ami azért
perzisztensebb, mint a sima stack! Az src természetesen épp függ attól, amely fájlt olvasunk, szóval a session kezdetén az
egész session storage..src végig törlődhet majd. Egy embedded (vagy pl. retró számítógép) memóriáját ezzel kihasználjuk!
Megj.:
Természetesen nem szükséges a sima Linuxos / normális bináris engine futtatókódja az engine-nek ezt így kezelnie!
A lényeg, hogy ha az API ilyen, akkor memory manager / OS nélkül mindenhol használható a dolog, egy nagy méretű
lineáris memória területet feltételezve az egész rendszer tehát (pl. shell-ként) működtethető! Viszont "normál" OS
mellett azonban persze érdemes ezeket simán "malloc-olgatással" megoldogatni hagyományosan!
Egyéb implementációs szempontok
- Legyen "sima C" kompatibilitási okokból a fordító amivel írom.
- Legyen az "engine" a fentiek fényében független a környezetétől (függvény pointeres megoldás)
- Lehetőleg könnyen self-host-olható legyen az egész történet (minimális számú szóból a többi implementálható)
Egy jó kezdőszett FORTH szavakból:
https://github.com/davidjade/MiniForth/blob/master/Words.c