Reeks 8 :   Compilatie
1 Praktisch
2 Decompilatie
2.1 Oefening a
2.2 Oefening b
2.3 Oefening c
2.4 Oefening d
3 Oefening:   Dynamische scope
4 Oefening:   Een efficiëntere compiler
4.1 open-coded?
4.2 spread-args
4.3 compile-open-coded
4.4 Uitbreiding registermachinesimulator
4.5 Randgevallen
5 Theorie
5.1 De top-levelprocedure compile
5.2 Instructiesequentie-ADT
5.3 Quasiquote
5.4 Codegeneratoren
8.9

Reeks 8 : Compilatie

Deze oefeningenreeks behandelt de compiler uit hoofdstuk 5.5 van Structure and Interpretation of Computer Programs. De code die je nodig hebt om deze oefeningen te maken vind je in dit zip-bestand: "compiler.zip". Een overzicht van de inhoud vind je in "overzicht.pdf".

1 Praktisch

Download het bestand compiler.zip van de oefeningenwebsite. Je kan de compiler starten door het bestand load-eceval-compiler.scm te evalueren. Deze laadt de compiler, de registermachinesimulator, de nodige functies voor lexicale analyse uit de meta-circulaire evaluator en ook enkele handige hulpfuncties die hieronder staan uitgelegd.

2 Decompilatie

Decompilatie is het process waarbij je, beginnend van een stuk gecompileerde code, probeert te reconstrueren wat de originele code was die aan compile werd meegegeven. Neem bijvoorbeeld het volgende stuk output van de compiler.

    (assign proc (op lookup-variable-value) (const +) (reg env))
    (assign val (const 2))
    (assign argl (op list) (reg val))
    (assign val (const 1))
    (assign argl (op cons) (reg val) (reg argl))
    (test (op primitive-procedure?) (reg proc))
    (branch (label primitive-branch1))
compiled-branch2
    (assign continue (label after-call3))
    (assign val (op compiled-procedure-entry) (reg proc))
    (goto (reg val))
primitive-branch1
    (assign val (op apply-primitive-procedure) (reg proc) (reg argl))
after-call3

Je ziet dat de waarde van + wordt opgezocht en het resultaat in het proc register gestoken. Dan worden 2 en 1 in het argl register gestoken. Achteraf splitst de code aan de hand van een branch statement. Dit hangt af van de waarde van +: is het een primitive of een compiled procedure? Dit zal pas geweten worden bij het uitvoeren van de code, daarom dat beide opties voorkomen in de gecompileerde code. Je ziet dat het hier gaat om een oproep van (+ 1 2).

Je kan ook terugvinden wat de target van de compile oproep was. Voor target, bedenk waar het resultaat van de oproep (+ 1 2) geschreven zal worden. Van de compiled procedure weet je dat diens resultaat in het val register zal gestoken worden. Bij de primitive zie je dat het resultaat van apply-primitive-procedure eveneens in val belandt.

In het geval van de linkage tenslotte, moet je bedenken wat er gebeurt nadat het stuk gecompileerde code is uitgevoerd. In dit voorbeeld moet je dus wederom kijken wat er op het einde van de procedure gebeurt. In het geval van een gecompileerde procedure, weet je dat die na uitvoering springt naar de waarde van het continue register. Die wordt hier op after-call3 gezet. In het geval van de primitive procedure, zie je dat er na het uitvoeren gewoon wordt verdergegaan. In beide gevallen belandt de uitvoering bij het volgende label na het uitvoeren van de procedure. Met andere woorden, de linkage was 'next.

De volledige compile oproep zag er zo uit:

> (pp (compile
        '(+ 1 2)
        'val
        'next))

    (assign proc (op lookup-variable-value) (const +) (reg env))

    (assign val (const 2))

    (assign argl (op list) (reg val))

    (assign val (const 1))

    (assign argl (op cons) (reg val) (reg argl))

    (test (op primitive-procedure?) (reg proc))

    (branch (label primitive-branch18))

compiled-branch19

    (assign continue (label after-call20))

    (assign val (op compiled-procedure-entry) (reg proc))

    (goto (reg val))

primitive-branch18

    (assign val (op apply-primitive-procedure) (reg proc) (reg argl))

after-call20

------------------------

Decompileer nu zelf de volgende outputs en reconstrueer de verschillende exp, target en linkage die aan compile werden meegegeven. Indien je iets niet herkent, probeer dan zelf wat input aan compile te geven en te analyseren wat eruit komt. Om de oefeningen wat interessanter te maken, zijn de labels onherkenbaar gemaakt.

2.1 Oefening a

(assign val (const 100))
(perform (op define-variable!) (const oef-a) (reg val) (reg env))
(assign resultaat (const ok))
(assign resultaat (op lookup-variable-value) (const oef-a) (reg env))

2.2 Oefening b

    (save continue)
    (assign proc (op lookup-variable-value) (const =) (reg env))
    (save proc)
    (assign proc (op lookup-variable-value) (const +) (reg env))
    (assign val (const 2))
    (assign argl (op list) (reg val))
    (assign val (const 1))
    (assign argl (op cons) (reg val) (reg argl))
    (test (op primitive-procedure?) (reg proc))
    (branch (label label2))
label1
    (assign continue (label label3))
    (assign val (op compiled-procedure-entry) (reg proc))
    (goto (reg val))
label2
    (assign val (op apply-primitive-procedure) (reg proc) (reg argl))
label3
    (assign argl (op list) (reg val))
    (assign val (const 10))
    (assign argl (op cons) (reg val) (reg argl))
    (restore proc)
    (test (op primitive-procedure?) (reg proc))
    (branch (label label5))
label4
    (assign continue (label label6))
    (assign val (op compiled-procedure-entry) (reg proc))
    (goto (reg val))
label5
    (assign val (op apply-primitive-procedure) (reg proc) (reg argl))
label6
    (restore continue)
    (test (op false?) (reg val))
    (branch (label label8))
label7
    (assign een-getal (const 100))
    (goto (reg continue))
label8
    (assign een-getal (const 200))
    (goto (reg continue))
label9

2.3 Oefening c

    (assign val (const 20))
    (perform (op define-variable!) (const x) (reg val) (reg env))
    (assign z (const ok))
    (assign proc (op lookup-variable-value) (const >) (reg env))
    (assign val (const 10))
    (assign argl (op list) (reg val))
    (assign val (op lookup-variable-value) (const x) (reg env))
    (assign argl (op cons) (reg val) (reg argl))
    (test (op primitive-procedure?) (reg proc))
    (branch (label label-1))
label-2
    (assign continue (label label-3))
    (assign val (op compiled-procedure-entry) (reg proc))
    (goto (reg val))
label-1
    (assign val (op apply-primitive-procedure) (reg proc) (reg argl))
label-3
    (test (op false?) (reg val))
    (branch (label label-4))
label-5
    (assign z (const 5))
    (goto (label label-6))
label-4
    (assign z (const 15))
    (goto (label label-6))
label-7

2.4 Oefening d

    (assign val (op make-compiled-procedure) (label label1) (reg env))
    (goto (label label17))
label1
    (assign env (op compiled-procedure-env) (reg proc))
    (assign env (op extend-environment) (const (n)) (reg argl) (reg env))
    (save continue)
    (save env)
    (assign proc (op lookup-variable-value) (const <) (reg env))
    (assign val (const 2))
    (assign argl (op list) (reg val))
    (assign val (op lookup-variable-value) (const n) (reg env))
    (assign argl (op cons) (reg val) (reg argl))
    (test (op primitive-procedure?) (reg proc))
    (branch (label label3))
label2
    (assign continue (label label4))
    (assign val (op compiled-procedure-entry) (reg proc))
    (goto (reg val))
label3
    (assign val (op apply-primitive-procedure) (reg proc) (reg argl))
label4
    (restore env)
    (restore continue)
    (test (op false?) (reg val))
    (branch (label label6))
label5
    (assign val (const 1))
    (goto (reg continue))
label6
    (assign proc (op lookup-variable-value) (const *) (reg env))
    (save continue)
    (save proc)
    (save env)
    (assign proc (op lookup-variable-value) (const f) (reg env))
    (save proc)
    (assign proc (op lookup-variable-value) (const -) (reg env))
    (assign val (const 1))
    (assign argl (op list) (reg val))
    (assign val (op lookup-variable-value) (const n) (reg env))
    (assign argl (op cons) (reg val) (reg argl))
    (test (op primitive-procedure?) (reg proc))
    (branch (label label8))
label7
    (assign continue (label label9))
    (assign val (op compiled-procedure-entry) (reg proc))
    (goto (reg val))
label8
    (assign val (op apply-primitive-procedure) (reg proc) (reg argl))
label9
    (assign argl (op list) (reg val))
    (restore proc)
    (test (op primitive-procedure?) (reg proc))
    (branch (label label11))
label10
    (assign continue (label label12))
    (assign val (op compiled-procedure-entry) (reg proc))
    (goto (reg val))
label11
    (assign val (op apply-primitive-procedure) (reg proc) (reg argl))
label12
    (assign argl (op list) (reg val))
    (restore env)
    (assign val (op lookup-variable-value) (const n) (reg env))
    (assign argl (op cons) (reg val) (reg argl))
    (restore proc)
    (restore continue)
    (test (op primitive-procedure?) (reg proc))
    (branch (label label14))
label13
    (assign val (op compiled-procedure-entry) (reg proc))
    (goto (reg val))
label14
    (assign val (op apply-primitive-procedure) (reg proc) (reg argl))
    (goto (reg continue))
label15
label16
label17
    (perform (op define-variable!) (const f) (reg val) (reg env))
    (assign s (const ok))
    (assign proc (op lookup-variable-value) (const f) (reg env))
    (assign val (const 11))
    (assign argl (op list) (reg val))
    (test (op primitive-procedure?) (reg proc))
    (branch (label label20))
label18
    (assign continue (label label19))
    (assign val (op compiled-procedure-entry) (reg proc))
    (goto (reg val))
label19
    (assign s (reg val))
    (goto (label label21))
label20
    (assign s (op apply-primitive-procedure) (reg proc) (reg argl))
label21

3 Oefening: Dynamische scope

Dit was een examenvraag in de eerste zit van 2018-2019.

De versie van de compiler die we in de les hebben gezien, zorgt ervoor dat de gecompileerde code lexicale scope gebruikt. Beschrijf welke aanpassingen je aan de compiler moet maken opdat dynamische scope gebruikt zou worden.

Hint die niet op het examen stond:

(compile-and-go
  '(begin
     (define n 100)
     (define (f)
       n)
     (define (h)
       (define n 200)
       (f))
     (h)))

Bij de lexicale geeft dit 100 als resultaat. Bij de dynamische geeft dit 200 als resultaat.

4 Oefening: Een efficiëntere compiler

Bekijk hoe momenteel primitieve functies zoals (+ a 5) gecompileerd worden naar registermachine-instructies. De gegenereerde registermachinecode kan efficiënter gemaakt worden. Dit wil zeggen: kortere code genereren die hetzelfde gedrag implementeert. In deze oefening gaan we deze efficiëntieverhoging realiseren voor de primitieven +, -, * en = door gebruik te maken van extra registers. M.a.w. we laten de ruimtecomplexiteit stijgen ten voordele van de tijdscomplexiteit. De bedoeling is om bij de primitieven +, -, * en = eerst alle subexpressies (dit zijn de argumenten) te compileren. Elk argument dient daarbij zijn resultaat weg te schrijven in de extra registers val0, val1, val2, etc. Merk op dat we vroeger slechts beschikten over één register om resultaten in weg te schrijven, namelijk val.

Uiteindelijk moet de gegenereerde code er als volgt uit zien.
> (pp (compile '(+ a 5)
               'val
               'next))
 
(assign val0 (op lookup-variable-value) (const a) (reg env))
(assign val1 (const 5))
(assign val (op +) (reg val0) (reg val1))

Voor deze oefening mag je gebruik maken van de volgende hulpfuncties:
(define (make-arg-target targ-num)
  (string->symbol (string-append "val" (number->string targ-num))))
(define (make-arg-targets from to)
  (if (< from to)
      (cons (make-arg-target from)
            (make-arg-targets (+ from 1) to))
      '()))

Onderneem daarvoor volgende stappen.

4.1 open-coded?

Implementeer een predikaat open-coded? dat applicaties met de primitieve procedures +, -, * en = onderschept. Breid de dispatching in de procedure compile uit.

4.2 spread-args

procedure

(spread-args arg-exps call-code)  instruction-sequence?

  arg-exps : list-of-scheme-args
  call-code : instruction-sequence?

Implementeer een procedure spread-args die
  1. aan de hand van compile de lijst met argumenten arg-exps compileert,

  2. aan de hand van preserving alle gecompileerde code van de argumenten aan elkaar plakt tot één grote instructiesequentie, en

  3. achter die grote instructiesequentie de instructiesequentie uit het argument call-code plakt. Dit zal nodig zijn wanneer compile-open-coded (zie later in deze oefening) gebruik maakt van spread-args. De call-code zal dan een instructie zijn die de waardes van al de verschillende registers optelt, aftrekt, ...

Let op, elk gecompileerd argument dient zijn resultaat respectievelijk weg te schrijven in het register val0, val1, val2, etc. Je kan hiervoor gebruik maken van de bovenstaande hulpfuncties.

Hier een voorbeeldwerking van spread-args.

> (pp (spread-args '(1 2 3) (empty-instruction-sequence)))
 
(assign val0 (const 1))
(assign val1 (const 2))
(assign val2 (const 3))
 
> (pp (spread-args '(1 2 3)
                    (make-instruction-sequence
                      '(continue)
                      '()
                      '((goto (reg continue))))))
 
(assign val0 (const 1))
(assign val1 (const 2))
(assign val2 (const 3))
(goto (reg continue))

Enkele extra tips:

4.3 compile-open-coded

Schrijf nu de codegenerator compile-open-coded die applicaties met primitieven +, -, * en = efficiënt compileert door gebruik te maken van extra registers val0, val1, val2, etc. Maak daarbij gebruik van de procedure spread-args die je zopas geïmplementeerd hebt.

procedure

(compile-open-coded exp target linkage)  instruction-sequence?

  exp : scheme-expressie
  target : register
  linkage : next/return/label

4.4 Uitbreiding registermachinesimulator

Om je uitbreiding gemakkelijk te testen, kan je compile-and-go gebruiken. Breng hiertoe de nodige aanpassingen aan de configuratie van de registermachinesimulator (registers, operaties, ...).

4.5 Randgevallen

Deze randgevallen zullen je meer inzicht geven in wat je misschien nog aan moet passen aan jouw code. Probeer de ene werkende te krijgen voor naar de volgende te gaan.

(compile '(+ 1 (+ 2 3) 4) 'val 'next)

(compile-and-go
  '(begin
     (define (f x)
       x)
     (define (g y)
       (define a 10)
       (+ 1 (f y) a))
     (g 2)))
; Resultaat moet 13 zijn.
(compile-and-go
  '(begin
     (define (f x)
       (+ x 1))
     (define (g y)
       (define a 10)
       (+ 1 (f y) a))
     (g 2)))
; Resultaat moet 14 zijn.
; Geen zorgen indien je deze niet opgelost krijgt.

5 Theorie

Deze sectie is toegevoegd om snel wat theorie te vermelden indien de oefeningen de theorie hebben ingehaald.

5.1 De top-levelprocedure compile

procedure

(compile exp target linkage)  instruction-sequence?

  exp : scheme-expressie
  target : register
  linkage : next/return/label

De top-level compile procedure zet een (hoog-niveau) Scheme-expressie exp om in een sequentie van (laag-niveau) registermachine-instructies. De compile procedure voert een lexicale analyse uit van exp om zo de juiste “code generator” te kiezen zoals compile-self-evaluating, compile-variable, compile-application, etc. De compile procedure heeft 3 argumenten:

5.2 Instructiesequentie-ADT

Speciale aandacht gaat uit naar het samenstellen van sequenties van registermachine-instructies: <seq1> + <seq2> = <seq>. Dit is geen eenvoudige zaak omdat de mogelijkheid bestaat dat <seq1> registers overschrijft die nodig zijn in <seq2>. Deze kwestie wordt als volgt aangepakt:

procedure

(make-instruction-sequence needs    
  modifies    
  statements)  instruction-sequence?
  needs : register-list
  modifies : register-list
  statements : register-machine-instructies

Er is een speciaal “instruction sequence” ADT ontwikkeld om sequenties van registermachine-instructies voor te stellen.
(define (make-instruction-sequence needs modifies statements)
  (list needs modifies statements))
Dit ADT is een drie-tupel bestaande uit:
  • needs : Een lijst met registers die geraadpleegd worden in de sequentie.

  • modifies : Een lijst met registers waarvan de waarde gemanipuleerd wordt.

  • statements : De sequentie van registermachine instructies.

Bijvoorbeeld, beschouwen we volgende sequentie van registermachine instructies:
(assign val (op lookup-variable-value) (const x) (reg env))
(goto (reg continue))
Dan is needs de lijst (continue env) en is modifies de lijst (val).

procedure

(preserving regs seq1 seq2)  instruction-sequence?

  regs : register-list
  seq1 : instruction-sequence?
  seq2 : instruction-sequence?

Sequenties worden (veilig) samengesteld met de procedure preserving. Deze procedure heeft de volgende parameters:
  • regs : Een lijst met registers die mogelijks kunnen overschreven worden in <seq1> zodat deze niet meer bruikbaar zijn in <seq2>. preserving zal op basis van de needs en modifies van <seq1> en <seq2> de nodige save’s en restore’s toevoegen om <seq1> en <seq2> veilig aan mekaar te plakken. (Zie onderstaande tabel.)

  • seq1 : Een sequentie van registermachine-instructies.

  • seq2 : Een sequentie van registermachine-instructies die aan <seq1> geplakt moeten worden.

Bijvoorbeeld, de samenstelling (preserving (list <reg1> <reg2>) <seq1> <seq2>) geeft aanleiding tot een van onderstaande mogelijkheden, afhankelijk van de needs en modifies van <seq1> en <seq2>.
<seq1>
<seq2>
(save <reg1>)
<seq1>
(restore <reg1>)
<seq2>
(save <reg2>)
<seq1>
(restore <reg2>)
<seq2>
(save <reg1>)
(save <reg2>)
<seq1>
(restore <reg2>)
(restore <reg1>)
<seq2>

5.3 Quasiquote

Quasiquoten is een manier om makkelijker symbolen en variabelen door mekaar heen te gebruiken wanneer je lijsten aan het maken bent. Je bent het gebruik hiervan normaal gezien reeds in Structuur van Computerprogramma’s 1 tegengekomen. Desondanks geven we hier nog kort een overzicht en enkele voorbeelden.

> (define naam 'ward)
> (list 'hallo 'daar naam)

(hallo daar ward)

> '(hallo daar naam)

(hallo daar naam)

> `(hallo daar naam)

(hallo daar naam)

> `(hallo daar ,naam)

(hallo daar ward)

> (define fruit '(aardbei banaan kiwi))
> `(welk fruit verkies je? ,fruit)

(welk fruit verkies je? (aardbei banaan kiwi))

> `(welk fruit verkies je? ,@fruit)

(welk fruit verkies je? aardbei banaan kiwi)

5.4 Codegeneratoren

De codegeneratoren zijn de procedures waar de effectieve vertaling van Scheme expressie naar sequenties van registermachine-instructies plaatsvindt. We bespreken beknopt één codegenerator om wat affiniteit op te bouwen.
(define (compile-self-evaluating exp target linkage)
  (end-with-linkage
   linkage
   (make-instruction-sequence
    '()
    (list target)
    `((assign ,target (const ,exp))))))

De procedure end-with-linkage voegt op basis van de parameter linkage (next, return of label) de nodige linkage-code toe op het einde van de gecompileerde instruction sequence.

De procedure make-instruction-sequence construeert een instruction sequence ADT. Deze instruction sequence kent een lege needs-lijst en 1 modifies-register target. Daarna volgt een geraamte van registermachinecode waarbij enkel nog target en exp ingevuld moeten worden.