SBCL: asdf:load-system fails when a string constant is defined
Asked Answered
L

2

9

Using SBCL, I have the problem that my system defined via ASDF does not load, when the lisp code defines a string constant. Here's the code:

constants.lisp

(defconstant A 1.0)
(defconstant B "B")

simple.asd

(defsystem :simple
:components ((:file "constants")))

On loading via

(asdf:load-system "simple")

I'm getting the following error (output has been shortened a bit):

* (asdf:load-system "simple")  
; compiling file "/Users/.../constants.lisp"
; compiling (DEFCONSTANT A ...)
; compiling (DEFCONSTANT B ...)
; /Users/.../constants-TMP.fasl written
; compilation finished in 0:00:00.003

debugger invoked on a DEFCONSTANT-UNEQL in thread
#<THREAD "main thread" RUNNING {1002BFEA93}>:
   The constant B is being redefined (from "B" to "B")

The error does not come up with clisp, ccl and abcl. Also, loading the file via

(load "constants.lisp")

works fine.

I'm using

SBCL 1.2.14, ASDF 3.1.3, MacOS

Thanks for any hints,

Oliver

Lawgiver answered 14/1, 2016 at 22:27 Comment(2)
Note that this problem is independent of ASDF. It already happens if your evaluate (defconstant B "B") twice in the sbcl repl.Handbill
Refer to sbcl.org/manual/#Defining-ConstantsDispersoid
C
12

Why does the string constant fails?

The specification for defconstant tells us that:

However, the consequences are undefined if an attempt is made to assign a value to the symbol using another operator, or to assign it to a different value using a subsequent defconstant.

The important word here is different: according to which comparison?

The consequences are undefined if there are any bindings of the variable named by name at the time defconstant is executed or if the value is not eql to the value of initial-value.

The comparison is done by eql.

SBCL compiles your file and then loads the result (the xxx-TMP.fasl file), and for that particular implementation, the defconstant form is thus evaluated twice in the same environment. A compiler needs not actually evaluate the form during compilation (it could declare it internally somehow so that further uses of the constants can be inlined) but this is a valid compilation strategy.

Here, since the compilation environment and the load environment are the same, and because the two occurences of the strings are not identical (not eq), an error is signaled. If you happened to load the FASL file with another instance of the same version of the SBCL interpreter, it would not give you this error.

What can you do?

  1. Don't reinvent the wheel, use alexandria:define-constant, which allows to specify under which test function the value is constant:

    (alexandria:define-constant b "B" :test #'string=)
    

    Thus, when it is evaluated multiple times, the new "B" value is compared against the existing value using string=, and since they are equal, no other action is performed.

  2. Use defvar and do not worry about it anymore until you deploy your code (often constants need to change during development).

Considering answered 14/1, 2016 at 23:1 Comment(4)
Please put the alexandria:define-constant solution first.Femmine
Note: The alexandria:define-constant solution doesn't work anymore with sbcl (I tested with 2.0.0), but the defconst macro above works.Grained
@OrmFinnendahl I probably should have been more explicit, the code to run looks like (define-constant B "B" :test #'equalp): the test function must be provided here. What kind of error message do you see?Considering
@Considering My fault: The error said that the two values weren't equal under eql. Since the constant was a vector with :test #'equalp it works now.Grained
D
2

Why this actually happens

The other answer is slightly incomplete because it missed the detail that you are using SBCL, which has the following caveat:

SBCL is quite strict about ANSI’s definition of defconstant. ANSI says that doing defconstant of the same symbol more than once is undefined unless the new value is eql to the old value. Conforming to this specification is a nuisance when the “constant” value is only constant under some weaker test like string= or equal.

It’s especially annoying because, in SBCL, defconstant takes effect not only at load time but also at compile time, so that just compiling and loading reasonable code like (defconstant +foobyte+ '(1 4)) runs into this undefined behavior. Many implementations of Common Lisp try to help the programmer around this annoyance by silently accepting the undefined code and trying to do what the programmer probably meant.

This is from the manual page https://www.sbcl.org/manual/#Defining-Constants, reformatted slightly and emphasis added.

Credit goes to to the user Flux in this comment for linking to the relevant page. Thank you also to the users of #commonlisp on Libera.chat for helping me to clarify my understanding.

Possible solutions

Invoke defconstant only at compilation time

I have worked around this problem using eval-when, e.g.:

(eval-when (:compile-toplevel)
  (defconstant no-bindings '((t . t))
    "Indicates pat-match success, with no variables."))

This does not help with subsequent recompilations, but it does prevent the surprising error described in the original question.

Ignore subsequent defconstant invocations

The SBCL manual suggests another approach, using a macro to check if the symbol is bound before invoking defconstant, re-assigning the constant to itself if it is bound:

(defmacro define-constant (name value &optional doc)
  `(defconstant ,name (if (boundp ',name)
                        (symbol-value ',name)
                        ,value)
     ,@(when doc (list doc))))

With this technique, subsequent invocations of defconstant are effectively ignored. If you re-define a constant with this macro, it will be silently ignored, even if the value is totally different.

Exercise greater control over the value comparison

As an alternative, and if you don't mind using an external dependency on the Alexandria library (which is nearly ubiquitous anyway), you can use alexandria:define-constant (source) for this purpose.

Instead of ignoring subsequent defconstant invocations as in the SBCL recipe, this version lets you control the function used for comparison. For instance, you might want to use equal instead of the default eql.

Using equal might be a good choice in general, as per the Hyperspec entry for EQUAL:

Returns true if x and y are structurally similar (isomorphic) objects. Objects are treated as follows by equal.

IRC user jcowan interprets "isomorphic" to mean

(roughly speaking) equality at the code level

which perhaps is what most people would want when binding literal values to a constant.

Decemvirate answered 26/7, 2022 at 16:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.