{"id":26986841,"url":"https://github.com/minmus-9/lip","last_synced_at":"2025-04-03T19:34:39.257Z","repository":{"id":282965756,"uuid":"949583674","full_name":"minmus-9/lip","owner":"minmus-9","description":"LISP In Python","archived":false,"fork":false,"pushed_at":"2025-03-31T23:28:41.000Z","size":306,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-01T00:26:32.531Z","etag":null,"topics":["call-cc","continuation","continuation-passing-style","cps","evaluator","interpreter","lisp","python","tail-call-optimization","tail-recursion","trampoline"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/minmus-9.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-03-16T19:16:30.000Z","updated_at":"2025-03-31T23:28:44.000Z","dependencies_parsed_at":"2025-04-01T00:23:23.864Z","dependency_job_id":"85762979-f47e-4378-bda0-dbaa5546b410","html_url":"https://github.com/minmus-9/lip","commit_stats":null,"previous_names":["minmus-9/lip"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/minmus-9%2Flip","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/minmus-9%2Flip/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/minmus-9%2Flip/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/minmus-9%2Flip/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/minmus-9","download_url":"https://codeload.github.com/minmus-9/lip/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247066109,"owners_count":20877910,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["call-cc","continuation","continuation-passing-style","cps","evaluator","interpreter","lisp","python","tail-call-optimization","tail-recursion","trampoline"],"created_at":"2025-04-03T19:34:38.477Z","updated_at":"2025-04-03T19:34:39.251Z","avatar_url":"https://github.com/minmus-9.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# LISP In Python\n\n\"OH NO! Another \u0026^%ing LISP written in Python??!\"\n\nThis one is different from the others I've studied in that it:\n- Has been written in continuation-passing-style (CPS) and uses\ntrampolines throughout (see refs) so it won't blow up the Python\nruntime stack,\n- Is usable enough to get through ``Structure and Interpretation of\nComputer Programs'' (SICP aka the Wizard Book, see the references\nbelow),\n- Has tail-call support, and\n- Has first-class continuations with unlimited extent.\n\nIt weighs in at ~1800 Python SLOC and ~550 LISP SLOC (for the standard\nlibrary). This may seem like a lot, but there's a lot in there -- see\n\"The Language\" below.\n\nIf you think of something *actually useful* to do with a\nlisp-embedded-in-python, please let me know.\n\n## Running the Code\n\nUse\n```\n./lisp.py -\n```\nto run the REPL and\n```\n./lisp.py examples/factorial.lisp\n```\nto run code from a file. Finally,\n```\n./lisp.py file1 file2 ... fileN -\n```\nloads the specified files and then enters the REPL.\n\nNote that `lisp.lisp` is automatically loaded by `lisp.py`.\n\n## The Language\n\nThe core language is pretty much complete I think:\n\n|Special Form|Description|\n|--------------------------|-----------------------------|\n|`(begin e1 e2 ...)`|evaluate the expressions in order and return the value of the last one|\n|`(cond ((p c) ...)`|return the `Consequent` for the `Predicate` that returns true|\n|`(define sym value)`|bind `value` to `sym` in the current environment|\n|`(define (sym args) body)`|bind `(lambda (args) body)` to `sym` in the current environment|\n|`(if p c a)`|if `Predicate` then `Consequent` else `Alternative`|\n|`(lambda (args) body)`|create a function|\n|`(quasiquote x)`|aka \\`, begin quasiquoted form|\n|`(quote obj)`|aka `'`, returns obj unevaluated|\n|`(set! sym value)`|redefine the innermost definition of `sym`|\n|`(special sym proc)`|define a special form|\n|`(special (sym args) body)`|define a special form as `(lambda (args) body)`|\n|`(trap obj)`|returns a list containing a success-flag and a result or error message|\n|`(unquote x)`|aka `,` unquote x|\n|`(unquote-splicing x)`|aka `,@` unquote and splice in x|\n\nQuasi-quotation is supported; see `lisp.lisp` for some examples. There is no\nmacro system in this LISP, just quasiquote.\n\n|Primitive|Description (see the source)|\n|--------------------------|------------------------------|\n|`()`|the empty list aka false|\n|`#t`|true singleton|\n|`(apply proc args)`|call `proc` with `args`|\n|`(atom? obj)`|return true if obj is an atom: `()` `#t` or symbol|\n|`(call/cc (lambda (cc) body))`|also `call-with-current-continuation`|\n|`(call/cc)`|fast sugar for `(call/cc (lambda (cc) cc))`|\n|`(car list)`|head of list|\n|`(cdr list)`|tail of list|\n|`(cons obj1 obj2)`|create a pair or prepend `obj1` to list `obj2`|\n|`(/ n1 n2)`|return `n1 / n2`|\n|`(eq? x y)`|return true if the atoms `x` and `y` are the same|\n|`(equal? n1 n2)`|return `#t` if `n1` and `n2` are equal else `()`|\n|`(error obj)`|raise `lcore.error` with `obj`|\n|`(eval obj)`|evaluate `obj`|\n|`(eval obj n_up)`|evaluate `obj` up `n_up` namespaces|\n|`(exit obj)`|raise `SystemExit` with the given `obj`|\n|`(\u003c n1 n2)`|return `#t` if `n1` \u003c `n2` else `()`|\n|`(list ...)`|construct list from arguments|\n|`(* n1 n2)`|return `n1 * n2`|\n|`(nand n1 n2)`|return `~(n1 \u0026 n2)`|\n|`(null? x)`|return `#t` if x is `()` else `()`|\n|`(pair? x)`|return `#t` if x is a pair, else `()`|\n|`(print ...)`|print a list of objects space-separated followed by a newline|\n|`(range start stop step)`|same as the python function but return a list|\n|`(read)`|read an expression from input|\n|`(set-car! pair value)`|set the car of a pair|\n|`(set-cdr! pair value)`|set the cdr of a pair|\n|`(- n1 n2)`|return `n1 - n2`|\n|`(type obj)`|return a symbol representing the type of `obj`|\n\nYou'll note that `+` is not in the list. It is implemented in the standard\nlibrary in terms of subtraction. `nand` is used to create all of the usual\nbasic bitwise ops. There's no predefined I/O either since it isn't clear\nwhat is wanted there, but this code is very easy to extend. Also, see the\nnext section on the FFI interface.\n\nThe `lisp.lisp` standard library defines a bunch of procedures in LISP:\n\n|Name|Description|\n|----|----|\n|`(+ x y)`|return `x + y`|\n|`(\u0026 x y)`|bitwise and|\n|`(\\| x y)`|bitwise or|\n|`(^ x y)`|bitwise exclusive-or|\n|`(~ x)`|invert bits of `x`|\n|`(\u003e= x y)`|return `#t` if `x \u003e= y` else `()`|\n|`(\u003c= x y)`|return `#t` if `x \u003c= y` else `()`|\n|`(\u003e x y)`|return `#t` if `x \u003e y` else `()`|\n|`(abs x)`|absolute value|\n|`(and ...)`|logical-and or args|\n|`(assert expr)`|raise an error unless `expr` is true|\n|`(caddr)...(caddddr)`|extract initial elements from a list|\n|`(copysign x y)`|return `\\|x\\|` with sign of `y`|\n|`(fold-left f x0 list)`|see SICP p.158-65|\n|`(fold-right f x0 list)`|see SICP p.158-65|\n|`(for f start stop step)`|call `(f i)` for each `i` in the given range|\n|`(foreach f list)`|call `f` for each element of `list` discarding the results|\n|`(ftranspose f lists)`|similar to `map` but over the transpose of `lists`|\n|`(gcd x y)`|greatest common divisor of `x` and `y`|\n|`(iter-func f x0 n)`|compose `f` with itself `n` times|\n|`(join list ...)`|concatenate lists|\n|`(last list)`|return the last element of `list`|\n|`(length list)`|return the length of `list`|\n|`(let ((var value) ...) body)`|same as `((lambda (vars) body) values)`|\n|`(let* ((var value ...) body)`|same but each `value` can access preceding `vars`|\n|`(letrec ((var value) ...) body)`|same but all vars are pre-declared|\n|`(loop f)`|infinite loop calling `(f)`|\n|`(loop-with-break f)`|infinite loop calling `(f break)`, use `(break)` to terminate loop|\n|`(lshift x n)`|bitwise left shift `x` by `n` bits|\n|`(map f . lists)`|return list of `(f x y ...)` for each `x,y,...` in `(map1 car lists)` over `lists`|\n|`(map1 f list)`|return list of `(f x)` for each `x` in `list`|\n|`(mod n d)`|return `n` mod `d`|\n|`(not x)`|logical negation of `x`|\n|`(or ...)`|logical-or of args|\n|`(queue)`|create a queue, see `lisp.lisp`|\n|`(reverse list)`|return a reversed copy of `list`|\n|`(rshift x n)`|bitwise right shift `x` by `n` bits|\n|`(table compare-proc)`|create an associative table, see `lisp.lisp`|\n|`(timeit proc reps)`|call `(f i)` for `i=0..reps-1` and report timing info|\n|`(transpose lists)`|equivalent to `(ftranspose (lambda (x) x) lists)`|\n|`(while f)`|loop on `(f)` while `(f)` returns true|\n\n## FFI\n\nRather than adding everything under the sun as a built-in (I'm thinking of\nthe large number of functions in the `math` module, specifically), I chose\nto create a Foreign Function Interface (FFI) to Python to ease incorporating\nadditional things into this LISP-ish doodad. With this interface, Python gets\nto work with native Python lists instead of LISP lists; values are converted\nback and forth automatically.\n\nThe net result is that the `math` module interface looks like\n```\n(math symbol args...)\n```\nso `sin(x)` can be obtained with\n```\n(math 'sin x)\n```\nwhere `(math)` is something close to (sans error checking):\n```\n@ffi(\"math\")\ndef op_ffi_math(args):\n    import math\n    sym = args.pop(0)\n    return getattr(math, str(sym))(*args)\n```\nwhich gets you the whole `math` module at once. See the \"ffi\" section of\n`lisp.py` for the whole scoop. There are interfaces to `math`, `random`,\nand `time` so far, along with some odds and ends like `(shuffle)` that\nrequire separate treatment. The basic support for FFI lives in `lcore.py`\nand is trampolined as well -- so potentially recursive LISP\u003c-\u003ePython\ndata structure conversions won't blow up the python stack. Just keep in\nmind that this makes it slow! For example, `(range 0 100000 1)` takes\nabout 25ms (`range` is implemented as an optimized primitive for\nbenchmarking); meanwhile,\n```\n@ffi(\"range\")\ndef op_ffi_range(args):\n    start, stop, step = args\n    return list(range(start, stop, step))\n```\ntakes over 1250ms.\n\n## The Files\n\nThe evaluator lives in 2 files: `lcore.py` and `lisp.py`. The runtime\nengine lives in `lcore.py` and is where the real action happens in\nterms of trampolines, CPS, etc. The file `lisp.py` implements all of\nthe special forms, primitives, LISP-Python Foreign Function\nInterface (FFI), etc. on top of `lcore.py`. A useful LISP runtime is\nin the file `lisp.lisp` and defines things like `let/let*/letrec` and\nwhatnot.\n\nPlease pardon my goofy Pythonic LISP coding style. I'm new to LISP and\nhaven't quite hit the groove yet.\n\n## Code Overview\n\nAside from the CPS thing, the code in `lisp.py` is fairly\nstraightforward: each operator receives an `lcore.Context` instance\nthat contains the interpreter's execution state (registers, stack,\nsymbol table, and global environment) and returns a continuation. In\nthe python realm, a continuation is just a python function that is\ncalled from `Context.trampoline()`. The trampoline is really a means\nof implementing `goto` for languages that don't have `goto`. Like\nPython or C. CPS is goto-driven programming.\n\nThe code in `lcore.py` is fairly optimized and is filled with\nunidiomatic and somewhat bizarre constructs including gems like\n```\ntry:\n    _ = proc.__call__\n    [do something with callable proc]\nexcept AttributeError:\n    pass\n```\ninstead of\n```\nif callable(proc):\n    [do something with callable proc]\n```\nand\n```\nif x.__class__ is list:\n    [do something]\n```\ninstead of\n```\nif isinstance(x, list):\n    [do something]\n```\n\nWhat's happening here is the elimination of Python function/method\ncalls *at all costs*; in particular, at the cost of readability :D\nFunction calls are so expensive that eliminating them can give you\na 100% speedup (CPython 3.10.12 Pop-OS! 22.04 LTS on a System76\ni7-based Meerkat in low power mode).\n\nPairs are represented as 2-lists so `cons(x, y)` is `[x, y]`. This,\nor its equivalent, is about the only thing that works with the\nmutators `set-car!` and `set-cdr!`. In particular, using regular\nPython lists as LISP lists breaks when you get to `set-cdr!`.\n\nThe runtime stack is also implemented as a LISP linked list of pairs.\nThis is almost twice as fast as using the `list.append()` and\n`lisp.pop()` methods (pronounced *function calls*). You get the\nidea. This choice makes continuations *cheap*. If you use a regular\nlist for the stack, you have to slice the whole thing to create or\ncall a continuation.\n\nThe `Context` class provides `.push()` and `.pop()` methods but\n`lcore.py` doesn't use them internally. The `leval()` family of\nfunctions inlines all of the stack operations for speed; this code\nneeds all the help it can get, speed-wise. You'll see things like\n```\nctx.s = [x, ctx.s]  ## push(x)\n```\nand\n```\nret, ctx.s = ctx.s\nreturn ret  ## pop()\n```\nall over the place in `lcore.py`.\n\nMuch of the code in `lisp.py` is more traditional and idiomatic. It\nuses `.push()`, `.pop()`, `car()`, `cdr()`, and so on to enhance\nclarity and let you focus on *what* is going on instead of *how*\nit's happening. The `unary()` and `binary()` helper functions are\noptimized a bit because they're used so much. Certain other \"hot\nspots\" have been optimized based on profiling data.\n\nPassing circular data structures into the core will definitely cause\ninfinite loops. Fixing this in general would have a grave performance\nimpact and so it hasn't been done.\n\nThe Python GC is the LISP GC and so any LISP circular references\nshould eventually get cleaned up.\n\nInternal exceptions (wrong #args etc) generate Python exceptions\nthat are handled in the usual Python way; LISP exception handling\nis very basic (see `trap`).\n\n## API\n\nXXX TODO\n\nSee the `__all__` portion of `lcore.py`. The `lisp.py` file is a\ndemo of the low-level stuff.\n\n## License\n\nThis code is licensed under the GPLv3:\n\n```\nlip - lisp in python\n       https://github.com/minmus-9/liplip\nCopyright (C) 2025  Mark Hays (github:minmus-9)\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see \u003chttps://www.gnu.org/licenses/\u003e.\n```\n\nThe file LICENSE contains a copy of the full GPL text.\n\n## References\n\nHere are some lisp-related refs that *heavily* influenced this code:\n\n- https://web.mit.edu/6.001/6.037/sicp.pdf\n- https://buildyourownlisp.com\n- https://www.hashcollision.org/hkn/python/pyscheme/\n- https://norvig.com/lispy.html\n- https://norvig.com/lispy2.html\n- https://github.com/rain-1/single_cream\n- https://github.com/Robert-van-Engelen/tinylisp\n- https://dl.acm.org/doi/pdf/10.1145/317636.317779\n- https://en.wikipedia.org/wiki/Continuation-passing_style\n- https://blog.veitheller.de/Lets_Build_a_Quasiquoter.html\n- https://paulgraham.com/rootsoflisp.html\n- https://www-formal.stanford.edu/jmc/index.html\n- https://www-formal.stanford.edu/jmc/recursive.pdf\n- https://legacy.cs.indiana.edu/~dyb/papers/3imp.pdf\n- https://www.cs.cmu.edu/~dst/LispBook/book.pdf\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fminmus-9%2Flip","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fminmus-9%2Flip","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fminmus-9%2Flip/lists"}