[C] Werkt mijn tail recursion?

Pagina: 1
Acties:

Vraag


Acties:
  • 0 Henk 'm!

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 15:13
Ik ben bezig met het porten van mijn lexer en interpreter naar een ESP(8266). Simpel gezegd parsed mijn lexer mijn syntax naar een AST die ik vervolgens mij met interpreter doorloop. Het doorlopen van mijn AST deed ik met een recursieve functie (zie onderaan). Het hele spul werkt prima op mijn X86 machine, maar niet op mijn ESP.

Ik heb sterk het vermoedde dat dit komt door stack overflows door de vele recursies. Nu zijn er twee manieren om dit op te lossen:
1) Voorkomen van recursie d.m.v. dubbele loops m.b.v. een eigen stack.
2) Implementeren van tail-recursion.

Nu moet ik zeggen dat ik beide methoden voor mijn aanvankelijk probleem onbekend waren. De eerste methode werkt op zich, maar vraagt meer resources dan een goed geïmplementeerde tweede methode.

Ik neem in onderstaande voorbeelden even deze simpele AST als uitgangspunt:
Afbeeldingslocatie: https://tweakers.net/i/mej1fXz9RqLJiRfYueb3FeD-DGs=/full-fit-in/4000x4000/filters:no_upscale():fill(white):strip_exif()/f/image/Os2BzwFcaqzGZOso5XwLztQT.png?f=user_large

Het parsen van de variabele ($a) heb ik even buiten beschouwing gelaten.

De vraag is echter of mijn tail-recurion (in C) ook echt zorgt voor een nette kleine stack. In een compact stukje code ziet dat er nu als volgt uit:

C:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int interpret(struct tree_t *tree, struct varcont_t *v_out, void (*cb)(...)) {
    switch(tree->token->type) {
        case TOPERATOR: {
            interpret_value(tree->child[0], &var1);
            interpret_value(tree->child[1], &var2);

            operator_callback(tree->token->value, &var1, &var2, v_out);

            ...

            return cb(tree->parent, v_out);
        } break;
        case TIF: {
            return interpret(tree->child[0],  ..., interpret_if);
        } break;
        case TTRUE: {
            ...
        } break;
        case TFALSE: {
            ...
        } break;
        default: {
            return -1;
        } break;
    }
}


De interpret functie eindigt nu gegarandeerd elke keer met een functie aanroep zonder daarna nog iets aan logica te hebben. Van wat ik van tail-recursion begrijp moet dit ervoor zorgen dat de stack schoon blijft.

Nu heb ik bijv. in de functie interpret_value het parsen van mijn waardes gezet, maar dit zou net zo goed in mijn interpret functie kunnen zoals dit:
C:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int interpret(struct tree_t *tree, struct varcont_t *v_out, void (*cb)(...)) {
    switch(tree->token->type) {
        case TOPERATOR: {
            interpret(tree->child[0], &var1);
            interpret(tree->child[1], &var2);

            operator_callback(tree->token->value, &var1, &var2, v_out);

            ...

            return cb(tree->parent, v_out);
        } break;
        case TIF: {
            return interpret(tree->child[0],  ..., interpret_if);
        } break;
        case TINTEGER: {
            ...
            return 0;
        } break;
        case TTRUE: {
            ...
        } break;
        case TFALSE: {
            ...
        } break;
        default: {
            return -1;
        } break;
    }
}


De functie aanroep van regels 4 en 5 zorgen nu voor tenminste 1 recursie die op de stack blijft staan, maar de TINTEGER case is een eindpunt, dus het zal nooit verder gaan en beide stack entries zullen snel van de stack verdwijnen. Is er dan een voordeel om voor de eerste implementatie te gaan of maakt dat t.o.v. de tweede implementatie geen verschil voor de stack? In beide versies eindigt de boel van een tail-recursive functie aanroep waardoor de stack vervolgens weer netjes wordt opgeruimd.

Heb ik het goed begrepen dat het doel van tail-recursion niet perse is het volledig voorkomen van het vullen van de stack met een aantal recursies, maar dat het doel vooral is te kunnen voorspellen wat de maximale aantal stack ruimte is die je functie zal gebruiken? Zoals nu dus maximaal 1 recursie.

Als laatste nogmaals. Kan ik er vanuit gaan dat deze versies van mijn code correct tail-recursie implementeren?

Ten overvloede nog even de oorspronkelijke recursieve implementatie van mijn functie die niet vriendelijk is voor de stack, omdat die oneindig kan groeien.
C:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int interpret(struct tree_t *tree, struct varcont_t *v_out) {
    switch(tree->token->type) {
        case TINTEGER: {
            ...
            return 0;
        } break;
        case TIF: {
            int x = interpret(tree->child[0], ...);
            if(x == 0) {
                interpret(tree->child[1], ...);
                return 0;
            } else if(tree->nrchildren == 3) {
                interpret(tree->child[2], ...);
                return 0;
            }
            return -1;
        } break;
        case TOPERATOR: {
            interpret(tree->child[0], &var1);
            interpret(tree->child[1], &var2);
            operator_callback(tree->token->value, &var1, &var2, v_out);
            return 0;
        } break;
    }
}

Sinds de 2 dagen regel reageer ik hier niet meer

Alle reacties


Acties:
  • 0 Henk 'm!

  • Gomez12
  • Registratie: Maart 2001
  • Laatst online: 17-10-2023
Ondersteunt je compiler wel tail-recursion?

Van wat ik er zo snel van kan vinden, ondersteunt bijv c# het bewust niet, dus ik zou me kunnen voorstellen dat een c-compiler voor een esp-8266 het ook maar niet doet.

En als ik het zo zit te bekijken, dan vraag ik me af of je vanwege het beoogde gedrag van tail_recursion je niet alles als bij ref moet meegeven, en niet sommige dingen als bij value (want dan moet hij toch de oude waarde op de stack houden)

Maar als je het echt wilt weten, dan zou ik gewoon een simpel voorbeeld-projectje opzetten en het dan in asm nakijken. Het doel moet zijn dat er in asm geen recursie zit.

Acties:
  • 0 Henk 'm!

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 15:13
Gomez12 schreef op dinsdag 19 januari 2021 @ 23:27:
Maar als je het echt wilt weten, dan zou ik gewoon een simpel voorbeeld-projectje opzetten en het dan in asm nakijken. Het doel moet zijn dat er in asm geen recursie zit.
Ik gebruik de Arduino omgeving omdat het project waar ik dit aan wil toevoegen dat al deed. Over arduino-cli zeggen ze op stackoverflow het volgende:
Tail call elimination is indeed supported and enabled by default in Arduino IDE. This is quite standard for micro-controller world where debug aids like proper stack frames are sacrificed for memory efficiency.
https://stackoverflow.com/a/40803788

Sinds de 2 dagen regel reageer ik hier niet meer


Acties:
  • 0 Henk 'm!

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 15:13
Hmm, hier zeggen ze inderdaad dat de ESP8266 compilers het inderdaad niet ondersteunen:
I'm pretty sure that no gcc version does tail call optimization for xtensa, for windowed ABI due to the calling convention, and for call0 because I didn't think about it when I did the port, so it inherited windowed ABI behavior. But it should be possible with call0, I'll take a look at it.
https://github.com/pfalco.../1#issuecomment-261710552

Sinds de 2 dagen regel reageer ik hier niet meer


Acties:
  • 0 Henk 'm!

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 15:13
Als het niet ondersteund wordt, dan pak ik gewoon de eerste methode weer op, met de dubbele loops. Dan is het nog steeds wel erg leerzaam om de verschillende varianten uitgewerkt te hebben :)

Sinds de 2 dagen regel reageer ik hier niet meer


Acties:
  • +1 Henk 'm!

  • Gomez12
  • Registratie: Maart 2001
  • Laatst online: 17-10-2023
Je kan toch gewoon die code uit de 1e stackoverflow draaien, die zegt je of je tail-recursion hebt of niet.

En als je het hebt, dan kan je daarna die functie uitbreiden naar meer jouw functie om te zien of het wel tail-recursion blijft en/of waar het breekt...

Acties:
  • 0 Henk 'm!

  • farlane
  • Registratie: Maart 2000
  • Laatst online: 27-09 13:03
FWIW, als je niet zeker weet of het een SO is : FreeRTOS stask overflow checking

Somniferous whisperings of scarlet fields. Sleep calling me and in my dreams i wander. My reality is abandoned (I traverse afar). Not a care if I never everwake.


Acties:
  • 0 Henk 'm!

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 15:13
De ESP8266 is nogal karig als het gaat om debug informatie. Door simpelweg mijn syntax te versimpelen en functies aan en uit te zetten, kwam ik tot de hypothese dat het bij de klassieke recursieve functies mis ging.

Sinds de 2 dagen regel reageer ik hier niet meer


Acties:
  • 0 Henk 'm!

  • Gomez12
  • Registratie: Maart 2001
  • Laatst online: 17-10-2023
Tail-recursion is in zijn simpelste uitleg idd een functie als laatste oproep, maar in de praktijk kan de rest van je code er ook voor zorgen dat de compiler het toch niet ziet/wil/kan gebruiken.

Kijk op het oog lijkt jouw code goed, alleen omdat je het binnen een switch doet is het maar net de vraag of het ook binnen de switch de laatste instructie is, of dat dat de break is.

En eigenlijk verwacht ik niet dat je switch voor tail-recusion in aanmerking komt. Puur omdat het waarschijnlijk omgezet wordt naar een setje if-else statements die er dan in de asm wel weer achteraan komen.

Maar dat is in principe dus makkelijk te testen door die 1e stackoverflow code (als die werkt) om te zetten naar een switch met de gewenste switch-positie in het midden.

Acties:
  • 0 Henk 'm!

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 15:13
Gomez12 schreef op dinsdag 19 januari 2021 @ 23:56:
Maar dat is in principe dus makkelijk te testen door die 1e stackoverflow code (als die werkt) om te zetten naar een switch met de gewenste switch-positie in het midden.
Die werkt vooralsnog niet. Theoretisch zou het betreffende getal dat steeds verminderd zo groot moeten kunnen zijn als MAX_INT.

De test faalt nu na ~ 4111 bytes, terwijl de functie ESP.getFreeHeap() 52696 bytes terug geeft. De vrije stack ruimte lijkt 4064 bytes te zijn. Daarmee lijkt de conclusie inderdaad dat er geen tail-recursion wordt ondersteund.

Sinds de 2 dagen regel reageer ik hier niet meer


Acties:
  • 0 Henk 'm!

  • Gomez12
  • Registratie: Maart 2001
  • Laatst online: 17-10-2023
CurlyMo schreef op woensdag 20 januari 2021 @ 00:05:
[...]

Die werkt vooralsnog niet. Theoretisch zou het betreffende getal dat steeds verminderd zo groot moeten kunnen zijn als MAX_INT.
Dat is ook weer extreem optimistisch, niets is gratis... Ook tail_recursion niet.

Maar als je al op tail_recursion zit, waarom dan niet goto gebruiken om de hele recursie eruit te halen?
Wellicht gaat hij daar beter mee om?

Alhoewel het voor velen een vuil woord is, toch heeft goto nog steeds zijn plaats in huidige programmeertalen, oftewel probeer het...

Acties:
  • 0 Henk 'm!

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 15:13
Gomez12 schreef op woensdag 20 januari 2021 @ 00:20:
[...]

Dat is ook weer extreem optimistisch, niets is gratis... Ook tail_recursion niet.

Maar als je al op tail_recursion zit, waarom dan niet goto gebruiken om de hele recursie eruit te halen?
Wellicht gaat hij daar beter mee om?

Alhoewel het voor velen een vuil woord is, toch heeft goto nog steeds zijn plaats in huidige programmeertalen, oftewel probeer het...
Nieuwe update. De volgende test draait lekker door:
C:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const int RAM_SIZE_IN_BYTES = 4000000;

void f(int i) {
  ESP.wdtFeed();
  Serial.println(i);
  if(i == 0) return;
  else f(i-1);
}

void setup() {
  Serial.begin(115200);
  f(RAM_SIZE_IN_BYTES);
}

void loop() {
}

In de vorige greep de Watchdog uiteindelijk in en was het geen stackoverflow.

Om het uit te sluiten heb ik eens -fno-optimize-sibling-calls aan de compiler toegevoegd. Dan doet die max. 300 iteraties. Nu is hij vanaf de 4.000.000 al op 3.790.254 en going :)

Oftewel, het platform ondersteund het.

Ik zal de boel eens laten lopen en kijken of hij inderdaad de volledige 4.000.000 weet af te lopen.

Gaan we terug naar mijn vraag of ik alles doe zoals het zou moeten om gebruik te maken van tail-recursion? Weet iemand of mijn switch mogelijk roet in het eten gooit? Als dat zo zou zijn dan is het wel vrij onmogelijk om tail-recursion in te zetten.


Hij is nu bij 3644730

[ Voor 6% gewijzigd door CurlyMo op 20-01-2021 00:28 ]

Sinds de 2 dagen regel reageer ik hier niet meer


Acties:
  • 0 Henk 'm!

  • Gomez12
  • Registratie: Maart 2001
  • Laatst online: 17-10-2023
CurlyMo schreef op woensdag 20 januari 2021 @ 00:27:
[...]
Gaan we terug naar mijn vraag of ik alles doe zoals het zou moeten om gebruik te maken van tail-recursion? Weet iemand of mijn switch mogelijk roet in het eten gooit? Als dat zo zou zijn dan is het wel vrij onmogelijk om tail-recursion in te zetten.
Dat is toch gewoon te testen door de if om te bouwen naar iets als :
C:
1
2
3
4
5
6
7
switch (i)
case 0 : return
break;
case 1 : f(2);
break;
case default : f(i-1);
break;

Alhoewel ik er een 2e counter bij zou zetten die het aantal x dat 1 geraakt is telt.
Als jouw idee klopt, dan moet je grofweg op hetzelfde getal uitkomen (extra counter kost meer geheugen etc, dus niet exact gelijk) als hij niet al eeuwig doorloopt.

Btw, dit soort oneindige loops zijn wel op eigen risico, ik heb geen idee hoe je een esp-8266 hieruit zou moeten halen >:)
AvAars schreef op woensdag 20 januari 2021 @ 00:38:
Is jouw code in de TS wel echte tail-recursion?
Tja, dat is nu net zijn vraag dus...

[ Voor 9% gewijzigd door Gomez12 op 20-01-2021 00:40 ]


Acties:
  • 0 Henk 'm!

  • AvAars
  • Registratie: Januari 2008
  • Laatst online: 15:33
Is jouw code in de TS wel echte tail-recursion? Daarvoor moet je niet alleen eindigen met een functie-aanroep, maar met de recursieve functie-aanroep. Op die manier herkennen compilers het als tail-recursion.

Ik zie je nu in de verschillende switch branches met verschillende functies eindigen.

Acties:
  • 0 Henk 'm!

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 15:13
Gomez12 schreef op woensdag 20 januari 2021 @ 00:37:
[...]

Dat is toch gewoon te testen door de if om te bouwen naar iets als :
C:
1
2
3
4
5
6
7
switch (i)
case 0 : return
break;
case 1 : f(2);
break;
case default : f(i-1);
break;

Alhoewel ik er een 2e counter bij zou zetten die het aantal x dat 1 geraakt is telt.
Als jouw idee klopt, dan moet je grofweg op hetzelfde getal uitkomen (extra counter kost meer geheugen etc, dus niet exact gelijk) als hij niet al eeuwig doorloopt.
Owja, sorry, je had dit al gesuggereerd. Het was eigenlijk te laat voor me om nog scherp te zijn ;) De vorige heeft inderdaad netjes 4.000.000 tellingen gedaan.

Ik ga nu de volgende implementatie testen:
C:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const int RAM_SIZE_IN_BYTES = 4000000;

void f(int i, int x) {
    ESP.wdtFeed();
    switch(x) {
        case 0: {
            Serial.print("a ");
            Serial.println(i);
            f(i-1, 1);
        }
        break;
        case 1: {
            Serial.print("b ");
            Serial.println(i);
            f(i-1, 0);
        } break;
        break;
        default: {
            f(i-1, 0);
        } break;
    }
    return;
}

void setup() {
  Serial.begin(115200);
  f(RAM_SIZE_IN_BYTES, 0);
}

void loop() {
}
Btw, dit soort oneindige loops zijn wel op eigen risico, ik heb geen idee hoe je een esp-8266 hieruit zou moeten halen >:)
Daarom staat die in de setup en niet in de loop.
AvAars schreef op woensdag 20 januari 2021 @ 00:38:
Is jouw code in de TS wel echte tail-recursion? Daarvoor moet je niet alleen eindigen met een functie-aanroep, maar met de recursieve functie-aanroep. Op die manier herkennen compilers het als tail-recursion.

Ik zie je nu in de verschillende switch branches met verschillende functies eindigen.
Tsja, zuiver gezien heb je gelijk. Als je uitgaat van mijn doel dan maakt het echter niet uit. Even voor de zekerheid. Een standaard recursieve functie heeft doorgaans dit effect op je stack:
code:
1
2
3
4
5
6
7
interpret
  interpret
    interpret
      interpret
        interpret
          interpret
            interpret

Oftewel, een steeds maar groeiende stack.

Terwijl de technieken onder Tail call - waarvan tail recursion er volgens mij een van is - dit proberen te voorkomen. Het doel is dus dit:
code:
1
2
3
4
5
6
7
interpret
interpret
interpret
interpret
interpret
interpret
interpret

Oftewel, een stack die dezelfde grootte blijft ondanks de recursie.

Wat ik er in het begin niet bij zei is dat ik gebruik maak van een combinatie van tail call en continuation-passing style. Dat betekent dus dat ik vanuit mijn recursieve functie een uitstap maak naar een continuation-passing style functie om daarna weer terug te keren naar mijn recursieve functie. In beide functies probeer ik te garanderen dat er telkens een tail call plaatsvind. Dat zou er dan voor moeten zorgen dat de stack klein blijft omdat de functie aan het eind van zijn run van de stack verdwijnt.

Dat is dus de theorie. Maar aangezien ik deze technieken nog nooit heb toegepast is de vraag of ik het zo goed doe :)

Sinds de 2 dagen regel reageer ik hier niet meer


Acties:
  • 0 Henk 'm!

  • SymbolicFrank
  • Registratie: November 2010
  • Laatst online: 14-07-2021
Ik weet niet hoe je programmeertaal er uit ziet, maar je slaat dus de parse-tree op, die je dan in de interpreter gaat doorlopen. Meestal is een VM veel meer lineair, en lijkt het meer op een macro-assembler. De bijbehorende byte-code is dan ook veel simpeler en bestaat voornamelijk uit berekeningen, evaluaties en goto's. Samengestelde statements worden dan in stukjes gehakt (functies). En pas tijdens de uitvoering worden die eventueel inline gezet.

Het verschil is dan, dat je de recursie tijdens het parsen doet en niet tijdens het uitvoeren.

Je kunt dan zelfs je eigen calling-conventie maken en zelf kiezen of je parameters in registers stopt, op de stack zet of in variabelen stopt. Het is tenslotte een VM, die kun je inrichten zoals je het wilt hebben, los van hoe de C-compiler daarmee omgaat.

Acties:
  • 0 Henk 'm!

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 15:13
SymbolicFrank schreef op woensdag 20 januari 2021 @ 11:18:
Ik weet niet hoe je programmeertaal er uit ziet, maar je slaat dus de parse-tree op, die je dan in de interpreter gaat doorlopen. Meestal is een VM veel meer lineair, en lijkt het meer op een macro-assembler. De bijbehorende byte-code is dan ook veel simpeler en bestaat voornamelijk uit berekeningen, evaluaties en goto's. Samengestelde statements worden dan in stukjes gehakt (functies). En pas tijdens de uitvoering worden die eventueel inline gezet.

Het verschil is dan, dat je de recursie tijdens het parsen doet en niet tijdens het uitvoeren.

Je kunt dan zelfs je eigen calling-conventie maken en zelf kiezen of je parameters in registers stopt, op de stack zet of in variabelen stopt. Het is tenslotte een VM, die kun je inrichten zoals je het wilt hebben, los van hoe de C-compiler daarmee omgaat.
Klinkt allemaal super interessant, maar nog verder van mijn bed dan wat ik nu doe. Heb je misschien een goede tutorial die me wegwijs maakt in het maken van een VM?

Overigens, voorbeeldje van mijn syntax is:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
if (1 == 1 && 1 == 0) || 5 >= 4 then
    $a = 1;
    if 6 == 5 then
        $a = 2;
    end
    $a = $a + 3;
    $b = (3 + $a *  5 + max(1, 2, 3) * min(1, 2, 3)) / 2;
    @c = 5;
elseif 2 == 2 then
    $a = 6;
else
    $a = 7;
end

De @c heeft een speciale status, dus is anders dan dat we gewend zijn :)

[ Voor 13% gewijzigd door CurlyMo op 20-01-2021 11:24 ]

Sinds de 2 dagen regel reageer ik hier niet meer


Acties:
  • 0 Henk 'm!

  • SymbolicFrank
  • Registratie: November 2010
  • Laatst online: 14-07-2021
CurlyMo schreef op woensdag 20 januari 2021 @ 11:22:
Klinkt allemaal super interessant, maar nog verder van mijn bed dan wat ik nu doe. Heb je misschien een goede tutorial die me wegwijs maakt in het maken van een VM?
Zo ingewikkeld is het niet. Je doet nu al hetzelfde.

Als we alleen naar dit stukje kijken:
code:
1
$b = (3 + $a *  5 + max(1, 2, 3) * min(1, 2, 3)) / 2;

Dan zijn dat 5 bewerkingen en 2 functies. En je moet natuurlijk de waarde van $a ophalen en het in $b stoppen. En er zit een bepaalde volgorde in. Je gaat eerst de max en de min aanroepen, die vermenigvuldigen, $a ophalen en vermenigvuldigen, de zaak bij elkaar optellen, delen door twee en het resultaat opslaan.

Voor al die stukjes heeft jouw interpreter al een functie, die je parameters meegeeft. In plaats van die functies meteen aan te roepen, zet je ze in een lijst. En achteraf ga je die lijst uitvoeren. Je hebt dan de parse-tree niet meer nodig.

Je krijgt dan zoiets:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  goto Start:
...
Max:
  r1 = p1
  if cmp(p1, p2) <= 0 then goto cont1:
  r1 = p2
cont1:
  if cmp(p2, p3) <= 0 then goto cont2:
  r1 = p3
cont2:
  return r1
cont1:   
...

Start:
  r1 = max(1,2,3)
  r2 = min(1,2,3)
  r1 = r1 * r2
...

Enzovoorts.

Alle labels en variabelen stop je in een aparte lijst:
code:
1
2
3
Nr    Name    Addr
1     Start:  100
2     Max:    150

Je roept dus functie 2 aan (Max) en die begint op regel 150.

En dat is dan gewoon je VM.

Acties:
  • 0 Henk 'm!

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 15:13
SymbolicFrank schreef op woensdag 20 januari 2021 @ 11:48:
[...]


Zo ingewikkeld is het niet. Je doet nu al hetzelfde.
Wat jij doet gaat al een stuk verder, aangezien je ook vrijwel alle functies terugbrengt naar hun bytecode versie, waardoor je ook geen functies meer hoeft aan te roepen. Dat is heel tof, maar nog iets te hoog gegrepen voor me. Als ik eens begin bij de basis, want denk je dan hiervan a.d.v. deze AST:
Afbeeldingslocatie: https://tweakers.net/i/9OznNHOk_p6KTz8d_ykHBDvLaes=/full-fit-in/4000x4000/filters:no_upscale():fill(white):strip_exif()/f/image/EAning5smIY4axGgn2yNwRDw.png?f=user_large

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
1 if:
 goto 2
 goto 7
 goto 11
2 cond:
 goto 3 && goto 6
3 cond:
 goto 4 && goto 5
4 cond:
 1 == 1
5 cond:
 3 >= 2
6 cond:
 3 >= 4
7 true:
 goto 8
 goto 9
8 var:
 $a = 5
9 var:
 $c = 6
11 false:
 goto 12
12 var:
 $b = 6

Waar mijn hoofd nu nog even op vastloopt is dat ik mijns inziens nog steeds recursie nodig heb en dat volgens mij bij jouw meer geavanceerde versie ook zo is.




Ik kom zo met een nieuwe versie puur sequentieel :)

Sinds de 2 dagen regel reageer ik hier niet meer


Acties:
  • +1 Henk 'm!

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 15:13
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
1 if:
 goto 2
2 cond:
 goto 3
3 cond:
 goto 4
4 cond:
 1 == 1
 goto 5
 goto 8
5 cond:
 3 >= 2
 goto 6
 goto 8
6 cond:
 3 >= 4
 goto 7
 goto 8
7 true:
 goto 9
 goto 10
8 false:
 goto 11
9 var:
 $a = 5
10 var:
 $c = $a
11 var:
 $b = 6


Deze is niet recursief. Met name omdat ik bij elke cond zelf aangeef wat de volgende stap is bij een succes or faal. Oftewel, het TRUE / FALSE vervolg vanuit de if wordt nu verplaatst naar elke cond. Hierbij begrijp ik dat je simpelweg van boven naar beneden kan lezen en daarmee kan springen naar de specifieke regel die je op dat moment nodig heb.

Sinds de 2 dagen regel reageer ik hier niet meer


Acties:
  • 0 Henk 'm!

  • SymbolicFrank
  • Registratie: November 2010
  • Laatst online: 14-07-2021
Ja, zoiets. Je kunt het maken zoals je het zelf wilt hebben. En je kunt ook gewoon C-functies aanroepen, je hoeft niet alles zo plat te slaan.

Het is wel heel fijn om er meteen al over na te denken hoe je het gaat opslaan. En dan bedoel ik: zonder pointers die naar C-structuren wijzen. Leesbaar is leuk, maar dat kost meer geheugen. En hoe lang is een regel? Ik zou zelf een gelinkte lijst met een index gebruiken.

Edit: misschien is een array met meerdere indexen nog makkelijker.

[ Voor 7% gewijzigd door SymbolicFrank op 20-01-2021 14:28 ]


Acties:
  • +1 Henk 'm!

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 15:13
SymbolicFrank schreef op woensdag 20 januari 2021 @ 14:25:
Ja, zoiets. Je kunt het maken zoals je het zelf wilt hebben. En je kunt ook gewoon C-functies aanroepen, je hoeft niet alles zo plat te slaan.
Ik snap wat je bedoeld, zie het maar even als pseudo code.
Het is wel heel fijn om er meteen al over na te denken hoe je het gaat opslaan. En dan bedoel ik: zonder pointers die naar C-structuren wijzen. Leesbaar is leuk, maar dat kost meer geheugen. En hoe lang is een regel? Ik zou zelf een gelinkte lijst met een index gebruiken.
Het lijkt me inderdaad evident dit in een numerieke array te zetten waardoor je direct naar het juiste code blok kan springen. Verder lijkt het me logisch om inderdaad zoveel mogelijk pointers te gebruiken. Anders zit ik weer een lexer te bouwen om mijn VM te parsen :p

Alleen de boel zo in elkaar te haken dat condities naar elkaar verwijzen en vanuit elke conditie al direct naar de false conditie gesprongen mag worden is volgens mij wel een dingetje. Je zegt wel dat dat net zo makkelijk is als het opbouwen van de AST, maar daar heb ik nog niet lang genoeg over nagedacht om daar net zo zeker van te zijn als jij :)

Sinds de 2 dagen regel reageer ik hier niet meer


Acties:
  • 0 Henk 'm!

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 15:13
@SymbolicFrank Ik heb er toch een beetje over na zitten denken en volgens mij moet dit prima via een boom kunnen. Zie bijv.:
Afbeeldingslocatie: https://tweakers.net/i/GJd8YMaQY5xEQan0vmWNQ72T8KI=/full-fit-in/4000x4000/filters:no_upscale():fill(white):strip_exif()/f/image/3ThW4w2dbm9yeaCwrbSAERdK.png?f=user_large

Wellicht maakt dat het toch makkelijker om mijn oorspronkelijke structuur om te zetten. Nu even puzzelen hoe ik de nodes correct aan elkaar knoop.

Sinds de 2 dagen regel reageer ik hier niet meer


Acties:
  • 0 Henk 'm!

  • SymbolicFrank
  • Registratie: November 2010
  • Laatst online: 14-07-2021
CurlyMo schreef op woensdag 20 januari 2021 @ 20:41:
@SymbolicFrank Ik heb er toch een beetje over na zitten denken en volgens mij moet dit prima via een boom kunnen.
Ja, die boom heb je nodig tijdens het parsen. Je kunt een functie / functies maken die twee waardes vergelijken, bijvoorbeeld -1 als de eerste kleiner is dan de tweede, 1 in het andere geval en 0 als ze gelijk zijn. Je kunt de wiskundige en logische berekeningen door een andere functie / functies laten uitvoeren. En dan een if .. then .. else die naar 1 of 2 bepaalde labels springt. Meer heb je eigenlijk niet nodig.

Labels heb je in 2 soorten: lokale en globale. Een globaal label kan je vanaf overal aanroepen. Een lokaal label kun je alleen binnen de functie aanroepen.

En je hebt dan zowel een "goto" die altijd uitgevoerd word, als een "call" die een functie aanroept. Om vanuit een functie het antwoord terug te geven, doe je dan een "return".

Dus:

Spring naar: goto
Roep functie aan: call
Spring terug vanuit functie: return

(Daar heb je natuurlijk je stack, want je moet dan bijhouden waar je vandaan kwam. Maar dat kun je ook oplossen door de locatie waar je weer terug naar toe moet mee te geven als parameter.)

En dan moet je gaan bedenken hoe je parameters en resultaten door gaat geven. Het is het eenvoudigste om 1 standaard formaat af te spreken, dus bijvoorbeeld 1 32-bits getal. Past een parameter of resultaat daar niet in, dan gebruik je een label. Dat is dus eigenlijk een pointer, maar dan naar een tabel of gelinkte lijst die je zelf bijhoud en niet een keihard geheugenadres. Het zijn dan Variants.

Als je dat te ingewikkeld vind, kun je het ook helemaal overlaten aan de aanroeper en functie hoe ze daar mee om gaan, maar dat is heel ingewikkeld debuggen. Dan moet je dat ook allemaal weer bij gaan houden tijdens het parsen.

En als je gewoon overal een lijst van bijhoud, heb je ook geen stack nodig. Maar je moet natuurlijk wel altijd in de gaten houden dat je nog genoeg vrij geheugen hebt.

Acties:
  • 0 Henk 'm!

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 15:13
@SymbolicFrank Het klinkt alsof je het allemaal veel moeilijker maakt dan het volgens mij is. Als ik gewoon een sequentiële boom maak, dan kan ik die boom gewoon sequentieel doorlopen en dan kom ik altijd gewoon alle stappen tegen die nodig zijn. De operators en functies zijn natuurlijk modulair.

Oftewel, 1 == 1 && 2 >= 3 doet dan het volgende:
1 == 1 roept de == functie aan die de beide waardes vergelijkt. Zijn de waar dan geeft die functie een boolean true terug anders een boolean false.
Daarna wordt >= aangeroepen en dat geeft ook een boolean terug.
Als laatste wordt de && aangeroepen met beide boolean functies.

Enz.

Maar dat is prima te doen door gewoon de boom te doorlopen.

Sinds de 2 dagen regel reageer ik hier niet meer


Acties:
  • 0 Henk 'm!

  • SymbolicFrank
  • Registratie: November 2010
  • Laatst online: 14-07-2021
CurlyMo schreef op woensdag 20 januari 2021 @ 22:02:
@SymbolicFrank Het klinkt alsof je het allemaal veel moeilijker maakt dan het volgens mij is. Als ik gewoon een sequentiële boom maak, dan kan ik die boom gewoon sequentieel doorlopen en dan kom ik altijd gewoon alle stappen tegen die nodig zijn. De operators en functies zijn natuurlijk modulair.

Oftewel, 1 == 1 && 2 >= 3 doet dan het volgende:
1 == 1 roept de == functie aan die de beide waardes vergelijkt. Zijn de waar dan geeft die functie een boolean true terug anders een boolean false.
Daarna wordt >= aangeroepen en dat geeft ook een boolean terug.
Als laatste wordt de && aangeroepen met beide boolean functies.

Enz.

Maar dat is prima te doen door gewoon de boom te doorlopen.
Dat bedoel ik ook. We zijn het dus eens :)

Acties:
  • 0 Henk 'm!

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 15:13
SymbolicFrank schreef op woensdag 20 januari 2021 @ 22:08:
[...]


Dat bedoel ik ook. We zijn het dus eens :)
Even hardop gedacht.

Ik zit alleen te stoeien om de operator precedence en associativity goed te krijgen. Tot nu toe gebruikte ik precedence climbing en die maakt inderdaad precies de AST zoals je zou verwachten van een AST. Maar voor een sequentiële boom kan ik denk ik beter shunting yard gebruiken. Die maakt tenminste een infix notatie van mijn operatoren.

Sinds de 2 dagen regel reageer ik hier niet meer


Acties:
  • 0 Henk 'm!

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 15:13
@SymbolicFrank Ik blijf stoeien met mijn operatoren. Met name door de associativity en precedence. Die zorgt er telkens voor dat ik vast blijf zitten aan een of andere vorm van recursie.

code:
1
if 1 == 1 || 3 >= 4 || 5 == 6 then $a = 5; end


Zie dit voorbeeld:
Afbeeldingslocatie: https://tweakers.net/i/sLbd9beAJwuSRbyOfKIho2y0jY0=/800x/filters:strip_exif()/f/image/TZOvHzTG5INSO5bf3tshYnQJ.png?f=fotoalbum_large

Het is me hier wel gelukt in om stappen te denken. Dus de TRUE / FALSE komen niet aan de IF maar aan de laatste operator van mijn conditie. Het evalueren van mijn conditie is echter nog steeds een boom. Waarbij ik dus niet weet hoe je dit nu puur sequentieel maakt.

Enige optie die ik (tot nu toe) heb kunnen bedenken is de operator notatie gewoon als infix mee te nemen naar de VM om hem daar sequentieel infix te parsen.

Afbeeldingslocatie: https://tweakers.net/i/bGEz9tXOjW6iJ7X8VjTQWzIeQVw=/full-fit-in/4000x4000/filters:no_upscale():fill(white):strip_exif()/f/image/CsQg2Jd9HiUYwM8orwQ95Nx8.png?f=user_large




Ik denk dat ik het nu zo heb kunnen oplossen. Het de route terug van waar je vandaan kwam.
Afbeeldingslocatie: https://tweakers.net/i/htZIBIIyivZpj4Hmkk-7C9Fh6FM=/800x/filters:strip_exif()/f/image/ITulWw6B0Qnsa6BchHj7gomw.png?f=fotoalbum_large

[ Voor 15% gewijzigd door CurlyMo op 22-01-2021 08:16 ]

Sinds de 2 dagen regel reageer ik hier niet meer


Acties:
  • 0 Henk 'm!

  • SymbolicFrank
  • Registratie: November 2010
  • Laatst online: 14-07-2021
Het maakt niet uit als je recursie nodig hebt tijdens het parsen, het is alleen een probleem bij het uitvoeren. Dus dat werkt allemaal.

Het is ook best een lastig probleem, omdat je in je parse boom meestal ook niet meer weet in welke volgorde ze stonden. De wiki heeft er ook een artikel over.

Wat je ook kunt doen is ze gewoon allemaal een score naar belangrijkheid geven en ze dan van minst belangrijk naar meest belangrijk verwerken en achter elkaar zetten. Dus eerst optellen en aftrekken, dan vermenigvuldigen en delen, dan machten en wortels, dan logische operators en als laatste de haakjes.

Je haalt 1 zo'n stukje er uit en vervangt het door een functie-aanroep. Je krijgt dan functies met parameters die weer functie-aanroepen zijn. En als laatste kun je dat gewoon parsen.

Acties:
  • +1 Henk 'm!

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 15:13
SymbolicFrank schreef op vrijdag 22 januari 2021 @ 11:20:
Het maakt niet uit als je recursie nodig hebt tijdens het parsen, het is alleen een probleem bij het uitvoeren. Dus dat werkt allemaal.
Helemaal waar, maar bij het uitvoeren krijg je toch een boom. Je ziet bij mijn eerste versie dus alsnog bij het parsen de noodzaak van recursie. Bij mijn laatste versie volgens mij niet meer, omdat ik door vooruit te gaan toch weer stappen terug kan zetten.

Overigens gaat het verwerken van precedence en associativity prima. Alleen de VM zo krijgen dat die sequentieel te parsen is, is waar de uitdaging zit :)

Sinds de 2 dagen regel reageer ik hier niet meer


Acties:
  • 0 Henk 'm!

  • Gomez12
  • Registratie: Maart 2001
  • Laatst online: 17-10-2023
Gewoon even een tussendoor vraag, maar werkte je experiment van 20 jan 08:51 wel of niet?

Want je bent daarna compleet op de fiets van SymbolicFrank gestapt (waar niets mis mee is, maar als je daar nu ook weer vastloopt vraag ik me af wat nu het resultaat was van het andere experiment)

Acties:
  • 0 Henk 'm!

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 15:13
Gomez12 schreef op vrijdag 22 januari 2021 @ 14:21:
Gewoon even een tussendoor vraag, maar werkte je experiment van 20 jan 08:51 wel of niet?
Werkte inderdaad.
Want je bent daarna compleet op de fiets van SymbolicFrank gestapt (waar niets mis mee is, maar als je daar nu ook weer vastloopt vraag ik me af wat nu het resultaat was van het andere experiment)
Klopt, want mijn tail-recursion methode werkte niet voor de operators.

Sinds de 2 dagen regel reageer ik hier niet meer


Acties:
  • 0 Henk 'm!

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 15:13
Ter info. Mijn rules library is nagenoeg af en kan je vinden op mijn github:
https://github.com/CurlyMoo/rules

In de README leg ik uitgebreid uit hoe ik het uiteindelijk aangepakt heb. Dus uiteindelijk zonder vrijwel enige tail-recursion.

Sinds de 2 dagen regel reageer ik hier niet meer


Acties:
  • 0 Henk 'm!

Verwijderd

Ik heb ook een parser gemaakt, eerste versie deed ook alles goed maar bij de 2e versie ben ik gaan wegschrijven naar een file. Dus van eigen code naar PHP code in mijn geval en die require ik vervolgens. Dan is ie met parsen iets langzamer maar indien de file bestaat sneller. Ik weet niet of je dit met c ook kunt doen, maar veel dingen hoef je dan vervolgens geen rekening mee te houden zoals operator precedence.

Acties:
  • 0 Henk 'm!

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 15:13
Verwijderd schreef op woensdag 21 april 2021 @ 19:15:
Ik heb ook een parser gemaakt, eerste versie deed ook alles goed maar bij de 2e versie ben ik gaan wegschrijven naar een file. Dus van eigen code naar PHP code in mijn geval en die require ik vervolgens. Dan is ie met parsen iets langzamer maar indien de file bestaat sneller. Ik weet niet of je dit met c ook kunt doen, maar veel dingen hoef je dan vervolgens geen rekening mee te houden zoals operator precedence.
Nee, want C is geen interpreted taal ;)

Sinds de 2 dagen regel reageer ik hier niet meer

Pagina: 1