NAME
oh
SYNOPSIS
Command Line Interface:
Evaluate code:
oh e "(print :oh)"
Evaluate code and launch a REPL:
oh e "(setq oh 3)" i
Load files:
oh file.oh file2.oh
Load files and launch a REPL:
oh file1 i file2 i
Load files named i and e:
oh f i f e
Launch a REPL:
oh
Module Interface:
use Oh qw/interpret_string evaluate_element/;
my $result = interpret_string '(+ 1 2 3)';
my $same_result = evaluate_element ['+', 1, 2, 3];
INSTALLATION
cpan Oh
DESCRIPTION
This is a lisp interpreter written in perl trying to get the best of both worlds:
From perl it inherits the portability, unix scripting features and the whole CPAN.
Fron lisp inherits lisp evaluation rules, s-expressions and macros.
I always thought of lisp as a black box with no access to the external world and always thought that perl could be the perfect slave for lisp, being lisp the mind and perl the body.
I think that the best way to blend both is to make an interpreter in perl or a transpiler to perl (or any other language where we want to steal features from).
The main concern is to interoperate with perl as much as possible while having proper lisp evaluation rules.
The current implementation is in a prototype state and everything is subject to change.
However you are encouraged to test this interpreter and push it to its limits in order to give constructive feedback and help with its improvement and evolution.
The language has the following operators, macros and functions:
set, setq, get, let, lambda, defun, define, fun, macro, defmacro, +, if, when, unless, perl-fun, perl-defun, print, car, cdr, funcall, eval, load, eval-string, try, anon, sub, quote, unquote, quasiquote, flatten...
It has more, but it's lacking a lot of things a proper language should have.
FUNCTIONS, MACROS AND OPERATORS
defun
defun is common lisp's defun:
(defun oh (one &optional (two 2) three &key four (five 5))
(list one two three four five))
define
define is scheme's define:
(define oh 3)
This creates a new symbol named 'oh' in the current environment with the value 3
(define (oh x . y)
(list x y))
(oh 1 2 3)
This defines a new function named 'oh' using scheme style lambda arguments.
It will return the list: (1 (2 3))
let
The operator let works a bit differently than in scheme or cl:
(let (a 1 b 2 c 3)
(list a b c))
In cl or scheme it would be instead:
(let ((a 1) (b 2) (c 3))
(list a b c))
defmacro
defmacro defines a macro with common lisp style lambda arguments:
(defmacro oh (arg1 &rest arg-list)
`(list ,arg1 ,@arg-list))
macro
macro defines a macro with scheme style lambda arguments:
(macro oh (arg1 . arg-list)
`(list ,arg1 ,@arg-list))
setq
setq sets or updates a symbol
(setq oh 3)
equivalent to:
(define oh 3)
with the difference that if setq finds the symbol in the parent scope it will mutate that symbol instead, while define will always create a new symbol in the current environment, no matter if another symbol exists.
set
set is a function instead of an operator and has dual behavior.
It can set symbols like setq if the symbol is quoted and set hash tables or lists if the first element is one of them.
(set 'oh 3)
This is equivalent to:
(setq oh 3)
It will also update it if it exists in a parent scope.
(setq some-list '(1 2 3))
This creates a new symbol named 'some-list' that retains the list: (1 2 3)
get
The function 'get' will return an element from a list or hash table.
(get some-list 0)
This returns the first element of the list returned by the 'some-list' symbol, which in our case was the 1.
(set some-list 0 24)
This mutates the first element of the list and sets it to the value 24. In our case the list is now: (24 2 3)
get and set when using lists can also receive negative indices, they will access the element starting from the end.
(get some-list -1)
This returns the last element of some list, in our case was the number 3.
hash tables
They work the same with hash tables, accepting symbols as keys.
(setq some-hash (hash one 1 two 2 three (+ 1 2)))
(get some-hash 'one)
(set some-hash 'one 24)
lambda
Lambda works with cl style lambda arguments:
(lambda (x &rest y)
(list x y))
But lambda is just a macro:
(macro lambda (args . code)
`(anon (lisp-args ,args) ,@code))
anon
The operator 'anon' creates a lambda with no argument bindings.
lisp-args and scheme-args are just an operator that performs the binding from a lambda argument list.
The 'anon' operator provides no bindings, but when the interpreter evaluates a lambda, it will set a symbol named 'args' with the list of arguments it receives.
((anon args) 1 2 3)
This returns the list of arguments: (1 2 3)
sub
The 'sub' operator creates a lambda and sets it in the current environment.
(sub list args)
(list 1 2 3)
sub and anon do not provide argument bindings. The arguments will be present in the symbol 'args' when a lambda is evaluated.
fun
defun, fun and define are macros using sub and scheme-args or lisp-args
(macro fun (name args . code)
`(sub ,name (scheme-args ,args) ,@code))
(macro defun (name args . code)
`(sub ,name (lisp-args ,args) ,@code))
(macro define (name . value)
(if (listp name)
`(fun ,(car name) ,(cdr name) ,@value)
`(setq ,name ,(car value))))
perl-sub
perl-sub creates perl subroutines in a given package:
(perl-sub package-name fun-name code...)
It does not provide argument bindings, being similar to sub, but it actually creates a perl subroutine in a perl package
(perl-sub oh hello_world
(print "Hello, world"))
Now this function is a perl subroutine named 'hello_world' and it exists in the perl package named 'oh'
Which means that is now accessible from perl:
oh::hello_world();
The interpreter can also call perl subroutines from a module:
(oh::hello_world)
perl-defun
The perl-defun macro provides a way to define perl subroutines using cl style bindings:
(macro perl-defun (module name args . code)
`(perl-sub ,module ,name (lisp-args ,args) ,@code))
(perl-defun oh print_something (&optional (something 'oh))
(print something))
(oh::print_something "Hello, world")
perl-fun
The perl-fun macro is similar, but provides scheme style bindings:
(macro perl-fun (module name args . code)
`(perl-sub ,module ,name (scheme-args ,args) ,@code))
(perl-fun oh print_something something
(print (car something)))
(oh::print_something "Hello, world")
method
There is also the function called 'method' which will expect an object as the first argument and the name of the method as the second. It can also take the name of a module as the first argument.
(use IO::Socket)
(method 'IO::Socket 'new 'localhost:8080)
That would return an IO::Socket object if there was a server in localhost listening at port 8080.
perl-sub, perl-fun and perl-defun allow us to create perl subroutines into a perl package.
If the perl package does not exist it will be created, because packages in perl autovivify.
That means that we can create perl classes, since they are just packages.
parent
There is also the 'parent' operator which creates inheritance by pushing to the perl's ISA array on the package.
(use Bot::BasicBot)
(perl-fun bot said (bot args)
(unless (or (eq (get args) 'NickServ)
(eq (get args) 'ChanServ))
'hey))
(parent bot Bot::BasicBot)
(method (method 'bot 'new (qw server irc.libera.chat port 6667 nick superbot)) 'run)
use
The use operator loads a perl module.
(use Path::Tiny)
(use IO::Socket)
bindings
The bindings function imports perl subroutines into the interpreter from a package name that receives as the first argument:
(use Curses)
(bindings (qw Curses initscr raw refresh endwin addstr getch))
Now those functions exist in the interpreter:
(initscr)
(raw)
(addstr "oh...")
(refresh)
(getch)
(endwin)
This way we do not need to do it like this:
(Curses::initscr)
(Curses::raw)
(Curses::addstr "oh...")
quote
quote is an operator that returns its first and only argument
(quote oh)
It is something so common in lisp that it deserves a shortcut.
'oh
The quote symbol is a reader symbol like ( or " that reads one element and returns the list: (quote oh)
Which means that 'oh and (quote oh) are equivalent.
quasiquote and unquote
quasiquote is an operator similar to quote, but when it receives a list as first and only argument, it will traverse that list recursively and check for lists with unquote as the first element like: (unquote x) and evaluate them, while quoting the other lists or elements.
(quasiquote (1 2 (unquote (+ 1 2))))
This returns a list with: (1 2 3) since the expression (+ 1 2) was evaluated as it's inside the unquote operator.
They also have their own shortcuts like quote does.
The comma ',' is unquote and the backtick '`' is quasiquote:
`(1 2 ,(+ 1 2))
The reader returns: (quasiquote (1 2 (unquote (+ 1 2))))
quote words
qw is an operator that autoquotes words.
(qw oh my cat is nice)
This operator does not actually return any list, the words will be 'flattened' as if they were symbols being quoted.
(list (qw oh my cat))
is equivalent to:
(list 'oh 'my 'cat)
It is taken from perl qw// and serves as an autoquote mechanism.
PREDICATES
eq
The eq operator checks for identity. Will return true if to objects are the same object, if two strings have the same characters and if two numbers are the same. It is just a binding for perl's eq operator
(eq 1 1)
returns true
(eq '(1 2 3) '(1 2 3))
returns false because they are not the same object
(setq oh '(1 2 3))
(eq oh oh)
returns true since it's the same object
equal
The equal operator does not check for identity if the objects are lists or hash tables. It will iterate on all elements and check whether they are equal or not.
(equal '(1 2 3) '(1 2 3))
Returns true
(equal (hash oh 3) (hash oh 3))
Returns true
The equal operator does not have any safeguard for comparing circular references. It cannot compare them and it will not trigger an error. It will recurse forever until it blows the perl return stack, which will blow after your memory is exhausted.
I could add a safeguard, but at the end you are not supposed to compare circular references and the only reason to add a check and trigger an error would be to warn you when you do that inadvertently. Perl will warn you that the function compare and compare_lists or compare_hashes went into deep recursion.
=
The = operator is for comparing numbers, it is equivalent to perl's == operator, with the difference that it accepts more than two elements.
(= 1 1 1)
Returns true
(= 1 1 2)
Returns false
<
The < operator works similar to = by accepting several arguments
(< 1 2 3)
Returns true
(< 1 2 2)
Returns false
>
The > operator works the same as <
(> 3 2 1)
Returns true
=< =>
They check if the elements are greater or equal and less or equal
(<= 1 2 2)
Returns true
null
The null function returns true if the value is 0, an empty string, nil and an empty list.
(null '())
(null 0)
(null ())
(null nil)
(null "")
Returns true in all those cases
defined
The defined function returns true if the element is not nil.
(defined '())
Returns true, since an empty list is not really nil.
(defined ())
Returns false, since the interpreter returns nil when evaluating an empty list.
(defined "")
(defined 0)
Returns true
not
Negates a value. If it's false (empty string, 0 or nil) it returns true, returns false otherwise
(not nil)
(not "")
(not 0)
(not)
Returns true
listp
Returns true if the argument is a list:
(listp '(1 2 3))
LIST OPERATORS
car and cdr
car returns the first element of a list:
(car '(1 2 3))
Returns 1
It is equivalent to:
(get '(1 2 3) 0)
cdr returns the list without the first element:
(cdr '(1 2 3))
Returns (2 3)
car and cdr can be combined:
(caaar x)
Is equivalent to:
(car (car (car x)))
cdr can be combined the same way
(cdddr x)
Is equivalent to:
(cdr (cdr (cdr x)))
They can both be mixed:
(cadr x)
Is equivalent to:
(car (cdr x))
They can be combined in any way:
(caddar x)
(caddr x)
(cdadr x)
It does not have limit of nesting:
(caaaaar '(((((oh))))))
Returns 'oh
remove
Remove will expect an element and a list and will remove the elements that are the same as that element.
(remove nil `(1 2 ,nil 3 4 ,nil))
Returns (1 2 3 4)
It uses 'equal' to test for equality, so it can compare lists or hash tables.
(remove '(1 2 3) '(1 (1 2 3) (1 2) (1 2 3) 3))
Returns (1 (1 2) 3)
mapcar
Expects a function and one or more lists. Iterates the lists and gives the current elements to the function, returns a list of the values returned by the function.
(mapcar (lambda (x) (+ 1 x)) '(1 2 3))
Returns (2 3 4)
If mapcar receives more than one list, the function should also receive more than one element, equivalent to the number of lists.
(mapcar (lambda (x y) (+ x y)) '(1 2 3) '(4 5 6))
Returns (5 7 9)
The first time mapcar iterates will take the first element of both lists, in this case the 1 from the first list and the 4 from the second, then it will give that to the lambda which will sum them, returning 5. Then it will take the second element of both lists and do the same, until it reaches the limit of the shorter list.
The interpreter has a special syntax to return a function. If a symbol is prefixed with a '#' symbol, the interpreter will return the function with that name or nil if does not find it.
#list
This returns the function named 'list'.
It is equivalent to:
(find-function 'list)
It is a shorthand to use it in functions like mapcar.
Note that in common lisp it's #'list instead and '#' is actually a read character.
I'm yet deciding if I want to make '#' a read character. Currently this is just a special case in the interpreter and it might change in the future.
As an example it can be used with mapcar like this:
(mapcar #list '(1 2 3) '(4 5 6) '(7 8 9))
Returns ((1 4 7) (2 5 8) (3 6 9))
remove-if
The function remove-if is similar to remove, but instead of an element it takes a function to use as predicate.
(remove-if (lambda (x) (equal 0 x)) '(0 1 2 0 3))
Returns (1 2 3)
In this case it was equivalent to:
(remove 0 '(0 1 2 0 3))
But it used a predicate instead.
remove-if-not
It removes element if the predicate returns false instead of true.
(remove-if-not (lambda (x) (equal 0 x)) '(0 1 2 0 3))
Returns (0 0)
This case is equivalent to:
(remove-if (lambda (x) (not (equal 0 x))) '(0 1 2 0 3))
CONDITIONALS
if
The if operator expects code to work as a predicate and code to evaluate if the condition is true and optionally code to evaluate if the condition is false
(if 1
(print "it's true")
(print "it's false"))
This would print "it's true" on the terminal since 1 was evaluated and returned a true value.
(if 0
(print "it's true")
(print "it's false"))
This would print "it's false" since 0 is a false value.
If we wanted to perform more actions than just one, we can use progn.
(if 1
(progn (print "it's true") (print "yeah, it's true"))
(print "it's false"))
when
The when expects code to evaluate as a predicate and the rest of code will be evauated if the predicate returned a true value.
when has an implicit progn,
(when 1 (print "it's true") (print "yeah, it's true"))
when does not evaluate anything if the condition is not true.
unless
unless is the reverse of when. it will evaluate the condition if it's not true, and do nothing if it's true.
(unless 0 (print "it's false") (print "yeah, it's false"))
This same code would be equivalent:
(when (not 0) (print "it's false") (print "yeah, it's false"))
cond
cond takes pairs of tests and code, it will evaluate the code of the test, and if it returns true it will evaluate the code.
(cond
((equal 'oh 'oh) (print "oh is oh"))
((< 1 2) (print "one is less than two")))
In this case, even if the two tests return true: (equal 'oh 'oh) and (< 1 2), only the first one will get executed, which will be the code for: (print "oh is oh"), cond will stop execution once it finds a match.
To evaluate code when any of the conditions match, we use t or :otherwise to set the code to evaluate when any condition returns true.
(cond
((equal 'oh 'meh) (print "that will never be true"))
(:otherwise (print "this will get executed")))
In this case the :otherwise clause will be executed because the other clause will fail.
Instead of :otherwise we can use t
(cond
((equal 'oh 'meh) (print "that will never be true"))
(t (print "this will get executed")))
It is equivalent to the previous one.
One thing to note is that t and :otherwise are not required to be the last clause as in common lisp. And that common lisp accepts 'otherwise' as a symbol as well as a keyword, while this interpreter only accepts :otherwise as a keyword.
case
case is similar to cond, but does not take a test as code to evaluate. Instead it only compares symbols.
(setq x 'oh)
(case x
(oh (print "The value of x is oh"))
(t (print "the value of x is not oh")))
It will fail with strings or other objects, but accepts nil which will only be executed if the value is undefined, it will not trigger if the value is the other false values like 0 or ""
(setq x nil)
(case x
(nil (print "the value of x is undefined"))
(:otherwise (print "the value of x is not undefined")))
case can also accept more than one symbol for the same clause.
(case x
((oh meh) (print "the value of x is oh or meh")))
LOOPS
dotimes
Expects a list with the first element as the name for the symbol that will retain the current number of the iteration starting from 0 and the number of times you want to iterate.
(dotimes (i 3)
(print i))
Will print on the screen:
0 1 2
dolist
Iterates on a list.
Expects a list with the first element as the name of the symbol that will retain the current element of the list.
(dolist (i '(1 2 3))
(print i))
Will print on the screen:
1 2 3
while
Expects a condition and evaluates the code as long as the condition is true.
(while 1
(print "this loops forever"))
until
Expects a condition and evaluates the code as long as the condition is false
(while (not 1)
(print "this loops forever"))
PREDEFINED SYMBOLS
- aeth
-
The galactic emperor
- nil
-
undef
- t
-
1
- version
-
1.00
TODO
I have to create a better abstraction for creating perl classes.
BUGS
I'm pretty sure that the common lisp style argument binding has some bugs, since I did not test it thoroughly.
The scheme like arguments might also have bugs.
It is likely that a lot of bugs are waiting for the right moment to destroy my dreams.
LICENSE
CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide.
This software is distributed without any warranty.
https://creativecommons.org/publicdomain/zero/1.0/