{"id":19430230,"url":"https://github.com/8dcc/sl","last_synced_at":"2025-04-24T18:33:25.895Z","repository":{"id":204977846,"uuid":"713076950","full_name":"8dcc/sl","owner":"8dcc","description":"Simple Lisp interpreter","archived":false,"fork":false,"pushed_at":"2024-10-26T15:34:50.000Z","size":712,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-10-27T08:54:18.057Z","etag":null,"topics":["evaluator","interpreter","lisp","lisp-interpreter","parser","simple"],"latest_commit_sha":null,"homepage":"","language":"C","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/8dcc.png","metadata":{"files":{"readme":"README.org","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":"2023-11-01T19:42:28.000Z","updated_at":"2024-10-26T08:12:58.000Z","dependencies_parsed_at":null,"dependency_job_id":"f7eb7222-98dd-4ef9-a624-347e53e0748e","html_url":"https://github.com/8dcc/sl","commit_stats":null,"previous_names":["8dcc/sl"],"tags_count":0,"template":false,"template_full_name":"8dcc/c-template","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/8dcc%2Fsl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/8dcc%2Fsl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/8dcc%2Fsl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/8dcc%2Fsl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/8dcc","download_url":"https://codeload.github.com/8dcc/sl/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":223963177,"owners_count":17232611,"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":["evaluator","interpreter","lisp","lisp-interpreter","parser","simple"],"created_at":"2024-11-10T14:24:04.483Z","updated_at":"2025-04-24T18:33:25.888Z","avatar_url":"https://github.com/8dcc.png","language":"C","funding_links":[],"categories":[],"sub_categories":[],"readme":"#+title: Simple Lisp\n#+options: toc:nil\n#+startup: showeverything\n#+author: 8dcc\n\n#+TOC: headlines 2\n\nSimple Lisp interpreter made in C with no dependencies.\n\nArticles I wrote about Lisp, and this interpreter in particular:\n\n- [[https://8dcc.github.io/programming/cons-of-cons.html][The pros and cons of Cons]], specifically about this implementation.\n- [[https://8dcc.github.io/programming/conditional-lisp-macros.html][Replacing conditional Lisp primitives with macros]], more of a proof of concept\n  than anything practical.\n- [[https://8dcc.github.io/programming/understanding-y-combinator.html][Understanding the Y combinator]], about lambda calculus and Lisp in general.\n- [[https://8dcc.github.io/programming/pool-allocator.html][Writing a simple pool allocator in C]], for my [[https://github.com/8dcc/libpool][libpool]] project, but also used in\n  this interpreter.\n\nSome good resources for learning about Lisp:\n\n- [[https://mitp-content-server.mit.edu/books/content/sectbyfn/books_pres_0/6515/sicp.zip/index.html][Structure and Interpretation of Computer Programs]] by Hal Abelson and\n  Gerald Jay Sussman.\n- [[https://paulgraham.com/acl.html][ANSI Common Lisp]] by Paul Graham.\n- The [[https://www.scheme.org/][Official Scheme website]].\n- The [[https://groups.csail.mit.edu/mac/ftpdir/scheme-7.4/doc-html/scheme_toc.html][MIT Scheme Reference]].\n- The [[https://www.gnu.org/software/guile/manual/][GNU Guile manual]].\n- The [[https://conservatory.scheme.org/schemers/Documents/Standards/R5RS/HTML/][Scheme R5RS Specification]].\n- [[https://www.gnu.org/software/emacs/manual/html_mono/eintr.html][An Introduction to Programming in Emacs Lisp]] by Robert J. Chassell.\n- [[https://www.buildyourownlisp.com/][Build Your Own Lisp]] by Daniel Holden, although this project doesn't\n  follow it.\n\n* Building\n\n#+begin_src console\n$ git clone https://github.com/8dcc/sl\n$ cd sl\n$ make\n...\n#+end_src\n\n* Usage\n\nThe full manual can be found in the [[file:doc/sl-manual.org][doc]] directory. When running =make= from that\ndirectory, the initial Org file is exported to =.texi= using Emacs, and then from\n=.texi= to =.pdf= and =.html= using =makeinfo=. Note that both Emacs and =makeinfo= can\nexport the original Org file to more formats, if needed.\n\nThe [[file:test/][test]] folder also contains the Lisp code used for testing the interpreter.\n\n#+begin_src console\n$ ./sl\nsl\u003e (+ 1 2 (- 5 4) (* 3 4) 10)\n26\n\nsl\u003e (cons 'a 'b)\n(a . b)\n\nsl\u003e (cons 'a '(b . (c . nil)))\n(a b c)\n\nsl\u003e (define my-global 10.0)\n10.000000\n\nsl\u003e (defun my-func (x y)\n      (* my-global (+ x y)))\n\u003clambda\u003e\n\nsl\u003e (my-func 3 4)\n70.000000\n\nsl\u003e `(a b ,my-global c d ,@(list 1 2 3))\n(a b 10.000000 c d 1 2 3)\n\nsl\u003e (defmacro inc (var-name)\n      `(define ,var-name (+ ,var-name 1)))\n\u003cmacro\u003e\n\nsl\u003e (inc my-global)\n11.000000\n\nsl\u003e my-global\n11.000000\n#+end_src\n\n* Overview of the code\n\nThis Lisp has a few components that are responsible for their own data. This is\nthe basic [[https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop][REPL]] process:\n\n1. The user input is read using =read_expr()=, defined in [[file:src/read.c][read.c]]. This function\n   will read a single Lisp expression across lines, and save the data in an\n   allocated string.\n2. The raw user input is converted into an array of =Token= structures using the\n   =tokenize()= function, which calls the static function =get_token()=. This step\n   might be redundant for a simple language as Lisp, but I decided to do it\n   anyway. After this step the =Token= array could be printed using =token_print()=,\n   if needed. All these functions are defined in [[file:src/lexer.c][lexer.c]].\n3. The =Token= array is converted into a linked list of =Expr= structures using the\n   =parse()= function from [[file:src/parser.c][parser.c]]. This linked list is essentially the\n   [[https://en.wikipedia.org/wiki/Abstract_syntax_tree][Abstract Syntax Tree]] (AST). At this point, nothing has been evaluated; it\n   should just be a different representation of the user input. All the values\n   allocated by =tokenize()= have been copied into the AST instead of reused, so\n   they can be freed safely.\n4. We evaluate the expression using =eval()=, defined in [[file:src/eval.c][eval.c]]. This function\n   will return another linked list of =Expr= structures but, just like =parse()=, it\n   will not reuse any data in the heap, so the old =Expr*= can be freed\n   safely. This is the rough evaluation process:\n   1. Before evaluating the expression, it checks if the current expression is a\n      [[https://web.mit.edu/6.001/6.037/sicp.pdf#subsection.4.1.1][Special Form]], which are evaluated differently from procedure calls. For\n      example =quote=, =lambda= or =if=. The arguments of these special forms are\n      passed to the C primitives (defined in [[file:src/primitives.c][primitives.c]]) un-evaluated.\n   2. If it wasn't a special form, the expression type is checked. Some\n      expressions like numbers evaluate to themselves, while some others\n      don't.\n   3. If the expression was a symbol, the environment is searched to find the\n      expression bound to that symbol. This is done with the =env_get()= function,\n      defined in [[file:src/env.c][env.c]].\n   4. If the expression was a list, it is evaluated as a procedure or macro\n      call. This is the evaluation process of a call:\n      1. The first element of the list is evaluated. It should return either a\n         lambda, a macro or a C primitive.\n      2. If the function was a macro, the arguments are *not* evaluated and they\n         are passed to =macro_call()=, defined in [[file:src/lambda.c][lambda.c]]. From there, the macro\n         is expanded with =macro_expand()= and the expanded expression is\n         evaluated and returned. For more information on how macros behave in\n         this Lisp, see [[https://www.gnu.org/software/emacs/manual/html_node/elisp/Macros.html][Emacs Lisp manual]].\n      3. If the function was not a macro, each argument should be evaluated\n         before calling the function. This is done with the =eval_list()= static\n         function. Then the function is applied to the arguments with =apply()=,\n         also defined in [[file:src/eval.c][eval.c]].\n      4. The function type is checked inside =apply()=. If it's a C primitive, the\n         function pointer stored in the =Expr= is called with the arguments we got\n         from =eval()=. If it's a lambda, it is called using =lambda_call()=,\n         defined in [[file:src/lambda.c][lambda.c]].\n      5. The =lambda_call()= function operates on the =LambdaCtx= structure of the\n         =Expr=. It binds each formal argument to the lambda's environment; sets\n         the parent environment (so the body can access globals); and evaluates\n         each expression in the body in order, returning the last one.\n5. After that, we print the evaluated expression using =expr_print()=, defined in\n   [[file:src/expr.c][expr.c]].\n\n* Todo list\n\nThese are some things that need to be done. Feel free to make a PR if you want\nto contribute.\n\n** Tail-call optimization\n\nThe following code defines a /recursive procedure/ that performs an /iterative\nprocess/.\n\n#+begin_src lisp\n(defun sum-iter (i end total)\n  (if (\u003e i end)\n      total\n      (sum-iter (+ i 1)\n                end\n                (+ total i))))\n\n(sum-iter 1 5 0) ; 15\n#+end_src\n\nEven though that /procedure/ is recursive, since it calls itself, the /process/ is\niterative, because it has all the necessary information for continuing the\ncomputation in its parameters. The interpreter doesn't *need* to keep track of\nwhere it was called from, it can just jump to the start of the function with the\nnew parameters and no information will be lost. This jump optimization is called\n/tail-call optimization/, and an interpreter with this feature is called\n/tail-recursive/. For more information, see [[https://web.mit.edu/6.001/6.037/sicp.pdf#subsection.1.2.1][section 1.2.1 of SICP]].\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F8dcc%2Fsl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F8dcc%2Fsl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F8dcc%2Fsl/lists"}