Add Perl 6 implementation
authorHinrik Örn Sigurðsson <hinrik.sig@gmail.com>
Mon, 6 Jun 2016 21:11:29 +0000 (21:11 +0000)
committerHinrik Örn Sigurðsson <hinrik.sig@gmail.com>
Sat, 11 Jun 2016 15:02:06 +0000 (15:02 +0000)
All tests pass, but readline support (via Linenoise module) is commented
out in step0_repl.pl as it is not a core module. Should maybe change it
when docker support is added.

21 files changed:
.gitignore
Makefile
README.md
perl6/Makefile [new file with mode: 0644]
perl6/core.pm [new file with mode: 0644]
perl6/env.pm [new file with mode: 0644]
perl6/printer.pm [new file with mode: 0644]
perl6/reader.pm [new file with mode: 0644]
perl6/run [new file with mode: 0755]
perl6/step0_repl.pl [new file with mode: 0644]
perl6/step1_read_print.pl [new file with mode: 0644]
perl6/step2_eval.pl [new file with mode: 0644]
perl6/step3_env.pl [new file with mode: 0644]
perl6/step4_if_fn_do.pl [new file with mode: 0644]
perl6/step5_tco.pl [new file with mode: 0644]
perl6/step6_file.pl [new file with mode: 0644]
perl6/step7_quote.pl [new file with mode: 0644]
perl6/step8_macros.pl [new file with mode: 0644]
perl6/step9_try.pl [new file with mode: 0644]
perl6/stepA_mal.pl [new file with mode: 0644]
perl6/types.pm [new file with mode: 0644]

index 6f2d57e..aab935d 100644 (file)
@@ -87,6 +87,7 @@ objpascal/*.ppu
 objpascal/pas-readline
 objpascal/regexpr/Source/RegExpr.ppu
 perl/mal.pl
+perl6/.precomp/
 php/mal.php
 ps/mal.ps
 python/mal.pyz
index 6433e44..3556e5a 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -65,6 +65,7 @@ mal_TEST_OPTS = --start-timeout 60 --test-timeout 120
 miniMAL_TEST_OPTS = --start-timeout 60 --test-timeout 120
 plpgsql_TEST_OPTS = --start-timeout 60 --test-timeout 180
 plsql_TEST_OPTS = --start-timeout 120 --test-timeout 120
+perl6_TEST_OPTS = --test-timeout=60
 
 DOCKERIZE=
 
@@ -78,8 +79,8 @@ DOCKERIZE =
 IMPLS = ada awk bash c d clojure coffee cpp crystal cs erlang elisp \
        elixir es6 factor forth fsharp go groovy guile haskell haxe \
        io java julia js kotlin lua make mal ocaml matlab miniMAL \
-       nim objc objpascal perl php plpgsql plsql ps python r racket \
-       rpython ruby rust scala swift swift3 tcl vb vhdl vimscript
+       nim objc objpascal perl perl6 php plpgsql plsql ps python r \
+       racket rpython ruby rust scala swift swift3 tcl vb vhdl vimscript
 
 step0 = step0_repl
 step1 = step1_read_print
@@ -174,6 +175,7 @@ nim_STEP_TO_PROG =     nim/$($(1))
 objc_STEP_TO_PROG =    objc/$($(1))
 objpascal_STEP_TO_PROG = objpascal/$($(1))
 perl_STEP_TO_PROG =    perl/$($(1)).pl
+perl6_STEP_TO_PROG =   perl6/$($(1)).pl
 php_STEP_TO_PROG =     php/$($(1)).php
 plpgsql_STEP_TO_PROG = plpgsql/$($(1)).sql
 plsql_STEP_TO_PROG =   plsql/$($(1)).sql
index eabc0e0..42e084a 100644 (file)
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
 
 Mal is a Clojure inspired Lisp interpreter.
 
-Mal is implemented in 54 languages:
+Mal is implemented in 55 languages:
 
 * Ada
 * GNU awk
@@ -45,6 +45,7 @@ Mal is implemented in 54 languages:
 * Objective C
 * OCaml
 * Perl
+* Perl 6
 * PHP
 * PL/pgSQL (Postgres)
 * PL/SQL (Oracle)
@@ -564,6 +565,16 @@ cd perl
 perl stepX_YYY.pl
 ```
 
+### Perl 6
+
+*The Perl 6 implementation was created by [Hinrik Örn Sigurðsson](https://github.com/hinrik)*
+
+The Perl 6 implementation was tested on Rakudo Perl 6 2016.04.
+
+```
+cd perl6
+perl6 stepX_YYY.pl
+```
 
 ### PHP 5.3
 
diff --git a/perl6/Makefile b/perl6/Makefile
new file mode 100644 (file)
index 0000000..09f99ce
--- /dev/null
@@ -0,0 +1,2 @@
+all:
+       @true
diff --git a/perl6/core.pm b/perl6/core.pm
new file mode 100644 (file)
index 0000000..04e92ea
--- /dev/null
@@ -0,0 +1,84 @@
+unit module core;
+use types;
+use printer;
+use reader;
+
+sub equal ($a, $b) {
+  if $a ~~ MalSequence && $b ~~ MalSequence {
+    return $FALSE if $a.elems != $b.elems;
+    for |$a Z |$b -> ($a_el, $b_el) {
+      return $FALSE if equal($a_el, $b_el) ~~ $FALSE;
+    }
+    return $TRUE;
+  }
+  elsif $a ~~ MalHashMap && $b ~~ MalHashMap {
+    return $FALSE if $a.elems != $b.elems;
+    for $a.pairs {
+      return $FALSE if !$b{.key} || equal(.value, $b{.key}) ~~ $FALSE;
+    }
+    return $TRUE;
+  }
+  else {
+    return $a.^name eq $b.^name && $a.val ~~ $b.val ?? $TRUE !! $FALSE;
+  }
+}
+
+our %ns = (
+  '+'         => MalCode({ MalNumber($^a.val + $^b.val) }),
+  '-'         => MalCode({ MalNumber($^a.val - $^b.val) }),
+  '*'         => MalCode({ MalNumber($^a.val * $^b.val) }),
+  '/'         => MalCode({ MalNumber(($^a.val / $^b.val).Int) }),
+  '<'         => MalCode({ $^a.val < $^b.val ?? $TRUE !! $FALSE }),
+  '<='        => MalCode({ $^a.val <= $^b.val ?? $TRUE !! $FALSE }),
+  '>'         => MalCode({ $^a.val > $^b.val ?? $TRUE !! $FALSE }),
+  '>='        => MalCode({ $^a.val >= $^b.val ?? $TRUE !! $FALSE }),
+  '='         => MalCode({ equal($^a, $^b) }),
+  prn         => MalCode({ say @_.map({ pr_str($_, True) }).join(' '); $NIL }),
+  println     => MalCode({ say @_.map({ pr_str($_) }).join(' '); $NIL }),
+  pr-str      => MalCode({ MalString(@_.map({ pr_str($_, True) }).join(' ') ) }),
+  str         => MalCode({ MalString(@_.map({ pr_str($_) }).join) }),
+  read-string => MalCode({ read_str($^a.val) }),
+  slurp       => MalCode({ MalString($^a.val.IO.slurp) }),
+  list        => MalCode({ MalList(@_) }),
+  'list?'     => MalCode({ $^a ~~ MalList ?? $TRUE !! $FALSE }),
+  'empty?'    => MalCode({ $^a.elems ?? $FALSE !! $TRUE }),
+  count       => MalCode({ MalNumber($^a ~~ $NIL ?? 0 !! $^a.elems) }),
+  atom        => MalCode({ MalAtom($^a) }),
+  'atom?'     => MalCode({ $^a ~~ MalAtom ?? $TRUE !! $FALSE }),
+  deref       => MalCode({ $^a.val }),
+  'reset!'    => MalCode({ $^a.val = $^b }),
+  'swap!'     => MalCode(-> $atom, $func, *@args { $atom.val = $func.apply($atom.val, |@args) }),
+  cons        => MalCode({ MalList([$^a, |$^b.val]) }),
+  concat      => MalCode({ MalList([@_.map({|$_.val})]) }),
+  nth         => MalCode({ $^a[$^b.val] // die X::MalOutOfRange.new }),
+  first       => MalCode({ $^a[0] // $NIL }),
+  rest        => MalCode({ MalList([$^a[1..*]]) }),
+  throw       => MalCode({ die X::MalThrow.new(value => $^a) }),
+  apply       => MalCode(-> $func, *@args { $func.apply(|@args[0..*-2], |@args[*-1].val) }),
+  map         => MalCode(-> $func, $list { MalList([$list.map({ $func.apply($_) })]) }),
+  'nil?'      => MalCode({ $^a ~~ MalNil ?? $TRUE !! $FALSE }),
+  'true?'     => MalCode({ $^a ~~ MalTrue ?? $TRUE !! $FALSE }),
+  'false?'    => MalCode({ $^a ~~ MalFalse ?? $TRUE !! $FALSE }),
+  'symbol?'   => MalCode({ $^a ~~ MalSymbol ?? $TRUE !! $FALSE }),
+  symbol      => MalCode({ MalSymbol($^a.val) }),
+  keyword     => MalCode({ $^a.val ~~ /^\x29E/ ?? $^a !! MalString("\x29E" ~ $^a.val) }),
+  'keyword?'  => MalCode({ $^a.val ~~ /^\x29E/ ?? $TRUE !! $FALSE }),
+  vector      => MalCode({ MalVector(@_) }),
+  'vector?'   => MalCode({ $^a ~~ MalVector ?? $TRUE !! $FALSE }),
+  hash-map    => MalCode({ MalHashMap(@_.map({ $^a.val => $^b }).Hash) }),
+  'map?'      => MalCode({ $^a ~~ MalHashMap ?? $TRUE !! $FALSE }),
+  assoc       => MalCode(-> $map, *@kv { MalHashMap(Hash.new(|$map.kv, |@kv.map({$^a.val, $^b}))) }),
+  dissoc      => MalCode(-> $map, *@keys { my %h = $map.val.clone; %h{@keys.map(*.val)}:delete; MalHashMap(%h) }),
+  get         => MalCode({ $^a.val{$^b.val} // $NIL }),
+  'contains?' => MalCode({ $^a.val{$^b.val}:exists ?? $TRUE !! $FALSE }),
+  keys        => MalCode({ MalList([$^a.keys.map({ MalString($_) })]) }),
+  vals        => MalCode({ MalList([$^a.values]) }),
+  'sequential?' => MalCode({ $^a ~~ MalList|MalVector ?? $TRUE !! $FALSE }),
+  readline    => MalCode({ with prompt($^a.val) { MalString($_) } else { $NIL } }),
+  time-ms     => MalCode({ MalNumber((now * 1000).Int) }),
+  conj        => MalCode(-> $seq, *@args { $seq.conj(@args) }),
+  'string?'   => MalCode({ $^a ~~ MalString && $^a.val !~~ /^\x29E/ ?? $TRUE !! $FALSE }),
+  seq         => MalCode({ $^a.seq }),
+  with-meta   => MalCode({ return $NIL if !$^a.can('meta'); my $x = $^a.clone; $x.meta = $^b; $x }),
+  meta        => MalCode({ $^a.?meta // $NIL }),
+);
diff --git a/perl6/env.pm b/perl6/env.pm
new file mode 100644 (file)
index 0000000..c0f4837
--- /dev/null
@@ -0,0 +1,36 @@
+unit class MalEnv;
+use types;
+
+has $.outer;
+has %.data;
+has @.binds;
+has @.exprs;
+
+method new ($outer?, @binds?, @exprs?) {
+  self.bless(:$outer, :@binds, :@exprs);
+}
+
+submethod BUILD (:@!binds, :@!exprs, :$!outer, :%!data) {
+  for @!binds.kv -> $idx, $key {
+    if $key eq '&' {
+      my $value = MalList([@!exprs[$idx..*]]);
+      self.set(@!binds[$idx+1], $value);
+      last;
+    }
+    my $value = @!exprs[$idx];
+    self.set($key, $value);
+  }
+}
+
+method set ($key, $value) {
+  %.data{$key} = $value;
+}
+
+method find ($key) {
+  return %.data{$key} ?? self !! $.outer && $.outer.find($key);
+}
+
+method get ($key) {
+  my $env = self.find($key) or die X::MalNotFound.new(name => $key);
+  return $env.data{$key};
+}
diff --git a/perl6/printer.pm b/perl6/printer.pm
new file mode 100644 (file)
index 0000000..0872f86
--- /dev/null
@@ -0,0 +1,29 @@
+unit module printer;
+use types;
+
+sub pr_str ($exp, $print_readably = False) is export {
+  given $exp {
+    when MalFunction { "#<fn* ({$exp.params}) {pr_str($exp.ast)}>" }
+    when MalCode { "#<builtin_fn* {$exp.fn.gist}>" }
+    when MalList {
+      '(' ~ join(' ', |$exp.map({ pr_str($_, $print_readably) })) ~ ')';
+    }
+    when MalVector {
+      '[' ~ join(' ', |$exp.map({ pr_str($_, $print_readably) })) ~ ']';
+    }
+    when MalHashMap {
+      '{' ~ $exp.kv.flatmap({ MalString($^a), $^b }).map({ pr_str($_, $print_readably) }) ~ '}'
+    }
+    when MalString {
+      my $str = $exp.val;
+      if $str ~~ s/^\x29E/:/ || !$print_readably {
+        $str;
+      }
+      else {
+        '"' ~ $str.trans(/\\/ => '\\\\', /\"/ => '\\"', /\n/ => '\\n') ~ '"';
+      }
+    }
+    when MalAtom { "(atom {pr_str($exp.val, $print_readably)})" }
+    when MalValue { $exp.val }
+  }
+}
diff --git a/perl6/reader.pm b/perl6/reader.pm
new file mode 100644 (file)
index 0000000..6f89a43
--- /dev/null
@@ -0,0 +1,84 @@
+unit module reader;
+use types;
+
+class Reader {
+  has @.tokens;
+  has $!position = 0;
+  method peek { @.tokens[$!position] }
+  method next { @.tokens[$!position++] }
+}
+
+sub read_form ($rdr) {
+  given $rdr.peek {
+    when "'"  { $rdr.next; MalList([MalSymbol('quote'), read_form($rdr)]) }
+    when '`'  { $rdr.next; MalList([MalSymbol('quasiquote'), read_form($rdr)]) }
+    when '~'  { $rdr.next; MalList([MalSymbol('unquote'), read_form($rdr)]) }
+    when '~@' { $rdr.next; MalList([MalSymbol('splice-unquote'), read_form($rdr)]) }
+    when '@'  { $rdr.next; MalList([MalSymbol('deref'), read_form($rdr)]) }
+    when '^'  {
+      $rdr.next;
+      my $meta = read_form($rdr);
+      MalList([MalSymbol('with-meta'), read_form($rdr), $meta]);
+    }
+    when ')'|']'|'}' { die X::MalUnexpected.new(token => $_) }
+    when '(' { MalList(read_list($rdr, ')')) }
+    when '[' { MalVector(read_list($rdr, ']')) }
+    when '{' { MalHashMap(read_list($rdr, '}').map({ $^a.val => $^b }).Hash) }
+    default  { read_atom($rdr) }
+  }
+}
+
+sub read_list ($rdr, $end) {
+  my @list;
+  my $token = $rdr.next;
+
+  while ($token = $rdr.peek).defined {
+    last if $token eq $end;
+    die X::MalIncomplete.new(end => $end) if !$token.defined;
+    @list.push(read_form($rdr));
+  }
+  $rdr.next;
+
+  return @list;
+}
+
+sub read_atom ($rdr) {
+  my $atom = $rdr.next;
+  given $atom {
+    when /^\"/ {
+      die X::MalIncomplete.new(end => '"') if $atom !~~ /\"$/;
+      s:g/^\"|\"$//;
+      MalString(.trans(/\\\"/ => '"', /\\n/ => "\n", /\\\\/ => '\\'));
+    }
+    when /^\:(.*)/ { MalString("\x29E$0") }
+    when /^'-'? <[0..9]>+$/ { MalNumber($_) }
+    when 'nil' { $NIL }
+    when 'true' { $TRUE }
+    when 'false' { $FALSE }
+    default { MalSymbol($_) }
+  }
+}
+
+my regex mal {
+  [
+    <[\s,]>*                          # whitespace/commas
+    $<token>=(
+    || '~@'                           # ~@
+    || <[\[\]{}()'`~^@]>              # special single-char tokens
+    || '"' [ \\. || <-[\"\\]> ]* '"'? # double-quoted strings
+    || ';'<-[\n]>*                    # comments
+    || <-[\s\[\]{}('"`,;)]>+          # symbols
+    )
+  ]+
+}
+
+sub tokenizer ($str) {
+  return [] if !$str.match(/^<mal>/);
+  return grep { ! /^\;/ }, $<mal><token>.map({~$_});
+}
+
+sub read_str ($str) is export {
+  my @tokens = tokenizer($str);
+  die X::MalNoTokens.new if !@tokens;
+  return read_form(Reader.new(tokens => @tokens));
+}
diff --git a/perl6/run b/perl6/run
new file mode 100755 (executable)
index 0000000..d22ca7b
--- /dev/null
+++ b/perl6/run
@@ -0,0 +1,2 @@
+#!/bin/bash
+exec perl6 $(dirname $0)/${STEP:-stepA_mal}.pl "${@}"
diff --git a/perl6/step0_repl.pl b/perl6/step0_repl.pl
new file mode 100644 (file)
index 0000000..b502b58
--- /dev/null
@@ -0,0 +1,27 @@
+use v6;
+#use Linenoise;
+
+sub read ($str) {
+  return $str;
+}
+
+sub eval ($ast) {
+  return $ast;
+}
+
+sub print ($exp) {
+  return $exp;
+}
+
+sub rep ($str) {
+  return print(eval(read($str)));
+}
+
+sub MAIN {
+  #while (my $line = linenoise('user> ')).defined {
+  #  say rep($line);
+  #}
+  while (my $line = prompt 'user> ').defined {
+    say rep($line);
+  }
+}
diff --git a/perl6/step1_read_print.pl b/perl6/step1_read_print.pl
new file mode 100644 (file)
index 0000000..f8f6deb
--- /dev/null
@@ -0,0 +1,30 @@
+use v6;
+use lib IO::Path.new($?FILE).dirname;
+use reader;
+use printer;
+use types;
+
+sub read ($str) {
+  return read_str($str);
+}
+
+sub eval ($ast) {
+  return $ast;
+}
+
+sub print ($exp) {
+  return pr_str($exp, True);
+}
+
+sub rep ($str) {
+  return print(eval(read($str)));
+}
+
+sub MAIN {
+  while (my $line = prompt 'user> ').defined {
+    say rep($line);
+    CATCH {
+      when X::MalException { .Str.say }
+    }
+  }
+}
diff --git a/perl6/step2_eval.pl b/perl6/step2_eval.pl
new file mode 100644 (file)
index 0000000..a2d010f
--- /dev/null
@@ -0,0 +1,52 @@
+use v6;
+use lib IO::Path.new($?FILE).dirname;
+use reader;
+use printer;
+use types;
+
+sub read ($str) {
+  return read_str($str);
+}
+
+sub eval_ast ($ast, $env) {
+  given $ast {
+    when MalSymbol  { $env{$ast.val} || die X::MalNotFound.new(name => $ast.val) }
+    when MalList    { MalList([$ast.map({ eval($_, $env) })]) }
+    when MalVector  { MalVector([$ast.map({ eval($_, $env) })]) }
+    when MalHashMap { MalHashMap($ast.kv.map({ $^a => eval($^b, $env) }).Hash) }
+    default         { $ast // $NIL }
+  }
+}
+
+sub eval ($ast, $env) {
+  return eval_ast($ast, $env) if $ast !~~ MalList;
+  return $ast if !$ast.elems;
+
+  my ($func, @args) = eval_ast($ast, $env).val;
+  my $arglist = MalList(@args);
+  return $func.apply($arglist);
+}
+
+sub print ($exp) {
+  return pr_str($exp, True);
+}
+
+my $repl_env;
+
+sub rep ($str) {
+  return print(eval(read($str), $repl_env));
+}
+
+sub MAIN {
+  $repl_env<+> = MalCode({ MalNumber($^a[0].val + $^a[1].val) });
+  $repl_env<-> = MalCode({ MalNumber($^a[0].val - $^a[1].val) });
+  $repl_env<*> = MalCode({ MalNumber($^a[0].val * $^a[1].val) });
+  $repl_env</> = MalCode({ MalNumber(($^a[0].val / $^a[1].val).Int) });
+
+  while (my $line = prompt 'user> ').defined {
+    say rep($line);
+    CATCH {
+      when X::MalException { .Str.say }
+    }
+  }
+}
diff --git a/perl6/step3_env.pl b/perl6/step3_env.pl
new file mode 100644 (file)
index 0000000..2730211
--- /dev/null
@@ -0,0 +1,67 @@
+use v6;
+use lib IO::Path.new($?FILE).dirname;
+use reader;
+use printer;
+use types;
+use env;
+
+sub read ($str) {
+  return read_str($str);
+}
+
+sub eval_ast ($ast, $env) {
+  given $ast {
+    when MalSymbol  { $env.get($ast.val) || die X::MalNotFound.new(name => $ast.val) }
+    when MalList    { MalList([$ast.map({ eval($_, $env) })]) }
+    when MalVector  { MalVector([$ast.map({ eval($_, $env) })]) }
+    when MalHashMap { MalHashMap($ast.kv.map({ $^a => eval($^b, $env) }).Hash) }
+    default         { $ast // $NIL }
+  }
+}
+
+sub eval ($ast, $env) {
+  return eval_ast($ast, $env) if $ast !~~ MalList;
+  return $ast if !$ast.elems;
+
+  my ($a0, $a1, $a2, $a3) = $ast.val;
+  given $a0.val {
+    when 'def!' {
+      return $env.set($a1.val, eval($a2, $env));
+    }
+    when 'let*' {
+      my $new_env = MalEnv.new($env);
+      for |$a1.val -> $key, $value {
+        $new_env.set($key.val, eval($value, $new_env));
+      }
+      return eval($a2, $new_env);
+    }
+    default {
+      my ($func, @args) = eval_ast($ast, $env).val;
+      return $func.apply(|@args);
+    }
+  }
+}
+
+sub print ($exp) {
+  return pr_str($exp, True);
+}
+
+my $repl_env = MalEnv.new;
+
+sub rep ($str) {
+  return print(eval(read($str), $repl_env));
+}
+
+sub MAIN {
+  $repl_env.set('+', MalCode({ MalNumber($^a.val + $^b.val) }));
+  $repl_env.set('-', MalCode({ MalNumber($^a.val - $^b.val) }));
+  $repl_env.set('*', MalCode({ MalNumber($^a.val * $^b.val) }));
+  $repl_env.set('/', MalCode({ MalNumber(($^a.val / $^b.val).Int) }));
+
+  while (my $line = prompt 'user> ').defined {
+    say rep($line);
+    CATCH {
+      when X::MalException { .Str.say }
+    }
+  }
+}
diff --git a/perl6/step4_if_fn_do.pl b/perl6/step4_if_fn_do.pl
new file mode 100644 (file)
index 0000000..0aa8d61
--- /dev/null
@@ -0,0 +1,80 @@
+use v6;
+use lib IO::Path.new($?FILE).dirname;
+use reader;
+use printer;
+use types;
+use env;
+use core;
+
+sub read ($str) {
+  return read_str($str);
+}
+
+sub eval_ast ($ast, $env) {
+  given $ast {
+    when MalSymbol  { $env.get($ast.val) || die X::MalNotFound.new(name => $ast.val) }
+    when MalList    { MalList([$ast.map({ eval($_, $env) })]) }
+    when MalVector  { MalVector([$ast.map({ eval($_, $env) })]) }
+    when MalHashMap { MalHashMap($ast.kv.map({ $^a => eval($^b, $env) }).Hash) }
+    default         { $ast // $NIL }
+  }
+}
+
+sub eval ($ast, $env) {
+  return eval_ast($ast, $env) if $ast !~~ MalList;
+  return $ast if !$ast.elems;
+
+  my ($a0, $a1, $a2, $a3) = $ast.val;
+  given $a0.val {
+    when 'def!' {
+      return $env.set($a1.val, eval($a2, $env));
+    }
+    when 'let*' {
+      my $new_env = MalEnv.new($env);
+      for |$a1.val -> $key, $value {
+        $new_env.set($key.val, eval($value, $new_env));
+      }
+      return eval($a2, $new_env);
+    }
+    when 'do' {
+      return eval_ast(MalList([$ast[1..*]]), $env)[*-1];
+    }
+    when 'if' {
+      return eval($a1, $env) !~~ MalNil|MalFalse
+        ?? return eval($a2, $env)
+        !! return $a3 ?? eval($a3, $env) !! $NIL;
+    }
+    when 'fn*' {
+      return MalCode(-> *@args {
+        my @binds = $a1 ?? $a1.map(*.val) !! ();
+        eval($a2, MalEnv.new($env, @binds, @args));
+      });
+    }
+    default {
+      my ($func, @args) = eval_ast($ast, $env).val;
+      return $func.apply(|@args);
+    }
+  }
+}
+
+sub print ($exp) {
+  return pr_str($exp, True);
+}
+
+my $repl_env = MalEnv.new;
+
+sub rep ($str) {
+  return print(eval(read($str), $repl_env));
+}
+
+sub MAIN {
+  $repl_env.set(.key, .value) for %core::ns;
+  rep(q{(def! not (fn* (a) (if a false true)))});
+
+  while (my $line = prompt 'user> ').defined {
+    say rep($line);
+    CATCH {
+      when X::MalException { .Str.say }
+    }
+  }
+}
diff --git a/perl6/step5_tco.pl b/perl6/step5_tco.pl
new file mode 100644 (file)
index 0000000..7e7cbb7
--- /dev/null
@@ -0,0 +1,91 @@
+use v6;
+use lib IO::Path.new($?FILE).dirname;
+use reader;
+use printer;
+use types;
+use env;
+use core;
+
+sub read ($str) {
+  return read_str($str);
+}
+
+sub eval_ast ($ast, $env) {
+  given $ast {
+    when MalSymbol  { $env.get($ast.val) || die X::MalNotFound.new(name => $ast.val) }
+    when MalList    { MalList([$ast.map({ eval($_, $env) })]) }
+    when MalVector  { MalVector([$ast.map({ eval($_, $env) })]) }
+    when MalHashMap { MalHashMap($ast.kv.map({ $^a => eval($^b, $env) }).Hash) }
+    default         { $ast // $NIL }
+  }
+}
+
+sub eval ($ast is copy, $env is copy) {
+  loop {
+    return eval_ast($ast, $env) if $ast !~~ MalList;
+    return $ast if !$ast.elems;
+
+    my ($a0, $a1, $a2, $a3) = $ast.val;
+    given $a0.val {
+      when 'def!' {
+        return $env.set($a1.val, eval($a2, $env));
+      }
+      when 'let*' {
+        my $new_env = MalEnv.new($env);
+        for |$a1.val -> $key, $value {
+          $new_env.set($key.val, eval($value, $new_env));
+        }
+        $env = $new_env;
+        $ast = $a2;
+      }
+      when 'do' {
+        eval_ast(MalList([$ast[1..*-2]]), $env);
+        $ast = $ast[*-1];
+      }
+      when 'if' {
+        if eval($a1, $env) ~~ MalNil|MalFalse {
+          return $NIL if $a3 ~~ $NIL;
+          $ast = $a3;
+        }
+        else {
+          $ast = $a2;
+        }
+      }
+      when 'fn*' {
+        my @binds = $a1 ?? $a1.map(*.val) !! ();
+        my &fn = -> *@args {
+          eval($a2, MalEnv.new($env, @binds, @args));
+        };
+        return MalFunction($a2, $env, @binds, &fn);
+      }
+      default {
+        my ($func, @args) = eval_ast($ast, $env).val;
+        return $func.apply(|@args) if $func !~~ MalFunction;
+        $ast = $func.ast;
+        $env = MalEnv.new($func.env, $func.params, @args);
+      }
+    }
+  }
+}
+
+sub print ($exp) {
+  return pr_str($exp, True);
+}
+
+my $repl_env = MalEnv.new;
+
+sub rep ($str) {
+  return print(eval(read($str), $repl_env));
+}
+
+sub MAIN {
+  $repl_env.set(.key, .value) for %core::ns;
+  rep(q{(def! not (fn* (a) (if a false true)))});
+
+  while (my $line = prompt 'user> ').defined {
+    say rep($line);
+    CATCH {
+      when X::MalException { .Str.say }
+    }
+  }
+}
diff --git a/perl6/step6_file.pl b/perl6/step6_file.pl
new file mode 100644 (file)
index 0000000..7a9c198
--- /dev/null
@@ -0,0 +1,99 @@
+use v6;
+use lib IO::Path.new($?FILE).dirname;
+use reader;
+use printer;
+use types;
+use env;
+use core;
+
+sub read ($str) {
+  return read_str($str);
+}
+
+sub eval_ast ($ast, $env) {
+  given $ast {
+    when MalSymbol  { $env.get($ast.val) || die X::MalNotFound.new(name => $ast.val) }
+    when MalList    { MalList([$ast.map({ eval($_, $env) })]) }
+    when MalVector  { MalVector([$ast.map({ eval($_, $env) })]) }
+    when MalHashMap { MalHashMap($ast.kv.map({ $^a => eval($^b, $env) }).Hash) }
+    default         { $ast // $NIL }
+  }
+}
+
+sub eval ($ast is copy, $env is copy) {
+  loop {
+    return eval_ast($ast, $env) if $ast !~~ MalList;
+    return $ast if !$ast.elems;
+
+    my ($a0, $a1, $a2, $a3) = $ast.val;
+    given $a0.val {
+      when 'def!' {
+        return $env.set($a1.val, eval($a2, $env));
+      }
+      when 'let*' {
+        my $new_env = MalEnv.new($env);
+        for |$a1.val -> $key, $value {
+          $new_env.set($key.val, eval($value, $new_env));
+        }
+        $env = $new_env;
+        $ast = $a2;
+      }
+      when 'do' {
+        eval_ast(MalList([$ast[1..*-2]]), $env);
+        $ast = $ast[*-1];
+      }
+      when 'if' {
+        if eval($a1, $env) ~~ MalNil|MalFalse {
+          return $NIL if $a3 ~~ $NIL;
+          $ast = $a3;
+        }
+        else {
+          $ast = $a2;
+        }
+      }
+      when 'fn*' {
+        my @binds = $a1 ?? $a1.map(*.val) !! ();
+        my &fn = -> *@args {
+          eval($a2, MalEnv.new($env, @binds, @args));
+        };
+        return MalFunction($a2, $env, @binds, &fn);
+      }
+      default {
+        my ($func, @args) = eval_ast($ast, $env).val;
+        return $func.apply(|@args) if $func !~~ MalFunction;
+        $ast = $func.ast;
+        $env = MalEnv.new($func.env, $func.params, @args);
+      }
+    }
+  }
+}
+
+sub print ($exp) {
+  return pr_str($exp, True);
+}
+
+my $repl_env = MalEnv.new;
+
+sub rep ($str) {
+  return print(eval(read($str), $repl_env));
+}
+
+sub MAIN ($source_file?, *@args) {
+  $repl_env.set(.key, .value) for %core::ns;
+  $repl_env.set('eval', MalCode({ eval($^a, $repl_env) }));
+  $repl_env.set('*ARGV*', MalList([@args.map({ MalString($_) })]));
+  rep(q{(def! not (fn* (a) (if a false true)))});
+  rep(q{(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) ")")))))});
+
+  if ($source_file.defined) {
+    rep("(load-file \"$source_file\")");
+    exit;
+  }
+
+  while (my $line = prompt 'user> ').defined {
+    say rep($line);
+    CATCH {
+      when X::MalException { .Str.say }
+    }
+  }
+}
diff --git a/perl6/step7_quote.pl b/perl6/step7_quote.pl
new file mode 100644 (file)
index 0000000..33e69f1
--- /dev/null
@@ -0,0 +1,120 @@
+use v6;
+use lib IO::Path.new($?FILE).dirname;
+use reader;
+use printer;
+use types;
+use env;
+use core;
+
+sub read ($str) {
+  return read_str($str);
+}
+
+sub eval_ast ($ast, $env) {
+  given $ast {
+    when MalSymbol  { $env.get($ast.val) || die X::MalNotFound.new(name => $ast.val) }
+    when MalList    { MalList([$ast.map({ eval($_, $env) })]) }
+    when MalVector  { MalVector([$ast.map({ eval($_, $env) })]) }
+    when MalHashMap { MalHashMap($ast.kv.map({ $^a => eval($^b, $env) }).Hash) }
+    default         { $ast // $NIL }
+  }
+}
+
+sub is_pair ($ast) {
+  return so $ast ~~ MalList|MalVector && $ast.elems;
+}
+
+sub quasiquote ($ast) {
+  if !is_pair($ast) {
+    return MalList([MalSymbol('quote'), $ast]);
+  }
+  elsif $ast[0] ~~ MalSymbol && $ast[0].val eq 'unquote' {
+    return $ast[1];
+  }
+  elsif is_pair($ast[0]) && $ast[0][0] ~~ MalSymbol && $ast[0][0].val eq 'splice-unquote' {
+    return MalList([MalSymbol('concat'), $ast[0][1], quasiquote(MalList([$ast[1..*]]))]);
+  }
+  else {
+    return MalList([MalSymbol('cons'), quasiquote($ast[0]), quasiquote(MalList([$ast[1..*]]))]);
+  }
+}
+
+sub eval ($ast is copy, $env is copy) {
+  loop {
+    return eval_ast($ast, $env) if $ast !~~ MalList;
+    return $ast if !$ast.elems;
+
+    my ($a0, $a1, $a2, $a3) = $ast.val;
+    given $a0.val {
+      when 'def!' {
+        return $env.set($a1.val, eval($a2, $env));
+      }
+      when 'let*' {
+        my $new_env = MalEnv.new($env);
+        for |$a1.val -> $key, $value {
+          $new_env.set($key.val, eval($value, $new_env));
+        }
+        $env = $new_env;
+        $ast = $a2;
+      }
+      when 'do' {
+        eval_ast(MalList([$ast[1..*-2]]), $env);
+        $ast = $ast[*-1];
+      }
+      when 'if' {
+        if eval($a1, $env) ~~ MalNil|MalFalse {
+          return $NIL if $a3 ~~ $NIL;
+          $ast = $a3;
+        }
+        else {
+          $ast = $a2;
+        }
+      }
+      when 'fn*' {
+        my @binds = $a1 ?? $a1.map(*.val) !! ();
+        my &fn = -> *@args {
+          eval($a2, MalEnv.new($env, @binds, @args));
+        };
+        return MalFunction($a2, $env, @binds, &fn);
+      }
+      when 'quote' { return $a1 }
+      when 'quasiquote' { $ast = quasiquote($a1) }
+      default {
+        my ($func, @args) = eval_ast($ast, $env).val;
+        return $func.apply(|@args) if $func !~~ MalFunction;
+        $ast = $func.ast;
+        $env = MalEnv.new($func.env, $func.params, @args);
+      }
+    }
+  }
+}
+
+sub print ($exp) {
+  return pr_str($exp, True);
+}
+
+my $repl_env = MalEnv.new;
+
+sub rep ($str) {
+  return print(eval(read($str), $repl_env));
+}
+
+sub MAIN ($source_file?, *@args) {
+  $repl_env.set(.key, .value) for %core::ns;
+  $repl_env.set('eval', MalCode({ eval($^a, $repl_env) }));
+  $repl_env.set('*ARGV*', MalList([@args.map({ MalString($_) })]));
+  rep(q{(def! not (fn* (a) (if a false true)))});
+  rep(q{(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) ")")))))});
+
+  if ($source_file.defined) {
+    rep("(load-file \"$source_file\")");
+    exit;
+  }
+
+  while (my $line = prompt 'user> ').defined {
+    say rep($line);
+    CATCH {
+      when X::MalException { .Str.say }
+    }
+  }
+}
diff --git a/perl6/step8_macros.pl b/perl6/step8_macros.pl
new file mode 100644 (file)
index 0000000..3949159
--- /dev/null
@@ -0,0 +1,143 @@
+use v6;
+use lib IO::Path.new($?FILE).dirname;
+use reader;
+use printer;
+use types;
+use env;
+use core;
+
+sub read ($str) {
+  return read_str($str);
+}
+
+sub eval_ast ($ast, $env) {
+  given $ast {
+    when MalSymbol  { $env.get($ast.val) || die X::MalNotFound.new(name => $ast.val) }
+    when MalList    { MalList([$ast.map({ eval($_, $env) })]) }
+    when MalVector  { MalVector([$ast.map({ eval($_, $env) })]) }
+    when MalHashMap { MalHashMap($ast.kv.map({ $^a => eval($^b, $env) }).Hash) }
+    default         { $ast // $NIL }
+  }
+}
+
+sub is_pair ($ast) {
+  return so $ast ~~ MalList|MalVector && $ast.elems;
+}
+
+sub quasiquote ($ast) {
+  if !is_pair($ast) {
+    return MalList([MalSymbol('quote'), $ast]);
+  }
+  elsif $ast[0] ~~ MalSymbol && $ast[0].val eq 'unquote' {
+    return $ast[1];
+  }
+  elsif is_pair($ast[0]) && $ast[0][0] ~~ MalSymbol && $ast[0][0].val eq 'splice-unquote' {
+    return MalList([MalSymbol('concat'), $ast[0][1], quasiquote(MalList([$ast[1..*]]))]);
+  }
+  else {
+    return MalList([MalSymbol('cons'), quasiquote($ast[0]), quasiquote(MalList([$ast[1..*]]))]);
+  }
+}
+
+sub is_macro_call ($ast, $env) {
+  return so $ast ~~ MalList && $ast[0] ~~ MalSymbol
+    && $env.find($ast[0].val).?get($ast[0].val).?is_macro;
+}
+
+sub macroexpand ($ast is copy, $env is copy) {
+  while is_macro_call($ast, $env) {
+    my $func = $env.get($ast[0].val);
+    $ast = $func.apply($ast[1..*]);
+  }
+  return $ast;
+}
+
+sub eval ($ast is copy, $env is copy) {
+  loop {
+    return eval_ast($ast, $env) if $ast !~~ MalList;
+    $ast = macroexpand($ast, $env);
+    return eval_ast($ast, $env) if $ast !~~ MalList;
+    return $ast if !$ast.elems;
+
+    my ($a0, $a1, $a2, $a3) = $ast.val;
+    given $a0.val {
+      when 'def!' {
+        return $env.set($a1.val, eval($a2, $env));
+      }
+      when 'let*' {
+        my $new_env = MalEnv.new($env);
+        for |$a1.val -> $key, $value {
+          $new_env.set($key.val, eval($value, $new_env));
+        }
+        $env = $new_env;
+        $ast = $a2;
+      }
+      when 'do' {
+        eval_ast(MalList([$ast[1..*-2]]), $env);
+        $ast = $ast[*-1];
+      }
+      when 'if' {
+        if eval($a1, $env) ~~ MalNil|MalFalse {
+          return $NIL if $a3 ~~ $NIL;
+          $ast = $a3;
+        }
+        else {
+          $ast = $a2;
+        }
+      }
+      when 'fn*' {
+        my @binds = $a1 ?? $a1.map(*.val) !! ();
+        my &fn = -> *@args {
+          eval($a2, MalEnv.new($env, @binds, @args));
+        };
+        return MalFunction($a2, $env, @binds, &fn);
+      }
+      when 'quote' { return $a1 }
+      when 'quasiquote' { $ast = quasiquote($a1) }
+      when 'defmacro!' {
+        my $func = eval($a2, $env);
+        $func.is_macro = True;
+        return $env.set($a1.val, $func);
+      }
+      when 'macroexpand' { return macroexpand($a1, $env) }
+      default {
+        my ($func, @args) = eval_ast($ast, $env).val;
+        return $func.apply(|@args) if $func !~~ MalFunction;
+        $ast = $func.ast;
+        $env = MalEnv.new($func.env, $func.params, @args);
+      }
+    }
+  }
+}
+
+sub print ($exp) {
+  return pr_str($exp, True);
+}
+
+my $repl_env = MalEnv.new;
+
+sub rep ($str) {
+  return print(eval(read($str), $repl_env));
+}
+
+sub MAIN ($source_file?, *@args) {
+  $repl_env.set(.key, .value) for %core::ns;
+  $repl_env.set('eval', MalCode({ eval($^a, $repl_env) }));
+  $repl_env.set('*ARGV*', MalList([@args.map({ MalString($_) })]));
+  rep(q{(def! not (fn* (a) (if a false true)))});
+  rep(q{(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) ")")))))});
+  rep(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)))))))});
+  rep(q{(defmacro! or (fn* (& xs) (if (empty? xs) nil (if (= 1 (count xs)) (first xs) `(let* (or_FIXME ~(first xs)) (if or_FIXME or_FIXME (or ~@(rest xs))))))))});
+
+  if ($source_file.defined) {
+    rep("(load-file \"$source_file\")");
+    exit;
+  }
+
+  while (my $line = prompt 'user> ').defined {
+    say rep($line);
+    CATCH {
+      when X::MalException { .Str.say }
+    }
+  }
+}
diff --git a/perl6/step9_try.pl b/perl6/step9_try.pl
new file mode 100644 (file)
index 0000000..30c3a4c
--- /dev/null
@@ -0,0 +1,152 @@
+use v6;
+use lib IO::Path.new($?FILE).dirname;
+use reader;
+use printer;
+use types;
+use env;
+use core;
+
+sub read ($str) {
+  return read_str($str);
+}
+
+sub eval_ast ($ast, $env) {
+  given $ast {
+    when MalSymbol  { $env.get($ast.val) || die X::MalNotFound.new(name => $ast.val) }
+    when MalList    { MalList([$ast.map({ eval($_, $env) })]) }
+    when MalVector  { MalVector([$ast.map({ eval($_, $env) })]) }
+    when MalHashMap { MalHashMap($ast.kv.map({ $^a => eval($^b, $env) }).Hash) }
+    default         { $ast // $NIL }
+  }
+}
+
+sub is_pair ($ast) {
+  return so $ast ~~ MalList|MalVector && $ast.elems;
+}
+
+sub quasiquote ($ast) {
+  if !is_pair($ast) {
+    return MalList([MalSymbol('quote'), $ast]);
+  }
+  elsif $ast[0] ~~ MalSymbol && $ast[0].val eq 'unquote' {
+    return $ast[1];
+  }
+  elsif is_pair($ast[0]) && $ast[0][0] ~~ MalSymbol && $ast[0][0].val eq 'splice-unquote' {
+    return MalList([MalSymbol('concat'), $ast[0][1], quasiquote(MalList([$ast[1..*]]))]);
+  }
+  else {
+    return MalList([MalSymbol('cons'), quasiquote($ast[0]), quasiquote(MalList([$ast[1..*]]))]);
+  }
+}
+
+sub is_macro_call ($ast, $env) {
+  return so $ast ~~ MalList && $ast[0] ~~ MalSymbol
+    && $env.find($ast[0].val).?get($ast[0].val).?is_macro;
+}
+
+sub macroexpand ($ast is copy, $env is copy) {
+  while is_macro_call($ast, $env) {
+    my $func = $env.get($ast[0].val);
+    $ast = $func.apply($ast[1..*]);
+  }
+  return $ast;
+}
+
+sub eval ($ast is copy, $env is copy) {
+  loop {
+    return eval_ast($ast, $env) if $ast !~~ MalList;
+    $ast = macroexpand($ast, $env);
+    return eval_ast($ast, $env) if $ast !~~ MalList;
+    return $ast if !$ast.elems;
+
+    my ($a0, $a1, $a2, $a3) = $ast.val;
+    given $a0.val {
+      when 'def!' {
+        return $env.set($a1.val, eval($a2, $env));
+      }
+      when 'let*' {
+        my $new_env = MalEnv.new($env);
+        for |$a1.val -> $key, $value {
+          $new_env.set($key.val, eval($value, $new_env));
+        }
+        $env = $new_env;
+        $ast = $a2;
+      }
+      when 'do' {
+        eval_ast(MalList([$ast[1..*-2]]), $env);
+        $ast = $ast[*-1];
+      }
+      when 'if' {
+        if eval($a1, $env) ~~ MalNil|MalFalse {
+          return $NIL if $a3 ~~ $NIL;
+          $ast = $a3;
+        }
+        else {
+          $ast = $a2;
+        }
+      }
+      when 'fn*' {
+        my @binds = $a1 ?? $a1.map(*.val) !! ();
+        my &fn = -> *@args {
+          eval($a2, MalEnv.new($env, @binds, @args));
+        };
+        return MalFunction($a2, $env, @binds, &fn);
+      }
+      when 'quote' { return $a1 }
+      when 'quasiquote' { $ast = quasiquote($a1) }
+      when 'defmacro!' {
+        my $func = eval($a2, $env);
+        $func.is_macro = True;
+        return $env.set($a1.val, $func);
+      }
+      when 'macroexpand' { return macroexpand($a1, $env) }
+      when 'try*' {
+        return eval($a1, $env);
+        CATCH {
+          my $ex = $_ ~~ X::MalThrow ?? .value !! MalString(.Str);
+          my $new_env = $env;
+          $env.set($a2[1].val, $ex);
+          return eval($a2[2], $new_env);
+        }
+      }
+      default {
+        my ($func, @args) = eval_ast($ast, $env).val;
+        return $func.apply(|@args) if $func !~~ MalFunction;
+        $ast = $func.ast;
+        $env = MalEnv.new($func.env, $func.params, @args);
+      }
+    }
+  }
+}
+
+sub print ($exp) {
+  return pr_str($exp, True);
+}
+
+my $repl_env = MalEnv.new;
+
+sub rep ($str) {
+  return print(eval(read($str), $repl_env));
+}
+
+sub MAIN ($source_file?, *@args) {
+  $repl_env.set(.key, .value) for %core::ns;
+  $repl_env.set('eval', MalCode({ eval($^a, $repl_env) }));
+  $repl_env.set('*ARGV*', MalList([@args.map({ MalString($_) })]));
+  rep(q{(def! not (fn* (a) (if a false true)))});
+  rep(q{(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) ")")))))});
+  rep(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)))))))});
+  rep(q{(defmacro! or (fn* (& xs) (if (empty? xs) nil (if (= 1 (count xs)) (first xs) `(let* (or_FIXME ~(first xs)) (if or_FIXME or_FIXME (or ~@(rest xs))))))))});
+
+  if ($source_file.defined) {
+    rep("(load-file \"$source_file\")");
+    exit;
+  }
+
+  while (my $line = prompt 'user> ').defined {
+    say rep($line);
+    CATCH {
+      when X::MalException { .Str.say }
+    }
+  }
+}
diff --git a/perl6/stepA_mal.pl b/perl6/stepA_mal.pl
new file mode 100644 (file)
index 0000000..76c843a
--- /dev/null
@@ -0,0 +1,156 @@
+use v6;
+use lib IO::Path.new($?FILE).dirname;
+use reader;
+use printer;
+use types;
+use env;
+use core;
+
+sub read ($str) {
+  return read_str($str);
+}
+
+sub eval_ast ($ast, $env) {
+  given $ast {
+    when MalSymbol  { $env.get($ast.val) || die X::MalNotFound.new(name => $ast.val) }
+    when MalList    { MalList([$ast.map({ eval($_, $env) })]) }
+    when MalVector  { MalVector([$ast.map({ eval($_, $env) })]) }
+    when MalHashMap { MalHashMap($ast.kv.map({ $^a => eval($^b, $env) }).Hash) }
+    default         { $ast // $NIL }
+  }
+}
+
+sub is_pair ($ast) {
+  return so $ast ~~ MalList|MalVector && $ast.elems;
+}
+
+sub quasiquote ($ast) {
+  if !is_pair($ast) {
+    return MalList([MalSymbol('quote'), $ast]);
+  }
+  elsif $ast[0] ~~ MalSymbol && $ast[0].val eq 'unquote' {
+    return $ast[1];
+  }
+  elsif is_pair($ast[0]) && $ast[0][0] ~~ MalSymbol && $ast[0][0].val eq 'splice-unquote' {
+    return MalList([MalSymbol('concat'), $ast[0][1], quasiquote(MalList([$ast[1..*]]))]);
+  }
+  else {
+    return MalList([MalSymbol('cons'), quasiquote($ast[0]), quasiquote(MalList([$ast[1..*]]))]);
+  }
+}
+
+sub is_macro_call ($ast, $env) {
+  return so $ast ~~ MalList && $ast[0] ~~ MalSymbol
+    && $env.find($ast[0].val).?get($ast[0].val).?is_macro;
+}
+
+sub macroexpand ($ast is copy, $env is copy) {
+  while is_macro_call($ast, $env) {
+    my $func = $env.get($ast[0].val);
+    $ast = $func.apply($ast[1..*]);
+  }
+  return $ast;
+}
+
+sub eval ($ast is copy, $env is copy) {
+  loop {
+    return eval_ast($ast, $env) if $ast !~~ MalList;
+    $ast = macroexpand($ast, $env);
+    return eval_ast($ast, $env) if $ast !~~ MalList;
+    return $ast if !$ast.elems;
+
+    my ($a0, $a1, $a2, $a3) = $ast.val;
+    given $a0.val {
+      when 'def!' {
+        return $env.set($a1.val, eval($a2, $env));
+      }
+      when 'let*' {
+        my $new_env = MalEnv.new($env);
+        for |$a1.val -> $key, $value {
+          $new_env.set($key.val, eval($value, $new_env));
+        }
+        $env = $new_env;
+        $ast = $a2;
+      }
+      when 'do' {
+        eval_ast(MalList([$ast[1..*-2]]), $env);
+        $ast = $ast[*-1];
+      }
+      when 'if' {
+        if eval($a1, $env) ~~ MalNil|MalFalse {
+          return $NIL if $a3 ~~ $NIL;
+          $ast = $a3;
+        }
+        else {
+          $ast = $a2;
+        }
+      }
+      when 'fn*' {
+        my @binds = $a1 ?? $a1.map(*.val) !! ();
+        my &fn = -> *@args {
+          eval($a2, MalEnv.new($env, @binds, @args));
+        };
+        return MalFunction($a2, $env, @binds, &fn);
+      }
+      when 'quote' { return $a1 }
+      when 'quasiquote' { $ast = quasiquote($a1) }
+      when 'defmacro!' {
+        my $func = eval($a2, $env);
+        $func.is_macro = True;
+        return $env.set($a1.val, $func);
+      }
+      when 'macroexpand' { return macroexpand($a1, $env) }
+      when 'try*' {
+        return eval($a1, $env);
+        CATCH {
+          my $ex = $_ ~~ X::MalThrow ?? .value !! MalString(.Str);
+          my $new_env = $env;
+          $env.set($a2[1].val, $ex);
+          return eval($a2[2], $new_env);
+        }
+      }
+      default {
+        my ($func, @args) = eval_ast($ast, $env).val;
+        return $func.apply(|@args) if $func !~~ MalFunction;
+        $ast = $func.ast;
+        $env = MalEnv.new($func.env, $func.params, @args);
+      }
+    }
+  }
+}
+
+sub print ($exp) {
+  return pr_str($exp, True);
+}
+
+my $repl_env = MalEnv.new;
+
+sub rep ($str) {
+  return print(eval(read($str), $repl_env));
+}
+
+sub MAIN ($source_file?, *@args) {
+  $repl_env.set(.key, .value) for %core::ns;
+  $repl_env.set('eval', MalCode({ eval($^a, $repl_env) }));
+  $repl_env.set('*ARGV*', MalList([@args.map({ MalString($_) })]));
+  $repl_env.set('*host-language*', MalString('perl6'));
+  rep(q{(def! not (fn* (a) (if a false true)))});
+  rep(q{(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) ")")))))});
+  rep(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)))))))});
+  rep(q{(def! *gensym-counter* (atom 0))});
+  rep(q{(def! gensym (fn* [] (symbol (str "G__" (swap! *gensym-counter* (fn* [x] (+ 1 x)))))))});
+  rep(q{(defmacro! or (fn* (& xs) (if (empty? xs) nil (if (= 1 (count xs)) (first xs) (let* (condvar (gensym)) `(let* (~condvar ~(first xs)) (if ~condvar ~condvar (or ~@(rest xs)))))))))});
+
+  if ($source_file.defined) {
+    rep("(load-file \"$source_file\")");
+    exit;
+  }
+  rep(q{(println (str "Mal [" *host-language* "]"))});
+
+  while (my $line = prompt 'user> ').defined {
+    say rep($line);
+    CATCH {
+      when X::MalException { .Str.say }
+    }
+  }
+}
diff --git a/perl6/types.pm b/perl6/types.pm
new file mode 100644 (file)
index 0000000..95695c4
--- /dev/null
@@ -0,0 +1,94 @@
+unit module types;
+
+class X::MalException is Exception is export {}
+class X::MalNoTokens is X::MalException is export {
+  method message() { "got no tokens" }
+}
+class X::MalIncomplete is X::MalException is export {
+  has $.end;
+  method message() { "expected '$.end', got EOF" }
+}
+class X::MalUnexpected is X::MalException is export {
+  has $.token;
+  method message() { "unexpected '$.token'" }
+}
+class X::MalNotFound is X::MalException is export {
+  has $.name;
+  method message() { "'$.name' not found" }
+}
+class X::MalOutOfRange is X::MalException is export {
+  method message() { "nth: index out of range" }
+}
+class X::MalThrow is X::MalException is export {
+  has $.value;
+}
+
+role MalValue is export {
+  has $.val is rw;
+  method CALL-ME ($val) { self.new(:$val) }
+}
+role MalSequence is export {
+  has $.val handles <cache AT-POS EXISTS-POS elems end iterator>;
+  has $.meta is rw;
+  method CALL-ME ($val) { self.new(:$val) }
+}
+role MalCallable is export {
+  has &.fn;
+  method apply (*@_) { &!fn(|@_) }
+}
+role MalMeta is export {
+  has $.meta is rw;
+}
+
+class MalNil does MalValue is export {
+  method seq { self }
+}
+class MalTrue does MalValue is export {}
+class MalFalse does MalValue is export {}
+
+our $NIL is export = MalNil('nil');
+our $TRUE is export = MalTrue('true');
+our $FALSE is export = MalFalse('false');
+
+class MalSymbol does MalValue does MalMeta is export {}
+
+class MalList does MalSequence is export {
+  method conj (@args) { return self.new(val => [|@args.reverse, |$.val]) }
+  method seq { return self.elems ?? self !! $NIL }
+}
+
+class MalVector does MalSequence is export {
+  method conj (@args) { return self.new(val => [|$.val, |@args]) }
+  method seq { return self.elems ?? MalList(self.val) !! $NIL }
+}
+
+class MalHashMap does MalMeta is export {
+  has $.val handles <cache AT-KEY EXISTS-KEY elems pairs keys values kv>;
+  method CALL-ME ($val) { self.new(:$val) }
+}
+
+class MalNumber does MalValue is export {}
+
+class MalString does MalValue is export {
+  method seq {
+    return self.val.chars
+      ?? MalList(self.val.comb.map({MalString($_)}))
+      !! $NIL;
+  }
+}
+
+class MalCode does MalCallable does MalMeta is export {
+  method CALL-ME (&fn) { self.new(:&fn) }
+}
+
+class MalFunction does MalCallable does MalMeta is export {
+  has $.ast;
+  has @.params;
+  has $.env;
+  has $.is_macro is rw = False;
+  method CALL-ME ($ast, $env, @params, &fn) {
+    self.bless(:$ast, :$env, :@params, :&fn);
+  }
+}
+
+class MalAtom does MalValue does MalMeta is export {}