perl: Replace _clone() with a ->clone method.
[jackhill/mal.git] / perl / stepA_mal.pl
... / ...
CommitLineData
1use strict;
2use warnings FATAL => "recursion";
3no if $] >= 5.018, warnings => "experimental::smartmatch";
4use feature qw(switch);
5use File::Basename;
6use lib dirname (__FILE__);
7
8use Data::Dumper;
9use List::Util qw(pairs pairmap);
10use Scalar::Util qw(blessed);
11
12use readline qw(mal_readline set_rl_mode);
13use types qw($nil $true $false _symbol_Q _list_Q);
14use reader;
15use printer;
16use env;
17use core;
18
19# read
20sub READ {
21 my $str = shift;
22 return reader::read_str($str);
23}
24
25# eval
26sub is_pair {
27 my ($x) = @_;
28 return $x->isa('Mal::Sequence') && @$x;
29}
30
31sub quasiquote {
32 my ($ast) = @_;
33 if (!is_pair($ast)) {
34 return Mal::List->new([Mal::Symbol->new("quote"), $ast]);
35 } elsif (_symbol_Q($ast->[0]) && ${$ast->[0]} eq 'unquote') {
36 return $ast->[1];
37 } elsif (is_pair($ast->[0]) && _symbol_Q($ast->[0]->[0]) &&
38 ${$ast->[0]->[0]} eq 'splice-unquote') {
39 return Mal::List->new([Mal::Symbol->new("concat"),
40 $ast->[0]->[1],
41 quasiquote($ast->rest())]);
42 } else {
43 return Mal::List->new([Mal::Symbol->new("cons"),
44 quasiquote($ast->[0]),
45 quasiquote($ast->rest())]);
46 }
47}
48
49sub is_macro_call {
50 my ($ast, $env) = @_;
51 if (_list_Q($ast) &&
52 _symbol_Q($ast->[0]) &&
53 $env->find($ast->[0])) {
54 my ($f) = $env->get($ast->[0]);
55 if ($f->isa('Mal::Function')) {
56 return $f->{ismacro};
57 }
58 }
59 return 0;
60}
61
62sub macroexpand {
63 my ($ast, $env) = @_;
64 while (is_macro_call($ast, $env)) {
65 my @args = @$ast;
66 my $mac = $env->get(shift @args);
67 $ast = &$mac(@args);
68 }
69 return $ast;
70}
71
72
73sub eval_ast {
74 my($ast, $env) = @_;
75 if ($ast->isa('Mal::Symbol')) {
76 return $env->get($ast);
77 } elsif ($ast->isa('Mal::Sequence')) {
78 return ref($ast)->new([ map { EVAL($_, $env) } @$ast ]);
79 } elsif ($ast->isa('Mal::HashMap')) {
80 return Mal::HashMap->new({ pairmap { $a => EVAL($b, $env) } %$ast });
81 } else {
82 return $ast;
83 }
84}
85
86sub EVAL {
87 my($ast, $env) = @_;
88
89 while (1) {
90
91 #print "EVAL: " . printer::_pr_str($ast) . "\n";
92 if (! _list_Q($ast)) {
93 return eval_ast($ast, $env);
94 }
95 @$ast or return $ast;
96
97 # apply list
98 $ast = macroexpand($ast, $env);
99 if (! _list_Q($ast)) {
100 return eval_ast($ast, $env);
101 }
102
103 my ($a0, $a1, $a2, $a3) = @$ast;
104 if (!$a0) { return $ast; }
105 given ($a0->isa('Mal::Symbol') ? $$a0 : $a0) {
106 when ('def!') {
107 my $res = EVAL($a2, $env);
108 return $env->set($a1, $res);
109 }
110 when ('let*') {
111 my $let_env = Mal::Env->new($env);
112 foreach my $pair (pairs @$a1) {
113 my ($k, $v) = @$pair;
114 $let_env->set($k, EVAL($v, $let_env));
115 }
116 $ast = $a2;
117 $env = $let_env;
118 # Continue loop (TCO)
119 }
120 when ('quote') {
121 return $a1;
122 }
123 when ('quasiquote') {
124 $ast = quasiquote($a1);
125 # Continue loop (TCO)
126 }
127 when ('defmacro!') {
128 my $func = EVAL($a2, $env)->clone;
129 $func->{ismacro} = 1;
130 return $env->set($a1, $func);
131 }
132 when ('macroexpand') {
133 return macroexpand($a1, $env);
134 }
135 when ('try*') {
136 local $@;
137 my $ret = eval { EVAL($a1, $env) };
138 return $ret unless $@;
139 if ($a2 && ${$a2->[0]} eq 'catch*') {
140 my $exc;
141 if (defined(blessed $@) && $@->isa('Mal::Type')) {
142 $exc = $@;
143 } else {
144 chomp(my $msg = $@);
145 $exc = Mal::String->new($msg);
146 }
147 my $catch_env = Mal::Env->new($env, [$a2->[1]], [$exc]);
148 return EVAL($a2->[2], $catch_env)
149 } else {
150 die $@;
151 }
152 }
153 when ('do') {
154 eval_ast($ast->slice(1, $#$ast-1), $env);
155 $ast = $ast->[$#$ast];
156 # Continue loop (TCO)
157 }
158 when ('if') {
159 my $cond = EVAL($a1, $env);
160 if ($cond eq $nil || $cond eq $false) {
161 $ast = $a3 ? $a3 : $nil;
162 } else {
163 $ast = $a2;
164 }
165 # Continue loop (TCO)
166 }
167 when ('fn*') {
168 return Mal::Function->new(\&EVAL, $a2, $env, $a1);
169 }
170 default {
171 my @el = @{eval_ast($ast, $env)};
172 my $f = shift @el;
173 if ($f->isa('Mal::Function')) {
174 $ast = $f->{ast};
175 $env = $f->gen_env(\@el);
176 # Continue loop (TCO)
177 } else {
178 return &$f(@el);
179 }
180 }
181 }
182
183 } # TCO while loop
184}
185
186# print
187sub PRINT {
188 my $exp = shift;
189 return printer::_pr_str($exp);
190}
191
192# repl
193my $repl_env = Mal::Env->new();
194sub REP {
195 my $str = shift;
196 return PRINT(EVAL(READ($str), $repl_env));
197}
198
199# core.pl: defined using perl
200foreach my $n (keys %core::ns) {
201 $repl_env->set(Mal::Symbol->new($n), $core::ns{$n});
202}
203$repl_env->set(Mal::Symbol->new('eval'),
204 bless sub { EVAL($_[0], $repl_env); }, 'Mal::CoreFunction');
205my @_argv = map {Mal::String->new($_)} @ARGV[1..$#ARGV];
206$repl_env->set(Mal::Symbol->new('*ARGV*'), Mal::List->new(\@_argv));
207
208# core.mal: defined using the language itself
209REP(q[(def! *host-language* "perl")]);
210REP(q[(def! not (fn* (a) (if a false true)))]);
211REP(q[(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) "\nnil)")))))]);
212REP(q[(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw "odd number of forms to cond")) (cons 'cond (rest (rest xs)))))))]);
213
214if (@ARGV && $ARGV[0] eq "--raw") {
215 set_rl_mode("raw");
216 shift @ARGV;
217}
218if (@ARGV) {
219 REP(qq[(load-file "$ARGV[0]")]);
220 exit 0;
221}
222REP(q[(println (str "Mal [" *host-language* "]"))]);
223while (1) {
224 my $line = mal_readline("user> ");
225 if (! defined $line) { last; }
226 do {
227 local $@;
228 my $ret;
229 eval {
230 print(REP($line), "\n");
231 1;
232 } or do {
233 my $err = $@;
234 if (defined(blessed $err) && $err->isa('Mal::BlankException')) {
235 # ignore and continue
236 } elsif (defined(blessed $err) && $err->isa('Mal::Type')) {
237 print "Error: ".printer::_pr_str($err)."\n";
238 } else {
239 chomp $err;
240 print "Error: $err\n";
241 }
242 };
243 };
244}