Pokrok :)

Hotový projekt

Asi bych to ani neměl psát, ale tuhle část kurzu jsem psal jako poslední. Snad i někde na začátku jsem říkal, že u našeho kompileru vlastně nepotřebujeme modul pro hlídání datových typů, protože se ve skriptu datové typy nepoužívají. Není to tak docela pravda. Typy nepoužíváme, ale při psaní kurzu jsem si uvědomil, že nikde nemáme ohlídanou situaci, kdy by někdo použil nějakou funkci, která nevrací hodnotu, tedy proceduru, uvnitř nějakého výrazu. Například při sčítání, odčítání, jako parametr nějaké funkce atd. Ukažme si příklad skriptu:

    fce (a, b)
    {
       a = b;
    }

    main()
    {
       a = fce(2,5);
    }

Tak tohle, uznejte, je absolutně špatně. Ale náš kompiler to zatím akceptuje. Nemůžeme přece do proměnné a přiřadit nějakou hodnotu, když funkce fce nic nevrací! A to se může vyskytnout v každém výrazu. Jediné možné použití funkce fce, je následující:

    main()
    {
       a;
       fce(2,5);
    }

A my musíme zajistit, aby to kompiler zajistil :) Jinými slovy, aby nedovolil použít funkci, která nevrací hodnotu, tam, kde to není možné. Myslím, že to můžeme zařadit pod typovou kontrolu a proto tento modul do kompileru také zařadíme. Implementace této kontroly není vůbec složitá. V souboru tree.h je definována struktura TYPE_ERROR:

    //****************************************************************************************************************
    //reprezentuje chybu v datovem typu
    //****************************************************************************************************************

    struct TYPE_ERROR
    {
       bool isError; //byla chyba?
       char* msgError; //zprava o chybe
       
       TYPE_ERROR()
       {
          isError = false;
          msgError = NULL;
       }
    };

Struktura EXPRESSION, která reprezentuje výraz v parsovacím stromu (který si pamatujete z předchozích lekcí :)), obsahuje jednu proměnnou typu TYPE_ERROR. Pokaždé, když budeme zpracovávat výraz typu volání funkce a funkce nebude vracet hodnotu, uložíme do proměnné isError true, jako signalizaci, že ve výrazu se vyskytuje funkce, která nevrací hodnotu. Pokud hodnotu vrací, bude proměnná samozřejmě false. Druhá proměnná, msgError, bude obsahovat jméno funkce, která nám dělá problémy. Jasnější to snad bude, když se na to podíváme do kódu funkce processEXPRESSION:

    void CTypeChecking::processEXPRESSION(EXPRESSION* expression,TYPE_ERROR* typeError)
    {
       if (expression == NULL)
          return;
       
       switch (expression->kind)
       {
       .
       .
       .
       case callT:
          if (expression->val.callE.arguments != NULL)
          {
             //zkontroluje parametry funkce
             processEXPRESSION(expression->val.callE.arguments,typeError);
          }
          //pokud funkce nevraci hodnotu, nastav chybu volani funkce, ktera nevraci hodnotu
          if (expression->val.callE.symbol->val.functionS->returns_value == false)
          {
             typeError->isError = true;
             typeError->msgError = expression->val.callE.name;
          }
          else
          {
             typeError->isError = false;
             typeError->msgError = NULL;
          }
          
          break;
       }
       
       if (expression->next != NULL)
          processEXPRESSION(expression->next,typeError);
    }

Funkce dělá přesně to co, co jsem psal, že by se dělat mělo. Pokud zpracovává výraz volání funkce, která nevrací hodnotu, uloží do isError true. Všimněte si také, že druhým parametrem funkce processEXPRESSION je ukazatel na strukturu TYPE_ERROR typeError a že pokaždé, když se volá rekurzivně funkce processEXPRESSION, tak se vždy předává parametr typeError, tedy ten samý ukazatel, který dostane funkce, která je spuštěna jako první. Díky tomu se funkce processEXPRESSION dost zjednodušuje, protože nemusíme v každé case kauzuli kontrolovat, zda v rekurzivním volání vznikla chyba s funkcí nevracející hodnotu. Pokud tedy vznikne chyba, je to zaznamenáno do typError a o výpis chyby se stará až kód následující za prvním voláním funkce processEXPRESSION. To je hlavně ve funkci processSTATEMENT, kterou si ukážeme za chvíli. Je to výhodné ještě také v případě většího počtu těchto chyb v jednom výrazu. Hodně kompilerů reaguje na každou chybu ve výrazu. Pokud máte složitý výraz a někde v něm máte chybu, většinou to způsobí další chyby, které se řetězí a kompiler na vás vychrlí spoustu chybových hlášek kvůli jedné chybě. Určitě si vzpomínáte na nějakou takovou situaci, kdy ve složitém výrazu použijete proměnnou se špatným datovým typem. Náš kompiler ale informuje o chybě ve výrazu jen jednou, protože pokud chyba ve výrazu je, uložíme si o tom informaci a o chybě informujeme, až když máme celý výraz zkontrolovaný. Chybová hláška tak může značit jednu či více chyb. Snad je to jasné :) Podívejme se nyní na úsek funkce processSTATEMENT:

    void CTypeChecking::processSTATEMENT(STATEMENT* statement)
    {
       switch (statement->kind)
       {
       case skipT:
          break;
       case returnT:
          processEXPRESSION(statement->val.returnS.expression,&(statement->val.returnS.expression->typeError));
          if (statement->val.returnS.expression->typeError.isError)
             theLog.ReportError(statement->val.returnS.expression->line_number," You cannot use function %s that does not return value inside return statement!
    ",statement->val.returnS.expression->typeError.msgError);
          break;
       .
       .
       .
       }
    }

Je to jen v konstrukci if, zkontroluje jestli byla chyba a pokud ano, vypíše (i zaznamená) chybu. Všude se to kontroluje stejně. Všude, kde je to potřeba. Tedy všude, kde se volá funkce processEXPRESSION kromě samotné funkce processEXPRESSION. Jedná se ještě o funkce processDECLARATION a processFORINIT, ale všude je stejná konstrukce if, jen se mění parametry a podmínka (protože výraz je vždy jiný :)).

Jednoduchý modul, že :) Určitě dneska stihnete ještě další lekci :)

Část 4
Část 6