{"id":13803869,"url":"https://github.com/dtenny/with-redefs","last_synced_at":"2025-05-13T17:30:41.863Z","repository":{"id":242727789,"uuid":"810395336","full_name":"dtenny/with-redefs","owner":"dtenny","description":"WITH-REDEFS enables rebinding of global functions, inspired by Clojure's with-redefs","archived":false,"fork":false,"pushed_at":"2024-06-04T16:09:08.000Z","size":19,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-06-08T15:06:40.058Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Common Lisp","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dtenny.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":"2024-06-04T15:58:02.000Z","updated_at":"2024-06-04T17:21:34.000Z","dependencies_parsed_at":"2024-06-04T18:02:27.319Z","dependency_job_id":null,"html_url":"https://github.com/dtenny/with-redefs","commit_stats":null,"previous_names":["dtenny/with-redefs"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dtenny%2Fwith-redefs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dtenny%2Fwith-redefs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dtenny%2Fwith-redefs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dtenny%2Fwith-redefs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dtenny","download_url":"https://codeload.github.com/dtenny/with-redefs/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":213870460,"owners_count":15650178,"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":[],"created_at":"2024-08-04T01:00:38.622Z","updated_at":"2024-08-04T01:02:55.865Z","avatar_url":"https://github.com/dtenny.png","language":"Common Lisp","funding_links":[],"categories":["Clojure"],"sub_categories":[],"readme":"# About\n\n`WITH-REDEFS` is a macro that allows you to rebind the _global_ functions associated\nwith symbols. It is inspired by the Clojure macro of the same name, however\nthe nature of Common Lisp prohibits it from functioning identically.\n\nThe tool is used mainly for mocking functions in test environments, or\nwrapping functions so that you can intercept and record their activity.\n\n*THIS IS A TEST TOOL, YOU SHOULD NOT USE IT IN CODE THAT GOES TO PRODUCTION ENVIRONMENTS*\n\nI've packaged this up as an independent project/repo because unlike Clojure\nwhere redefining a function is very easy, Common Lisp is chock full of\ncaveats.  A compiled lisp follows a different set of rules than Clojure's\ncompile-on-the-fly-after-loading JVM environment. That said, Common Lisp's\ncompilation model also has strengths that help ensure your code will work\nin production *before* it gets to production. Contrast this to Clojure,\nwhere I could bundle up my grandmother's cookie recipes in an uberjar and\nit might not fail until it's invoked by some other library dependent on it\nwhen it is executed in production.\n\nAlso (why I put this in distinct repo), I haven't released a general\npurpose clojure compatibility library that would be suitable for this\nmacro, however it's useful enough I wanted it where I could quickload it\nfor ad-hoc use and/or just share this documentation. The implementation is\ntrivial, it's the documentation and tests that took a bit of time.\n\n## Usage\n\nThis lisp system is available via the Ultralisp distribution in QUICKLISP.\n\nIf you didn't get this via quickload a quicklisp/ultralisp repo, add it to\nyour `~/quicklisp/localprojects/` directory, update/delete the\n`system-index.txt` file accordingly, and then you can quickload it.\n(Quicklisp rebuilds the `system-index.txt` if it is missing).\n\n    ;; To load just the `WITH-REDEFS` package\n    (ql:quickload :with-redefs)\n\n    ;; To load `WITH-REDEFS` and `WITH-REDEFS-TEST` packages\n    (ql:quickload :with-redefs-test)\n    (with-redefs-test:run-tests)\n\nThen you can:\n\n    (with-redefs (some-global-function-name replacement-function\n                  ...) ; possible additional symbol/replacement-function pairs\n      ... body ...) ; returns value of body\n\nSee examples below and test module for more examples.\n\n## Tested Lisps and implementation-specific notes\n\nAll tests were run at default compiler optimization settings. YMMV.\n\n- SBCL: WORKS\n\n- CCL 1.12.2: WORKS\n\n- Allegro CL Express 11.0 (`alisp` executable): WORKS\n\n  Unable to test NOTINLINE defstruct accessors.\n\n  Allegro also does not return TYPE-ERROR where the spec appears to require it, noted\n  in one conditionalized test.\n\n- ECL 21.2.1: WORKS\n\n- ABCL 1.9.0, OpenJDK 17.0.9: WORKS - with style warnings\n\n  It might be nice to use `MUFFLE-WARNING` for the style warnings on\n  redefined functions, but for now that's left to the caller. Only ABCL\n  issued such style warnings of the lisps tested here.\n\n  Also allows symbols for new-function, though you should consider this non-portable.\n\n- LispWorks 8.0.1 Personal Edition: WORKS\n\n  Also allows symbols for new-function, though you should consider this non-portable.\n\n\n## Examples and some caveats\n\n    (defun my-function (x y)\n      (format t \"MY-FUNCTION called with args ~s ~s~%\" x y)\n      (+ x y))\n\n    ;; Simple replacement of a pre-existing function associated with the symbol MY-FUNCTION\n    (with-redefs (my-function \n                  (lambda (\u0026rest args)\n                    (format t \"MY-FUNCTION redef called with args ~s~%\"\n                            args))) ;returns nil\n      (my-function 1 2 3))\n\n    MY-FUNCTION redef called with args (1 2 3)\n    =\u003e NIL\n\nNote that `WITH-REDEFS` uses `UNWIND-PROTECT` to ensure restoration the\noriginal function value.\n\n    ;; Wrapping a pre-existing function\n    (let ((old-function #'my-function))   ;NOTE: `FUNCTION` (i.e `#'`) is important\n    (with-redefs (my-function \n                  (lambda (\u0026rest args)\n                    (format t \"MY-FUNCTION redef called with args ~s~%\" args)\n                    (funcall old-function (first args) (second args))))\n      (my-function 1 2 3))\n\n    MY-FUNCTION redef called with args (1 2 3)\n    MY-FUNCTION called with args 1 2\n    =\u003e 3\n\nNote that because the symbol-function of the symbol is rebound by `WITH-REDEFS`\ncallers of the function will not see the changed function slot unless they are \neither:\n    \na. calling the function in the normal function position of a form, e.g. `(my-function 1 2)` or\nb. calling the function via symbol, e.g. `(funcall 'my-function 1 2)`.\n\nHowever if some caller is calling via `(funcall #'my-function 1 2)` they are\nnot going to see the redefined symbol-function value because it was\nobtained prior to the `with-redefs` update of the symbol function.\n\nYou may ask \"why can `CL:TRACE` instrument functions that `WITH-REDEFS`\ncan't?\" even though they are also called with `#'my-function`.  The answer\nis that `CL:TRACE` _may_ know enough about the implementation to update the\nactual compiled code vector so it doesn't need to change the\nsymbol-function slot.\n\nThese effects can vary by platform.  In all likelihood if you want to\nredefine a function using `WITH-REDEFS`, you should plan for it, making\nsure *NOT* to use `#'my-function` (which is the same as `(function\nmy-function)`).\n\nThat may or may not be a small performance hit (having to\nderef the symbol-function slot) depending on lisp implementation and\ncontext, but it's nicer than having test-mode-only logic cluttering the\nfunction(s) in question.\n\n## More Examples\n\nOne of my favorite uses of WITH-REDEFS in testing is to be able to assert\nwhether or not a function was called.  E.g.\n\n    ;; in the production code that I do NOT want to leave permanently\n    ;; instrumented for testing\n    (defun function-a ()\n      ...)\n\n    (defun function-b (thing)\n      (if thing\n          (function-a)\n          (function-b)))\n\n    ;;; in the test module\n    (test \n      (let ((counter 0)\n            (old #'function-a))\n        (with-redefs (function-a \n                      (lambda () \n                        (incf counter)\n                        (funcall old)))\n           ... stuff that calls function-b ...\n           ;; Ensure that function-a was/was-not called as per expectations\n           (is (= \u003ctruth\u003e counter)))))\n\nIf your functions are called in a multi-threaded system, ensure you \nuse an atomic or otherwise mutexed counter.\n\n## SETF update functions\n\nYou can also redefine setf updaters (subject to general caveats outlined\nbelow), see the `set-f` test for examples in `with-redefs-test.lisp`.\n\n## More caveats\n\n### Inlined functions, `(DECLAIM (NOTINLINE F))`\n\nCommon Lisp implementations (of which there are many), compiled lisps that\nmay utilize both static and dynamic compilation tricks, also have different\nideas about the realm of optimizations that can be applied to code.\n\nUsers have some control over this with the `OPTIMIZE` declarations, however\neven if you expect that you are not running in super-optimized modes, the\ncompiler may still inline your functions, for example if the function being\ncalled is not called or exported outside of the module that defines it.\n\nIf your implementation aggressively inlines a function you want to\nredefine, try `(declaim (notinline my-function))` in the compilation unit\nand see if that helps.  Note that `defstruct` accessors are\nhigh-probability candidates for inlining unless directed otherwise.\n\nNote that in some lisps declaring a defstruct accessor notinline is not enough\n(with with a global DECLAIM, or a local DECLARE in the test), e.g. Allegro.\nAllegro's `describe` says:\n\n    WITH-REDEFS-TEST(5): (describe 'ship-x)\n    SHIP-X is a NEW SYMBOL.\n      It is unbound.\n      It is INTERNAL in the WITH-REDEFS-TEST package.\n      Its function binding is\n        #\u003cClosure SAFE-DEFSTRUCT-ACCESSOR [SHIP] @ #x10007830ad2\u003e\n        which function takes arguments (OBJECT)\n      Its property list has these indicator/value pairs:\n    SYSTEM::.INLINE.            NOTINLINE\n    EXCL::SETF-INVERSE          (EXCL::DEFSTRUCT-SLOT-DEFSETF-HANDLER . 1)\n    SYSTEM::LISP-DUAL-ENTRY     SYSTEM::STRUCT-ACCESSOR-HOOK\n      Its source-file location is: /home/dave/CommonLisp/with-redefs/with-redefs-test.lisp\n\nSo it appears to see something about \"notinline\" but isn't acting on it.\nI wasn't able to determine if there's a workaround for Allegro with a\ncursory search.\n\n### Package-locked functions\n\n`WITH-REDEFS` does not attempt to unlock locked packages, so any attempt to\nredefine a symbol in those packages will likely fail.\n\n### COMMON-LISP package functions\n\nPackage locks aside, it is probably a bad idea to try to redefine functions\nin the `COMMON-LISP` package.\n\n### Compile-time effects\n\nBeware compile-time effects/interactions.  For example if you call\n`EVAL` in the body of your `WITH-REDEFS`, and the eval causes the function\nyou've redefined to be recompiled, then your redefinition will likely be lost.\n\n### WITH-REDEFS is not thread-safe\n\nThe symbol whose symbol-function is being redefined is not manipulated\nunder the control of a mutex. This is fine if you're doing single-threaded\ntest logic.\n\n### WITH-REDEFS does not update symbol-value slots\n\nClojure, being a lisp-1, doesn't distinguish between symbol-value and\nsymbol-function slots.  Common Lisp, being a lisp-2, has both, and\n`WITH-REDEFS` updates _only_ the symbol-function slot.\n\n### `FDEFINITION` is key, implementations vary\n\nAt the end of the day, a global function definition is side-effected\nthrough the use of `(SETF (FDEFINITION FUNCTION-NAME) NEW-FUNCTION)`.\nAccording to the standard the `NEW-FUNCTION` argument must be a `FUNCTION`,\nwhich is what is returned by the `FUNCTION` special form, the function\n`COERCE`, or the function `COMPILE`.\n\nMost tested lisps implement this and will not accept a symbol for\n`NEW-FUNCTION`.  Some lisps such as ABCL and LispWorks do accept\nsymbols. `WITH-REDEFS` doesn't judge, whatever `SETF (FDEFINITION X)`\naccepts is fine, but be aware of the differences for portability, if you care.\n\nNote that one difference between `WITH-REDEFS` and `SETF (FDEFINITION X)`\nis that `WITH-REDEFS` take symbols which are not `FBOUNDP` and turn them\ninto symbols which _are_ `FBOUNDP`, and vice versa.\n\nSee also\n[FDEFINITION](https://www.lispworks.com/documentation/HyperSpec/Body/f_fdefin.htm)\nHyperspec content.\n\n### You may not rebind macro and special operator definitions\n\n`WITH-REDEFS` uses the CL functions FBOUNDP, SPECIAL-OPERATOR-P, and\nMACRO-FUNCTION to try to signal an error if you attempt to rebind macros\nand special operators. Your mileage may vary, hopefully the logic is\nportable from a standards compliant standpoint though.  Some of these\nfunctions, like SPECIAL-OPERATOR-P underwent changes in the standards work. \n\nSee also\n[Common Lisp external symbol constraints](https://www.lispworks.com/documentation/lw50/CLHS/Body/11_abab.htm)\nin the hyperspec.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdtenny%2Fwith-redefs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdtenny%2Fwith-redefs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdtenny%2Fwith-redefs/lists"}