Jak kompilator może sam się skompilować?

168

Szukam CoffeeScript na stronie http://coffeescript.org/ i ma on tekst

Kompilator CoffeeScript jest sam napisany w CoffeeScript

W jaki sposób kompilator może sam się skompilować lub co oznacza to stwierdzenie?

AlexanderRD
źródło
14
Innym terminem określającym kompilator, który może sam się kompilować, jest self-hostingkompilator. Zobacz programmers.stackexchange.com/q/263651/6221
oɔɯǝɹ
37
Dlaczego kompilator nie miałby się skompilować?
user253751
48
Istnieją co najmniej dwie kopie kompilatora. Istniejąca wcześniej kompiluje nową kopię. Nowy może, ale nie musi, być identyczny ze starym.
bdsl
12
Możesz być także zainteresowany Gitem: jego kod źródłowy jest oczywiście śledzony w repozytorium Git.
Greg d'Eon
7
To tak jakby zapytać „W jaki sposób drukarka Xerox mogłaby wydrukować same schematy?” Kompilatory kompilują tekst do kodu bajtowego. Jeśli kompilator może skompilować się do dowolnego użytecznego kodu bajtowego, możesz napisać kod kompilatora w odpowiednim języku, a następnie przekazać kod przez kompilator, aby wygenerować dane wyjściowe.
RLH

Odpowiedzi:

219

Pierwsza edycja kompilatora nie może być wygenerowana maszynowo z określonego dla niego języka programowania; twoje zamieszanie jest zrozumiałe. Późniejsza wersja kompilatora z większą liczbą funkcji językowych (z przepisanym kodem źródłowym w pierwszej wersji nowego języka) mogłaby zostać zbudowana przez pierwszy kompilator. Ta wersja mogłaby następnie skompilować następny kompilator i tak dalej. Oto przykład:

  1. Pierwszy kompilator CoffeeScript został napisany w języku Ruby, tworząc pierwszą wersję CoffeeScript
  2. Kod źródłowy kompilatora CS został przepisany w CoffeeScript 1
  3. Oryginalny kompilator CS kompiluje nowy kod (napisany w CS 1) do wersji 2 kompilatora
  4. Wprowadzane są zmiany w kodzie źródłowym kompilatora, aby dodać nowe funkcje językowe
  5. Drugi kompilator CS (pierwszy napisany w CS) kompiluje poprawiony nowy kod źródłowy do wersji 3 kompilatora
  6. Powtórz kroki 4 i 5 dla każdej iteracji

Uwaga: nie jestem pewien, jak numerowane są wersje CoffeeScript, to był tylko przykład.

Ten proces jest zwykle nazywany ładowaniem początkowym . Innym przykładem kompilatora ładującego jest rustckompilator języka Rust .

Ben N.
źródło
5
Inną drogą ładowania kompilatora jest napisanie interpretera dla (podzbioru) twojego języka.
Aron
Jako kolejna alternatywa dla ładowania początkowego za pomocą kompilatora lub interpretera napisanego w innym języku, bardzo starą drogą byłoby ręczne złożenie źródła kompilatora. Chuck Moore omawia, jak to zrobić dla interpretera Forth w rozdziale 9, „Programy, które ładują się”, na końcu książki Programming a Problem-Oriented Language ( web.archive.org/web/20160327044521/www.colorforth.com/POL .htm ), opierając się na tym, że zrobiłeś to wcześniej ręcznie. Wprowadzanie kodu odbywa się tutaj za pośrednictwem panelu przedniego, który umożliwia bezpośrednie przechowywanie wartości w adresach pamięci kontrolowanych przez przełączniki dwustabilne dla bitów.
Jeremy W. Sherman
59

W artykule Reflections on Trusting Trust , Ken Thompson, jeden z twórców Uniksa, pisze fascynujący (i łatwy do odczytania) przegląd tego, jak kompilator C kompiluje się sam. Podobne koncepcje można zastosować do CoffeeScript lub dowolnego innego języka.

Pomysł kompilatora, który kompiluje swój własny kod, jest nieco podobny do quine : kodu źródłowego, który po wykonaniu generuje na wyjściu oryginalny kod źródłowy. Oto jeden przykład quine CoffeeScript. Thompson podał ten przykład C quine:

char s[] = {
    '\t',
    '0',
    '\n',
    '}',
    ';',
    '\n',
    '\n',
    '/',
    '*',
    '\n',
    … 213 lines omitted …
    0
};

/*
 * The string s is a representation of the body
 * of this program from '0'
 * to the end.
 */

main()
{
    int i;

    printf("char\ts[] = {\n");
    for(i = 0; s[i]; i++)
        printf("\t%d,\n", s[i]);
    printf("%s", s);
}

Następnie możesz się zastanawiać, w jaki sposób kompilator jest nauczany, że sekwencja ucieczki, taka jak '\n'reprezentuje kod ASCII 10. Odpowiedź jest taka, że ​​gdzieś w kompilatorze C istnieje procedura, która interpretuje literały znakowe, zawierając takie warunki, aby rozpoznawać sekwencje z ukośnikiem odwrotnym:

…
c = next();
if (c != '\\') return c;        /* A normal character */
c = next();
if (c == '\\') return '\\';     /* Two backslashes in the code means one backslash */
if (c == 'r')  return '\r';     /* '\r' is a carriage return */
…

Więc możemy dodać jeden warunek do powyższego kodu…

if (c == 'n')  return 10;       /* '\n' is a newline */

… Aby stworzyć kompilator, który wie, że '\n'reprezentuje ASCII 10. Co ciekawe, ten kompilator i wszystkie kolejne kompilatory przez niego skompilowane „znają” to mapowanie, więc w następnej generacji kodu źródłowego można zmienić tę ostatnią linię na

if (c == 'n')  return '\n';

… I zrobi to dobrze! 10Pochodzi z kompilatora, i nie musi być wyraźnie zdefiniowane w kodzie źródłowym kompilatora. 1

To jeden z przykładów funkcji języka C, która została zaimplementowana w kodzie C. Teraz powtórz ten proces dla każdej funkcji języka, a otrzymasz kompilator „samowystarczający”: kompilator C napisany w języku C.


1 Spójność opisana w artykule polega na tym, że skoro kompilator można „nauczyć” takich faktów, można go również błędnie nauczyć generowania trojanów plików wykonywalnych w sposób trudny do wykrycia, a taki akt sabotażu może się utrzymywać we wszystkich kompilatorach utworzonych przez skażony kompilator.

200_success
źródło
7
Chociaż jest to interesująca informacja, nie sądzę, aby odpowiadała na pytanie. Twoje przykłady zakładają, że masz już bootstrapowany kompilator lub w jakim języku jest napisany kompilator C?
Arturo Torres Sánchez
9
@ ArturoTorresSánchez Różne wyjaśnienia działają dobrze dla różnych osób. Nie zamierzam powtarzać tego, co zostało powiedziane w innych odpowiedziach. Uważam raczej, że inne odpowiedzi mówią na wyższym poziomie niż to, jak lubię myśleć. Osobiście wolę konkretną ilustrację dodania jednej pojedynczej funkcji i pozwolenie czytelnikowi na ekstrapolację z tego, zamiast krótkiego przeglądu.
200_success
5
OK, rozumiem twoją perspektywę. Chodzi tylko o to, że pytanie brzmi bardziej „jak kompilator może się skompilować, skoro kompilator do kompilacji kompilatora nie istnieje”, a mniej „jak dodać nowe funkcje do bootstrapowanego kompilatora”.
Arturo Torres Sánchez
17
Samo pytanie jest niejednoznaczne i otwarte. Wydaje się, że niektórzy interpretują to jako „jak kompilator CoffeeScript może się skompilować?”. Lekceważąca odpowiedź, jak podano w komentarzu, brzmi: „dlaczego nie miałby być w stanie skompilować się, tak jak kompiluje dowolny kod?” Interpretuję to jako oznaczające „jak może powstać samodzielny kompilator?” I podałem ilustrację, w jaki sposób kompilator można nauczyć o jednej z jego własnych funkcji językowych. Odpowiada na pytanie w inny sposób, przedstawiając niskopoziomową ilustrację tego, jak jest wdrażana.
200_success,
1
@ ArturoTorresSánchez: "[I] n w jakim języku jest napisany kompilator C?" Dawno temu utrzymywałem oryginalny kompilator C zapisany w starym dodatku K&R (tym dla IBM 360). Wiele osób wie, że najpierw był BCPL, potem B i że C było ulepszoną wersją B. W rzeczywistości było wiele części tego starego kompilatora, które wciąż były napisane w B i nigdy nie zostały przepisane do C. Zmienne miały postać pojedynczej litery / cyfry, nie zakładano, że arytmetyka wskaźnika była automatycznie skalowana, itd. Ten stary kod świadczy o bootstrapping z B do C. Pierwszy kompilator "C" został napisany w B.
Eliyahu Skoczylas
29

Otrzymałeś już bardzo dobrą odpowiedź, ale chcę zaproponować Ci inną perspektywę, która, miejmy nadzieję, będzie dla Ciebie pouczająca. Najpierw ustalmy dwa fakty, co do których oboje możemy się zgodzić:

  1. Kompilator CoffeeScript to program, który może kompilować programy napisane w języku CoffeeScript.
  2. Kompilator CoffeeScript to program napisany w języku CoffeeScript.

Jestem pewien, że możesz się zgodzić, że oba punkty 1 i 2 są prawdziwe. Teraz spójrz na te dwa stwierdzenia. Czy widzisz teraz, że kompilator CoffeeScript może całkowicie skompilować kompilator CoffeeScript jest całkowicie normalnym zjawiskiem?

Kompilator nie dba o to , co kompiluje. Dopóki jest to program napisany w CoffeeScript, może go skompilować. Tak się składa, że ​​sam kompilator CoffeeScript jest właśnie takim programem. Kompilator CoffeeScript nie przejmuje się tym, że kompiluje sam kompilator CoffeeScript. Wszystko, co widzi, to kod CoffeeScript. Kropka.

W jaki sposób kompilator może sam się skompilować lub co oznacza to stwierdzenie?

Tak, dokładnie to oznacza to stwierdzenie i mam nadzieję, że teraz widzisz, jak to stwierdzenie jest prawdziwe.

Jörg W Mittag
źródło
2
Nie wiem zbyt wiele o skrypcie kawy, ale możesz wyjaśnić punkt 2, stwierdzając, że został napisany w skrypcie kawy, ale został skompilowany, a następnie jest kodem maszynowym. A w każdym razie, czy mógłbyś wtedy wyjaśnić problem z jajami i kurczakiem. Jeśli kompilator został napisany w języku, dla którego kompilator nie został jeszcze napisany, to w jaki sposób może on w ogóle działać lub być kompilowany?
barlop
6
Twoje oświadczenie 2 jest niekompletne / niedokładne i bardzo mylące. ponieważ, jak mówi pierwsza odpowiedź, pierwsza odpowiedź nie została napisana pismem kawowym. To jest tak istotne dla jego pytania. A jeśli chodzi o „Jak kompilator może sam się skompilować lub co oznacza to stwierdzenie?” Mówisz „Tak”. Myślę, że tak (chociaż mój umysł jest trochę mały), widzę, że jest używany do kompilowania wcześniejszych wersji siebie, a nie siebie. Ale czy jest również używany do kompilowania się? Przypuszczałem, że byłoby to bezcelowe.
barlop
2
@barlop: Zmień instrukcję 2 na „ Dziś kompilator CoffeeScript to program napisany w języku CoffeeScript”. Czy to pomoże ci to lepiej zrozumieć? Kompilator to „tylko” program, który tłumaczy dane wejściowe (kod) na wyjście (program). Więc jeśli masz kompilator dla języka Foo, napisz kod źródłowy dla kompilatora Foo w samym języku Foo i prześlij to źródło do swojego pierwszego kompilatora Foo, jako wyjście otrzymasz drugi kompilator Foo. Robi się to w wielu językach (na przykład wszystkie znane mi kompilatory C są napisane w… C).
DarkDust
3
Kompilator nie może się skompilować. Plik wyjściowy nie jest tym samym wystąpieniem, co kompilator, który tworzy plik wyjściowy. Mam nadzieję, że teraz widzisz, że to stwierdzenie jest fałszywe.
pabrams
3
@pabrams Dlaczego tak zakładasz? Dane wyjściowe mogą być identyczne z kompilatorem użytym do ich utworzenia. Na przykład, jeśli skompiluję GCC 6.1 z GCC 6.1, otrzymam wersję GCC 6.1 skompilowaną z GCC 6.1. A potem, jeśli użyję go do kompilacji GCC 6.1, otrzymam również wersję GCC 6.1 skompilowaną z GCC 6.1, która powinna być identyczna (ignorując takie rzeczy, jak znaczniki czasu).
user253751
9

W jaki sposób kompilator może sam się skompilować lub co oznacza to stwierdzenie?

To dokładnie oznacza. Przede wszystkim kilka kwestii do rozważenia. Są cztery obiekty, którym musimy się przyjrzeć:

  • Kod źródłowy dowolnego programu CoffeScript
  • (Wygenerowany) zestaw dowolnego dowolnego programu CoffeScript
  • Kod źródłowy kompilatora CoffeScript
  • (Wygenerowany) zestaw kompilatora CoffeScript

Teraz powinno być oczywiste, że można użyć wygenerowanego zestawu - pliku wykonywalnego - kompilatora CoffeScript do skompilowania dowolnego programu CoffeScript i wygenerowania zestawu dla tego programu.

Teraz sam kompilator CoffeScript jest po prostu dowolnym programem CoffeScript, a zatem może być skompilowany przez kompilator CoffeScript.

Wygląda na to, że twoje zamieszanie wynika z faktu, że kiedy tworzysz swój własny nowy język, nie masz jeszcze kompilatora, którego możesz użyć do skompilowania swojego kompilatora. To z pewnością wygląda na problem z jajkiem kurzym , prawda?

Przedstaw proces zwany ładowaniem początkowym .

  1. Piszesz kompilator w już istniejącym języku (w przypadku CoffeScript oryginalny kompilator został napisany w języku Ruby), który może skompilować podzbiór nowego języka
  2. Piszesz kompilator, który może skompilować podzbiór nowego języka w samym nowym języku. Możesz używać tylko funkcji językowych, które kompilator z powyższego kroku może skompilować.
  3. Używasz kompilatora z kroku 1 do kompilacji kompilatora od kroku 2. To pozostawia Ci zestaw, który został pierwotnie napisany w podzbiorze nowego języka i który jest w stanie skompilować podzbiór nowego języka.

Teraz musisz dodać nowe funkcje. Powiedzmy, że zaimplementowałeś tylko while-loops, ale chcesz także for-loops. Nie stanowi to problemu, ponieważ możesz przepisać dowolną for-loop w taki sposób, aby była while-loopem. Oznacza to, że możesz używać while-loops tylko w kodzie źródłowym swojego kompilatora, ponieważ zestaw, który masz pod ręką, może tylko je skompilować. Ale możesz tworzyć funkcje wewnątrz swojego kompilatora, które mogą z nim pase i kompilować forpętle. Następnie używasz zestawu, który już masz, i kompilujesz nową wersję kompilatora. A teraz masz zestaw kompilatora, który może również analizować i kompilować for-loops! Możesz teraz wrócić do pliku źródłowego swojego kompilatora i przepisać wszystkie whileniechciane forpętle do -loops.

Przepłucz i powtarzaj, aż wszystkie żądane funkcje językowe będą mogły zostać skompilowane za pomocą kompilatora.

whilei foroczywiście były to tylko przykłady, ale to działa dla każdej nowej funkcji językowej, którą chcesz. A potem jesteś w sytuacji, w której jest teraz CoffeScript: kompilator kompiluje się sam.

Jest tam dużo literatury. Refleksje na temat zaufania do zaufania to klasyka, którą każdy zainteresowany tym tematem powinien przeczytać przynajmniej raz.

Polygnome
źródło
5
(Zdanie „Kompilator CoffeeScript sam jest napisany w CoffeeScript” jest prawdą, ale „Kompilator może sam się skompilować” jest fałszywe.)
pabrams
4
Nie, to całkowicie prawda. Kompilator może się skompilować. To po prostu nie ma sensu. Powiedzmy, że masz plik wykonywalny, który może skompilować wersję X języka. Piszesz kompilator, który może skompilować wersję X + 1 i kompilujesz go za pomocą kompilatora, który posiadasz (który jest wersją X). Otrzymujesz plik wykonywalny, który może skompilować wersję X + 1 języka. Teraz możesz użyć tego nowego pliku wykonywalnego do ponownej kompilacji kompilatora. Ale po co? Ty już masz plik wykonywalny, który robi to, co chcesz. Kompilator może skompilować dowolny prawidłowy program, więc może całkowicie się skompilować!
Polygnome
1
Rzeczywiście, nie jest niczym niezwykłym budować kilka razy, iirc modern freepascal buduje kompilator łącznie 5 razy.
plugwash
1
@pabrams Pisanie „Nie dotykaj” i „Gorący obiekt. Nie dotykaj” nie ma znaczenia dla zamierzonego przesłania frazy. O ile docelowi odbiorcy wiadomości (programiści) rozumieją zamierzony przekaz frazy (kompilacja kompilatora może skompilować swoje źródło) niezależnie od tego, jak jest napisana, ta dyskusja jest bezcelowa. W obecnym stanie twój argument jest nieważny. Jeśli nie jesteś w stanie wykazać, że docelowym odbiorcą wiadomości są nieprogramiści, wtedy i tylko wtedy masz rację.
DarkDestry
2
@pabrams „Dobry angielski” to język angielski, który jasno przekazuje pomysły docelowej publiczności w sposób zamierzony przez autora lub mówcę. Jeśli docelową publicznością są programiści i programiści to rozumieją, jest to dobry angielski. Powiedzenie „Światło istnieje zarówno w postaci cząstek, jak i fal” jest zasadniczo równoważne z określeniem „Światło istnieje zarówno jako fotony, jak i fale elektromagnetyczne”. Dla fizyka oznaczają dosłownie to samo. Czy to oznacza, że ​​powinniśmy zawsze używać dłuższego i jaśniejszego zdania? Nie! Ponieważ komplikuje czytanie, gdy znaczenie jest już jasne dla zamierzonej publiczności.
DarkDestry
7

Małe, ale ważne wyjaśnienie

Tutaj termin kompilator” wyjaśnia fakt, że w grę wchodzą dwa pliki. Jeden z nich to plik wykonywalny, który przyjmuje pliki wejściowe zapisane w CoffeScript i tworzy jako plik wyjściowy inny plik wykonywalny, plik obiektowy z możliwością łączenia lub bibliotekę współdzieloną. Drugi to plik źródłowy CoffeeScript, który przypadkiem opisuje procedurę kompilacji CoffeeScript.

Stosujesz pierwszy plik do drugiego, tworząc trzeci, który jest w stanie wykonać tę samą czynność kompilacji co pierwszy (być może więcej, jeśli drugi plik definiuje funkcje nie zaimplementowane w pierwszym), a więc może zastąpić pierwszy, jeśli tak pragnienie.

nbro
źródło
4
  1. Kompilator CoffeeScript został po raz pierwszy napisany w języku Ruby.
  2. Kompilator CoffeeScript został następnie ponownie napisany w CoffeeScript.

Ponieważ kompilator CoffeeScript w wersji Ruby już istniał, został użyty do utworzenia wersji kompilatora CoffeeScript dla języka CoffeeScript.

wprowadź opis obrazu tutaj Jest to znane jako kompilator samoobsługowy .

Jest to niezwykle powszechne i zwykle wynika z chęci autora do używania własnego języka, aby utrzymać jego rozwój.

Trevor Hickey
źródło
3

Nie chodzi tutaj o kompilatory, ale o wyrazistość języka, ponieważ kompilator to tylko program napisany w jakimś języku.

Kiedy mówimy, że „język jest napisany / zaimplementowany”, w rzeczywistości mamy na myśli, że zaimplementowano kompilator lub interpreter tego języka. Istnieją języki programowania, w których można pisać programy, które implementują język (są kompilatorami / tłumaczami dla tego samego języka). Te języki nazywane są językami uniwersalnymi .

Aby móc to zrozumieć, pomyśl o metalowej tokarce. Jest to narzędzie służące do kształtowania metalu. Możliwe jest, używając tylko tego narzędzia, stworzyć inne, identyczne narzędzie, tworząc jego części. Tym samym narzędzie to jest maszyną uniwersalną. Oczywiście pierwsza została stworzona innymi środkami (innymi narzędziami) i prawdopodobnie była niższej jakości. Ale pierwszy był używany do budowania nowych z większą precyzją.

Drukarka 3D to prawie uniwersalna maszyna. Możesz wydrukować całą drukarkę 3D za pomocą drukarki 3D (nie możesz zbudować końcówki, która topi plastik).

Paul92
źródło
Podoba mi się analogia z tokarką. Jednak w przeciwieństwie do analogii tokarki, niedoskonałości w pierwszej iteracji kompilatora są przekazywane do wszystkich kolejnych kompilatorów. Na przykład powyższa odpowiedź wspomina o dodaniu funkcji pętli for, w której oryginalny kompilator używa tylko pętli while. Wyjście rozumie pętle for, ale implementacja jest z pętlami while. Jeśli oryginalna implementacja pętli while jest wadliwa lub nieefektywna, to zawsze będzie!
@ Physics-Compute to po prostu błąd. W przypadku braku złośliwych defektów zwykle nie rozprzestrzeniają się podczas kompilacji kompilatora.
plugwash
Tłumaczenia asemblera z pewnością przechodzą od iteracji do iteracji, dopóki tłumaczenie asemblera nie zostanie naprawione. Nowe funkcje, które opierają się na starych funkcjach, nie zmieniają podstawowej implementacji. Pomyśl o tym przez chwilę.
@plugwash Zobacz „Reflections on Trusting Trust” autorstwa Kena Thompsona - ece.cmu.edu/~ganger/712.fall02/papers/p761-thompson.pdf
3

Dowód przez indukcję

Krok indukcyjny

Wersja n + 1 kompilatora jest napisana w X.

W ten sposób może być skompilowany przez n-tą wersję kompilatora (również napisaną w X).

Podstawa

Ale pierwsza wersja kompilatora napisana w X musi zostać skompilowana przez kompilator dla X, który jest napisany w języku innym niż X. Ten krok jest nazywany bootstrapowaniem kompilatora.

Guy Argo
źródło
1
Pierwszy kompilator kompilatora dla języka X można łatwo napisać w X. Jest to możliwe, że ten pierwszy kompilator można zinterpretować . (Przez tłumacza X napisanego w języku innym niż X).
Kaz
0

Kompilatory pobierają specyfikację wysokiego poziomu i przekształcają ją w implementację niskiego poziomu, taką jak może być wykonywana na sprzęcie. Dlatego nie ma żadnego związku między formatem specyfikacji a rzeczywistym wykonaniem, poza semantyką docelowego języka.

Kompilatory krzyżowe przenoszą się z jednego systemu do innego, kompilatory międzyjęzykowe kompilują specyfikację jednego języka na specyfikację innego języka.

Zasadniczo kompilacja jest zwykłym tłumaczeniem, a poziom jest zwykle od wyższego poziomu do niższego poziomu języka, ale istnieje wiele wariantów.

Kompilatory bootstrapowe są oczywiście najbardziej zagmatwane, ponieważ kompilują język, w którym są napisane. Nie zapomnij o początkowym etapie ładowania, który wymaga przynajmniej minimalnej istniejącej wersji, która jest wykonywalna. Wiele kompilatorów uruchomionych najpierw pracuje nad minimalnymi funkcjami języka programowania i dodaje w przyszłości dodatkowe złożone funkcje językowe, o ile nowa funkcja może być wyrażona przy użyciu poprzednich funkcji. Gdyby tak nie było, wymagałoby to wcześniejszego opracowania tej części „kompilatora” w innym języku.

nbro
źródło