Skip to content

Latest commit

 

History

History
225 lines (193 loc) · 9.22 KB

defl.org

File metadata and controls

225 lines (193 loc) · 9.22 KB

defl: Buffer local functions

Intro

The best way to avoid these issues is to upstream common functions into ow- directly, but orgstrap is designed to be completely distributed, so it does fall on the user.

The other way around this issue is to run orgstrapped files in a separate Emacs process. This is suboptimal.

There might be a way to implement buffer local functions using advice-add :around and manually dispatching by buffer. info:elisp#Advice Combinators

Internally defun uses defalias to bind the name so in theory we can replace that step with a check to see if a function is already bound to that name. If not we create a stub (lambda (&rest rest) nil) or something for the global name. Once the function exists advise it with has-local-defuns which can be used to handle the dispatch per buffer. Most of the time it will fall through.

The other possibility would be to keep a list of all the functions as add a hook to switch buffer hook or something like that an have it advise to :override with the buffer local value on the way in, and then remove the override with leave buffer hook, but those hooks don’t actually exist info:elisp#Standard Hooks, so we are left with checking each time.

The good news is actually that most of the time you don’t actually have to do this. You just have to keep in place in case the name gets redefined? Then you dispatch? None if this is going to be performant. Especially in a tight loop.

So after implementing defun-local I’m wondering whether it works as expected in org babel blocks. It would seem so. This won’t work with reval unless reval-materialize is run since reval runs in a separate buffer, but most things in reval should conform to general elisp best practices so that is less of an issue.

Apparently this isn’t entirely new, I just couldn’t find the right search terms https://www.emacswiki.org/emacs/BufferLocalCommand. The approach described there is much more elegant than the one below, but it doesn’t handle name collisions which is out primary use case.

Code

defl.el

;;; defl.el --- Buffer local functions. -*- lexical-binding: t -*-

;; Author: Tom Gillespie
;; Homepage: https://github.com/tgbugs/orgstrap
;; Version: 9999
;; Package-Requires: ((emacs "24.4"))
;; Is-Version-Of: https://raw.githubusercontent.com/tgbugs/orgstrap/master/defl.el
;; Reval-Get-Immutable: defl--reval-update

;;;; License and Commentary

;; License:
;; SPDX-License-Identifier: GPL-3.0-or-later

;;; Commentary:

;; This is a horribly hacked implementation of buffer local functions.
;; The primary use case is to make it possible to define file local
;; functions for orgstrap blocks for use in closures in the org file
;; itself. Having buffer local functions in this context vasly simplifies
;; the issue of potential name collisions for functions that have short
;; names but different definitions between different files. If elisp had
;; namespaces this wouldn't be an issue, for the orgstrap use case buffer
;; local is good enough to prevent accidental redefinition.

;; defl.el is compatible with `reval-update'.

;;; Code:

<<defl-impl>>

<<defl-extra-impl>>

(defun defl--reval-update ()
  "Get the immutable url for the current remote version of this file."
  (reval-get-imm-github "tgbugs" "orgstrap" "defl.el"))

(provide 'defl)

;;; defl.el ends here

Impl

(require 'cl-lib)

(defvar-local defl--local-defuns nil
  "A hash table that maps global closures to local function symbols.
Needed to dispatch on command passed to :around advice.")

(defvar-local defl--local-defun-names nil
  "A hash table that maps global function symbols to local function symbols.")

(defun defl--has-local-defuns (command &rest args)
  "Advise COMMAND with ARGS to check if there are buffer local defuns."
  (let ((command (or (and defl--local-defuns
                          (gethash command defl--local-defuns))
                     command)))
    (apply command args)))

(defmacro defl (name arglist &optional docstring &rest body)
  "Define a buffer local function.
ARGLIST, DOCSTRING, and BODY are passed unmodified to `defun'

WARNING: If you redefine NAME with `defun' after using `defun-local'
then the current implementation will break."
  (declare (doc-string 3) (indent 2))
  (unless defl--local-defuns
    (setq-local defl--local-defuns (make-hash-table :test #'equal)))
  (unless defl--local-defun-names
    (setq-local defl--local-defun-names (make-hash-table)))
  (let ((docstring (if docstring (list docstring) docstring))
        (local-name (or (gethash name defl--local-defun-names)
                        (puthash name (cl-gentemp (format "%S-" name)) defl--local-defun-names))))
    `(prog1
         (defun ,local-name ,arglist ,@docstring ,@body)
       (unless (fboundp ',name)
         (defun ,name (&rest args) (error "Global stub for defun-local %s" #',name))
         (put ',name 'defun-local-stub t))
       (puthash (symbol-function #',name) #',local-name defl--local-defuns) ; XXX broken if the stub is overwritten
       (advice-add #',name :around #'defl--has-local-defuns))))

(defalias 'defun-local #'defl)
(defun defl-defalias-local (symbol definition &optional docstring)
  "Define a buffer local alias. NOTE only works for functions.
It is not really needed for variables since `setq-local' covers
nearly every use case. Note that the way this is defined uses
`defun-local' so it probably does not behave like a real alias."
  (if (symbol-function definition)
      (defun-local symbol (&rest args)
        docstring
        (apply definition args))
    (error "%S does not point to a function" definition)))

(defun defl--raw-symbol-function (name)
  "Return unadvised form of NAME. NOT THREAD SAFE."
  (if (advice-member-p #'defl--has-local-defuns name)
      (unwind-protect
          (progn
            (advice-remove name #'defl--has-local-defuns)
            (symbol-function name))
        ;; FIXME > assuming that name was previously advised
        (advice-add name :around #'defl--has-local-defuns))
    (symbol-function name)))

(defun defl--fmakunbound-local (command &rest args)
  "Advise COMMAND `fmakunbound' to be aware of `defun-local' forms."
  (if defl--local-defun-names
      (let* ((name (car args))
             (local-name (gethash name defl--local-defun-names)))
        ;; FIXME If we mimic the behavior of defvar-local then
        ;; we should never remove the error stub, but this is
        ;; a bit different because we can't change how defun works to
        ;; mimic how setq works and then have defun-default that mimics
        ;; how setq-default works, the behavior of local variables is
        ;; already confusing enough without having to also deal with the
        ;; the fact that defun and defvar have radically different behavior
        ;; with regard to redefinition
        ;; FIXME it would still be nice to be able to remove the advice
        ;; from the global function when the last local function ceases
        ;; to be defined but that can be for later
        (if local-name
            (progn
              (apply command (list local-name))
              (remhash (defl--raw-symbol-function name) defl--local-defuns)
              (remhash name defl--local-defun-names))
          (apply command args)))
    (apply command args)))

;;(advice-add #'fmakunbound :around #'defl--fmakunbound-local)
(defun-local hrm (a b c) 1 2 3 "OH YEAH")
(hrm 1 2 3)
(defun-local hrm (a b c d) 1 "OH NO")
(hrm 1 2 3 4)
(advice-member-p #'defl--has-local-defuns 'hrm)
(defl--raw-symbol-function 'hrm)
(defun hrm ()
    "I break things yeah?")
;(fmakunbound 'hrm)
;;(defun-local )

Testing local variable behavior

Understanding how makunbound works on local variables so we can try to match some of the behavior for defun-local. One fundamental difference right now is that unlike defvar-local, defun-local does not set the default top level global function, it defines only the local function, defun is still used to set the global function and if it is used after defun-local everything will break.

(defvar my-test-var 1)
my-test-var
(setq-local my-test-var 2)
; (setq-local my-test-var 3) ; run this via M-: in some other buffer
my-test-var
(makunbound 'my-test-var)
my-test-var ; -> void variable error but ONLY in the local buffer
;; according to https://emacs.stackexchange.com/questions/1064/make-a-buffer-local-variable-become-global-again
;; you have to use unintern ONLY IF `defvar-local' was used becuase ANY new assignment to that variable
;; will be local `kill-local-variable' works in other cases, but un-defvar-local is different
(kill-local-variable 'my-test-var)
(local-variable-p 'my-test-var)
;; make-local-variable
my-test-var ; now this points to the global variable again
(default-toplevel-value 'my-test-var)
(setq my-test-var 3)
; setq continues to function as normal in other buffers
; (setq my-test-var 4) ; run this in another buffer where the variable is not buffer local