Jak języki programowania definiują i zapisują funkcje / metody? Tworzę zinterpretowany język programowania w Ruby i próbuję wymyślić, jak zaimplementować deklarację funkcji.
Moim pierwszym pomysłem jest zapisanie treści deklaracji na mapie. Na przykład, gdybym zrobił coś takiego
def a() {
callSomething();
x += 5;
}
Następnie dodałbym wpis do mojej mapy:
{
'a' => 'callSomething(); x += 5;'
}
Problem polega na tym, że stałoby się ono rekurencyjne, ponieważ musiałbym wywołać moją parse
metodę na łańcuchu, który następnie zadzwoniłby parse
ponownie, gdy się napotkał doSomething
, a następnie w końcu zabrakłoby miejsca na stosie.
Jak zatem radzą sobie z tym języki interpretowane?
Odpowiedzi:
Czy miałbym rację zakładając, że twoja funkcja „parsuj” nie tylko analizuje kod, ale także wykonuje go w tym samym czasie? Jeśli chcesz to zrobić w ten sposób, zamiast zapisywać zawartość funkcji na mapie, zapisz jej położenie .
Ale jest lepszy sposób. To wymaga nieco więcej wysiłku z góry, ale daje znacznie lepsze wyniki wraz ze wzrostem złożoności: użyj abstrakcyjnego drzewa składni.
Podstawową ideą jest to, że parsujesz kod tylko raz, zawsze. Następnie masz zestaw typów danych reprezentujących operacje i wartości i tworzysz ich drzewo, tak jak to:
staje się:
(Jest to tylko tekstowa reprezentacja struktury hipotetycznej AST. Rzeczywiste drzewo prawdopodobnie nie byłoby w formie tekstowej.) W każdym razie parsujesz swój kod na AST, a następnie albo uruchamiasz interpretera bezpośrednio nad AST, lub użyj drugiego przejścia („generowanie kodu”), aby zmienić AST w jakąś formę wyjściową.
W przypadku twojego języka prawdopodobnie zrobiłbyś mapę, która odwzorowuje nazwy funkcji na funkcje AST, zamiast nazw funkcji na ciągi funkcji.
źródło
(((((((((((((((( x )))))))))))))))))
. W rzeczywistości stosy mogą być znacznie większe, a złożoność gramatyczna prawdziwego kodu jest dość ograniczona. Z pewnością jeśli ten kod musi być czytelny dla człowieka.Nie powinieneś nazywać parsowaniem po zobaczeniu
callSomething()
(zakładam, że miałeś na myślicallSomething
raczej niżdoSomething
). Różnica międzya
icallSomething
polega na tym, że jedna jest definicją metody, a druga jest wywołaniem metody.Gdy zobaczysz nową definicję, będziesz chciał sprawdzić, czy możesz dodać tę definicję, więc:
Zakładając, że testy zostaną zaliczone, możesz dodać je do mapy i rozpocząć sprawdzanie zawartości tej metody.
Po znalezieniu takiego wywołania metody
callSomething()
należy wykonać następujące kontrole:callSomething
istnieje na twojej mapie?Jeśli uznasz, że
callSomething()
jest to w porządku, to w tym momencie to, co chcesz zrobić, naprawdę zależy od tego, jak chcesz do tego podejść. Ściśle mówiąc, kiedy już wiesz, że takie wywołanie jest w tej chwili w porządku, możesz zapisać nazwę metody i argumenty bez wchodzenia w dalsze szczegóły. Po uruchomieniu programu wywołujesz metodę z argumentami, które powinieneś mieć w czasie wykonywania.Jeśli chcesz pójść dalej, możesz zapisać nie tylko ciąg znaków, ale link do faktycznej metody. Byłoby to bardziej wydajne, ale jeśli musisz zarządzać pamięcią, może to być mylące. Radziłbym najpierw po prostu trzymać się sznurka. Później możesz spróbować zoptymalizować.
Zauważ, że wszystko to zakłada, że leksykowałeś swój program, co oznacza, że rozpoznałeś wszystkie tokeny w swoim programie i wiesz, czym one są . Nie oznacza to, że wiesz, czy mają one jeszcze sens razem, co jest fazą analizy. Jeśli jeszcze nie wiesz, jakie są tokeny, sugeruję, abyś najpierw skupił się na uzyskaniu tych informacji.
Mam nadzieję że to pomogło! Witamy w Programmers SE!
źródło
Czytając twój post, zauważyłem dwa pytania w twoim pytaniu. Najważniejszy z nich to jak analizować. Istnieje wiele rodzajów analizatorów (np Recursive zejście parser , LR parser , packrat parserami ) i generatory parsera (np GNU bizon , ANTLR ) można użyć, aby przechodzić program tekstowy „rekurencyjnie” podane gramatyki (explicite lub implicite).
Drugie pytanie dotyczy formatu pamięci dla funkcji. Gdy nie wykonujesz tłumaczenia ukierunkowanego na składnię , tworzysz pośrednią reprezentację swojego programu, która może być abstrakcyjnym drzewem składni lub jakimś niestandardowym językiem pośrednim, w celu dalszego przetwarzania z nim (kompilacja, transformacja, wykonywanie, pisanie dalej plik itp.).
źródło
Z ogólnego punktu widzenia definicja funkcji jest niewiele więcej niż etykietą lub zakładką w kodzie. Większość innych operatorów pętli, zakresu i warunków jest podobnych; są one dostępne dla podstawowych poleceń „skok” lub „goto” na niższych poziomach abstrakcji. Wywołanie funkcji sprowadza się zasadniczo do następujących poleceń komputerowych niskiego poziomu:
Instrukcja „return” lub podobna będzie wtedy wykonywać następujące czynności:
Dlatego funkcje są po prostu abstrakcjami w specyfikacji języka wyższego poziomu, które pozwalają ludziom organizować kod w bardziej konserwowalny i intuicyjny sposób. Po skompilowaniu w asemblerze lub w języku pośrednim (JIL, MSIL, ILX), a na pewno w renderowaniu jako kod maszynowy, prawie wszystkie takie abstrakcje znikają.
źródło