[Top][All Lists]
[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
mockmod.el --- The mockery of a module system for Emacs Lisp
From: |
Oliver Scholz |
Subject: |
mockmod.el --- The mockery of a module system for Emacs Lisp |
Date: |
Tue, 28 Mar 2006 14:11:42 +0200 |
User-agent: |
Gnus/5.11 (Gnus v5.11) Emacs/22.0.50 (gnu/linux) |
This package is declared to be experimental and I mean it. I post it
here for review and to get comments on the approach used in it, before
I start to test it by rewriting my own packages to use it.
It would help a lot, if I did something similar for the interpreter,
thus making `eval-defun', `eval-buffer' and, of course, edebug DTRT. I
believe it would be possible to make this work intuitively *most of
the time*. But I think this would require to hack `eval' itself, which
is a built-in.
Anyways, I am not going to continue development of this package
without a review by more experienced hackers.
Please email me or crosspost to gnu.emacs.help. I am going to
subscribe to the latter for a while.
Oliver
;;; mockmod.el --- The mockery of a module system for Emacs Lisp
;; Copyright (C) 2005 Oliver Scholz
;; Author: Oliver Scholz <address@hidden>
;; Keywords: lisp
;; This file is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 2, or (at your option)
;; any later version.
;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING. If not, write to
;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.
;;; Commentary:
;; This package is EXPERIMENTAL and INCOMPLETE.
;; This package implements an ersatz module system.
;; A module system allows to hide function and variable bindings
;; within a module and "export" only those bindings the programmer
;; intentionally declares to be visible from "the outside".
;; This has two advantages: 1. Name clashes are less likely, even if
;; you don't use a package prefix in function and variable names. In
;; fact, you can refrain from using such prefixes at all, thus getting
;; shorter function names and more readable code. 2. You get a clear
;; distinction between functions and variables that are "internal" to
;; the code and those that are supposed to be used by the user or by
;; other programmers. This helps clarifying the code's architecture.
;; Unfortunately, the implementation of Emacs Lisp does not allow for
;; a proper module system. Mockmod.el tries to *simulate* one. This is
;; explained below.
;; How to use it: In order to make your file of Emacs Lisp source code
;; a module, you have to put a module declaration in front of the code
;; (example below). It *must* be the first s-expression in the file.
;; After loading mockmod.el, the Emacs Lisp byte compiler looks for
;; such a module declaration; if it finds one, it compiles the file as
;; a module. The byte compiler will then *in the compiled code* "hide"
;; every function and variable binding not declared to be "exported"
;; in the module declaration.
;; Example:
;; (module lirum-larum
;; ; Import the modules `foo' and `bar'.
;; (import (foo "foo.el")
;; (bar "bar.el"))
;; ; Import module `baz', exporting again whatever it exports.
;; (from (baz "baz.el"))
;; ; Make the variables `schubi' and `dubi' and the function
;; ; `lalala' available for use in other modules.
;; (export schubi dubi
;; (lalala arg1 &optional arg2 &rest args))
;; ; Make the symbol `lirum-larum' available in Emacs.
;; (global lirum-larum))
;; The difference between the `global' and `export' clause is that the
;; former will export the binding to the global namespace---i.e. just
;; as without a module. Whereas the `export' clause it meant for
;; `import' to other modules. This is useful for writing libraries
;; that are meant to provide functions for other modules or for
;; splitting large packages into several modules.
;; Note: Importing is not implemented yet.
;; Note: The `from' clause is not implemented yet.
;; MODULE := (module MODULE-NAME CLAUSE+)
;; MODULE-NAME := SYMBOL
;; CLAUSE := IMPORT-CLAUSE | EXPORT-CLAUSE
;; IMPORT-CLAUSE := (IMPORT MODULE-SPEC)
;; MODULE-SPEC := (MODULE-NAME FILE-NAME)
;; FILE-NAME := STRING
;; IMPORT := import | from
;; EXPORT-CLAUSE := (EXPORT SYMBOL-DECLARATION+)
;; EXPORT := export | global
;; SYMBOL-DECLARATION := VARIABLE-DECLARATION | FUNCITION-DECLARATION
;; VARIABLE-DECLARATION := SYMBOL
;; FUNCTION-DECLARATION := (SYMBOL ARGS)
;; ARGS := SYMBOL* OPT-ARG? REST-ARG?
;; OPT-ARG := &optional SYMBOL+
;; REST-ARG := &rest SYMBOL
;; How does it work?
;; In a proper module system the interpreter/compiler resolves symbols
;; identifying a variable or a function in the code to pointers to the
;; appropriate variables (read: "value containers"). A module system
;; tells the interpreter/compiler which symbol<->variable relations
;; are visible within a given module.
;; Mockmod simulates this by rewriting every occurence of function and
;; variable names in the compiled code, if they are defined (with
;; `defvar', `defun' etc.) inside the module. The module name from the
;; module declaration serves as a package prefix for the new name. For
;; instance, if you have an *internal* function named `foo-bar' in a
;; module `baz', Mockmod rewrites its name to `baz::foo-bar' (double
;; colon). If that function is *exported* Mockmod changes its name to
;; `baz:foo-bar' (single colon).
;; Note: At first glance the convention for renaming symbols might
;; resemble some *package systems*. But Mockmod is the mockery of a
;; module system, not the mockery of a package system. A package
;; system works by reading each and every symbol into "packages". The
;; fact that Mockmod does not, has some consequences, for instance
;; with regard to quoted symbols:
;; Emacs Lisp proper does not distinguish between `quote' and
;; `function'. Thus the expressions `(funcall 'function)' and
;; `(funcall #'function)' are equivalent.
;; But `mockmod' does introduce that difference. `mockmod' will
;; resolve function names only within `function' but it will leave any
;; `quote' expression untouched. So you *have* to write, for instance,
;; `(apply #'foo-bar ...)' instead of `(apply 'foo-bar ...)', if
;; `foo-bar' is a function that you defined within your module.
;; Now, a symbol within a `quote' expression is for the
;; interpreter/compiler at first glance just a symbol, i.e. a data
;; type. If the symbol is accessible from outside the body of the
;; function where it is read, `mockmod' is not able to tell whether it
;; is going to be "applied" or "funcalled" or not. Rather than doing
;; some adhoc analysis (like for the simple case of a symbol as the
;; first argument to `apply' or `funcall') `mockmod' forces the
;; programmer to maintain a difference between symbols-as-data and
;; symbols-as-variable-or-function-identifiers.
;; How does it work internally?
;; Mockmod puts a `defadvice' on the function
;; `byte-compile-from-buffer' from `bytecomp.el' which is at the heart
;; of the byte compiler. Called from this advice, mockmod will scan
;; the buffer containing the code for a module declaration. If it
;; finds one, it will then scan the buffer for variable and function
;; definitions and setup two rewrite tables (one for functions names,
;; one for variable names) in the buffer local variables
;; `mockmod-vhash' and `mockmod-fhash'. Also it temporarily redefines
;; some functions from bytecomp.el, like
;; `mockmod-b-c-file-form-defmumble', `byte-compile-normal-call'.
;; These redefinitions do nothing but provide a wrapper to the normal
;; functionality that replaces appropriate symbols in the `form'
;; argument before compilation.
;;; Code:
(eval-when-compile (require 'cl))
(defun mockmod-check-module-declaration (spec &optional name)
"Check whether SPEC is a valid module declaration.
This raises an error, if SPEC is not valid. If the optional
second argument NAME is non-nil, it should be a symbol specifying
the module name. This function will then raise an error if NAME
does not match the name given in the declaration."
'not-implemented-yet)
(defun mockmod-get-module-declaration (filename &optional module-name)
"Return module declaration from fILE."
(with-temp-buffer
(insert-file-contents filename)
(goto-char (point-min))
(let ((module-decl (read (current-buffer))))
(mockmod-check-module-declaration module-decl module-name)
module-decl)))
(defun mockmod-module-declaration-name (module-declaration)
(cadr module-declaration))
;; (defun mockmod-exported-symbols (module-decl filename)
;; (let ((exports (mockmod-declaration-clause module-decl 'export))
;; (global-exports (mockmod-declaration-clause module-decl
'export-global))
;; (module-name (mockmod-module-declaration-name module-decl))
;; (make-spec (lambda (sym) (list sym
;; module-name
;; (expand-file-name filename)))))
;; (cons (mapcar make-spec exports)
;; (mapcar make-spec global-exports))))
;; (defun mockmod-exported-symbols-from-file (filename module-name)
;; (mockmod-exported-symbols (mockmod-get-module-declaration
;; filename module-name)
;; filename))
;; (defun mockmod-imported-symbols (module-decl)
;; (let ((imports (mockmod-declaration-clause module-decl 'import))
;; imported-syms imported-global-syms)
;; (dolist (spec imports)
;; (let ((res (mockmod-exported-symbols-from-file (cadr spec) (car
spec))))
;; (setq imported-syms (append imported-syms (car res))
;; imported-global-syms (append imported-global-syms (cdr res)))))
;; (cons imported-syms imported-global-syms)))
(defun mockmod-declaration-clause (module-decl clause-name)
"Return the body of all clauses denoted by the symbol CLAUSE-NAME.
If there is more than one such clause, return their bodies merged
into a single list."
(let (lst)
(dolist (clause (cddr module-decl) lst)
(when (eq (car clause) clause-name)
(setq lst (append lst (cdr clause)))))))
(defvar mockmod-module-prefix nil)
(defvar mockmod-vhash nil)
(defvar mockmod-fhash nil)
(defun mockmod-initialise-hashes (vhash fhash module-spec)
"Initialise VHASH and FHASH according to MODULE-SPEC.
This fills the hash tables for variable and functions names with
rewrite rules according to the `export', `import' and `global'
clauses from the module declaration. Each key is the unresolved
name. A value in the variable name hash table (VHASH) is just the
resolved name. A value in the function name hash table has the
form:
\(RESOLVED-NAME . LAMBDA-LIST)"
;; FIXME: There is an interface bug here. This function should check
;; whether exported functions actually *are* defined in the code.
(let ((exported (mockmod-declaration-clause module-spec 'export))
(globals (mockmod-declaration-clause module-spec 'global))
(module-name (mockmod-module-declaration-name module-spec)))
(dolist (exp exported)
(cond ((symbolp exp)
(puthash exp (intern (format "%s:%s" module-name exp))
vhash))
((listp exp)
(puthash (car exp)
(cons (intern (format "%s:%s" module-name (car exp)))
(cdr exp))
fhash))
(t (error))))
(dolist (exp globals)
(cond ((symbolp exp)
(puthash exp exp vhash))
((listp exp)
(puthash (car exp)
exp
fhash))
(t (error))))))
(defun mockmod-make-internal-name (name)
"Return the resolved name for an internal function.
NAME is the unresolved name. This function requires the variable
`mockmod-module-prefix' to refer to the proper package prefix."
(intern (format "%s::%s" mockmod-module-prefix name)))
(defun mockmod-scan-buffer ()
"Initialize compilation input buffer.
This reads a module declaration, if there is one, and initializes
`mockmod-vhash', `mockmod-fhash' and `mockmod-module-prefix'.
Then it scans the buffer for function and variable definitions
and updates the hashes accordingly."
(save-excursion
(goto-char (point-min))
;; Check for module declaration.
(while (progn (skip-chars-forward " \t\n\l\v")
(looking-at ";"))
(forward-line 1))
(unless (eobp)
(let ((decl (read (current-buffer))))
(when (eq (car-safe decl) 'module)
(mockmod-check-module-declaration decl)
(set (make-local-variable 'mockmod-module-prefix)
(mockmod-module-declaration-name decl))
(let ((vhash (make-hash-table :test 'eq))
(fhash (make-hash-table :test 'eq))
macroenv)
(mockmod-initialise-hashes vhash fhash decl)
;; Scan all forms in the buffer for `defvar's, `defun's etc.
(while (progn (while (progn (skip-chars-forward " \t\n\l\v")
(looking-at ";"))
(forward-line 1))
(not (eobp)))
(setq macroenv
(mockmod-scan-form (read (current-buffer)) vhash fhash
macroenv)))
(set (make-local-variable 'mockmod-vhash) vhash)
(set (make-local-variable 'mockmod-fhash) fhash)))))))
(defun mockmod-scan-sequence (list vhash fhash macroenv)
"Scan a list of s-expressions for function or variable definitions.
Return the (possibly updated) macroenvironment."
(dolist (form list macroenv)
(setq macroenv (mockmod-scan-form form vhash fhash macroenv))))
(defun mockmod-scan-form (form vhash fhash macroenv)
"Scan a form for function or variable definitions.
Return the (possibly updated) macroenvironment."
(let ((form (macroexpand form macroenv)))
(case (car form)
(defun (mockmod-process-defun form fhash))
((defvar defconst) (mockmod-process-defvar form vhash))
((progn prog1 prog2)
(setq macroenv (mockmod-scan-sequence (cdr form) vhash fhash macroenv)))
((let let*)
(setq macroenv (mockmod-scan-sequence (cddr form) vhash fhash macroenv)))
;; What about `if', `cond' ...?
)
(if (eq (car form) 'defmacro)
(cons (cons (cadr form)
(cons 'lambda (nthcdr 2 form)))
macroenv)
macroenv)))
(defun mockmod-check-lambda (lambda-list pattern)
"Warn, if LAMBDA-LIST does not match PATTERN.
PATTERN is supposed to be the LAMBDA-LIST as defined in the
module declaration."
'not-implemented-yet)
(defun mockmod-process-defun (form fhash)
"Update FHASH with function or macro name."
(let ((entry (gethash (cadr form) fhash)))
(if entry
(mockmod-check-lambda (car (cddr form)) (cdr entry))
(puthash (cadr form)
(cons (mockmod-make-internal-name (cadr form))
(car (cddr form)))
fhash))))
(defun mockmod-process-defvar (form vhash)
"Update VHASH with variable or constant name."
(let ((entry (gethash (cadr form) vhash)))
(unless entry
(puthash (cadr form)
(mockmod-make-internal-name (cadr form))
vhash))))
;; (defun mockmod-test ()
;; (interactive)
;; (set (make-local-variable 'mockmod-module-prefix) 'test)
;; (save-excursion
;; (goto-char (point-min))
;; (mockmod-scan-buffer)
;; (with-output-to-temp-buffer "*mockmod*"
;; (let ((print-func (lambda (k v)
;; (print k)
;; (princ " : ")
;; (print v)
;; (terpri))))
;; (maphash print-func mockmod-vhash)
;; (maphash print-func mockmod-fhash)))))
(defmacro module (&rest args)
(declare (indent 1))
(error "Trying to evaluate module declaration"))
(defadvice byte-compile-from-buffer (around mockmod-scan activate)
(with-current-buffer (ad-get-arg 0)
(mockmod-scan-buffer))
(let ((mockmod-old-module-fdef (symbol-function 'module))
(mockmod-b-c-normal-call (symbol-function 'byte-compile-normal-call))
(mockmod-b-c-file-form-defmumble (symbol-function
'byte-compile-file-form-defmumble))
(mockmod-b-c-file-form-defvar (symbol-function
'byte-compile-file-form-defvar))
(mockmod-b-c-variable-ref (symbol-function 'byte-compile-variable-ref))
(mockmod-b-c-callargs-warn (symbol-function
'byte-compile-callargs-warn))
(mockmod-b-c-file-form-defsubst (symbol-function
'byte-compile-file-form-defsubst))
(mockmod-b-c-function-form (symbol-function
'byte-compile-function-form)))
(unwind-protect
(progn (fset 'module (cons 'macro (lambda (&rest args) nil)))
;; FIXME: the above temporarily redefines `module' to
;; be a macro evaluating to nil. This is so, because we
;; can't stop the byte compiler from reading the whole
;; buffer. Actually a module declaration other than
;; being the first form in the buffer should raise an
;; error.
(fset 'byte-compile-normal-call #'mockmod-b-c-normal-call)
(fset 'byte-compile-file-form-defmumble
#'mockmod-b-c-file-form-defmumble)
(fset 'byte-compile-file-form-defvar
#'mockmod-b-c-file-form-defvar)
(fset 'byte-compile-variable-ref #'mockmod-b-c-variable-ref)
(fset 'byte-compile-callargs-warn #'mockmod-b-c-callargs-warn)
(fset 'byte-compile-file-form-defsubst
#'mockmod-b-c-file-form-defsubst)
(fset 'byte-compile-function-form #'mockmod-b-c-function-form)
ad-do-it)
(fset 'module mockmod-old-module-fdef)
(fset 'byte-compile-normal-call mockmod-b-c-normal-call)
(fset 'byte-compile-file-form-defmumble mockmod-b-c-file-form-defmumble)
(fset 'byte-compile-file-form-defvar mockmod-b-c-file-form-defvar)
(fset 'byte-compile-variable-ref mockmod-b-c-variable-ref)
(fset 'byte-compile-callargs-warn mockmod-b-c-callargs-warn)
(fset 'byte-compile-file-form-defsubst mockmod-b-c-file-form-defsubst)
(fset 'byte-compile-function-form mockmod-b-c-function-form))))
;; The following wrapper functions could be defined with the help of a
;; macro, but I believe it is more readable this way, if a bit
;; tedious.
(defvar mockmod-b-c-normal-call nil)
(defun mockmod-b-c-normal-call (form)
(let (nname)
(if (and mockmod-module-prefix
mockmod-fhash
(setq nname (car (gethash (car-safe form) mockmod-fhash))))
(funcall mockmod-b-c-normal-call (cons nname (cdr form)))
(funcall mockmod-b-c-normal-call form))))
(defvar mockmod-b-c-file-form-defmumble nil)
(defun mockmod-b-c-file-form-defmumble (form macrop)
(let (nname)
(if (and mockmod-module-prefix
mockmod-fhash
(setq nname (car (gethash (cadr form) mockmod-fhash))))
(funcall mockmod-b-c-file-form-defmumble
(list* (car form)
nname
(cddr form))
macrop)
(funcall mockmod-b-c-file-form-defmumble form macrop))))
(defvar mockmod-b-c-file-form-defvar nil)
(defun mockmod-b-c-file-form-defvar (form)
(let (nname)
(if (and mockmod-module-prefix
mockmod-vhash
(setq nname (gethash (cadr form) mockmod-vhash)))
(funcall mockmod-b-c-file-form-defvar
(list* (car form)
nname
(cddr form)))
(funcall mockmod-b-c-file-form-defvar form))))
(defvar mockmod-b-c-variable-ref nil)
(defun mockmod-b-c-variable-ref (base-op var)
(let (nname)
(if (and (symbolp var)
mockmod-module-prefix
mockmod-vhash
(setq nname (gethash var mockmod-vhash)))
(funcall mockmod-b-c-variable-ref base-op nname)
(funcall mockmod-b-c-variable-ref base-op var))))
(defvar mockmod-b-c-callargs-warn nil)
(defun mockmod-b-c-callargs-warn (form)
(let (nname)
(if (and mockmod-module-prefix
mockmod-fhash
(setq nname (car (gethash (car form) mockmod-fhash))))
(funcall mockmod-b-c-callargs-warn
(cons nname (cdr form)))
(funcall mockmod-b-c-callargs-warn form))))
(defvar mockmod-b-c-file-form-defsubst nil)
(defun mockmod-b-c-file-form-defsubst (form)
(let (nname)
(if (and mockmod-module-prefix
mockmod-fhash
(setq nname (car (gethash (cadr form) mockmod-fhash))))
(funcall mockmod-b-c-file-form-defsubst
(list* (car form)
nname
(cddr form)))
(funcall mockmod-b-c-file-form-defsubst form))))
(defvar mockmod-b-c-function-form nil)
(defun mockmod-b-c-function-form (form)
(let (nname)
(if (and (symbolp (cadr form))
mockmod-module-prefix
mockmod-fhash
(setq nname (car (gethash (cadr form) mockmod-fhash))))
(funcall mockmod-b-c-function-form (list 'function nname))
(funcall mockmod-b-c-function-form form))))
(provide 'mockmod)
;;; mockmod.el ends here
--
8 Germinal an 214 de la Révolution
Liberté, Egalité, Fraternité!
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- mockmod.el --- The mockery of a module system for Emacs Lisp,
Oliver Scholz <=