Skip to content

Development Workflows

gilch edited this page Sep 7, 2025 · 3 revisions

Lisp traditionally has a more interactive development workflow than you may be used to, more like a notebook than a static IDE project. But Hissp was designed to fit into Python projects and can approximate either style.

Lissp Module Workflow

The usual workflow for developing a Lissp module is REPL-driven. At minimum, you want a terminal running the Lissp REPL and a text editor. Even IDLE will do (it can edit arbitrary text files). Ideally, your editor is Lisp-aware and can easily send snippets to an integrated terminal running the REPL. But a dumb editor and copy-paste to the REPL is sufficient.

Create a Lissp file, like foo.lissp in your editor. You probably want a prelude, but this is optional. Hissp bundles a small one. Add it to your Lissp file in the editor:

;; file foo.lissp
hissp..prelude#:

Activate your venv (if you're using one) and change to your project source directory. Start the REPL in your terminal with

$ lissp
(additional options...)

If using a venv, you can instead use the full path to the lissp script without activating first.

You can also start the LisspREPL without the lissp script, with

$ python -m hissp

This form allows additional Python options, like

$ python -Om hissp

to disable __debug__/assertions.

(or, if starting from a Python REPL...)

(For example, in IDLE.) You need to make sure your working directory is your project source directory.

>>> import os; os.getcwd()

Fix that if it isn't (e.g., start IDLE using python -m idlelib from the directory, or use os.chdir()). Then start the Lissp REPL:

>>> import hissp; hissp.interact()

Compile and load your Lissp module with the Lissp REPL:

#> hissp..refresh#'foo

This generates a corresponding foo.py file.

Then launch a subREPL in the new module it returned:

#> hissp..subrepl#_
(alternatively...)

You can do this in one line if you like:

#> hissp..subrepl#hissp..refresh#'foo

or later via any expression evaluating to the module, like a module handle:

#> hissp..subrepl#foo.

You can quit a subREPL with EOF (Usually Ctrl+D, but may be Ctrl+Z in some Windows terminals.), and start a subREPL in some other module (even pure Python modules). As long as you don't end the Python session, the module state is preserved, and you can start a subREPL in that module again to get back to where you were.

If you forget which module you're working in, try

#> __name__

Note

Refreshing is a convenience command for a two-part process. It first transpiles the file from .lissp to .py (which compiles and executes each top-level form in turn), and then reloads the resulting .py module. Any import-time side effects will happen for both of these steps. The top-level forms of Lissp modules should be idempotent definition forms. If you still need side effects, guard them using the

(when (eq __name__ '__main__)
  (main))

pattern (where main is whatever effects).

(without bundled macros...)

In standard Hissp:

((.get (dict : __main__ main) __name__ (lambda :)))

Or, more compactly, via a Python injection:

|if __name__=='__main__':main()|

The Refresh Cycle

Add or update definitions in your editor, make sure it saved the file, and use

#> H##refresh :

to recompile and reload the subREPL's working module. You can repeat this to iteratively refine your definitions without stopping your session or losing your program state.

(without the alias...)

If you don't have the H# alias for hissp. (e.g., from the prelude), use the fully qualified tag:

#> hissp..refresh#:

The : argument is a convenience, but the named version we used previously will also still work:

#> hissp..refresh#'foo

Note

If you remove or rename a definition, remember it will persist in the namespace until deleted:

#> (del foo)

See the documentation for importlib.reload() for more on how module reloading works in Python.

You can enter small amounts of Lissp code directly in the REPL, but more complicated tests or things more than a few lines should be composed in the editor and then sent to the REPL. Your editor is for editing. The REPL isn't good at that; it only has basic line entry. You can hide experimental module code from a refresh by using the discard tag (_#), which can drop any expression (although it still gets read, which runs tags), even a tuple and its contents. Rather than throwing away these manual tests, once they're working, upgrade them to top-level assure forms, copy REPL snippets into doctests to document usage, or rewrite them as full unittests.

Script Workflow

The lissp (or python -m hissp) command can run a .lissp file as the main module without generating a .py file for it. It will also ignore a shebang line, which can be an absolute path to the lissp command in a venv. Use the same workflow you'd use in Python, except the language is Lissp.

The module workflow with the aforementioned "name equals main" guard for side effects can also work, but this generates a .py file. You don't need both files to run the script. You can safely delete the .py file once you're done developing (unless you're also using it as an importable module). You can always regenerate it from the .lissp file. Or, if you'd prefer to use the .py file, use a Python shebang. You can put the shebang in a fragment token on the first line, and it will write through. If you respected the standalone property, the hissp package need not be installed for that Python interpreter for the .py file to work. You probably still want to keep the .lissp source in case you want to make modifications later. The .py file is just Python, so additions would be easy even without the source, but it's not the preferred form for modifications.

Package Workflow

The REPL-driven module workflow also works for modules in packages. However, especially in larger projects, or projects that are mainly Python, you may want to trigger transpilation automatically on package import.

Add

import hissp

hissp.transpile(__name__, 'foo', 'bar')

to your package __init__.py, where 'foo', 'bar', etc. are the names of the other modules in the package. There should be a corresponding foo.lissp, bar.lissp, etc. file for this to work, which transpile will generate corresponding .py files for. Add/remove/rename the listed names whenever you add/remove/rename the .lissp files, or if you want to stop compiling one for some reason. You can also call hissp.transpile() from the main module for the same package-style transpilation of non-packaged modules.

Note

This form violates Hissp's standalone property. This is fine if Hissp is a dependency, but

you could also disable this code in various ways...
try:
   import hissp
except ModuleNotFoundError:
    pass
else:
    hissp.transpile(__name__, 'foo', 'bar')

or, more explicitly,

import os

if os.getenv('TRANSPILING_LISSP'):
    import hissp
    hissp.transpile(__name__, 'foo', 'bar')

or cherry pick alternate __init__.py files on a deployment branch, etc.

Static Typing

Hissp has no included support for static typing. Run time uses of type annotations are still available by mutating the appropriate __annotations__ property.

It's a myth that static typing reduces the defect rate in delivered programs, which seems to be a near-constant fraction of the number of kilobytes of source code, regardless of language (and whether that language is statically or dynamically typed). Type errors are among the easiest kind to notice and fix, especially with good test coverage and fast feedback from the REPL, while static typing is no benefit for the other, more important problems. Lissp modules control the defect rate better than static typing by being shorter than the equivalent Python, especially the equivalent Python with verbose static typing annotations, which only bloat the codebase further, and worse, prevent the use of many of the more expressive dynamic approaches that could keep a codebase simpler, because they're too hard to type (if it's possible at all). Of course, it's possible to write bad code in any language. You don't get the benefits of Lissp's conciseness if you don't write it concisely. Keeping it simple has to be a priority.

Nevertheless, if Python project rules force the use of static typing on you, you can always write separate .pyi files for the Hissp packages it uses, and could even use metaprogramming to help generate them. That is, if the type system can even express what you're trying to say.

Notebook Workflow

There are numerous methods. See the dedicated wiki page for details.

The two main styles are either to add a cell to create a %%lissp custom cell magic for writing Lissp cells, or to add a cell to create a preprocessor that compiles Lissp to Python for all code cells. The former is for Python-first notebooks that occasionally need Lissp. The latter is for Lissp-first notebooks. You work with these just like a Python notebook, except the language is Lissp.

Additionally, because Hissp is distributed as a pure-python wheel, Pyodide (the web front-end Python kernel used by Jupyterlite) can install it with micropip.

Neither of those styles respects Hissp's standalone property, because notebooks are meant to be interactive like the REPL. But there are other styles that do, for example, LisspREPL sessions show the Python compilation, while the Lissp prompts look like Python comments. IPython strips Python prompts by default, so you can paste the prompt lines of a LisspREPL session into a notebook cell and it should just work. You may want to collapse these cells for better presentation.

Readerless Mode Workflow

Hissp doesn't need to generate .py files to work. It does need to generate Python code, but Python can execute that dynamically, without importing a module, via eval() or exec(). Python beginners (who know they exist) tend to overuse these functions to accomplish things that Python can handle in simpler ways. Furthermore, running either of these on untrusted inputs is a security risk, so use of these functions is discouraged, and many are under the mistaken impression that they should never be used.

But import-time metaprogramming for things Python can't handle in simpler ways is an appropriate use, and even the standard library does this (see namedtuple, although even that doesn't need it). But this kind of string metaprogramming is difficult and error prone. Replacing these kinds of things with a more reliable form of metaprogramming is a major use case of Hissp.

Hissp provides three readerless-mode convenience functions: hissp.readerless(), which returns the Python compilation of a single Hissp form without executing it; hissp.evaluate(), which evaluates a single Hissp form and returns its result; and hissp.execute(), which executes any number of Hissp forms (effectively, entire embedded modules) and returns their Python compilation. More advanced usage might require instantiating or even subclassing the Hissp compiler.

Lissp tags and templates are a reader-level concept, and so are unavailable in readerless mode, but manipulations of Hissp forms can be done directly in Python before passing them to the compiler, which effectively replaces these features.

The _macro_ namespace is typically defined using a class statement in readerless mode, as demonstrated in the README. Macro functions can be written in Python as "methods" in the class body, although it's just being used as a function namespace, so they're not technically methods and shouldn't have a self argument. Some cases might add imports in the class body or subclass other _macro_ classes to inherit a basic set. Advanced _macro_ objects may be instances of a class that overrides __getattr__ (or even __getattribute__) for dynamic macro names. In this case, because _macro_ is an instance, @staticmethod may be required for the non-dynamic names.

When debugging dynamically generated Hissp, try running it through pprint.pp() to make it more readable. You can also inspect the Python compilation.

Clone this wiki locally