Merge branch 'fix-562' into 'develop'

Compiler improvements

See merge request embeddable-common-lisp/ecl!190
This commit is contained in:
Daniel Kochmański 2020-03-28 08:16:53 +00:00
commit fa24f95f7b
12 changed files with 317 additions and 90 deletions

View file

@ -154,6 +154,15 @@ pop_maybe_nil(cl_object *l) {
return head; return head;
} }
static cl_object
push(cl_object v, cl_object *l) {
cl_object list = *l;
unlikely_if (!ECL_LISTP(list))
FEill_formed_input();
*l = ecl_cons(v, *l);
return *l;
}
/* ------------------------------ ASSEMBLER ------------------------------ */ /* ------------------------------ ASSEMBLER ------------------------------ */
static cl_object static cl_object
@ -1310,7 +1319,7 @@ c_register_functions(cl_env_ptr env, cl_object l)
static int static int
c_labels_flet(cl_env_ptr env, int op, cl_object args, int flags) { c_labels_flet(cl_env_ptr env, int op, cl_object args, int flags) {
#define push(v,l) { cl_object c = *l = CONS(v, *l); l = &ECL_CONS_CDR(c); } #define push_back(v,l) { cl_object c = *l = CONS(v, *l); l = &ECL_CONS_CDR(c); }
cl_object l, def_list = pop(&args); cl_object l, def_list = pop(&args);
cl_object old_vars = env->c_env->variables; cl_object old_vars = env->c_env->variables;
cl_object old_funs = env->c_env->macros; cl_object old_funs = env->c_env->macros;
@ -1331,7 +1340,7 @@ c_labels_flet(cl_env_ptr env, int op, cl_object args, int flags) {
FEprogram_error FEprogram_error
("~s: The function ~s was already defined.", ("~s: The function ~s was already defined.",
2, (op == OP_LABELS ? @'LABELS' : @'FLET'), v); 2, (op == OP_LABELS ? @'LABELS' : @'FLET'), v);
push(v, f); push_back(v, f);
} }
/* If compiling a LABELS form, add the function names to the lexical /* If compiling a LABELS form, add the function names to the lexical
@ -1626,9 +1635,7 @@ c_load_time_value(cl_env_ptr env, cl_object args, int flags)
} else if (ECL_SYMBOLP(value) || ECL_LISTP(value)) { } else if (ECL_SYMBOLP(value) || ECL_LISTP(value)) {
/* Using the form as constant, we force the system to coalesce multiple /* Using the form as constant, we force the system to coalesce multiple
* copies of the same load-time-value form */ * copies of the same load-time-value form */
c_env->load_time_forms = push(cl_list(3, args, value, ECL_NIL), &c_env->load_time_forms);
ecl_cons(cl_list(3, args, value, ECL_NIL),
c_env->load_time_forms);
value = args; value = args;
} }
return compile_constant(env, value, flags); return compile_constant(env, value, flags);
@ -2232,8 +2239,7 @@ maybe_make_load_forms(cl_env_ptr env, cl_object constant)
return; return;
make = _ecl_funcall2(@'make-load-form', constant); make = _ecl_funcall2(@'make-load-form', constant);
init = (env->nvalues > 1)? env->values[1] : ECL_NIL; init = (env->nvalues > 1)? env->values[1] : ECL_NIL;
c_env->load_time_forms = ecl_cons(cl_list(3, constant, make, init), push(cl_list(3, constant, make, init), &c_env->load_time_forms);
c_env->load_time_forms);
} }
static int static int
@ -2681,12 +2687,12 @@ c_listA(cl_env_ptr env, cl_object args, int flags)
} }
for (form = ECL_CONS_CDR(form); !Null(form); ) { for (form = ECL_CONS_CDR(form); !Null(form); ) {
cl_object sentence = pop(&form); cl_object sentence = pop(&form);
declarations = ecl_cons(sentence, declarations); push(sentence, &declarations);
if (pop(&sentence) == @'special') { if (pop(&sentence) == @'special') {
while (!Null(sentence)) { while (!Null(sentence)) {
cl_object v = pop(&sentence); cl_object v = pop(&sentence);
assert_type_symbol(v); assert_type_symbol(v);
specials = ecl_cons(v, specials); specials = push(v, &specials);
} }
} }
} }

View file

@ -351,6 +351,20 @@ read_symbol(int code)
return name; return name;
} }
char *
read_string()
{
char c, *str = poolp;
char end = '"';
pushstr("ecl_make_constant_base_string(\"");
do {
c = readc();
pushc(c);
} while (c != end);
pushstr(", -1)\0");
return str;
}
char * char *
search_function(char *name) search_function(char *name)
{ {
@ -441,6 +455,9 @@ read_token(void)
poolp--; poolp--;
} else if (c == '@') { } else if (c == '@') {
pushc(c); pushc(c);
} else if (c == '"') {
read_string();
poolp--;
} else { } else {
char *name; char *name;
unreadc(c); unreadc(c);
@ -933,6 +950,13 @@ main_loop(void)
fprintf(out,"%s",p); fprintf(out,"%s",p);
poolp = tmp; poolp = tmp;
goto LOOP; goto LOOP;
} else if (c == '"') {
char *p;
char * tmp = poolp;
p = read_string();
fprintf(out,"%s",p);
poolp = tmp;
goto LOOP;
} else if (c != '(') { } else if (c != '(') {
char *p; char *p;
char * tmp = poolp; char * tmp = poolp;

View file

@ -1994,6 +1994,9 @@ cl_symbols[] = {
{EXT_ "WITH-UNIQUE-NAMES", EXT_ORDINARY, NULL, -1, OBJNULL}, {EXT_ "WITH-UNIQUE-NAMES", EXT_ORDINARY, NULL, -1, OBJNULL},
{EXT_ "WITH-GENSYMS", EXT_ORDINARY, NULL, -1, OBJNULL}, {EXT_ "WITH-GENSYMS", EXT_ORDINARY, NULL, -1, OBJNULL},
{EXT_ "WITH-CLEAN-SYMBOLS", EXT_ORDINARY, NULL, -1, OBJNULL}, {EXT_ "WITH-CLEAN-SYMBOLS", EXT_ORDINARY, NULL, -1, OBJNULL},
{EXT_ "IF-LET", EXT_ORDINARY, NULL, -1, OBJNULL},
{EXT_ "WHEN-LET", EXT_ORDINARY, NULL, -1, OBJNULL},
{EXT_ "WHEN-LET*", EXT_ORDINARY, NULL, -1, OBJNULL},
{SYS_ "HANDLE-SIGNAL", SI_ORDINARY, si_handle_signal, 2, OBJNULL}, {SYS_ "HANDLE-SIGNAL", SI_ORDINARY, si_handle_signal, 2, OBJNULL},

View file

@ -1994,6 +1994,9 @@ cl_symbols[] = {
{EXT_ "WITH-UNIQUE-NAMES",NULL,-1}, {EXT_ "WITH-UNIQUE-NAMES",NULL,-1},
{EXT_ "WITH-GENSYMS",NULL,-1}, {EXT_ "WITH-GENSYMS",NULL,-1},
{EXT_ "WITH-CLEAN-SYMBOLS",NULL,-1}, {EXT_ "WITH-CLEAN-SYMBOLS",NULL,-1},
{EXT_ "IF-LET",NULL,-1},
{EXT_ "WHEN-LET",NULL,-1},
{EXT_ "WHEN-LET*",NULL,-1},
{SYS_ "HANDLE-SIGNAL","si_handle_signal",2}, {SYS_ "HANDLE-SIGNAL","si_handle_signal",2},

View file

@ -128,9 +128,11 @@ printer and we should rather use MAKE-LOAD-FORM."
x)))) x))))
(and init-forms `(progn ,@init-forms))))) (and init-forms `(progn ,@init-forms)))))
(cons (cons
(values `(cons ,(maybe-quote (car object)) nil) (values `(cons nil nil)
(and (rest object) `(rplacd ,(maybe-quote object) `(progn (rplaca ,(maybe-quote object)
,(maybe-quote (cdr object)))))) ,(maybe-quote (car object)))
(rplacd ,(maybe-quote object)
,(maybe-quote (cdr object))))))
(hash-table (hash-table
(let* ((content (ext:hash-table-content object)) (let* ((content (ext:hash-table-content object))
(make-form `(make-hash-table (make-form `(make-hash-table

View file

@ -268,6 +268,9 @@ lines are inserted, but the order is preserved")
(defvar *top-level-forms* nil) ; holds { top-level-form }* (defvar *top-level-forms* nil) ; holds { top-level-form }*
(defvar *make-forms* nil) ; holds { top-level-form }* (defvar *make-forms* nil) ; holds { top-level-form }*
(defvar *objects-being-created* nil) ; helps detecting circular references
(defvar *objects-init-deferred* nil) ; helps avoiding circularity
;;; ;;;
;;; top-level-form: ;;; top-level-form:
;;; ( 'DEFUN' fun-name cfun lambda-expr doc-vv sp ) ;;; ( 'DEFUN' fun-name cfun lambda-expr doc-vv sp )

View file

@ -134,6 +134,14 @@
Compiler assumes it is a global." Compiler assumes it is a global."
(slot-value c 'variable))))) (slot-value c 'variable)))))
(define-condition circular-dependency (compiler-error)
()
(:report
(lambda (c stream)
(compiler-message-report stream c
"Circular references in creation form for ~S."
(compiler-message-form c)))))
(defun print-compiler-message (c stream) (defun print-compiler-message (c stream)
(unless (typep c *suppress-compiler-messages*) (unless (typep c *suppress-compiler-messages*)
#+cmu-format #+cmu-format

View file

@ -155,60 +155,85 @@
(add-object 0 :duplicate t :permanent t)) (add-object 0 :duplicate t :permanent t))
(defun add-load-form (object location) (defun add-load-form (object location)
(when (clos::need-to-make-load-form-p object *cmp-env*) (unless (clos::need-to-make-load-form-p object *cmp-env*)
(if (not (eq *compiler-phase* 't1)) (return-from add-load-form))
(cmperr "Unable to internalize complex object ~A in ~a phase" object *compiler-phase*) (unless (eq *compiler-phase* 't1)
(multiple-value-bind (make-form init-form) (make-load-form object) (cmperr "Unable to internalize complex object ~A in ~a phase." object *compiler-phase*))
(setf (gethash object *load-objects*) location) (multiple-value-bind (make-form init-form) (make-load-form object)
(when make-form (setf (gethash object *load-objects*) location)
(push (make-c1form* 'MAKE-FORM :args location (c1expr make-form)) *make-forms*)) (let (deferred)
(when init-form (when make-form
(push (make-c1form* 'INIT-FORM :args location (c1expr init-form)) *make-forms*)))))) (let ((*objects-init-deferred* nil)
(*objects-being-created* (list* object *objects-being-created*)))
(push (make-c1form* 'MAKE-FORM :args location (c1expr make-form)) *make-forms*)
(setf deferred (nreverse *objects-init-deferred*))))
(flet ((maybe-init (loc init)
(handler-case
(push (make-c1form* 'INIT-FORM :args loc (c1expr init)) *make-forms*)
(circular-dependency (c)
(if *objects-being-created*
(push (cons location init-form) *objects-init-deferred*)
(error c))))))
(loop for (loc . init) in deferred
do (maybe-init loc init)
finally (when init-form
(maybe-init location init-form)))))))
(defun add-object (object &key (duplicate nil) (defun add-object (object &key
(permanent (or (symbolp object) *permanent-data*)) (duplicate nil)
(used-p nil)) (used-p nil)
;; FIXME! Currently we have two data vectors and, when compiling (permanent (or (symbolp object)
;; files, it may happen that a constant is duplicated and stored *permanent-data*)))
;; both in VV and VVtemp. This would not be a problem if the (when-let ((vv (add-static-constant object)))
;; constant were readable, but due to using MAKE-LOAD-FORM we may (when used-p
;; end up having two non-EQ objects created for the same value. (setf (vv-used-p vv) t))
(return-from add-object vv))
(let* ((test (if *compiler-constants* 'eq 'equal)) (let* ((test (if *compiler-constants* 'eq 'equal))
(array (if permanent *permanent-objects* *temporary-objects*)) (item (if permanent
(x (or (and (not permanent) ;; FIXME! Currently we have two data vectors and,
(find object *permanent-objects* :test test ;; when compiling files, it may happen that a
:key #'first)) ;; constant is duplicated and stored both in VV
(find object array :test test :key #'first))) ;; and VVtemp. This would not be a problem if the
(next-ndx (length array)) ;; constant were readable, but due to using
(forced duplicate) ;; MAKE-LOAD-FORM we may end up having two non-EQ
found) ;; objects created for the same value.
(setq x (find object *permanent-objects* :test test :key #'first)
(cond ((add-static-constant object)) (or (find object *permanent-objects* :test test :key #'first)
((and x duplicate) (find object *temporary-objects* :test test :key #'first))))
(setq x (make-vv :location next-ndx :used-p forced (array (if permanent
:permanent-p permanent *permanent-objects*
:value object *temporary-objects*))
:used-p t)) (vv (cond ((and item duplicate)
(vector-push-extend (list object x next-ndx) array) (let* ((ndx (length array))
x) (vv (make-vv :location ndx
(x :permanent-p permanent
(second x)) :value object)))
((and (not duplicate) (vector-push-extend (list object vv ndx) array)
(symbolp object) vv))
(multiple-value-setq (found x) (si::mangle-name object))) (item
x) (when (member object *objects-being-created*)
(t (error 'circular-dependency :form object))
(setq x (make-vv :location next-ndx :used-p forced (second item))
:permanent-p permanent ;; FIXME! all other branches return VV instance
:value object ;; while this branch returns a STRING making the
:used-p used-p)) ;; function return value inconsistent.
(vector-push-extend (list object x next-ndx) array) ((and (not item) (not duplicate) (symbolp object)
(unless *compiler-constants* (multiple-value-bind (foundp symbol)
(add-load-form object x)) (si::mangle-name object)
x))) (and foundp
(when (and used-p (typep x 'vv)) (return-from add-object symbol)))))
(setf (vv-used-p x) t)) (t
x)) (let* ((ndx (length array))
(vv (make-vv :location ndx
:permanent-p permanent
:value object)))
(vector-push-extend (list object vv ndx) array)
(unless *compiler-constants*
(add-load-form object vv))
vv)))))
(when (or duplicate used-p)
(setf (vv-used-p vv) t))
vv))
(defun add-symbol (symbol) (defun add-symbol (symbol)
(add-object symbol :duplicate nil :permanent t)) (add-object symbol :duplicate nil :permanent t))
@ -347,10 +372,10 @@
#+msvc #+msvc
nil nil
#-msvc #-msvc
;; FIXME! The Microsoft compiler does not allow static initialization of bit fields. ;; FIXME! The MSVC compiler does not allow static initialization of
;; SSE uses always unboxed static constants. No reference ;; bit fields. SSE uses always unboxed static constants. No
;; is kept to them -- it is thus safe to use them even on code ;; reference is kept to them -- it is thus safe to use them even on
;; that might be unloaded. ;; code that might be unloaded.
(unless (or *compiler-constants* (unless (or *compiler-constants*
(and (not *use-static-constants-p*) (and (not *use-static-constants-p*)
#+sse2 #+sse2

View file

@ -194,3 +194,33 @@ ones, which is useful for creating hygienic macros."
(cons s (make-symbol (symbol-name s)))) (cons s (make-symbol (symbol-name s))))
symbols) symbols)
body))) body)))
;;; Curbed from alexandria
(defmacro if-let (bindings &body (then-form &optional else-form))
(let* ((binding-list (if (and (consp bindings) (symbolp (car bindings)))
(list bindings)
bindings))
(variables (mapcar #'car binding-list)))
`(let ,binding-list
(if (and ,@variables)
,then-form
,else-form))))
(defmacro when-let (bindings &body forms)
`(if-let ,bindings
(progn ,@forms)))
(defmacro when-let* (bindings &body body)
(let ((binding-list (if (and (consp bindings) (symbolp (car bindings)))
(list bindings)
bindings)))
(labels ((bind (bindings body)
(if bindings
`((let (,(car bindings))
(when ,(caar bindings)
,@(bind (cdr bindings) body))))
body)))
`(let (,(car binding-list))
(when ,(caar binding-list)
,@(bind (cdr binding-list) body))))))

View file

@ -185,14 +185,21 @@
(flet ((handler (condition) (flet ((handler (condition)
(cond ((typep condition expected) (cond ((typep condition expected)
(return-from %signals (passed))) (return-from %signals (passed)))
(t ((typep condition 'serious-condition)
(return-from %signals (return-from %signals
(let ((fmt-ctrl (if args (car args) "Expected to signal ~s, but got ~s:~%~a")) (let ((fmt-ctrl
(fmt-args (if args (cdr args) (list expected (type-of condition) condition)))) (if args
(car args)
"Expected to signal ~s, but got ~s:~%~a"))
(fmt-args
(if args
(cdr args)
(list expected (type-of condition) condition))))
(failed (make-condition 'test-failure (failed (make-condition 'test-failure
:name *test-name* :name *test-name*
:format-control fmt-ctrl :format-control fmt-ctrl
:format-arguments fmt-args)))))))) :format-arguments fmt-args)))))
(t #|ignore non-serious unexpected conditions|#))))
(handler-bind ((condition #'handler)) (handler-bind ((condition #'handler))
(funcall fn))) (funcall fn)))
(let ((fmt-ctrl (if args (car args) "Expected to signal ~s, but got nothing")) (let ((fmt-ctrl (if args (car args) "Expected to signal ~s, but got nothing"))

View file

@ -107,6 +107,9 @@ as a second value."
;; (when delete-files ;; (when delete-files
;; (delete-file filename) ;; (delete-file filename)
;; (delete-file compiled-file)) ;; (delete-file compiled-file))
(when (null compiled-file)
(delete-file ,filename)
(error "Compiling file ~a failed:~%~a" ,filename output))
(values compiled-file output)))) (values compiled-file output))))
(defmacro with-temporary-file ((var string &rest args) &body body) (defmacro with-temporary-file ((var string &rest args) &body body)

View file

@ -631,8 +631,8 @@
;;; Fixed: 18/05/2006 (juanjo) ;;; Fixed: 18/05/2006 (juanjo)
;;; Description: ;;; Description:
;;; ;;;
;;; The detection of when a lisp constant has to be externalized using MAKE-LOAD-FORM ;;; The detection of when a lisp constant has to be externalized
;;; breaks down with some circular structures ;;; using MAKE-LOAD-FORM breaks down with some circular structures
;;; ;;;
(defclass compiler-test-class () (defclass compiler-test-class ()
((parent :accessor compiler-test-parent :initform nil) ((parent :accessor compiler-test-parent :initform nil)
@ -666,25 +666,35 @@
;;; printed representation. In that case MAKE-LOAD-FORM should be ;;; printed representation. In that case MAKE-LOAD-FORM should be
;;; used. ;;; used.
;;; ;;;
;;;
;;; Date: 2020-02-12
;;; URL: https://gitlab.com/embeddable-common-lisp/ecl/issues/562
;;; Description:
;;;
;;; Circular structures are not properly initialized because make
;;; and init form order of evaluation is not always correct.
(test cmp.0030.make-load-form (test cmp.0030.make-load-form
(let ((output (multiple-value-bind (file output)
(with-compiler ("make-load-form.lsp") (with-compiler ("make-load-form.lsp")
"(in-package cl-test)" "(in-package cl-test)"
"(eval-when (:compile-toplevel) "(eval-when (:compile-toplevel)
(defvar s4 (make-instance 'compiler-test-class)) (defparameter s4.0030 (make-instance 'compiler-test-class))
(defvar s5 (make-instance 'compiler-test-class)) (defparameter s5.0030 (make-instance 'compiler-test-class))
(setf (compiler-test-parent s5) s4) (setf (compiler-test-parent s5.0030) s4.0030)
(setf (compiler-test-children s4) (list s5)))" (setf (compiler-test-children s4.0030) (list s5.0030)))"
"(defvar a '#.s5)" "(defparameter a.0030 '#.s5.0030)"
"(defvar b '#.s4)" "(defparameter b.0030 '#.s4.0030)"
"(defvar c '#.s5)" "(defparameter c.0030 '#.s5.0030)"
"(defun foo () "(defun foo.0030 ()
(let ((*print-circle* t)) (let ((*print-circle* t))
(with-output-to-string (s) (princ '#1=(1 2 3 #.s4 #1#) s))))"))) (with-output-to-string (s) (princ '#1=(1 2 3 #.s4.0030 #1#) s))))")
(load output) (declare (ignore output))
(load file)
(delete-file "make-load-form.lsp") (delete-file "make-load-form.lsp")
(delete-file output)) (delete-file file))
(is-equal "#1=(1 2 3 #<a CL-TEST::COMPILER-TEST-CLASS> #1#)" (foo))) (is-equal "#1=(1 2 3 #<a CL-TEST::COMPILER-TEST-CLASS> #1#)" (foo.0030))
(is (eq (compiler-test-parent a.0030) b.0030))
(is (eq (first (compiler-test-children b.0030)) a.0030)))
;;; Date: 9/06/2006 (Pascal Costanza) ;;; Date: 9/06/2006 (Pascal Costanza)
;;; Fixed: 13/06/2006 (juanjo) ;;; Fixed: 13/06/2006 (juanjo)
@ -1629,3 +1639,106 @@
3))) 3)))
(is (eq result 4)) (is (eq result 4))
(is (eq (funcall *function*) 4))))) (is (eq (funcall *function*) 4)))))
;;; Date 2020-03-13
;;; URL: https://gitlab.com/embeddable-common-lisp/ecl/-/issues/565
;;; Description
;;;
;;; COMPILE-FILE produces two vectors VV and VVtemp which
;;; represent the fasl data segment. The latter is deallocated
;;; after all top-level forms are evaluated. As compiler processes
;;; them currently if the object is first pushed to the temporary
;;; segment and then we try to add it to the permanent segment we
;;; have two versions of the same objects which are not EQ. File
;;; src/cmp/cmpwt.lsp has an appropriate FIXME in the ADD-OBJECT
;;; function definition.
(test cmp.0076.make-load-form-non-eq
(multiple-value-bind (file output)
(with-compiler ("make-temp.lsp")
"(in-package #:cl-test)"
"(eval-when (:compile-toplevel :load-toplevel :execute)
(defclass my-class ()
((name :initarg :name :accessor name)))
(defmethod print-object ((obj my-class) stream)
(print-unreadable-object (obj stream :identity t)
(format stream \"~s ~s\" (name obj) (class-name (class-of obj)))))
(defmethod make-load-form ((x my-class) &optional environment)
(declare (ignore environment))
`(make-instance ',(class-of x) :name ',(slot-value x 'name))))"
"(eval-when (:compile-toplevel)
(defparameter s4 (make-instance 'my-class :name :s4)))"
"(defparameter *s4-a* nil)"
"(defparameter *s4-b* nil)"
"(let ((a '#.s4))
(setf *s4-a* a))"
"(defun foo ()
(let ((x #.s4))
(values x *s4-a* *s4-b*)))"
"(let ((b '#.s4))
(setf *s4-b* b))")
(declare (ignore output))
(load file)
(delete-file "make-temp.lsp")
(delete-file file))
(multiple-value-bind (x a b) (foo)
(is (eq x a) "~a is not eq to ~a" x a)
;; This test passes because B toplevel form is compiled after the
;; function FOO. Included here for completness.
(is (eq x b) "~a is not eq to ~a" x b)
(is (eq a b) "~a is not eq to ~a" a b)))
(ext:with-clean-symbols (class)
(test cmp.0077.make-load-form.circular-dep
(macrolet ((make-template (&body extra)
`(with-compiler ("make-circle.lsp")
'(progn
(in-package #:cl-test)
(eval-when (:compile-toplevel :load-toplevel :execute)
(defclass class ()
((peer :initform nil :initarg :peer :accessor peer)
(peer* :initform nil :initarg :peer* :accessor peer*)))
(defmethod make-load-form ((x class) &optional env)
(declare (ignore env))
(values `(make-instance 'class :peer ',(peer x))
`(setf (peer* ',x) ',(peer* x)))))
(eval-when (:compile-toplevel)
(defparameter var1 (make-instance 'class))
(defparameter var2 (make-instance 'class :peer var1))
,@extra))
"(defun foo () (values '#.var1 '#.var2))")))
;; Ordinary case (reference).
(multiple-value-bind (file output)
(make-template)
(load file)
(delete-file "make-circle.lsp")
(delete-file file)
(multiple-value-bind (v1 v2) (foo)
(is (eq (peer v2) v1))))
;; Circularity between make forms (should signal an error).
(signals error
(unwind-protect (multiple-value-bind (file output)
(make-template (setf (peer var1) var2))
(when file (delete-file file)))
(delete-file "make-circle.lsp"))
"Successfully compiled a file with a circular dependency.")
;; Circularity between make and init forms (is not an error!).
(multiple-value-bind (file output)
(make-template (setf (peer* var1) var2))
(load file)
(delete-file "make-circle.lsp")
(delete-file file)
(multiple-value-bind (v1 v2) (foo)
(is (eq (peer v2) v1))
(is (eq (peer* v1) v2))))
;; Circularity between init forms (is not an error!).
(multiple-value-bind (file output)
(make-template (setf (peer* var1) var2)
(setf (peer* var2) var1))
(load file)
(delete-file "make-circle.lsp")
(delete-file file)
(multiple-value-bind (v1 v2) (foo)
(is (eq (peer v2) v1))
(is (eq (peer* v1) v2))
(is (eq (peer* v2) v1)))))))