Package: hissp
It's Python with aLissp.Hissp is a modular Lisp implementation that compiles to a functional subset of
Python—Syntactic macro metaprogramming with full access to the Python ecosystem!Table of ContentsInstallationExamples!Quick Start: Readerless ModeSpecial FormsMacrosThe Lissp ReaderA Small Lissp ApplicationAlternate ReadersHebigoGarden of EDNFeatures and DesignRadical ExtensibilityMinimal implementationInteroperabilityUseful error messagesSyntax compatible with Emacs'lisp-modeand ParlinterStandalone outputReproducible buildsREPLSame-module macro helpersModularityInstallationHissp requires Python 3.8+.Install the latest PyPI release withpython -m pip install --upgrade hisspOr install the bleeding-edge version directly from GitHub withpython -m pip install --upgrade git+https://github.com/gilch/hisspConfirm install withpython -m hissp --help
lissp -c "__hello__."Examples!Quick Start: Readerless ModeHissp is ametaprogrammingintermediate language composed of simple Python data structures,
easily generated programmatically,>>>hissp_code=(...('lambda',('name',)...,('print',('quote','Hello'),'name',),)...)which are compiled to Python code,>>>fromhisspimportreaderless>>>python_code=readerless(hissp_code)>>>print(python_code)(lambdaname:print('Hello',name))and evaluated by Python.>>>greeter=eval(python_code)>>>greeter('World')HelloWorld>>>greeter('Bob')HelloBobTo a first approximation,
tuples represent calls
and strings represent raw Python code in Hissp.
(Take everything else literally.)Special FormsLike Python, argument expressions are evaluated before being passed to the function,
however, the quote and lambda forms are special cases in the compiler and break this rule.Strings also have a few special cases:control words, which start with:(and may have various special interpretations in certain contexts);method calls, which start with., and must be the first element in a tuple representing a call;and module handles, which end with.(and do imports).>>>adv_hissp_code=(...('lambda'# Anonymous function special form....# Parameters....,(':'# Control word: remaining parameters are paired with a target....,'name'# Target: Raw Python: Parameter identifier....# Default value for name....,('quote'# Quote special form: string, not identifier....,'world'),)...# Body....,('print'# Function call form, using the identifier for the builtin....,('quote','Hello,'),)...,('print'...,':'# Control word: Remaining arguments are paired with a target....,':*'# Target: Control word for unpacking....,('.upper','name',)# Method calls start with a dot....,'sep'# Target: Keyword argument....,':'# Control words compile to strings, not raw Python....,'file'# Target: Keyword argument....# Module handles like `sys.` end in a dot....,'sys..stdout',),)# print already defaults to stdout though....)...>>>print(readerless(adv_hissp_code))(lambdaname='world':(print('Hello,'),print(*name.upper(),sep=':',file=__import__('sys').stdout))[-1])>>>greetier=eval(readerless(adv_hissp_code))>>>greetier()Hello,W:O:R:L:D>>>greetier('alice')Hello,A:L:I:C:EMacrosThe ability to make lambdas and call out to arbitrary Python helper functions entails that Hissp can do anything Python can.
For example, control flow via higher-order functions.>>>any(map(lambdas:print(s),"abc"))# HOF loop.abcFalse>>>defbranch(condition,consequent,alternate):# Conditional HOF....return(consequentifconditionelsealternate)()# Pick one to call....>>>branch(1,lambda:print('yes'),lambda:print('no'))# Now just a function call.yes>>>branch(0,lambda:print('yes'),lambda:print('no'))noThis approach works fine in Hissp,
but we can express that more succinctly via metaprogramming.
Unlike functions,
the special forms don't (always) evaluate their arguments first.
Macros can rewrite forms in terms of these,
extending that ability to custom tuple forms.>>>class_macro_:# This name is special to Hissp....defthunk(*body):# No self. _macro_ is just used as a namespace....# Python code for writing Hissp code. Macros are metaprograms....return('lambda',(),*body,)# Delayed evaluation....defif_else(condition,consequent,alternate):...# Delegates both to a helper function and another macro....return('branch',condition,('thunk',consequent,),('thunk',alternate,),)...>>>expansion=readerless(...('if_else','0==1'# Macro form, not a run-time call....,('print',('quote','yes',),)# Side effect not evaluated!...,('print',('quote','no',),),),...globals())# Pass in globals for _macro_.>>>print(expansion)# if_elsebranch(0==1,# thunk(lambda:print('yes')),# thunk(lambda:print('no')))>>>eval(expansion)noThe Lissp ReaderThe Hissp data-structure language can be written directly in Python using the "readerless mode" demonstrated above,
or it can be read in from a lightweight textual language calledLisspthat represents the Hissp
a little more neatly.>>>lissp_code="""... (lambda (name)...   (print 'Hello name))... """As you can see, this results in exactly the same Hissp code as our earlier example.>>>fromhissp.readerimportLissp>>>next(Lissp().reads(lissp_code))('lambda',('name',),('print',('quote','Hello'),'name'))>>>_==hissp_codeTrueHissp comes with a basic REPL (read-eval-print loop, or interactive command-line interface)
which compiles Hissp (read from Lissp) to Python and passes that to the Python REPL for execution.Lissp can also be read from.lisspfiles,
which compile to Python modules.A Small Lissp ApplicationThis is a Lissp web app for converting between Celsius and Fahrenheit,
which demonstrates a number of language features.
Run as the main script or enter it into the Lissp REPL.
RequiresBottle.(hissp.._macro_.prelude)(defineenjoinen#X#(.join""(mapstrX)))(definetag(lambda(tag::*contents)(enjoin"<"tag">"(enjoin::*contents)"</"(get#0(.splittag))">")))(defmacroscript(::*forms)`',(tag"script type='text/python'"#"\n"(.join#"\n"(maphissp.compiler..readerlessforms))))((bottle..route"/"); https://bottlepy.orgO#(enjoin(let(s(tag"script src='https://cdn.jsdelivr.net/npm/brython@3/brython{}.js'"))(enjoin(.formats".min")(.formats"_stdlib")))(tag"body onload='brython()'"; Browser Python: https://brython.info(script(definegetEX#(.getElementByIdbrowser..documentX))(definegetf@vX#(float(X#X.value(getEX))))(defineset@vXY#(setattr(getEY)'valueX))(attachbrowser..window:CelsiusO#(->(getf@v'Celsius)(X#.#"X*1.8+32")(set@v'Fahrenheit))FahrenheitO#(->(getf@v'Fahrenheit)(X#.#"(X-32)/1.8")(set@v'Celsius))))(let(row(enjoin(tag"input id='{0}' onkeyup='{0}()'")(tag"label for='{0}'""°{1}")))(enjoin(.formatrow"Fahrenheit""F")"<br>"(.formatrow"Celsius""C"))))))(bottle..run:host"localhost"port8080debugTrue)Consult theHissp documentationfor an explanation of each form.Alternate ReadersHissp is modular, and the reader included for Lissp is not the only one.HebigoHere's a native unit test class from the separateHebigoprototype,
a Hissp reader and macro suite implementing a language designed to resemble Python:class:TestOr:TestCasedef:.test_null:selfself.assertEqual:()or:def:.test_one:selfx:@given:st.from_type:typeself.assertIs:xor:xdef:.test_two:selfxy:@given:st.from_type:typest.from_type:typeself.assertIs:(xory)or:xydef:.test_shortcut:selfor:1(0/0)or:01(0/0)or:1(0/0)(0/0)def:.test_three:selfxyz:@given:st.from_type:typest.from_type:typest.from_type:typeself.assertIs:(xoryorz)or:xyzThe same Hissp macros work in readerless mode, Lissp, and Hebigo, and can be written in any of these.
Given Hebigo's macros, the class above could be written in the equivalent way in Lissp:(class_(TestOrTestCase)(def_(.test_nullself)(self.assertEqual()(or_)))(def_(.test_oneselfx):@(given(st.from_typetype))(self.assertIsx(or_x)))(def_(.test_twoselfxy):@(given(st.from_typetype)(st.from_typetype))(self.assertIs.#"x or y"(or_xy)))(def_(.test_shortcutself)(or_1.#"0/0")(or_01.#"0/0")(or_1.#"0/0".#"0/0"))(def_(.test_threeselfxyz):@(given(st.from_typetype)(st.from_typetype)(st.from_typetype))(self.assertIs.#"x or y or z"(or_xyz))))Hebigo looks very different from Lissp, but they are both Hissp!
If you quote this Hebigo code and print it out,
you get Hissp code, just like you would with Lissp.In Hebigo's REPL, that looks likeIn [1]: pprint..pp:quote:class: TestOr: TestCase
   ...:   def: .test_null: self
   ...:     self.assertEqual: () or:
   ...:   def: .test_one: self x
   ...:     :@ given: st.from_type: type
   ...:     self.assertIs: x or: x
   ...:   def: .test_two: self x y
   ...:     :@ given:
   ...:       st.from_type: type
   ...:       st.from_type: type
   ...:     self.assertIs: (x or y) or: x y
   ...:   def: .test_shortcut: self
   ...:     or: 1 (0/0)
   ...:     or: 0 1 (0/0)
   ...:     or: 1 (0/0) (0/0)
   ...:   def: .test_three: self x y z
   ...:     :@ given:
   ...:       st.from_type: type
   ...:       st.from_type: type
   ...:       st.from_type: type
   ...:     self.assertIs: (x or y or z) or: x y z
   ...: 
('hebi.basic.._macro_.class_',
 ('TestOr', 'TestCase'),
 ('hebi.basic.._macro_.def_',
  ('.test_null', 'self'),
  ('self.assertEqual', '()', ('hebi.basic.._macro_.or_',))),
 ('hebi.basic.._macro_.def_',
  ('.test_one', 'self', 'x'),
  ':@',
  ('given', ('st.from_type', 'type')),
  ('self.assertIs', 'x', ('hebi.basic.._macro_.or_', 'x'))),
 ('hebi.basic.._macro_.def_',
  ('.test_two', 'self', 'x', 'y'),
  ':@',
  ('given', ('st.from_type', 'type'), ('st.from_type', 'type')),
  ('self.assertIs', '((x or y))', ('hebi.basic.._macro_.or_', 'x', 'y'))),
 ('hebi.basic.._macro_.def_',
  ('.test_shortcut', 'self'),
  ('hebi.basic.._macro_.or_', 1, '((0/0))'),
  ('hebi.basic.._macro_.or_', 0, 1, '((0/0))'),
  ('hebi.basic.._macro_.or_', 1, '((0/0))', '((0/0))')),
 ('hebi.basic.._macro_.def_',
  ('.test_three', 'self', 'x', 'y', 'z'),
  ':@',
  ('given',
   ('st.from_type', 'type'),
   ('st.from_type', 'type'),
   ('st.from_type', 'type')),
  ('self.assertIs',
   '((x or y or z))',
   ('hebi.basic.._macro_.or_', 'x', 'y', 'z'))))Garden of EDNExtensible Data Notation (EDN) is a subset of Clojure used for data exchange,
as JSON is to JavaScript, only more extensible.
Any standard Clojure editor should be able to handle EDN.The separateGarden of EDNprototype contains a variety of EDN readers in Python,
and two of them read EDN into Hissp.Here's little snake game in PandoraHissp,
one of the EDN Hissp dialects,
which includes Clojure-like persistent data structures.0 ; from garden_of_edn import _this_file_as_main_; """#"
(hissp/_macro_.prelude)
(attach _macro_ . ors #hissp/$"_macro_.||", ands #hissp/$"_macro_.&&")
(defmacro #hissp/$"m#" t (tuple (.extend [(quote pyrsistent/m) (quote .)] t)))
(defmacro #hissp/$"j#" j (complex 0 j))

(define TICK 100)
(define WIDTH 40)
(define HEIGHT 20)
(define SNAKE (pyrsistent/dq (complex 3 2) (complex 2 2)))
(define BINDS #m(w [#j -1], a [-1], s [#j 1], d [1]))

(define arrow (collections/deque))

(define root (doto (tkinter/Tk)
               (.resizable 0 0)
               (.bind "<Key>" #X(.extendleft arrow (.get BINDS X.char ())))))

(define label (doto (tkinter/Label) .pack (.configure . font "TkFixedFont"
                                                      justify "left"
                                                      height (add 1 HEIGHT)
                                                      width WIDTH)))

(define wall? (lambda z (ors (contains #{WIDTH  -1} z.real)
                             (contains #{HEIGHT -1} z.imag))))

(define food! #O(complex (random/randint 0 (sub WIDTH 1))
                         (random/randint 0 (sub HEIGHT 1))))

(define frame (lambda (state)
                (-<>> (product (range HEIGHT) (range WIDTH))
                      (starmap #XY(complex Y X))
                      (map (lambda z (concat (cond (contains state.snake z) "O"
                                                   (eq z state.food) "@"
                                                   :else " ")
                                             (if-else (eq 0 z.real) "\n" ""))))
                      (.join ""))))

(define move (lambda (state new-food arrow)
               (let (direction (if-else (ands arrow (ne arrow (neg state.direction)))
                                 arrow state.direction))
                 (let (head (add (#get 0 state.snake) direction))
                   (-> state
                       (.update (if-else (eq head state.food)
                                  #m(score (add 1 state.score)
                                     food new-food)
                                  #m(snake (.pop state.snake)))
                                #m(direction direction))
                       (.transform [(quote snake)] #X(.appendleft X head)))))))

(define lost? (lambda (state)
                (let (head (#get 0 state.snake))
                  (ors (wall? head)
                       (contains (#get(slice 1 None) state.snake)
                                 head)))))

(define update!
  (lambda (state)
    (-<>> (if-else (lost? state)
            " GAME OVER!"
            (prog1 "" (.after root TICK update! (move state (food!) (when arrow
                                                                      (.pop arrow))))))
          (.format "Score: {}{}{}" state.score :<> (frame state))
          (.configure label . text))))

(when (eq __name__ "__main__")
  (update! #m(score 0, direction 1, snake SNAKE, food (food!)))
  (.mainloop root))
;; """#"Features and DesignRadical ExtensibilityAny sufficiently complicated C or Fortran program contains an ad hoc,
informally-specified, bug-ridden, slow implementation of half of Common Lisp.— Greenspun's Tenth RulePython is already a really nice language, a lot closer to Lisp than C or Fortran.
It has dynamic types and automatic garbage collection, for example.
So why do we need Hissp?If the only programming languages you've tried are those designed to feel familiar to C programmers,
you might think they're all the same.I assure you, they are not.While any Turing-complete language has equivalent theoretical power,
they are not equallyexpressive.
They can be higher or lower level.
You already know this.
It's why you don't write assembly language when you can avoid it.
It's not that assembly isn't powerful enough to do everything Python can.
Ultimately, the machine only understands machine code.
The best programming languages have some kind of expressive superpower.
Features that lesser languages lack.Lisp's superpower ismetaprogramming,
and it's the power to copy the others.
It's not that Python can't do metaprogramming at all.
(Python is Turing complete, after all.)
You can already do all of this in Python,
and more easily than in lower languages.
But it's too difficult (compared to Lisp),
so it's done rarely and by specialists.
The use ofexec()is frowned upon.
It's easy enough to understand, but hard to get right.
Python Abstract Syntax Tree (AST)
manipulation is a somewhat more reliable technique,
but not for the faint of heart.
Python AST is not simple, because Python isn't.Python really is a great language to work with.
"Executable pseudocode" is not far off.
But it is too complex to be good at metaprogramming.
By stripping Python down to a minimal subset,
and encoding that subset as simple data structures rather than text
(or complicated and error-prone Python AST),
Hissp makes metaprogramming as easy as
the kind of data manipulation you already do every day.
On its own, meta-power doesn't seem that impressive.
But the powers you can make with it can be.
Those who've mastered metaprogramming wonder how they ever got along without it.Actively developed languages keep accumulating features,
Python included.
Often they're helpful, but sometimes it's a misstep.
The more complex a language gets,
the more difficult it becomes to master.Hissp takes the opposite approach: extensibility through simplicity.
Major features that would require a new language version in lower languages
can be a library in a Lisp.
It's how Clojure got Goroutines like Go and logic programming like Prolog,
without changing the core language at all.
The Lissp reader and Hissp compiler are both extensible with macros.It's not just about getting other superpowers from other languages,
but all the minor powers you can make yourself along the way.
You're not going to campaign for a new Python language feature
and wait six months for another release
just for something that might be nice to have for you special problem at the moment.
But in Hissp you can totally have that.
You can program the language itself to fit your problem domain.Once your Python project is "sufficiently complicated",
you'll start hacking in new language features just to cope.
And it will be hard,
because you'll be using a language too low-level for your needs,
even if it's a relatively high-level language like Python.Lisp is as high level as it gets,
because you can program in anything higher.Minimal implementationHissp serves as a modular component for other projects.
The language and its implementation are meant to be small and comprehensible
by a single individual.The Hissp compiler should include what it needs to achieve its goals,
but no more. Bloat is not allowed.
A goal of Hissp is to be as small as reasonably possible, but no smaller.
We're not code golfing here; readability still counts.
But this project haslimited scope.
Hissp's powerful macro system means that additions to the compiler are
rarely needed.
Feature creep belongs in external libraries,
not in the compiler proper.Hissp compiles to an unpythonicfunctional subsetof Python.
This subset has a direct and easy-to-understand correspondence to the Hissp code,
which makes it straightforward to debug, once you understand Hissp.
But it is definitely not meant to be idiomatic Python.
That would require a much more complex compiler,
because idiomatic Python is not simple.Hissp's bundled macros are meant to be just enough to bootstrap native unit tests
and demonstrate the macro system.
They may suffice for small embedded Hissp projects,
but you will probably want a more comprehensive macro suite for general use.Currently, that means usingHebigo,
which has macro equivalents of most Python statements.The Hebigo project includes an alternative indentation-based Hissp reader,
but the macros are written in readerless mode and are also compatible with the
S-expression "Lissp" reader bundled with Hissp.InteroperabilityWhy base a Lisp on Python when there are already lots of other Lisps?Python has a rich selection of libraries for a variety of domains
and Hissp can mostly use them as easily as the standard library.
This gives Hissp a massive advantage over other Lisps with less selection.
If you don't care to work with the Python ecosystem,
perhaps Hissp is not the Lisp for you.Note that the Hissp compiler is written in Python 3.8,
and the bundled macros assume at least that level.
(Supporting older versions is not a goal,
because that would complicate the compiler.
This may limit the available libraries.)
But because the compiler's target functional Python subset is so small,
the compiled output can usually be made to run on Python 3.5 without too much difficulty.
Watch out for positional-only arguments (new to 3.8)
and changes to the standard library.
Running on versions even older than 3.5 is not recommended,
but may likewise be possible if you carefully avoid using newer Python features.Python code can also import and use packages written in Hissp,
because they compile to Python.Useful error messagesOne of Python's best features.
Any errors that prevent compilation should be easy to find.Syntax compatible with Emacs'lisp-modeand ParlinterA language is not very usable without tools.
Hissp's basic reader syntax (Lissp) should work with Emacs.The alternative EDN readers are compatible with Clojure editors.Hebigo was designed to work with minimal editor support.
All it really needs is the ability to cut, paste, and indent/dedent blocks of code.
EvenIDLEwould do.Standalone outputThis is part of Hissp's commitment to modularity.One can, of course, write Hissp code that depends on any Python library.
But the compiler does not depend on emitting calls out to any special
Hissp helper functions to work.
You do not need Hissp installed to run the final compiled Python output,
only Python itself.Hissp bundles some limited Lisp macros to get you started.
Their expansions have no external requirements either.Libraries built on Hissp need not have this restriction.Reproducible buildsA newer Python feature that Lissp respects.Lissp's gensym format is deterministic,
yet unlikely to collide even among standalone modules compiled at different times.
If you haven't changed anything,
your code will compile the same way.One could, of course, write randomized macros,
but that's no fault of Lissp's.REPLA Lisp tradition, and Hissp is no exception.
Even though it's a compiled language,
Hissp has an interactive command-line interface like Python does.
The REPL displays the compiled Python and evaluates it.
Printed values use the normal Python reprs.
(Translating those to back to Lissp is not a goal.
Lissp is not the only Hissp reader.)Same-module macro helpersFunctions are generally preferable to macros when functions can do the job.
They're more reusable and composable.
Therefore, it makes sense for macros to delegate to functions where possible.
But such a macro should work in the same module as its helper functions.
This requires incremental compilation and evaluation of forms in Lissp modules,
like the REPL.ModularityThe Hissp language is made of tuples (and atoms), not text.
The S-expression reader included with the project (Lissp) is just a convenient
way to write them.
It's possible to write Hissp in "readerless mode"
by writing these tuples in Python.Batteries are not included because Python already has them.
Hissp's standard library is Python's.
There are only two special forms:quoteandlambda.
Hissp does include a few bundled macros and reader macros,
just enough to write native unit tests,
but you are not obligated to use them when writing Hissp.It's possible for an external project to provide an alternative
reader with different syntax, as long as the output is Hissp code.
One example of this isHebigo,
which has a more Python-like indentation-based syntax.Because Hissp produces standalone output, it's not locked into any one Lisp paradigm.
It could work with a Clojure-like, Scheme-like, or Common-Lisp-like, etc.,
reader, function, and macro libraries.It is a goal of the project to allow a more Clojure-like reader and
a complete function/macro library.
But while this informs the design of the compiler,
it is beyond the scope of Hissp proper,
and does not belong in the Hissp repository.