BASED/BASED.md

49 KiB
Raw Blame History

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 ones self or secure in ones 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

  1. Zárójel, adattag-hozzáférés, függvényhívás, konstruktor, alaptípus-konverzió, postfix ++ és --, typeof, sizeof
  2. Pozitív és negatív operátorok, logikai és bináris tagadás, prefix ++ és --, $ a típusra és kifejezésre
  3. Szorzás, maradékos és maradék nélküli osztás, modulus
  4. Összeadás, kivonás
  5. Bit-eltoló operátorok
  6. Kisebb, nagyobb (vagy egyenlő), array(ptr, n), asif, is (típus equals check generic-re), islike(???)
  7. Egyenlő és nem egyenlő operátorok
  8. Logikai/Bináris ÉS
  9. Logikai/Bináris XOR
  10. Logikai/Bináris VAGY
  11. Feltételes ÉS
  12. Feltételes VAGY
  13. Feltételes operátor (?:)
  14. É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 képest is jobb szó.

Lesz továbbá tagged union is - lásd lejjebb - és interfészek (ami tagged union-á fordul le, hacsak ki nem optimizálja a fordító).

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 struct 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 (class/ struct megfelelője) tartozik egy "resources" vagy "pointers" blokk... Valszeg csak külön syntax van erre, nem akarok ilyen hosszú szavakat kiírva látni, mert öli a produktivitást, hanem mint a C++ban pl van az inicializálós rész a konstruktornál, úgy a class-nál valami hasonló kis syntax erre...
  • 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).
  • 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. Tehát ennek a "pointeres" blokknak vagy lehetnek nem pointeres változói, ami mondjuk egy integer csak, de úgy jobb, ha minden resource-t szerintem oda írsz - alternatíva, hogy tényleg csak a pointereket jelöljük külön is és akkor sima mezőnek ad értéket a konstruktorban a @resource-os függvény ami mondjuk egy adatbázis handle-t ad vissza valami hívásból és tárol el.... Lehet hogy tisztább ez így, csak így a resource handle-k nem egy helyen vannak, hanem félig a pointerek - félig a sima adatok közt. Valszeg érdemes pointer blokkot csinálni ami tényleg csak pointereket tartalmaz és ha nem használod - hát nem használod.... Egyre inkább hajlok az utóbbira, mert mégis tisztábbnak hangzik...
  • 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...
  • 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 class/struct pointer blokkjára is engedhetek rátenni, ami azt jelentené, hogy az ő pointereit más is látja, esetleg egész class-ra / struct-ra, 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

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.

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

(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...

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

Jobb pointerek

Valahogy jó lenne elérni, hogy a default a "restrict" kulcsszóhoz legyen közelebb. Szerintem tömbre a default a restrict, de a 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.

A tömböknél a restrict lehet ESETLEG a default - de ennek fényében kellene unrestrict kulcsszó rájuk!

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?

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!

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;

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

  • 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).

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!

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
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...

Ez valamilyen szinten jogos elvárás és az únió részben tudja is ezt, mert csinálhat az ember egy tagged union-t amiben ő maga sorolja fel így a típusait amik a tömbben lesznek és a fun() függvényeikben simán this->fun() átírányít / delegál akkor.

No de! Ennél tudunk jobbat is! Mert ez nem kezeli azt az esetet, ha egy library-t akarok írni és ezt forrással együtt kiadni! Ugyanis olyankor a lib írójaként én megszabom mely dolgokat veheti fel a tagged union - tehát előre kéne tudnom a use case-eket! Ez nincs így! Ellenben a fordítót kihasználhatjuk arra, hogy generálja ezeket a tagged union-okat le, az alapján, hogy kik implementálnak egy interfészt!

Első változat:

interface IStringifyable {
    string stringify();
}

handle A implements IStringifyable {
   ... 
}

handle A implements IStringifyable {
   ... 
}

// Ez ilyenkor egy tömb, aminek az elemei igazából tagged union elemek,
// a build-ben meglévő összes azt implementáló dolog alapján...
void process(IStringifyable arr[]) {
    ...
}

Második változat:

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!

+ ha nem tömbről van szó, akkor a hívási ponton az egy lightweight generic támogatás / ad-hoc polimorfizmus is (kell-e vajon?)

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 struct-ok!

Láthatóság

  • 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 érhető el. 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/

Strukturált formában tárolt programszöveg (tm) fordítás

Az első pontot 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).

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?

Exception és hibakezelés

  • Exception-ök 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 struct returns / double return values.
    FileHandle h = open_file("help.txt") onfail(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(err2) {
                default:
                    printf("help.txt not found and secondary_help.txt errs with: %s\n", err2.msg());
            }
        PERMISSION_ERROR:
            puts(err.msg());
    }
}

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!

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.

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...

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...

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...

LSP

  • Sajnos egyetértek JonBlow-val és fasságnak tartom... De ha valaki akar, próbáljon írni egyet oké...