{"id":20836729,"url":"https://github.com/tommay/risp","last_synced_at":"2026-05-26T12:32:56.899Z","repository":{"id":66918543,"uuid":"87661057","full_name":"tommay/risp","owner":"tommay","description":"Lazy lisp in ruby.  It's not \"cons should not evaluate its arguments\", it's \"eval should not evaluate its arguments.\"","archived":false,"fork":false,"pushed_at":"2024-02-02T06:34:10.000Z","size":105,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-12-26T01:43:57.194Z","etag":null,"topics":["lazy","lazy-evaluation","lisp","lisp-interpreter","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tommay.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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":"2017-04-08T20:07:41.000Z","updated_at":"2024-05-14T23:31:09.000Z","dependencies_parsed_at":"2024-11-18T01:46:46.404Z","dependency_job_id":null,"html_url":"https://github.com/tommay/risp","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/tommay/risp","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tommay%2Frisp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tommay%2Frisp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tommay%2Frisp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tommay%2Frisp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tommay","download_url":"https://codeload.github.com/tommay/risp/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tommay%2Frisp/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33520646,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T03:12:49.672Z","status":"ssl_error","status_checked_at":"2026-05-26T03:12:47.976Z","response_time":63,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["lazy","lazy-evaluation","lisp","lisp-interpreter","ruby"],"created_at":"2024-11-18T00:31:46.900Z","updated_at":"2026-05-26T12:32:56.880Z","avatar_url":"https://github.com/tommay.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# RISP\n\nThis is a little lisp interpreter I put together to test the ideas in\nthe paper \"Cons should not evaluate its arguments\", in which `cons`\nwas modified to put \"suspensions\" in the `car` and `cdr` of newly\nallocated cells and `car`/`cdr` would evaluate the suspensions.  The\nhope being to get lazy evaluation.\n\nI found out that doesn't make everything lazy because not everything\nis in a `cons` cell and accessed with `car` and `cdr`.  It should be\n\"Eval should not evaluate its argument\".\n\n## Miscelleous\n\nThis lisp dialect is a mish-mash of scheme and emacs-lisp.  It is\nlexically scoped and uses `define` and `null?` but it has `t` and\n`nil` atoms and `cond` has no \"else\" clause.  There's probably more\nmish-mash as well.  Doing everything the scheme way would be better\nbut this was faster/easier to write.\n\nVariables are immutable, i.e., there are no side-effect functions\nother than define and define-macro which define top-level variables.\nTop-level variables can be redefined.\n\nWell.  Despite proving that their interpreter is strictly more\npowerful than McCarthy's interpreter because it can evaluate some\nthings that would cause McCarthy to diverge, it turns out that \"Cons\nshould not evaluate its arguments\" is not powerful enough.  What seems\nto be needed is the idea in the next section.\n\n## Eval should not evaluate its arguments\n\nOn the master branch is an interpreter written from my own\nunderstanding of how a lexically scoped lisp interpreter should work.\n\nAnd then I made `eval` return thunks containing its `form` and\n`bindings` arguments instead of evaluating `form`.  The thunks are\nevaluated later when the evaluated value is actually needed, e.g., to\nprint out, to check for `null?`, to add numbers, etc.\n\nThis works great.  The only (I think) problem being ruby's (MRI 2.4.0)\nlack of tail-call optimization so trying to print an infinite list,\neven though it's done incrementally, will blow the stack.\n\nUpdate: I've tried using trampolines to do tail recursion without\nblowing the stack and they don't seem to fix the memory problem.\nPerhaps it has to do with risp creating an evaluation tree instead of\nan evaluation graph.  Somewhere here there is risp, Haskell, and Frege\ncode where both risp and Frege have the memory problem but Haskell\ndoes not, IIRC it's a zip/filter example but I could be wrong.  That's\na good place to start looking into things.\n\nOr consider this code:\nones = 1 : ones\n(define ones (cons 1 ones))\n\nIn risp, evaluating the tree will create a new Thunk with new bindings\nfor each invocation of \"ones\".\n\n## What's wrong with cons should not evaluate its arguments?\n\nIt's just not the right place to create thunks.  It only creates\nthunks on `cons`.  But not everything `cons`es.  A lot of things work,\nbut some don't.\n\nConsider an infinite list of atoms:\n\n~~~~\n(define (atoms) (cons 'atom (atoms)))\n~~~~\n\nThis worked fine:\n\n~~~~\n(zip '(1) (atoms)\n=\u003e ((1 atom))\n~~~~\n\nBut this blew the stack:\n\n~~~~\n(zip '(1) (filter (lambda (n) (eq n 'other)) (cons 'other (atoms))))\n~~~~\n\nThe `filter` should produce `(other ...)` and the result should be\n`((1 other))`.  Since the first list has only one element, only one element\nfrom the second list should be retrieved.  But `filter`, which is:\n\n~~~~\n(define (filter pred lst)\n  (cond\n   ((null? lst) nil)\n   ((pred (car lst))\n    (cons (car lst) (filter fn (cdr lst))))\n   (t\n    (filter fn (cdr lst)))))\n~~~~\n\nwould return the first `cons` then get into a loop where it never\ncalled `cons` again and therefore would never return.\n\nSince risp.rb in the cons-does-not-evaluate branch is very close to\nMcCarthy's lisp, it doesn't have `define` and uses `atom` to test for\nempty list, and it has dynamic scoping.  Here's the actual failing\ncode for that branch where zip, filter, and atoms are created as\nlambda expressions that call themselves recursively through their\ndynamically scoped bindings then are passed into a lambda that does\nthe actual zip/filter expression:\n\n~~~~\n((lambda (zip filter atoms)\n     (zip '(1) (filter (lambda (z) (eq z 'other)) (cons 'other (atoms)))))\n   (lambda (a b)\n     (cond\n       ((atom a) nil)\n       ((atom b) nil)\n       (t (cons (cons (car a) (cons (car b) nil)) (zip (cdr a) (cdr b))))))\n   (lambda (fn lst)\n     (cond\n       ((atom lst) nil)\n       ((fn (car lst)) (cons (car lst) (filter fn (cdr lst))))\n       (t (filter fn (cdr lst)))))\n   (lambda () (cons 'atom (atoms))))\n~~~~\n\n## On divergence\n\nA lot of things that diverge will blow the stack instead of just\nrunning forever.  For example, this runs correctly:\n\n~~~~\n(define evens\n  (cons 0 (map (lambda (a) (+ a 2)) evens)))\n\n(zip '(a) (filter (lambda (n) (eq n 4)) evens))\n=\u003e ((a 4))\n~~~~\n\nbut if the arguments to zip are reversed then zip diverges since\nfiltering an infinite list gives an infinite list and zip will never\nterminate, even though it would be fine if it did since the second\nlist is finite.  However, it will blow the stack.\n\nHaskell also diverges in the same case, but doesn't blow the stack:\n\n~~~~\nevens = map (+2) (0 : evens)\nzip [\"a\"] (filter (== 4) evens)\n=\u003e [(\"a\",4)]\nzip (filter (== 4) evens) [\"a\"]\n=\u003e [(4,\"a\")  -- then runs forever\n~~~~\n\nI think blowing the stack is due to a lack of tail-call optimization.\nI could possibly use trampolines to get around this.\n\nUpdate: I tried trampolines (commit 8fb6c578187beb89baeb0865a26845b81e98bd7e) and that didn't help.  It may be an issue of expression tree vs. expression graph.\n\n## Problems\n\n### Some things that don't diverge blow the stack:\n\nAll of these blow the stack.\n\n~~~~\n(load 'numbers)\n(nth 1000000000 ones)\n(nth 1000 numbers1)\n(nth 1000 numbers1a)\n(nth 1000 (numbers 1))\n~~~~\n\nI've used a trampoline so arbitrarily long thunk/memo chains can be\ndethunked but the problem is more insidious.  The thunks/bindings become\narbitrarily large.\n\n### Thunk memos blow the heap\n\nThunk memos can make arbitrarily long chains and blow the heap because\nthey are strongly referenced.  `WeakRef` doesn't help because the\n`WeakRef`s are aggressively garbage collected and don't live long enough\nto be effective.  SoftReferences are what's needed.  See commit\n6f9bd3a268fa8afd0837fc8cbdf8c1932bbd825f.\n\nMaybe do it in jruby and use java's SoftReferences.\n\n### Blowing the heap but not the stack\n\nThis will use arbitrary heap but limited stack:\n\n~~~~\n(nth 1000000 ones)\n~~~~\n\nThis will blow the stack:\n\n~~~~\n(apply + (take 10000 ones))\n~~~~\n\nUsing the Y-combinator version, `yones`, has the same limitations.\n\n### `and`/`or` should iterate or use trampolines\n\nIf/when everything works nicely and infinite lists don't cause\nproblems, `and` and `or` should be changed from tail recursion\nto iteration or trampolines so they can handle arbitrarily long\nlists such as `(all? ...)` or `(any? ...)` might want.\n\nUpdate: this has been done in commit be3a47a475a49c6434a2133b75b88860b4a827eb.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftommay%2Frisp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftommay%2Frisp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftommay%2Frisp/lists"}