diff --git a/README.md b/README.md index a1f6855..0a8fdd7 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,26 @@ For another example, lets combine `if` and `not` into a macro named `unless`, th The `quasiquote` form described above is essential for declaring macros. Internally macros are just functions with a special flag. +## Exceptions + +The language has support for `try-catch` style exception handlers. The syntax is `(try A (catch B C))` where A is evaluated first and if exception is thrown, then C is evaluated with the symbol B bound to the value of the exception. Exceptions are thrown using the `throw` core function. You can give any data structure as argument to `throw` and it is passed along to `catch`. This way exceptions can contain more data than just a string that represents an error message. + +Simple example of throwing and catching an exception: + +``` +> (try (throw {"msg":"message"}) (catch ex (str "error: " (get ex "msg")))) +"error: message" +``` + +Exceptions generated by PHP are catched as well. Their value will be a hash-map with keys `type`, `file`, `line` and `message`: + +``` +> (try (get "wrong" "type") (catch ex (get ex "type"))) +"TypeError" +``` + +The Repl contains its own exception handler defined in PHP that will catch any exceptions thrown outside of `try-catch` form. + ## Reflection You can use the `meta` special form to retrieve the arguments, body, code or full definition of user-defined functions: @@ -276,6 +296,7 @@ or | yes | `(or false 0 1)` | `1` | Return the first value that evaluates to quote | yes | | | See the section Quoting. quasiquote | yes | | | See the section Quoting. quasiquote-expand | yes | | | See the section Quoting. +try | yes | | | See the section Exceptions. undef | yes | `(undef myFn)` | `` | Remove a definition from the current environment. Return the removed value. ## Functions @@ -293,7 +314,7 @@ print | no | `(print "hello world")` | `hello world` | Print expression on the pstr | yes | `(pstr {"a":"b"})` | `"{\"a\":\"b\"}"` | Print expression to string. read | yes | `(read "(+ 1 2 3)")` | `(+ 1 2 3)` | Read a string as code and return the expression. sleep | no | `(sleep 2000)` | `null` | Sleep for the given period given in milliseconds using [usleep](https://www.php.net/manual/en/function.usleep). -throw | yes | `(throw "invalid value")` | `error: "invalid value"` | Throw an exception. The given value is passed to catch. +throw | yes | `(throw "invalid value")` | `error: "invalid value"` | Throw an exception. The given value is passed to catch. See the section Exceptions. timer | no | `(timer (fn (d) (sleep d)) 200)` | `0.20010209` | Measure the execution time of a function and return it in seconds. ### Collection functions diff --git a/src/Evaller.php b/src/Evaller.php index bc3cf65..8e57a7c 100644 --- a/src/Evaller.php +++ b/src/Evaller.php @@ -116,9 +116,7 @@ class Evaller } elseif ($symbolName == 'def') { if ($astLength != 3) { throw new MadLispException("def requires exactly 2 arguments"); - } - - if (!($astData[1] instanceof Symbol)) { + } elseif (!($astData[1] instanceof Symbol)) { throw new MadLispException("first argument to def is not symbol"); } @@ -170,9 +168,7 @@ class Evaller } elseif ($symbolName == 'fn' || $symbolName == 'macro') { if ($astLength != 3) { throw new MadLispException("$symbolName requires exactly 2 arguments"); - } - - if (!($astData[1] instanceof Seq)) { + } elseif (!($astData[1] instanceof Seq)) { throw new MadLispException("first argument to $symbolName is not seq"); } @@ -213,9 +209,7 @@ class Evaller } elseif ($symbolName == 'let') { if ($astLength != 3) { throw new MadLispException("let requires exactly 2 arguments"); - } - - if (!($astData[1] instanceof MList)) { + } elseif (!($astData[1] instanceof MList)) { throw new MadLispException("first argument to let is not list"); } @@ -367,12 +361,47 @@ class Evaller } return $this->quasiquote($astData[1]); + } elseif ($symbolName == 'try') { + if ($astLength != 3) { + throw new MadLispException("try requires exactly 2 arguments"); + } elseif (!($astData[2] instanceof MList)) { + throw new MadLispException("second argument to try is not list"); + } + + $catch = $astData[2]->getData(); + + if (count($catch) == 3 && $catch[0] instanceof Symbol && + $catch[0]->getName() == 'catch' && $catch[1] instanceof Symbol) { + + try { + return $this->eval($astData[1], $env, $depth + 1); + } catch (\Throwable $ex) { + if ($ex instanceof MadLispUserException) { + $exVal = $ex->getValue(); + } else { + // Return a hash-map for PHP exceptions + $exVal = new Hash([ + 'type' => get_class($ex), + 'file' => $ex->getFile(), + 'line' => $ex->getLine(), + 'message' => $ex->getMessage() + ]); + } + + $newEnv = new Env('catch', $env); + $newEnv->set($catch[1]->getName(), $exVal); + + $env = $newEnv; + $ast = $catch[2]; + continue; // tco + } + } else { + throw new MadLispException("invalid form for catch"); + } } elseif ($symbolName == 'undef') { if ($astLength != 2) { throw new MadLispException("undef requires exactly 1 argument"); - } - - if (!($astData[1] instanceof Symbol)) { + } elseif (!($astData[1] instanceof Symbol)) { throw new MadLispException("first argument to undef is not symbol"); }