560 lines
20 KiB
HTML
560 lines
20 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta name="generator" content=
|
||
"HTML Tidy for HTML5 for Linux version 5.2.0">
|
||
<title>Debugging</title>
|
||
<meta charset="utf-8">
|
||
<meta name="description" content="A collection of examples of using Common Lisp">
|
||
<meta name="viewport" content=
|
||
"width=device-width, initial-scale=1">
|
||
<link rel="stylesheet" href=
|
||
"assets/style.css">
|
||
<script type="text/javascript" src=
|
||
"assets/highlight-lisp.js">
|
||
</script>
|
||
<script type="text/javascript" src=
|
||
"assets/jquery-3.2.1.min.js">
|
||
</script>
|
||
<script type="text/javascript" src=
|
||
"assets/jquery.toc/jquery.toc.min.js">
|
||
</script>
|
||
<script type="text/javascript" src=
|
||
"assets/toggle-toc.js">
|
||
</script>
|
||
|
||
<link rel="stylesheet" href=
|
||
"assets/github.css">
|
||
|
||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
|
||
</head>
|
||
<body>
|
||
<h1 id="title-xs"><a href="index.html">The Common Lisp Cookbook</a> – Debugging</h1>
|
||
<div id="logo-container">
|
||
<a href="index.html">
|
||
<img id="logo" src="assets/cl-logo-blue.png"/>
|
||
</a>
|
||
|
||
<div id="searchform-container">
|
||
<form onsubmit="duckSearch()" action="javascript:void(0)">
|
||
<input id="searchField" type="text" value="" placeholder="Search...">
|
||
</form>
|
||
</div>
|
||
|
||
<div id="toc-container" class="toc-close">
|
||
<div id="toc-title">Table of Contents</div>
|
||
<ul id="toc" class="list-unstyled"></ul>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="content-container">
|
||
<h1 id="title-non-xs"><a href="index.html">The Common Lisp Cookbook</a> – Debugging</h1>
|
||
|
||
<!-- Announcement we can keep for 1 month or more. I remove it and re-add it from time to time. -->
|
||
<p class="announce">
|
||
📹 <a href="https://www.udemy.com/course/common-lisp-programming/?couponCode=6926D599AA-LISP4ALL">NEW! Learn Lisp in videos and support our contributors with this 40% discount.</a>
|
||
</p>
|
||
<p class="announce-neutral">
|
||
📕 <a href="index.html#download-in-epub">Get the EPUB and PDF</a>
|
||
</p>
|
||
|
||
|
||
<div id="content"
|
||
<p>You entered this new world of Lisp and now wonder: how can we debug
|
||
what’s going on? How is it more interactive than other platforms?
|
||
What does the interactive debugger bring, apart from stack traces?</p>
|
||
|
||
<h2 id="print-debugging">Print debugging</h2>
|
||
|
||
<p>Well of course we can use the famous technique of “print
|
||
debugging”. Let’s just recap a few print functions.</p>
|
||
|
||
<p><code>print</code> works, it prints a <code>read</code>able representation of its argument,
|
||
which means what is <code>print</code>ed can be <code>read</code> back in by the Lisp
|
||
reader.</p>
|
||
|
||
<p><code>princ</code> focuses on an <em>aesthetic</em> representation.</p>
|
||
|
||
<p><code>(format t "~a" …)</code>, with the <em>aesthetic</em> directive, prints a string (in <code>t</code>, the standard output
|
||
stream) and returns nil, whereas <code>format nil …</code> doesn’t print anything
|
||
and returns a string. With many format controls we can print several
|
||
variables at once.</p>
|
||
|
||
<h2 id="logging">Logging</h2>
|
||
|
||
<p>Logging is already a good evolution from print debugging ;)</p>
|
||
|
||
<p><a href="https://github.com/sharplispers/log4cl/">log4cl</a> is the popular,
|
||
de-facto logging library but it isn’t the only one. Download it:</p>
|
||
|
||
<pre><code class="language-lisp">(ql:quickload "log4cl")
|
||
</code></pre>
|
||
|
||
<p>and let’s have a dummy variable:</p>
|
||
|
||
<pre><code class="language-lisp">(defvar *foo* '(:a :b :c))
|
||
</code></pre>
|
||
|
||
<p>We can use log4cl with its <code>log</code> nickname, then it is as simple to use as:</p>
|
||
|
||
<pre><code class="language-lisp">(log:info *foo*)
|
||
;; <INFO> [13:36:49] cl-user () - *FOO*: (:A :B :C)
|
||
</code></pre>
|
||
|
||
<p>We can interleave strings and expressions, with or without <code>format</code>
|
||
control strings:</p>
|
||
|
||
<pre><code class="language-lisp">(log:info "foo is " *foo*)
|
||
;; <INFO> [13:37:22] cl-user () - foo is *FOO*: (:A :B :C)
|
||
(log:info "foo is ~{~a~}" *foo*)
|
||
;; <INFO> [13:39:05] cl-user () - foo is ABC
|
||
</code></pre>
|
||
|
||
<p>With its companion library <code>log4slime</code>, we can interactively change
|
||
the log level:</p>
|
||
|
||
<ul>
|
||
<li>globally</li>
|
||
<li>per package</li>
|
||
<li>per function</li>
|
||
<li>and by CLOS methods and CLOS hierarchy (before and after methods)</li>
|
||
</ul>
|
||
|
||
<p>It is very handy, when we have a lot of output, to turn off the
|
||
logging of functions or packages we know to work, and thus narrowing
|
||
our search to the right area. We can even save this configuration and
|
||
re-use it in another image, be it on another machine.</p>
|
||
|
||
<p>We can do all this through commands, keyboard shortcuts and also through a
|
||
menu or mouse clicks.</p>
|
||
|
||
<p><img src="assets/log4cl.png" alt=""changing the log level with log4slime"" /></p>
|
||
|
||
<p>We invite you to read log4cl’s README.</p>
|
||
|
||
<h2 id="using-the-powerful-repl">Using the powerful REPL</h2>
|
||
|
||
<p>Part of the joy of Lisp is the excellent REPL. Its existence usually
|
||
delays the need to use other debugging tools, if it doesn’t annihilate
|
||
them for the usual routine.</p>
|
||
|
||
<p>As soon as we define a function, we can try it in the REPL. In Slime,
|
||
compile a function with <code>C-c C-c</code> (the whole buffer with <code>C-c C-k</code>),
|
||
switch to the REPL with <code>C-c C-z</code> and try it. Eventually enter the
|
||
package you are working on with <code>(in-package :your-package)</code>
|
||
or <code>C-c ~</code> (<code>slime-sync-package-and-default-directory</code>),
|
||
(which will also change the default working directory to the package definition’s directory).</p>
|
||
|
||
<p>The feedback is immediate. There is no need to recompile everything,
|
||
nor to restart any process, nor to create a main function and define
|
||
command line arguments for use in the shell (which we can of course do later on
|
||
when needed).</p>
|
||
|
||
<p>We usually need to create some data to test our function(s). This is a
|
||
subsequent art of the REPL existence and it may be a new discipline
|
||
for newcomers. A trick is to write the test data alongside your
|
||
functions but below a <code>#+nil</code> feature test (or safer, <code>+(or nil)</code>) so that only you can
|
||
manually compile them:</p>
|
||
|
||
<pre><code class="language-lisp">#+nil
|
||
(progn
|
||
(defvar *test-data* nil)
|
||
(setf *test-data* (make-instance 'foo …)))
|
||
</code></pre>
|
||
|
||
<p>When you load this file, <code>*test-data*</code> won’t exist, but you can
|
||
manually create it with <code>C-c C-c</code>.</p>
|
||
|
||
<p>We can define tests functions like this.</p>
|
||
|
||
<p>Some do similarly inside <code>#| … |#</code> comments.</p>
|
||
|
||
<p>All that being said, keep in mind to write unit tests when time comes ;)</p>
|
||
|
||
<h2 id="inspect-and-describe">Inspect and describe</h2>
|
||
|
||
<p>These two commands share the same goal, printing a description of an
|
||
object, <code>inspect</code> being the interactive one.</p>
|
||
|
||
<pre><code>(inspect *foo*)
|
||
|
||
The object is a proper list of length 3.
|
||
0. 0: :A
|
||
1. 1: :B
|
||
|
||
2. 2: :C
|
||
> q
|
||
</code></pre>
|
||
|
||
<p>We can also, in editors that support it, right-click on any object in
|
||
the REPL and <code>inspect</code> them. We are presented a screen where we can
|
||
dive deep inside the data structure and even change it.</p>
|
||
|
||
<p>Let’s have a quick look with a more interesting structure, an object:</p>
|
||
|
||
<pre><code class="language-lisp">(defclass foo ()
|
||
((a :accessor foo-a :initform '(:a :b :c))
|
||
(b :accessor foo-b :initform :b)))
|
||
;; #<STANDARD-CLASS FOO>
|
||
(make-instance 'foo)
|
||
;; #<FOO {100F2B6183}>
|
||
</code></pre>
|
||
|
||
<p>We right-click on the <code>#<FOO</code> object and choose “inspect”. We are
|
||
presented an interactive pane (in Slime):</p>
|
||
|
||
<pre><code>#<FOO {100F2B6183}>
|
||
--------------------
|
||
Class: #<STANDARD-CLASS FOO>
|
||
--------------------
|
||
Group slots by inheritance [ ]
|
||
Sort slots alphabetically [X]
|
||
|
||
All Slots:
|
||
[ ] A = (:A :B :C)
|
||
[ ] B = :B
|
||
|
||
[set value] [make unbound]
|
||
</code></pre>
|
||
|
||
<p>When we click or press enter on the line of slot A, we inspect it further:</p>
|
||
|
||
<pre><code>#<CONS {100F5E2A07}>
|
||
--------------------
|
||
A proper list:
|
||
0: :A
|
||
1: :B
|
||
2: :C
|
||
</code></pre>
|
||
|
||
<h2 id="the-interactive-debugger">The interactive debugger</h2>
|
||
|
||
<p>Whenever an exceptional situation happens (see
|
||
<a href="error_handling.html">error handling</a>), the interactive debugger pops
|
||
up.</p>
|
||
|
||
<p>It presents the error message, available actions (<em>restarts</em>),
|
||
and the backtrace. A few remarks:</p>
|
||
|
||
<ul>
|
||
<li>the restarts are programmable, we can create our own</li>
|
||
<li>in Slime, press <code>v</code> on a stack trace frame to view the corresponding
|
||
source file location</li>
|
||
<li>hit enter on a frame for more details</li>
|
||
<li>we can explore the functionality with the menu that should appear
|
||
in our editor. See the “break” section below for a few
|
||
more commands (eval in frame, etc).</li>
|
||
</ul>
|
||
|
||
<h3 id="compile-with-maximum-debugging-information">Compile with maximum debugging information</h3>
|
||
|
||
<p>Usually your compiler will optimize things out and this will reduce
|
||
the amount of information available to the debugger. For example
|
||
sometimes we can’t see intermediate variables of computations. We can
|
||
change the optimization choices with:</p>
|
||
|
||
<pre><code class="language-lisp">(declaim (optimize (speed 0) (space 0) (debug 3)))
|
||
</code></pre>
|
||
|
||
<p>and recompile our code. You can achieve the same with a handy shortcut: <code>C-u C-c C-c</code>: the form is compiled with maximum debug settings. You can on the contrary use a negative prefix argument (<code>M--</code>) to compile for speed. And use a numeric argument to set the setting to it (you should read the docstring of <code>slime-compile-defun</code>).</p>
|
||
|
||
<h2 id="trace">Trace</h2>
|
||
|
||
<p><a href="http://www.xach.com/clhs?q=trace">trace</a> allows us to see when a
|
||
function was called, what arguments it received, and the value it
|
||
returned.</p>
|
||
|
||
<pre><code class="language-lisp">(defun factorial (n)
|
||
(if (plusp n)
|
||
(* n (factorial (1- n)))
|
||
1))
|
||
</code></pre>
|
||
|
||
<pre><code class="language-lisp">(trace factorial)
|
||
|
||
(factorial 2)
|
||
0: (FACTORIAL 3)
|
||
1: (FACTORIAL 2)
|
||
2: (FACTORIAL 1)
|
||
3: (FACTORIAL 0)
|
||
3: FACTORIAL returned 1
|
||
2: FACTORIAL returned 1
|
||
1: FACTORIAL returned 2
|
||
0: FACTORIAL returned 6
|
||
6
|
||
|
||
(untrace factorial)
|
||
</code></pre>
|
||
|
||
<p>To untrace all functions, just evaluate <code>(untrace)</code>.</p>
|
||
|
||
<p>In Slime we also have the shortcut <code>C-c M-t</code> to trace or untrace a
|
||
function.</p>
|
||
|
||
<p>If you don’t see recursive calls, that may be because of the
|
||
compiler’s optimizations. Try this before defining the function to be
|
||
traced:</p>
|
||
|
||
<pre><code class="language-lisp">(declaim (optimize (debug 3)))
|
||
</code></pre>
|
||
|
||
<p>The output is printed to <code>*trace-output*</code> (see the CLHS).</p>
|
||
|
||
<p>In Slime, we also have an interactive trace dialog with <code>M-x
|
||
slime-trace-dialog</code> bound to <code>C-c T</code>.</p>
|
||
|
||
<h3 id="tracing-method-invocation">Tracing method invocation</h3>
|
||
|
||
<p>In SBCL, we can use <code>(trace foo :methods t)</code> to trace the execution order of method combination (before, after, around methods). For example:</p>
|
||
|
||
<pre><code class="language-lisp">(trace foo :methods t)
|
||
|
||
(foo 2.0d0)
|
||
0: (FOO 2.0d0)
|
||
1: ((SB-PCL::COMBINED-METHOD FOO) 2.0d0)
|
||
2: ((METHOD FOO (FLOAT)) 2.0d0)
|
||
3: ((METHOD FOO (T)) 2.0d0)
|
||
3: (METHOD FOO (T)) returned 3
|
||
2: (METHOD FOO (FLOAT)) returned 9
|
||
2: ((METHOD FOO :AFTER (DOUBLE-FLOAT)) 2.0d0)
|
||
2: (METHOD FOO :AFTER (DOUBLE-FLOAT)) returned DOUBLE
|
||
1: (SB-PCL::COMBINED-METHOD FOO) returned 9
|
||
0: FOO returned 9
|
||
9
|
||
</code></pre>
|
||
|
||
<p>See the <a href="clos.html">CLOS</a> section for a tad more information.</p>
|
||
|
||
<h2 id="step">Step</h2>
|
||
|
||
<p><a href="http://www.xach.com/clhs?q=step">step</a> is an interactive command with
|
||
similar scope than <code>trace</code>. This:</p>
|
||
|
||
<pre><code class="language-lisp">(step (factorial 2))
|
||
</code></pre>
|
||
|
||
<p>gives an interactive pane with the available restarts:</p>
|
||
|
||
<pre><code>Evaluating call:
|
||
(FACTORIAL 2)
|
||
With arguments:
|
||
2
|
||
[Condition of type SB-EXT:STEP-FORM-CONDITION]
|
||
|
||
Restarts:
|
||
0: [STEP-CONTINUE] Resume normal execution
|
||
1: [STEP-OUT] Resume stepping after returning from this function
|
||
2: [STEP-NEXT] Step over call
|
||
3: [STEP-INTO] Step into call
|
||
4: [RETRY] Retry SLIME REPL evaluation request.
|
||
5: [*ABORT] Return to SLIME's top level.
|
||
--more--
|
||
|
||
Backtrace:
|
||
0: ((LAMBDA ()))
|
||
1: (SB-INT:SIMPLE-EVAL-IN-LEXENV (LET ((SB-IMPL::*STEP-OUT* :MAYBE)) (UNWIND-PROTECT (SB-IMPL::WITH-STEPPING-ENABLED #))) #S(SB-KERNEL:LEXENV :FUNS NIL :VARS NIL :BLOCKS NIL :TAGS NIL :TYPE-RESTRICTIONS ..
|
||
2: (SB-INT:SIMPLE-EVAL-IN-LEXENV (STEP (FACTORIAL 2)) #<NULL-LEXENV>)
|
||
3: (EVAL (STEP (FACTORIAL 2)))
|
||
</code></pre>
|
||
|
||
<p>Stepping is useful, however it may be a sign that you need to simplify your function.</p>
|
||
|
||
<h2 id="break">Break</h2>
|
||
|
||
<p>A call to <a href="http://www.xach.com/clhs?q=break">break</a> makes the program
|
||
enter the debugger, from which we can inspect the call stack.</p>
|
||
|
||
<h3 id="breakpoints-in-slime">Breakpoints in Slime</h3>
|
||
|
||
<p>Look at the <code>SLDB</code> menu, it shows navigation keys and available
|
||
actions. Of which:</p>
|
||
|
||
<ul>
|
||
<li><code>e</code> (<em>sldb-eval-in-frame</em>) prompts for an expression and evaluates
|
||
it in the selected frame. This is how we can explore our
|
||
intermediate variables</li>
|
||
<li><code>d</code> is similar with the addition of pretty printing the result</li>
|
||
</ul>
|
||
|
||
<p>Once we are in a frame and detect a suspicious behavior, we can even
|
||
re-compile a function at runtime and resume the program execution from
|
||
where it stopped (using the “step-continue” restart).</p>
|
||
|
||
<h2 id="advise-and-watch">Advise and watch</h2>
|
||
|
||
<p><em>advise</em> and
|
||
<a href="http://www.xach.com/clhs?q=watch">watch</a> are available in some
|
||
implementations, like CCL
|
||
(<a href="https://ccl.clozure.com/manual/chapter4.3.html#Advising">advise</a> and
|
||
<a href="https://ccl.clozure.com/manual/chapter4.12.html#watched-objects">watch</a>)
|
||
and <a href="http://www.lispworks.com/">LispWorks</a>. They do exist in
|
||
SBCL but are not exported. <code>advise</code> allows to modify a function without changing its
|
||
source, or to do something before or after its execution, similar
|
||
to CLOS method combination (before, after, around methods).</p>
|
||
|
||
<p><code>watch</code> will signal a condition when a thread attempts to write to an
|
||
object being watched. It can be coupled with the display of the
|
||
watched objects in a GUI.
|
||
For a certain class of bugs (someone is changing this value, but I
|
||
don’t know who), this can be extremely helpful.</p>
|
||
|
||
<h2 id="unit-tests">Unit tests</h2>
|
||
|
||
<p>Last but not least, automatic testing of functions in isolation might
|
||
be what you’re looking for! See the <a href="testing.html">testing</a> section and a list of
|
||
<a href="https://github.com/CodyReichert/awesome-cl#unit-testing">test frameworks and libraries</a>.</p>
|
||
|
||
<h2 id="remote-debugging">Remote debugging</h2>
|
||
|
||
<p>You can have your software running on a machine over the network,
|
||
connect to it and debug it from home, from your development
|
||
environment.</p>
|
||
|
||
<p>The steps involved are to start a <strong>Swank server</strong> on the remote machine (Swank is the backend companion of Slime), create an
|
||
ssh tunnel and connect to the Swank server from our editor. Then we
|
||
can browse and evaluate code on the running instance transparently.</p>
|
||
|
||
<p>To test this, let’s define a function that prints forever.</p>
|
||
|
||
<p>If needed, import the dependencies first:</p>
|
||
|
||
<pre><code class="language-lisp">(ql:quickload '("swank" "bordeaux-threads"))
|
||
</code></pre>
|
||
|
||
<pre><code class="language-lisp">;; a little common lisp swank demo
|
||
;; while this program is running, you can connect to it from another terminal or machine
|
||
;; and change the definition of doprint to print something else out!
|
||
|
||
(require :swank)
|
||
(require :bordeaux-threads)
|
||
|
||
(defparameter *counter* 0)
|
||
|
||
(defun dostuff ()
|
||
(format t "hello world ~a!~%" *counter*))
|
||
|
||
(defun runner ()
|
||
(swank:create-server :port 4006)
|
||
(format t "we are past go!~%")
|
||
(bt:make-thread (lambda ()
|
||
(loop repeat 5 do
|
||
(sleep 5)
|
||
(dostuff)
|
||
(incf *counter*)))
|
||
:name "do-stuff"))
|
||
|
||
(runner)
|
||
</code></pre>
|
||
|
||
<p>On the server, we can run this code with</p>
|
||
|
||
<pre><code>sbcl --load demo.lisp
|
||
</code></pre>
|
||
|
||
<p>If you check with <code>(bt:all-threads)</code>, you’ll see your Swank server running on port 4006, as well
|
||
as the other thread ready to do stuff:</p>
|
||
|
||
<pre><code>(#<SB-THREAD:THREAD "do-stuff" RUNNING {10027CEDC3}>
|
||
#<SB-THREAD:THREAD "Swank Sentinel" waiting on:
|
||
#<WAITQUEUE {10027D0003}>
|
||
{10027CE8B3}>
|
||
#<SB-THREAD:THREAD "Swank 4006" RUNNING {10027CEB63}>
|
||
#<SB-THREAD:THREAD "main thread" RUNNING {1007C40393}>)
|
||
</code></pre>
|
||
|
||
<p>We do port forwarding on our development machine:</p>
|
||
|
||
<pre><code>ssh -L4006:127.0.0.1:4006 username@example.com
|
||
</code></pre>
|
||
|
||
<p>this will securely forward port 4006 on the server at example.com to
|
||
our local computer’s port 4006 (Swank only accepts connections from
|
||
localhost).</p>
|
||
|
||
<p>We connect to the running Swank with <code>M-x slime-connect</code>, choosing localhost for the host
|
||
and port 4006.</p>
|
||
|
||
<p>We can write new code:</p>
|
||
|
||
<pre><code class="language-lisp">(defun dostuff ()
|
||
(format t "goodbye world ~a!~%" *counter*))
|
||
(setf *counter* 0)
|
||
</code></pre>
|
||
|
||
<p>and eval it as usual with <code>C-c C-c</code> or <code>M-x slime-eval-region</code> for instance. The output should change.</p>
|
||
|
||
<p>That’s how Ron Garret debugged the Deep Space 1 spacecraft from the earth
|
||
in 1999:</p>
|
||
|
||
<blockquote>
|
||
<p>We were able to debug and fix a race condition that had not shown up during ground testing. (Debugging a program running on a $100M piece of hardware that is 100 million miles away is an interesting experience. Having a read-eval-print loop running on the spacecraft proved invaluable in finding and fixing the problem.</p>
|
||
</blockquote>
|
||
|
||
<h2 id="references">References</h2>
|
||
|
||
<ul>
|
||
<li><a href="https://successful-lisp.blogspot.com/p/httpsdrive.html">“How to understand and use Common Lisp”</a>, chap. 30, David Lamkins (book download from author’s site)</li>
|
||
<li><a href="https://malisper.me/debugging-lisp-part-1-recompilation/">Malisper: debugging Lisp series</a></li>
|
||
<li><a href="https://two-wrongs.com/debugging-common-lisp-in-slime.html">Two Wrongs: debugging Common Lisp in Slime</a></li>
|
||
<li><a href="https://common-lisp.net/project/slime/doc/html/Connecting-to-a-remote-lisp.html#Connecting-to-a-remote-lisp">Slime documentation: connecting to a remote Lisp</a></li>
|
||
<li><a href="http://cvberry.com/tech_writings/howtos/remotely_modifying_a_running_program_using_swank.html">cvberrycom: remotely modifying a running Lisp program using Swank</a></li>
|
||
<li><a href="http://www.flownet.com/gat/jpl-lisp.html#1994-1999%20-%20Remote%20Agent">Ron Garret: Lisping at the JPL</a></li>
|
||
<li><a href="https://www.youtube.com/watch?v=_gZK0tW8EhQ&feature=youtu.be&t=4175">the Remote Agent experiment: debugging code from 60 million miles away (youtube)</a> (<a href="https://www.reddit.com/r/lisp/comments/a7156w/lisp_and_the_remote_agent/">“AMA” on reddit</a>)</li>
|
||
</ul>
|
||
|
||
|
||
<p class="page-source">
|
||
Page source: <a href="https://github.com/LispCookbook/cl-cookbook/blob/master/debugging.md">debugging.md</a>
|
||
</p>
|
||
</div>
|
||
|
||
<script type="text/javascript">
|
||
|
||
// Don't write the TOC on the index.
|
||
if (window.location.pathname != "/cl-cookbook/") {
|
||
$("#toc").toc({
|
||
content: "#content", // will ignore the first h1 with the site+page title.
|
||
headings: "h1,h2,h3,h4"});
|
||
}
|
||
|
||
$("#two-cols + ul").css({
|
||
"column-count": "2",
|
||
});
|
||
$("#contributors + ul").css({
|
||
"column-count": "4",
|
||
});
|
||
</script>
|
||
|
||
|
||
|
||
<div>
|
||
<footer class="footer">
|
||
<hr/>
|
||
© 2002–2021 the Common Lisp Cookbook Project
|
||
</footer>
|
||
|
||
</div>
|
||
<div id="toc-btn">T<br>O<br>C</div>
|
||
</div>
|
||
|
||
<script text="javascript">
|
||
HighlightLisp.highlight_auto({className: null});
|
||
</script>
|
||
|
||
<script type="text/javascript">
|
||
function duckSearch() {
|
||
var searchField = document.getElementById("searchField");
|
||
if (searchField && searchField.value) {
|
||
var query = escape("site:lispcookbook.github.io/cl-cookbook/ " + searchField.value);
|
||
window.location.href = "https://duckduckgo.com/?kj=b2&kf=-1&ko=1&q=" + query;
|
||
// https://duckduckgo.com/params
|
||
// kj=b2: blue header in results page
|
||
// kf=-1: no favicons
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<script async defer data-domain="lispcookbook.github.io/cl-cookbook" src="https://plausible.io/js/plausible.js"></script>
|
||
|
||
</body>
|
||
</html>
|