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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
|
#lang racket
;; Oświadczam, że rozwiązanie zadania egzaminacyjnego przygotowałem
;; w pełni samodzielnie, korzystając wyłącznie z materiałów do wykładu,
;; notatek, podręcznika, oraz materiałów zacytowanych w treści rozwiązania.
;; Oświadczam że nie korzystałem w żadnej formie z pomocy osób trzecich
;; w przygotowaniu rozwiązania ani też takiej pomocy nie udzielałem
;; i nie udostępniałem nikomu swojego rozwiązania.
;; ZADANIE 1
;; =========
;; W tym zadaniu rozważamy język WHILE (w formie z grubsza
;; odpowiadającej tej z wykładu), z blokami deklarującymi zmienne o
;; lokalnym zakresie.
;; Zadanie polega na dodaniu do języka procedur definiowanych na
;; zewnątrz głównego polecenia programu (podobnie jak w C, gdzie
;; główne polecenie odpowiadałoby procedurze main, czy Pascalu) — o
;; dowolnym wybranym przez siebie modelu działania. W tym celu należy:
;; · rozszerzyć składnię abstrakcyjną o składnię procedur i rozbudować odpowiednio składnię programów
;; · rozszerzyć procedurę parsowania
;; · rozszerzyć ewaluator
;; · *opisać* wybrany model działania procedur, w tym jego potencjalne zalety lub ograniczenia
;; Należy rozszerzyć poniższy szablon, a część słowną zadania umieścić
;; w komentarzu, podobnie jak niniejsze polecenie.
;; Uwaga! Zadanie jest *bardzo* szeroko sformułowane, jest wiele
;; sensownych rozwiązań które stosowały liczne języki imperatywne w
;; historii — nie jest treścią zadania znalezienie *najlepszego*,
;; tylko swojego, które *rozumiecie*. Wybrany model działania procedur
;; *może* być relatywnie ubogi, jednak jeśli tak się zrobi, warto
;; pokazać że ma się tego świadomość w słownym opisie jego działania.
;; Postanowiłem, że struktura programów w moim języku będzie miała trochę z pythona i trochę z C.
;; Istotną decyzją którą podjąłem jest to, że wszystkie funkcje w naszym języku muszą zwracać
;; jakąś wartość (zawsze zwracają inta), łącznie z funkcją main, przy pomocy dyrektywy "return".
;; To, co zwraca main, jest tym co zwraca
;; cały program (z małym wyjątkiem, ale o tym później). Okazało się, że takie podejście
;; do sprawy jest bardzo wygodne -- nie musiałem się dzięki temu nawet przejmować
;; osobnym implementowaniem funkcji rekurencyjnych, wzajemnie rekurencyjnych
;; czy nawet zagnieżdżonych, a do tego można definiować funkcje w dowolnej kolejności!
;; Co więcej, funkcje przyjmują dowolnie wiele argumentów, również 0.
;; On top of that, do funkcji można przekazywać cokolwiek co ewaluuje się do wartości
;; Czyli mozna przekazywać wartości zmiennych, jak i dowolne wyrażenia!
;; Oto przykładowy kod, po którym raczej jasno widać w jak wygląda nowa składnia:
(define BINOM '({func main ()
(return (call binom (N K)))}
{func fact (t)
(if (= t 0)
(return 1)
({func decr (x) (return (- x 1))}
(return (* t (call fact ((call decr (t))))))))}
{func binom (n k)
(if (= k 0)
(return 1)
(var (num (call fact (n)))
(var (den (* (call fact (k)) (call fact ((- n k)))))
(return (/ num den)))))}
))
(define (bin n k)
(eval-prog (parse-cmd BINOM) (mem-alloc 'i 1 (mem-alloc 'N n (mem-alloc 'K k empty-mem)))))
;; Specjalnie trochę pokomplikowałem, ale widać featury naszego języka.
;; Jak to w ogóle działa?
;; Za każdym razem, kiedy definiuję funkcję, to do środowiska dodaję parę (nazwa funkcji . clo),
;; gdzie clo jest takim quasi-domknięciem, jest to po prostu struktura trzymająca nazwy
;; argumentów funkcji oraz jej ciało. Właśnie takie podejście bardzo dobrze
;; załatwiło łatwość w definiowaniu funkcji rekurencyjnych oraz wzajemnie rekurencyjnych i
;; zagnieżdżonych -- żadna funkcja nie zostanie wywołana, dopóki nie wywołam maina,
;; a tego wywołam dopiero po zewaluowaniu wszystkich definicji (tym samym dodaniu ich do środowiska).
;; Takie podejście ma trochę problemów, chyba największym z nich jest to, że nie ma możliwości
;; zmiany wartości globalnych wewnątrz funkcji. Tj. możemy je zmieniać, ale zmiany będą
;; widoczne jedynie w jej lokalnym zakresie.
;; W zasadzie nie jest to aż tak bolesne -- globalne zmienne możemy traktować po prostu
;; jak argumenty wywołania funkcji main.
;; Wywoływać funkcję mogę tylko za pomocą specjalnego wyrażenia call,
;; które jako pierwszy argument
;; przyjmuje nazwę funkcji, a jako drugi przyjmuje listę argumentów.
;; Żeby wiedzieć jak działa call, spójrzmy najpierw jak działa return.
;; return napisane jest tak, że jeśli w jakimkolwiek miejscu funkcji
;; się na niego trafi, to reszta funkcji nie jest już wywoływana
;; (czyli tak jakbyśmy sie spodziewali). Jak on w sumie działa?
;; Na samym początku eval-prog, zanim zacznę w ogóle ewaluować definicje funkcji,
;; dodaje do środowiska specjalną zmienną o nazwie RETURN o wartości #f.
;; Jeśli w funkcji gdziekolwiek wywołam returna, to
;; zmieniam wartość RETURN w środowisku na to, co chcę zwrócić.
;; W eval-cmd za każdym razem sprawdzam jaka jest wartość RETURN.
;; Jeśli jest to #f, to pracuje jakby nigdy nic, a jeśli jest to coś innego,
;; to po prostu zwracam aktualne środowisko.
;; Zatem funkcja zwraca środowisko, w którym zmienna RETURN
;; ustawiona jest na wynik jej obliczenia.
;; Teraz już prosto widać, że jedyne co robi call, to szuka ciała funkcji
;; w środowisku i wywołuje ją dla podanych argumentów, dostaje od tej
;; funkcji środowisko, a następnie odzyskuje wartość RETURN w zwróconym
;; przez nią środowisku. Dzięki temu po wywołaniu funkcji
;; wewnątrz innej funkcji nie zmienią się wartości żadnych zmiennych (w tym globalnych).
;; Jest to dosyć podobne do pythona -- tam inty są immutable i nie można ich wysłać przez
;; referencję. Ale możemy to robić jeśli się uprzemy np. tak:
;; {func decr (x)
;; (return (- x 1))}
;; {func main ()
;; {(i := (call decr (i)))
;; (return i)}
;; Uruchomienie takiego programu ze zmienną globalną i zwróci oczywiście i-1.
;; Mały problem którego za bardzo nie umiem rozwiązać jest taki, że jeśli gdzieś poza
;; jakąkolwiek funkcją wywołam return, to wartość którą tam zwrócę będzie
;; wartością dla całego programu, bo zmienna RETURN w środowisku zmieni swoją wartość
;; na coś innego od #f i niestety main nawet się nie wykona (na samym wstępie stwierdzi,
;; że coś zostało już zwrócone). Widać to w TEST10. Generalnie co za tym idzie,
;; między definicjami funkcji mogą być jakieś instrukcje, które zostaną
;; wywołane razem z ewaluacją programu, zanim zostanie wywołany main.
;; Dodatkowe informacje umieściłem w komentarzach w odpowiednich miejscach pliku.
;; Na dole umieściłem kilka testów które pokazują co jak działa.
(struct const (val) #:transparent)
(struct binop (op l r) #:transparent)
(struct var-expr (name) #:transparent)
(struct call-expr (name args) #:transparent) ;; wywołanie funkcji
(define (operator? x)
(member x '(+ * - / > < = >= <=)))
(define (keyword? x)
(member x '(skip while if := func func-rec call return))) ;; kilka nowych słów kluczowych
(define (expr? e)
(match e
[(const v)
(integer? v)]
[(var-expr x)
(and (symbol? x)
(not (keyword? x)))]
[(binop op l r)
(and (operator? op)
(expr? l)
(expr? r))]
[(call-expr n a)
(and (symbol? n)
(list? a)
(andmap expr? a))]
[_ false]))
(struct skip () #:transparent)
(struct assign (id exp) #:transparent)
(struct if-cmd (exp ct cf) #:transparent)
(struct while (exp cmd) #:transparent)
(struct comp (left right) #:transparent)
(struct var-in (name expr cmd) #:transparent)
(struct function (name args cmd) #:transparent) ;; dodane funkcje, funkcje rekurencyjne oraz return
(struct funcrec (name args cmd) #:transparent)
(struct return-stat (exp) #:transparent)
(define (cmd? c)
(match c
[(skip) true]
[(assign x e) (and (symbol? x) (expr? e))]
[(if-cmd e ct cf) (and (expr? e) (cmd? ct) (cmd? cf))]
[(while e c) (and (expr? e) (cmd? c))]
[(comp c1 c2) (and (cmd? c1) (cmd? c2))]
[(var-in x e c) (and (symbol? x) (expr? e) (cmd? c))]
[(function f a c) (and (symbol? f) (list? a) (andmap symbol? a) (cmd? c))]
[(funcrec f a c) (and (symbol? f) (list? a) (andmap symbol? a) (cmd? c))]
[(return-stat exp) (expr? exp)]))
(define (prog? p)
(cmd? p))
(define (parse-expr p)
(cond
[(number? p) (const p)]
[(and (symbol? p)
(not (keyword? p)))
(var-expr p)]
[(and (list? p)
(= 3 (length p))
(operator? (car p)))
(binop (first p)
(parse-expr (second p))
(parse-expr (third p)))]
[(and (list? p) ; <------ wywołanie funkcji
(= (length p) 3)
(eq? (first p) 'call)
(symbol? (second p))
(list? (third p)))
(call-expr (second p) (map parse-expr (third p)))]
[else false]))
(define (parse-cmd q)
(cond
[(eq? q 'skip) (skip)]
[(and (list? q)
(= (length q) 3)
(eq? (second q) ':=))
(assign (first q) (parse-expr (third q)))]
[(and (list? q)
(= (length q) 4)
(eq? (first q) 'if))
(if-cmd (parse-expr (second q)) (parse-cmd (third q)) (parse-cmd (fourth q)))]
[(and (list? q)
(= (length q) 3)
(eq? (first q) 'while))
(while (parse-expr (second q)) (parse-cmd (third q)))]
[(and (list? q)
(= (length q) 3)
(eq? (first q) 'var)
(list? (second q))
(= (length (second q)) 2))
(var-in (first (second q))
(parse-expr (second (second q)))
(parse-cmd (third q)))]
[(and (list? q) ; <------ funkcje
(= (length q) 4)
(eq? (first q) 'func)
(symbol? (second q))
(list? (third q))
(andmap symbol? (third q)))
(function (second q) (third q) (parse-cmd (fourth q)))]
[(and (list? q) ; <------ return
(= (length q) 2)
(eq? (first q) 'return))
(return-stat (parse-expr (second q)))]
[(and (list? q)
(>= (length q) 2))
(desugar-comp (map parse-cmd q))]
[else false]))
(define (desugar-comp cs)
(if (null? (cdr cs))
(car cs)
(comp (car cs)
(desugar-comp (cdr cs)))))
(define (value? v)
(number? v))
(struct mem (xs) #:transparent)
(define (mem-lookup x m)
(define (assoc-lookup xs)
(cond
[(null? xs) (error "Undefined variable" x)]
[(eq? x (caar xs)) (cdar xs)]
[else (assoc-lookup (cdr xs))]))
(assoc-lookup (mem-xs m)))
(define (mem-update x v m)
(define (assoc-update xs)
(cond
[(null? xs) (error "Undefined variable" x)]
[(eq? x (caar xs)) (cons (cons x v) (cdr xs))]
[else (cons (car xs) (assoc-update (cdr xs)))]))
(mem (assoc-update (mem-xs m))))
(define (mem-alloc x v m)
(mem (cons (cons x v) (mem-xs m))))
(define (mem-drop-last m)
(cond
[(null? (mem-xs m))
(error "Deallocating from empty memory")]
[else
(mem (cdr (mem-xs m)))]))
(define empty-mem
(mem null))
(define (op->proc op)
(match op
['+ +]
['- -]
['* *]
['/ /]
['< (lambda (x y) (if (< x y) 1 0))]
['> (lambda (x y) (if (> x y) 1 0))]
['= (lambda (x y) (if (= x y) 1 0))]
['<= (lambda (x y) (if (<= x y) 1 0))]
['>= (lambda (x y) (if (>= x y) 1 0))]
))
;; zał: (expr? e) i (mem? m) jest prawdą
;; (value? (eval e m)) jest prawdą
(define (eval e m)
(match e
[(const v) v]
[(var-expr x) (mem-lookup x m)]
[(binop op l r)
(let ((vl (eval l m))
(vr (eval r m))
(p (op->proc op)))
(p vl vr))]
[(call-expr name args) ;; <------ ewaluacja wywołania funkcji
(match (mem-lookup name m)
[(clo func-args cmd)
(if (= (length args) (length func-args)) ;; <------ sprawdzanie arnosci
(let* ([func-mem (assign-values args func-args m)]
[final-mem (eval-cmd cmd func-mem)]
[ret (mem-lookup 'RETURN final-mem)])
(if ret
ret
(error "No return statement in function" name)))
(error "Arity mismatch, function" name "takes" (length func-args) "arguments, got" (length args)))]
[else (error "Undefined function" name)])]))
(define (assign-values args func-args mem) ;; <------ przypisanie wartosci do argumentow funkcji
(define (iter args func-args new-mem)
(if (null? args)
new-mem
(iter (cdr args) (cdr func-args) (mem-alloc (car func-args) (eval (car args) mem) new-mem))))
(iter args func-args mem))
(struct clo (args cmd)) ; <----- tak trzymana jest funkcja w środowisku, tj. jako lista nazw argumentow i cialo funkcji
;; zał: (cmd? c) (mem? m)
;; (mem? (eval-cmd c m))
(define (eval-cmd c m)
(if (mem-lookup 'RETURN m) ; <----- jeśli RETURN jest na coś ustawione, to chcemy zrwócic pamięc
m
(match c
[(skip) m]
[(assign x e) (mem-update x (eval e m) m)]
[(if-cmd e ct cf) (if (= (eval e m) 0)
(eval-cmd cf m)
(eval-cmd ct m))]
[(while e cw) (if (= (eval e m) 0)
m
(let* ((m1 (eval-cmd cw m))
(m2 (eval-cmd c m1)))
m2))]
[(comp c1 c2) (let* ((m1 (eval-cmd c1 m))
(m2 (eval-cmd c2 m1)))
m2)]
[(var-in x e c) (let* ((v (eval e m))
(m1 (mem-alloc x v m))
(m2 (eval-cmd c m1)))
(mem-drop-last m2))]
[(function name args cmd) ; <------ dodanie ciała funkcji do środowiska
(mem-alloc name (clo args cmd) m)]
[(return-stat val) ; <------ zmiana wartości zmiennej RETURN
(mem-update 'RETURN (eval val m) m)]
[_ (error "Unknown command" c "— likely a syntax error")])))
;; program ewaluowany jest tak
;; ewaluowane są wszystkie definicje funkcji, wtedy
;; ręcznie szukam definicji main i ewaluuje jej ciało i zwracam to co zwróci main.
;; zakładam, że main nie przyjmuje żadnych argumentów.
(define (eval-prog p m)
(let ((final-mem (eval-cmd p (mem-alloc 'RETURN #f m))))
(match (mem-lookup 'main final-mem)
[(clo args cmd)
(let ((res (mem-lookup 'RETURN (eval-cmd cmd final-mem))))
(if res res (error "No return statement in main")))])))
(define WHILE_FACT
'({func decr (x)
{(x := (- x 1))
(return x)}}
{func main ()
{(i := 1)
(while (> t 0)
{(i := (* i t))
(t := (call decr (t)))})
(return i)}}
))
(define (fact n)
(let* ([init-env (mem-alloc 'i 1 (mem-alloc 't n empty-mem))])
(eval-prog (parse-cmd WHILE_FACT) init-env)))
(define TEST
'({func decr (x) (return (- x 1))}
{func main ()
(var (x 1)
{(x := (+ x 2))
(return (call decr (x)))})}))
(define (test) (eval-prog (parse-cmd TEST) empty-mem))
(define TEST2
'({func decr (x) (return (- x 1))}
{func main () (return (call decr (3)))}))
(define (test2) (eval-prog (parse-cmd TEST2) empty-mem))
; nie da się zmienić wartości zmiennej globalnej, zmienne są wysyłane przez kopie
(define TEST3
'({func sth (x)
{(i := -1)
(return x)}}
{func main ()
{(i := 2)
(return (call sth (i)))}}))
(define (test3) (eval-prog (parse-cmd TEST3) (mem-alloc 'i 3 empty-mem)))
; nie ma maina, wywala błąd
(define TEST4
'(func f ()
{return 1}))
(define (test4) (eval-prog (parse-cmd TEST4) empty-mem))
; funkcje wieloargumentowe
(define TEST5
'({func f1 (x y z)
(return y)}
{func f2 (x y z)
(return (+ (+ x y) z))}
{func main ()
{(if (> X 3)
(var (x 2)
(return (call f1 (1 x 3))))
(x := 5))
(return (call f2 (x 3 4)))}}))
(define (test5) (eval-prog (parse-cmd TEST5) (mem-alloc 'x -1 (mem-alloc 'X 4 empty-mem))))
; Działa rekurencja!!
(define TEST6
'({func f (x)
(if (= x 0)
(return 1)
(return (* x (call f ((- x 1))))))}
{func main ()
(return (call f (X)))}))
(define (test6) (eval-prog (parse-cmd TEST6) (mem-alloc 'X 5 empty-mem)))
; kolejnośc deklaracji funkcji nie ma znaczenia, można zagnieżdżać funkcje
(define TEST7
'(
{func main ()
(return (call f (2)))}
{func f (x)
(return (call f1 (x)))}
{func f1 (x)
{{func local-fun (x)
(return (+ 1 x))}
(return (call local-fun (x)))}}))
(define (test7) (eval-prog (parse-cmd TEST7) empty-mem))
; instrukcje poza jakimikolwiek funkcjami sa wykonywane przed wywołaniem main
(define TEST8
'({func main ()
(return i)}
(i := 2)))
(define (test8) (eval-prog (parse-cmd TEST8) (mem-alloc 'i 1 empty-mem)))
; nic nie zwraca main, wywala błąd
(define TEST9
'(func main ()
(i := 1)))
(define (test9) (eval-prog (parse-cmd TEST9) (mem-alloc 'i 1 empty-mem)))
; return poza jakąkolwiek funkcją jest wynikiem programu
(define TEST10
'({func main ()
(return i)}
(i := 2)
(return -1)))
(define (test10) (eval-prog (parse-cmd TEST10) (mem-alloc 'i 1 empty-mem)))
; arity mismatch
(define TEST11
'({func main ()
(return (call decr ()))}
{func decr (x)
(return (- x 1))}))
(define (test11) (eval-prog (parse-cmd TEST11) empty-mem))
|